diff --git a/CLAUDE.md b/CLAUDE.md index 94694bde64..74a15d2d71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,10 +80,11 @@ These are structural properties of the graph model, not validation rules layered - **Every node has at least one label.** An empty label set is not representable; there is no "unlabeled node". An import file with a node whose label list is empty/null is malformed input, not a node to be created label-less. - **Every edge has exactly one edge type.** A null/empty edge type is not representable; an edge cannot exist without a type. A null `__type` in an import file is malformed input. -- **A property value is a scalar, a list or a map.** The scalar types are `ValueType`'s Int64, UInt64, Double, Bool, String, Embedding, DateTime and Duration; `ValueType::List` is the list one, held by `TypedPropertyContainer` over a `ListContainer` that owns its elements (see `docs/lists.md`). A stored list may be heterogeneous and may hold nested lists, so a LIST-typed column in an import file is a property to ingest, not unsupported input. `ValueType::Map` is the map one, held by `TypedPropertyContainer` over a `MapContainer` (see `docs/maps.md`): string keys, stored sorted, over values that may be scalars, lists or nested maps. A stored list may hold maps too, owned by its `ListContainer`'s own `MapContainer`. -- **`ValueType` and `CellTag` are append-only.** Both are written to disk as their raw ordinals — the property tag in `PropertyTypeMapDumper`, and the cell tag of an element inside an `EncodedList` and of a value inside an `EncodedMap` — with no format version beside them. A new member goes immediately before the sentinel; inserting one anywhere else silently reinterprets every datapart already written. The same holds for `net::proto::ColumnInternalKind` on the wire. +- **A property value is a scalar, a list or a map.** The scalar types are `ValueType`'s Int64, UInt64, Double, Bool, String, Embedding, DateTime, Duration and Point; `ValueType::List` is the list one, held by `TypedPropertyContainer` over a `ListContainer` that owns its elements (see `docs/lists.md`). A stored list may be heterogeneous and may hold nested lists, so a LIST-typed column in an import file is a property to ingest, not unsupported input. `ValueType::Map` is the map one, held by `TypedPropertyContainer` over a `MapContainer` (see `docs/maps.md`): string keys, stored sorted, over values that may be scalars, lists or nested maps. A stored list may hold maps too, owned by its `ListContainer`'s own `MapContainer`. +- **`ValueType` and `CellTag` are append-only.** Both are written to disk as their raw ordinals — the property tag in `PropertyTypeMapDumper`, and the cell tag of an element inside an `EncodedList` and of a value inside an `EncodedMap` — with no format version beside them. A new member goes immediately before the sentinel; inserting one anywhere else silently reinterprets every datapart already written. The same holds for `net::proto::ColumnInternalKind` on the wire, and for `PointCRS`, whose ordinal every stored point carries. - **`DateTime` is microseconds since the Unix epoch, UTC** (`storage/metadata/DateTime.h`). `datetime("...")` parses ISO-8601 and spends any offset on the instant, so `...T16:05:00+02:00` and `...T14:05:00Z` are one value and both render as `Z`. One int64 carries no zone; a zoned datetime would be a second type, not a widening of this one. - **`Duration` is a signed count of microseconds** (`storage/metadata/Duration.h`). It has no month component, because a month has no fixed length; a calendar duration would be a second type. `duration()` reads the count as microseconds, and a duration renders as ISO-8601 hours, minutes and seconds (`PT25H1M1S`). +- **A `Point` is x, y and z in one of four CRSs** (`storage/metadata/Point.h`): cartesian, cartesian-3d, wgs-84 and wgs-84-3d, Neo4j's. It is 32 bytes, so two equal points are equal in every byte: a 2D point holds a z of 0, no coordinate is -0.0, and the padding is zero. No coordinate is non-finite, a geographic latitude lies in [-90, 90] and its longitude in [-180, 180]. A point renders as the text `point({x: 1.0, y: 2.0, crs: 'cartesian'})`. ### Server (`server/`, `net/`) - Single TCP listener speaking TuringDB's binary protocol framed as HTTP/1.1 chunked transfer. The in-tree `net/http_parser/HTTPParser` reads the request (endpoints routed in `server/DBURIParser.h` / `server/Endpoints.h`), `server/TuringProtoServerProcessor` handles dispatch, and `net/turing_proto_server/TuringProtoWriter` emits one proto packet per HTTP chunk. There is no JSON protocol and no third-party web framework. Client side: `net/turing_proto_client/TuringClient` (C++), `python/turingdb/binary_client.py` (`BinaryClient`), `wasm/npm` (JS). diff --git a/io/parquet/ParquetEdgeVisitor.cpp b/io/parquet/ParquetEdgeVisitor.cpp index 6e44edd160..583d25a0e9 100644 --- a/io/parquet/ParquetEdgeVisitor.cpp +++ b/io/parquet/ParquetEdgeVisitor.cpp @@ -283,6 +283,7 @@ void ParquetEdgeVisitor::addEdgeProperty(const EdgeRecord& edge, case ValueType::List: // ValueTypes that aren't represented in Parquet case ValueType::Map: + case ValueType::Point: case ValueType::UInt64: case ValueType::Embedding: case ValueType::Invalid: diff --git a/io/parquet/ParquetNodeVisitor.cpp b/io/parquet/ParquetNodeVisitor.cpp index ec2e81676e..e81a4ea242 100644 --- a/io/parquet/ParquetNodeVisitor.cpp +++ b/io/parquet/ParquetNodeVisitor.cpp @@ -291,6 +291,7 @@ void ParquetNodeVisitor::addNodeProperty(NodeID id, case ValueType::List: // ValueTypes that aren't represented in Parquet case ValueType::Map: + case ValueType::Point: case ValueType::UInt64: case ValueType::Embedding: case ValueType::Invalid: diff --git a/memory/LocalMemory.h b/memory/LocalMemory.h index 8ddee05c5b..496b447452 100644 --- a/memory/LocalMemory.h +++ b/memory/LocalMemory.h @@ -80,6 +80,7 @@ class LocalMemory { MakeMemoryPool>::type, MakeMemoryPool>::type, MakeMemoryPool>::type, + MakeMemoryPool>::type, MakeMemoryPool>>::type, MakeMemoryPool>>::type, @@ -91,6 +92,7 @@ class LocalMemory { MakeMemoryPool>>::type, MakeMemoryPool>>::type, MakeMemoryPool>>::type, + MakeMemoryPool>>::type, MakeMemoryPool>::type, @@ -102,6 +104,7 @@ class LocalMemory { MakeMemoryPool>::type, MakeMemoryPool>::type, MakeMemoryPool>::type, + MakeMemoryPool>::type, MakeMemoryPool>::type, MakeMemoryPool>::type, @@ -114,6 +117,7 @@ class LocalMemory { MakeMemoryPool>::type, MakeMemoryPool>::type, MakeMemoryPool>::type, + MakeMemoryPool>::type, MakeMemoryPool>::type, MakeMemoryPool>::type, MakeMemoryPool>::type, diff --git a/net/decoders/ParameterMapSink.cpp b/net/decoders/ParameterMapSink.cpp index 0672405901..e295bf894e 100644 --- a/net/decoders/ParameterMapSink.cpp +++ b/net/decoders/ParameterMapSink.cpp @@ -71,6 +71,9 @@ void readScalar(const char* bytes, db::ParameterValue& slot) { case db::CellTag::Duration: slot.setDuration(readValue(value)); break; + case db::CellTag::Point: + throwUnsupportedType("Point"); + break; default: throw TuringException(fmt::format("Invalid value tag {} in a parameter", static_cast(tag))); break; diff --git a/net/decoders/TuringProtoDecoder.h b/net/decoders/TuringProtoDecoder.h index 75f5cb2a21..2ebb35afd8 100644 --- a/net/decoders/TuringProtoDecoder.h +++ b/net/decoders/TuringProtoDecoder.h @@ -125,6 +125,8 @@ decltype(auto) TuringProtoDecoder::dispatchColumnType(ColumnInternalKind t return fn.template operator()(encoding); case ColumnInternalKind::GRAPH_PATH: return fn.template operator()(encoding); + case ColumnInternalKind::POINT: + return fn.template operator()(encoding); } throw TuringException("Unsupported incoming column type"); diff --git a/net/decoders/TuringProtoDecoderConcepts.h b/net/decoders/TuringProtoDecoderConcepts.h index 724ef59507..7fd653129a 100644 --- a/net/decoders/TuringProtoDecoderConcepts.h +++ b/net/decoders/TuringProtoDecoderConcepts.h @@ -16,6 +16,7 @@ concept TrivialInternalTypes = std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v +|| std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v diff --git a/net/encoders/OutputValues.h b/net/encoders/OutputValues.h index 252d9d33d6..21e4c89976 100644 --- a/net/encoders/OutputValues.h +++ b/net/encoders/OutputValues.h @@ -85,6 +85,9 @@ concept IsDateTime = std::is_same_v, types::Date template concept IsDuration = std::is_same_v, types::Duration::Primitive>; +template +concept IsPoint = std::is_same_v, types::Point::Primitive>; + struct ColumnTypeGenerator { std::string& _name; @@ -96,6 +99,8 @@ struct ColumnTypeGenerator { _name = fmt::format("DateTime"); } else if constexpr (IsDuration) { _name = fmt::format("Duration"); + } else if constexpr (IsPoint) { + _name = fmt::format("Point"); } else if constexpr (IsUInt64) { _name = fmt::format("UInt64"); } else if constexpr (IsInt64) { diff --git a/net/encoders/TuringProtoEncoder.h b/net/encoders/TuringProtoEncoder.h index 2fd94cd099..a1fa505ee3 100644 --- a/net/encoders/TuringProtoEncoder.h +++ b/net/encoders/TuringProtoEncoder.h @@ -60,6 +60,8 @@ struct ColInternalKindToProtoEnum { return Enum::DATE_TIME; } else if constexpr (db::IsDuration) { return Enum::DURATION; + } else if constexpr (db::IsPoint) { + return Enum::POINT; } else if constexpr (db::IsListView) { return Enum::LIST_VIEW; } else if constexpr (db::IsMap) { diff --git a/net/turing_proto_common/TuringProtoHeaders.h b/net/turing_proto_common/TuringProtoHeaders.h index 26cdd5d9cd..5f79bf096a 100644 --- a/net/turing_proto_common/TuringProtoHeaders.h +++ b/net/turing_proto_common/TuringProtoHeaders.h @@ -80,6 +80,7 @@ enum class ColumnInternalKind : uint32_t { DATE_TIME, DURATION, GRAPH_PATH, + POINT, }; enum class ColumnKind : uint8_t { diff --git a/procedures/ProcUtils.cpp b/procedures/ProcUtils.cpp index a4c9869334..3ca5f8c45b 100644 --- a/procedures/ProcUtils.cpp +++ b/procedures/ProcUtils.cpp @@ -92,6 +92,13 @@ void appendCell(std::string& out, CellView element) { ProcUtils::appendJsonString(out, formatted); } break; + case CellTag::Point: { + std::string formatted; + Point::format(formatted, element.getAs()); + + ProcUtils::appendJsonString(out, formatted); + } + break; case CellTag::MapView: appendMapValue(out, element.getAs()); break; @@ -171,6 +178,11 @@ void appendPropertyValue(std::string& out, const PropertyVariant& value) { std::string formatted; Duration::format(formatted, *ptr); + ProcUtils::appendJsonString(out, formatted); + } else if constexpr (std::is_same_v) { + std::string formatted; + Point::format(formatted, *ptr); + ProcUtils::appendJsonString(out, formatted); } else if constexpr (std::is_same_v) { appendMapValue(out, *ptr); diff --git a/python/turingdb/__init__.py b/python/turingdb/__init__.py index f0db7a9fe6..55014785a2 100644 --- a/python/turingdb/__init__.py +++ b/python/turingdb/__init__.py @@ -6,13 +6,14 @@ from .binary_client import BinaryClient from .embedded_client import EmbeddedClient from .exceptions import TuringDBException +from .point import Point try: from ._version import version as __version__ except ImportError: __version__ = "unknown" -__all__ = ["TuringDB", "BinaryClient", "EmbeddedClient", "TuringDBException", "__version__"] +__all__ = ["TuringDB", "BinaryClient", "EmbeddedClient", "TuringDBException", "Point", "__version__"] def get_executable_path(executable_name="turingdb"): """Get the path to a compiled C++ executable shipped with the package.""" diff --git a/python/turingdb/_common/NanobindUtils.cpp b/python/turingdb/_common/NanobindUtils.cpp index f4728a08ba..60e1d79a79 100644 --- a/python/turingdb/_common/NanobindUtils.cpp +++ b/python/turingdb/_common/NanobindUtils.cpp @@ -190,6 +190,7 @@ namespace { // them as members of one struct lets them call each other without a forward declaration. struct ValueToPyObject { mutable PyTimeTypes _times; + mutable nb::object _point; nb::object duration(db::Duration value) const { return _times.getTimedelta()(nb::arg("microseconds") = value.getMicroseconds()); @@ -199,6 +200,30 @@ struct ValueToPyObject { return _times.getEpoch() + duration(db::Duration {value.getMicroseconds()}); } + nb::object point(const db::Point& value) const { + if (!_point.is_valid()) { + _point = nb::module_::import_("turingdb.point").attr("Point"); + } + + const db::PointCRS crs = value.getCRS(); + const nb::object z = db::Point::getDimension(crs) == 3 ? nb::cast(value.getZ()) : nb::none(); + + return _point(db::Point::getSRID(crs), value.getX(), value.getY(), z); + } + + nb::object point(const std::optional& value) const { + return value.has_value() ? point(*value) : nb::none(); + } + + template + nb::object pointColumn(const std::vector& src) const { + nb::list out; + for (const T& value : src) { + out.append(point(value)); + } + return out; + } + nb::object view(const db::ListView& listView) const { nb::list out; for (const db::CellView element : listView.elements()) { @@ -241,6 +266,8 @@ struct ValueToPyObject { return duration(element.getAs()); } else if constexpr (std::is_same_v) { return dateTime(element.getAs()); + } else if constexpr (std::is_same_v) { + return point(element.getAs()); } else if constexpr (std::is_same_v) { nb::list ids; for (const uint64_t id : element.getAs().getIDs()) { @@ -367,6 +394,12 @@ nb::dict dataframeToNumpy(db::Dataframe* df) { dtypeName = "Duration"; break; } + case db::ColumnVector::staticKind(): { + const auto& src = static_cast*>(col)->getRaw(); + value = listVisitor.pointColumn(src); + dtypeName = "Point"; + break; + } case db::ColumnVector::staticKind(): { const auto& src = static_cast*>(col)->getRaw(); nb::list lst; @@ -533,6 +566,13 @@ nb::dict dataframeToNumpy(db::Dataframe* df) { break; } + case db::ColumnOptVector::staticKind(): { + const auto& src = static_cast*>(col)->getRaw(); + value = listVisitor.pointColumn(src); + dtypeName = "Point"; + break; + } + case db::ColumnConst::staticKind(): { const auto& v = static_cast*>(col)->getRaw(); value = repeatValueAsNdarray(v, rowCount); @@ -614,6 +654,28 @@ nb::dict dataframeToNumpy(db::Dataframe* df) { dtypeName = "Duration"; break; } + case db::ColumnConst::staticKind(): { + const auto& v = static_cast*>(col)->getRaw(); + const nb::object pointObj = listVisitor.point(v); + nb::list lst; + for (size_t i = 0; i < rowCount; ++i) { + lst.append(pointObj); + } + value = lst; + dtypeName = "Point"; + break; + } + case db::ColumnConst>::staticKind(): { + const auto& v = static_cast>*>(col)->getRaw(); + const nb::object pointObj = listVisitor.point(v); + nb::list lst; + for (size_t i = 0; i < rowCount; ++i) { + lst.append(pointObj); + } + value = lst; + dtypeName = "Point"; + break; + } case db::ColumnConst::staticKind(): { const auto& v = static_cast*>(col)->getRaw(); nb::object py = embeddingToNdarray(v); diff --git a/python/turingdb/point.py b/python/turingdb/point.py new file mode 100644 index 0000000000..1f7e506afd --- /dev/null +++ b/python/turingdb/point.py @@ -0,0 +1,16 @@ +from typing import NamedTuple, Optional + +_CRS_NAMES = {7203: "cartesian", 9157: "cartesian-3d", 4326: "wgs-84", 4979: "wgs-84-3d"} + + +class Point(NamedTuple): + """A point a query returns. z is None for a 2D point.""" + + srid: int + x: float + y: float + z: Optional[float] = None + + @property + def crs(self) -> str: + return _CRS_NAMES[self.srid] diff --git a/query/AST/FunctionDecls.cpp b/query/AST/FunctionDecls.cpp index 8075eca6d1..7e7fb6c2c8 100644 --- a/query/AST/FunctionDecls.cpp +++ b/query/AST/FunctionDecls.cpp @@ -157,6 +157,11 @@ void FunctionDecls::initDefault() { countDurations->setReturnTypes({{EvaluatedType::Integer}}); countDurations->setIsAggregate(true); + FunctionSignature* countPoints = createFunction("count"); + countPoints->setArguments({EvaluatedType::Point}); + countPoints->setReturnTypes({{EvaluatedType::Integer}}); + countPoints->setIsAggregate(true); + FunctionSignature* countAnyValues = createFunction("count"); countAnyValues->setArguments({EvaluatedType::Any}); countAnyValues->setReturnTypes({{EvaluatedType::Integer}}); @@ -200,6 +205,12 @@ void FunctionDecls::initDefault() { collectDurations->setIsAggregate(true); collectDurations->setCollectsItsArgument(true); + FunctionSignature* collectPoints = createFunction("collect"); + collectPoints->setArguments({EvaluatedType::Point}); + collectPoints->setReturnTypes({{EvaluatedType::List}}); + collectPoints->setIsAggregate(true); + collectPoints->setCollectsItsArgument(true); + FunctionSignature* collectNodes = createFunction("collect"); collectNodes->setArguments({EvaluatedType::NodePattern}); collectNodes->setReturnTypes({{EvaluatedType::List}}); @@ -658,7 +669,8 @@ void FunctionDecls::initDefault() { EvaluatedType::Integer, {EvaluatedType::String, EvaluatedType::Integer, EvaluatedType::Double, EvaluatedType::Bool}, {EvaluatedType::List, EvaluatedType::Map, EvaluatedType::NodePattern, EvaluatedType::EdgePattern, - EvaluatedType::GraphPath, EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Embedding}, + EvaluatedType::GraphPath, EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Point, + EvaluatedType::Embedding}, }, { "toFloat", @@ -666,7 +678,8 @@ void FunctionDecls::initDefault() { EvaluatedType::Double, {EvaluatedType::String, EvaluatedType::Integer, EvaluatedType::Double}, {EvaluatedType::Bool, EvaluatedType::List, EvaluatedType::Map, EvaluatedType::NodePattern, EvaluatedType::EdgePattern, - EvaluatedType::GraphPath, EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Embedding}, + EvaluatedType::GraphPath, EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Point, + EvaluatedType::Embedding}, }, { "toBoolean", @@ -674,14 +687,15 @@ void FunctionDecls::initDefault() { EvaluatedType::Bool, {EvaluatedType::String, EvaluatedType::Integer, EvaluatedType::Bool}, {EvaluatedType::Double, EvaluatedType::List, EvaluatedType::Map, EvaluatedType::NodePattern, EvaluatedType::EdgePattern, - EvaluatedType::GraphPath, EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Embedding}, + EvaluatedType::GraphPath, EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Point, + EvaluatedType::Embedding}, }, { "toString", "toStringOrNull", EvaluatedType::String, {EvaluatedType::String, EvaluatedType::Integer, EvaluatedType::Double, EvaluatedType::Bool, - EvaluatedType::DateTime, EvaluatedType::Duration}, + EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Point}, {EvaluatedType::List, EvaluatedType::Map, EvaluatedType::NodePattern, EvaluatedType::EdgePattern, EvaluatedType::GraphPath, EvaluatedType::Embedding}, }, @@ -743,6 +757,14 @@ void FunctionDecls::initDefault() { durationOfCell->setArguments({EvaluatedType::Any}); durationOfCell->setReturnTypes({{EvaluatedType::Duration}}); + FunctionSignature* pointOfMap = createFunction("point"); + pointOfMap->setArguments({EvaluatedType::Map}); + pointOfMap->setReturnTypes({{EvaluatedType::Point}}); + + FunctionSignature* pointOfCell = createFunction("point"); + pointOfCell->setArguments({EvaluatedType::Any}); + pointOfCell->setReturnTypes({{EvaluatedType::Point}}); + // Numeric functions for (const EvaluatedType numberType : {EvaluatedType::Integer, EvaluatedType::Double, EvaluatedType::Any}) { FunctionSignature* abs = createFunction("abs"); @@ -837,7 +859,7 @@ void FunctionDecls::initDefault() { const std::vector nonString = { EvaluatedType::Integer, EvaluatedType::Double, EvaluatedType::Bool, EvaluatedType::List, EvaluatedType::Map, EvaluatedType::NodePattern, EvaluatedType::EdgePattern, EvaluatedType::GraphPath, - EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Embedding, + EvaluatedType::DateTime, EvaluatedType::Duration, EvaluatedType::Point, EvaluatedType::Embedding, }; const std::vector normalForm = {EvaluatedType::NormalForm}; @@ -901,6 +923,7 @@ void FunctionDecls::initCollectionFunctions() { EvaluatedType::Bool, EvaluatedType::DateTime, EvaluatedType::Duration, + EvaluatedType::Point, EvaluatedType::Embedding, EvaluatedType::NodePattern, EvaluatedType::EdgePattern, diff --git a/query/AST/decl/EvaluatedType.h b/query/AST/decl/EvaluatedType.h index 90e622aefd..768a0a0879 100644 --- a/query/AST/decl/EvaluatedType.h +++ b/query/AST/decl/EvaluatedType.h @@ -39,6 +39,7 @@ enum class EvaluatedType : uint8_t { EdgeType, DateTime, Duration, + Point, NormalForm, _SIZE, @@ -93,11 +94,12 @@ using EvaluatedTypeName = EnumToString::Create< EnumStringPair, EnumStringPair, EnumStringPair, + EnumStringPair, EnumStringPair>; // Evaluated Type to ValueType conversion static constexpr size_t VALUE_TYPE_COUNT = std::to_underlying(ValueType::_SIZE); -static_assert(VALUE_TYPE_COUNT == 11, "ValueType added: please update below map."); +static_assert(VALUE_TYPE_COUNT == 12, "ValueType added: please update below map."); static constexpr std::pair EVConversions[VALUE_TYPE_COUNT + 1] = { {ValueType::Int64, EvaluatedType::Integer }, @@ -109,6 +111,7 @@ static constexpr std::pair EVConversions[VALUE_TYPE_CO {ValueType::List, EvaluatedType::List }, {ValueType::DateTime, EvaluatedType::DateTime }, {ValueType::Duration, EvaluatedType::Duration }, + {ValueType::Point, EvaluatedType::Point }, {ValueType::Map, EvaluatedType::Map }, {ValueType::Invalid, EvaluatedType::Invalid }, @@ -170,6 +173,7 @@ inline bool namesAComparableValue(EvaluatedType e) { || e == EvaluatedType::List || e == EvaluatedType::DateTime || e == EvaluatedType::Duration + || e == EvaluatedType::Point || e == EvaluatedType::Map; } diff --git a/query/analyzer/ExprAnalyzer.cpp b/query/analyzer/ExprAnalyzer.cpp index 5c373695e9..76eda3f484 100644 --- a/query/analyzer/ExprAnalyzer.cpp +++ b/query/analyzer/ExprAnalyzer.cpp @@ -1491,6 +1491,8 @@ bool ExprAnalyzer::propTypeCompatible(ValueType vt, EvaluatedType exprType) { return vt == ValueType::DateTime; case EvaluatedType::Duration: return vt == ValueType::Duration; + case EvaluatedType::Point: + return vt == ValueType::Point; case EvaluatedType::Map: return vt == ValueType::Map; case EvaluatedType::Wildcard: diff --git a/query/analyzer/WriteStmtAnalyzer.cpp b/query/analyzer/WriteStmtAnalyzer.cpp index f4ed685e25..94ef4ed1f8 100644 --- a/query/analyzer/WriteStmtAnalyzer.cpp +++ b/query/analyzer/WriteStmtAnalyzer.cpp @@ -610,6 +610,8 @@ db::ValueType WriteStmtAnalyzer::evaluatedToValueType(EvaluatedType type) { return ValueType::DateTime; case EvaluatedType::Duration: return ValueType::Duration; + case EvaluatedType::Point: + return ValueType::Point; case EvaluatedType::Map: return ValueType::Map; case EvaluatedType::Null: diff --git a/query/ir/codegen/DBProgramGenerator.cpp b/query/ir/codegen/DBProgramGenerator.cpp index 42e681332b..f8b906ff67 100644 --- a/query/ir/codegen/DBProgramGenerator.cpp +++ b/query/ir/codegen/DBProgramGenerator.cpp @@ -513,6 +513,7 @@ const std::unordered_map unaryFunctionEm {"toBooleanOrNull", &emitUnaryFunction}, {"datetime", &emitUnaryFunction}, {"duration", &emitUnaryFunction}, + {"point", &emitUnaryFunction}, {"size", &emitUnaryFunction}, {"length", &emitUnaryFunction}, {"isEmpty", &emitUnaryFunction}, diff --git a/query/ir/common/IRValueTypes.cpp b/query/ir/common/IRValueTypes.cpp index 35ed0508da..5d692cc448 100644 --- a/query/ir/common/IRValueTypes.cpp +++ b/query/ir/common/IRValueTypes.cpp @@ -68,6 +68,10 @@ mlir::Type db::valueTypeToElementType(mlir::OpBuilder& builder, ValueType valueT return mlir::storage::DurationType::get(builder.getContext()); break; + case ValueType::Point: + return mlir::storage::PointType::get(builder.getContext()); + break; + case ValueType::Map: return mlir::storage::MapType::get(builder.getContext()); break; @@ -92,6 +96,8 @@ ValueType db::elementTypeToValueType(mlir::Type elementType) { return ValueType::DateTime; } else if (mlir::isa(elementType)) { return ValueType::Duration; + } else if (mlir::isa(elementType)) { + return ValueType::Point; } else if (mlir::isa(elementType)) { return ValueType::Map; } else if (mlir::isa(elementType)) { diff --git a/query/ir/dialect/db/DBColumnElement.cpp b/query/ir/dialect/db/DBColumnElement.cpp index 2a8bad1e1c..33bf79c321 100644 --- a/query/ir/dialect/db/DBColumnElement.cpp +++ b/query/ir/dialect/db/DBColumnElement.cpp @@ -72,6 +72,10 @@ mlir::Type durationElement(mlir::MLIRContext* context, mlir::Type inputElement) return storage::DurationType::get(context); } +mlir::Type pointElement(mlir::MLIRContext* context, mlir::Type inputElement) { + return storage::PointType::get(context); +} + // A list function reads a list cell, or the type-erased cell an unwind of a list of lists // hands its nested lists on as bool readsAList(mlir::Type inputElement) { @@ -142,6 +146,7 @@ const std::unordered_map elementRules = { {"db.to_boolean", &booleanElement}, {"db.to_datetime", &dateTimeElement}, {"db.to_duration", &durationElement}, + {"db.to_point", &pointElement}, {"db.datetime_year", &integerElement}, {"db.datetime_month", &integerElement}, diff --git a/query/ir/dialect/db/DBHeldTags.cpp b/query/ir/dialect/db/DBHeldTags.cpp index e7ef8ad024..9eaa9462b5 100644 --- a/query/ir/dialect/db/DBHeldTags.cpp +++ b/query/ir/dialect/db/DBHeldTags.cpp @@ -61,6 +61,8 @@ CellTag elementTag(mlir::Type element) { return CellTag::DateTime; } else if (mlir::isa(element)) { return CellTag::Duration; + } else if (mlir::isa(element)) { + return CellTag::Point; } else if (mlir::isa(element)) { return CellTag::Embedding; } diff --git a/query/ir/dialect/db/DBOps.td b/query/ir/dialect/db/DBOps.td index 528eaacecf..fc2294a22f 100644 --- a/query/ir/dialect/db/DBOps.td +++ b/query/ir/dialect/db/DBOps.td @@ -3005,6 +3005,10 @@ def ToFloat : ConversionFunctionOp<"to_float">; def ToBoolean : ConversionFunctionOp<"to_boolean">; def ToDateTime : TypedUnaryFunctionOp<"to_datetime", "::mlir::storage::StringType">; def ToDuration : TypedUnaryFunctionOp<"to_duration", "::mlir::IntegerType">; + +// Any map can name an invalid point - a latitude past 90, an unknown crs - so it is never +// speculatable +def ToPoint : UnaryFunctionOp<"to_point", [MayRaise]>; def ToString : ConversionFunctionOp<"to_string">; def ToIntegerOrNull : UnaryFunctionOp<"to_integer_or_null", [Pure]>; diff --git a/query/ir/dialect/nl/NLOps.td b/query/ir/dialect/nl/NLOps.td index 3962524ec6..ae2b1e7d5e 100644 --- a/query/ir/dialect/nl/NLOps.td +++ b/query/ir/dialect/nl/NLOps.td @@ -3123,6 +3123,7 @@ def ToFloat : NLUnaryFunctionOp<"to_float", [ConstantThroughOperands]>; def ToBoolean : NLUnaryFunctionOp<"to_boolean", [ConstantThroughOperands]>; def ToDateTime : NLUnaryFunctionOp<"to_datetime", [ConstantThroughOperands]>; def ToDuration : NLUnaryFunctionOp<"to_duration", [ConstantThroughOperands]>; +def ToPoint : NLUnaryFunctionOp<"to_point", [ConstantThroughOperands]>; def ToString : NLUnaryFunctionOp<"to_string", [ConstantThroughOperands]>; def ToIntegerOrNull : NLUnaryFunctionOp<"to_integer_or_null", [ConstantThroughOperands]>; diff --git a/query/ir/dialect/storage/StorageTypes.td b/query/ir/dialect/storage/StorageTypes.td index 61b1c06b3e..c5fcd16016 100644 --- a/query/ir/dialect/storage/StorageTypes.td +++ b/query/ir/dialect/storage/StorageTypes.td @@ -111,6 +111,15 @@ def Duration : StorageType<"Duration", "duration"> { }]; } +def Point : StorageType<"Point", "point"> { + let summary = "A point property value"; + let description = [{ + The element type of a chunk holding one location per row - storage's + ColumnVector. It holds x, y and z and the coordinate reference system + they are given in. + }]; +} + def Nullable : StorageType<"Nullable", "nullable"> { let summary = "A nullable property value"; let description = [{ diff --git a/query/ir/exec/NLEntityProperties.cpp b/query/ir/exec/NLEntityProperties.cpp index 5135e6a510..dd5c717a5b 100644 --- a/query/ir/exec/NLEntityProperties.cpp +++ b/query/ir/exec/NLEntityProperties.cpp @@ -57,6 +57,9 @@ MapBuffer<>::MapItemVariant storedValue(const PropertyContainer& container, Enti case ValueType::Duration: return container.cast().get(entity); break; + case ValueType::Point: + return container.cast().get(entity); + break; case ValueType::Invalid: case ValueType::_SIZE: break; @@ -113,6 +116,9 @@ MapBuffer<>::MapItemVariant writtenValue(NLWrittenValues& written, case ValueType::Duration: return readWritten(written, readValues, value); break; + case ValueType::Point: + return readWritten(written, readValues, value); + break; case ValueType::Invalid: case ValueType::_SIZE: break; diff --git a/query/ir/exec/NLExecutor.cpp b/query/ir/exec/NLExecutor.cpp index 1b55e2123c..02edb3ad7b 100644 --- a/query/ir/exec/NLExecutor.cpp +++ b/query/ir/exec/NLExecutor.cpp @@ -2970,6 +2970,12 @@ void distinctAppendElementBytes(std::string& key, const CellView element) { return; break; + case CellTag::Point: + key.push_back(static_cast(tag)); + distinctAppendValueBytes(key, element.getAs()); + return; + break; + case CellTag::Embedding: throw IRException("cannot dedup by an embedding element"); break; @@ -3139,6 +3145,10 @@ uint64_t hashKeyValue(types::Duration::Primitive value) { return mixKeyBits(static_cast(value.getMicroseconds())); } +uint64_t hashKeyValue(types::Point::Primitive value) { + return mixKeyBits(std::hash {}(value)); +} + // The two keys of a join need not share a type, so two values `=` holds equal hash alike // whatever they are held as: a double holding a whole number hashes as that integer. -0.0 // hashes as +0.0, the one value it equals; a NaN never reaches a lookup. @@ -3240,6 +3250,10 @@ uint64_t hashKeyValue(CellView cell) { return hashKeyValue(cell.getAs()); break; + case CellTag::Point: + return hashKeyValue(cell.getAs()); + break; + case CellTag::Path: return hashKeyValue(cell.getAs()); break; @@ -5222,6 +5236,10 @@ Item taggedItem(const CellView element) { return Item {element.getAs()}; break; + case CellTag::Point: + return Item {element.getAs()}; + break; + case CellTag::MapView: return Item {element.getAs()}; break; @@ -5561,6 +5579,7 @@ NLKeyedValue readKeyedCell(const CellView cell) { case CellTag::Embedding: case CellTag::ListView: case CellTag::Path: + case CellTag::Point: return NLKeylessValue {tag}; break; case CellTag::INVALID: @@ -5890,6 +5909,7 @@ NLSubscript subscriptOf(const CellView cell) { case CellTag::MapView: case CellTag::Duration: case CellTag::Path: + case CellTag::Point: return NLNeitherPositionNorKey {tag}; break; case CellTag::INVALID: @@ -6562,6 +6582,10 @@ NLGroupAggregateFoldFunction selectGroupFirstFold(ValueType inputType) { return &groupFoldFirst; break; + case ValueType::Point: + return &groupFoldFirst; + break; + case ValueType::List: return &groupFoldFirst; break; @@ -10993,6 +11017,10 @@ NLUnaryFn NLExecutor::selectToNullable(ValueType valueType, const Column* operan return selectToNullableOf(operand, memory, result); break; + case ValueType::Point: + return selectToNullableOf(operand, memory, result); + break; + // A cell is a view into the query's list buffer, so the nullable column holds the // views the plain one held and an empty optional where a row has no list at all case ValueType::List: @@ -11183,6 +11211,7 @@ NLUnaryFunctionKernel NLExecutor::selectConversion(const Column* input, bool inp selectForElement.template operator()(); selectForElement.template operator()(); selectForElement.template operator()(); + selectForElement.template operator()(); if (kernel) { return kernel; @@ -11413,6 +11442,7 @@ template NLUnaryFunctionKernel NLExecutor::selectConversion(co template NLUnaryFunctionKernel NLExecutor::selectConversion(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectConversion(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectConversion(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectConversion(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectOrNull(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result) { @@ -12314,6 +12344,10 @@ NLCollectFoldFunction NLExecutor::selectCollectFold(ValueType valueType) { return &collectFold; break; + case ValueType::Point: + return &collectFold; + break; + default: throw IRException("collect does not support this value type"); break; @@ -12359,6 +12393,10 @@ NLCollectFoldFunction NLExecutor::selectCollectDistinctFold(ValueType valueType) return &collectFoldDistinct; break; + case ValueType::Point: + return &collectFoldDistinct; + break; + default: throw IRException("collect does not support this value type"); break; @@ -12469,6 +12507,10 @@ NLUnwindCollectValueEmitFunction NLExecutor::selectUnwindCollectValueEmit(ValueT return &unwindCollectValueEmit; break; + case ValueType::Point: + return &unwindCollectValueEmit; + break; + default: throw IRException("unwind does not support this value type"); break; @@ -12511,6 +12553,10 @@ NLCollectListEmitFunction NLExecutor::selectCollectListEmit(ValueType valueType) return &collectListEmit; break; + case ValueType::Point: + return &collectListEmit; + break; + default: throw IRException("collect does not support this value type"); break; @@ -13020,6 +13066,7 @@ NLUnaryFn NLExecutor::selectRowConstant(const Column* row, LocalMemory* memory, selectForElement.template operator()(); selectForElement.template operator()(); selectForElement.template operator()(); + selectForElement.template operator()(); selectForElement.template operator()>(); selectForElement.template operator()>(); selectForElement.template operator()>(); @@ -13028,6 +13075,7 @@ NLUnaryFn NLExecutor::selectRowConstant(const Column* row, LocalMemory* memory, selectForElement.template operator()>(); selectForElement.template operator()>(); selectForElement.template operator()>(); + selectForElement.template operator()>(); selectForElement.template operator()(); selectForElement.template operator()(); selectForElement.template operator()(); @@ -13441,6 +13489,10 @@ NLKeyAppendFunction NLExecutor::selectKeyAppendFunction(NLChunkKind kind) { return &distinctKeyAppendPlainColumn; break; + case NLChunkKind::Point: + return &distinctKeyAppendPlainColumn; + break; + case NLChunkKind::PathRef: throw IRException("A path column cannot be a DISTINCT or grouping key: expand it into its list first"); break; @@ -13493,6 +13545,10 @@ NLKeyAppendFunction NLExecutor::selectOptKeyAppendFunction(ValueType valueType) return &distinctKeyAppendOptColumn; break; + case ValueType::Point: + return &distinctKeyAppendOptColumn; + break; + case ValueType::Map: return &distinctKeyAppendOptMapColumn; break; @@ -13573,6 +13629,10 @@ NLJoinKeyFunctions NLExecutor::selectJoinKeyFunctions(NLChunkKind kind) { return joinKeyFunctions(); break; + case NLChunkKind::Point: + return joinKeyFunctions(); + break; + case NLChunkKind::PathRef: throw IRException("A path column cannot be a join key: expand it into its list first"); break; @@ -14051,6 +14111,10 @@ NLGroupAggregateFoldFunction NLExecutor::selectGroupCountDistinctFold(ValueType return &groupFoldCountDistinctPresent; break; + case ValueType::Point: + return &groupFoldCountDistinctPresent; + break; + case ValueType::Map: return &groupFoldCountDistinctPresentMap; break; @@ -14283,6 +14347,11 @@ NLKeyAppendFunction NLExecutor::selectPlainMergeKeyAppendFunction(NLChunkKind ki throwUnlessKeyedAsItsOwnType(ValueType::Duration, keyType); return &mergeKeyAppendPlainColumn; break; + + case NLChunkKind::Point: + throwUnlessKeyedAsItsOwnType(ValueType::Point, keyType); + return &mergeKeyAppendPlainColumn; + break; } bioassert(false, "Unknown NLChunkKind"); @@ -14331,6 +14400,11 @@ NLKeyAppendFunction NLExecutor::selectConstMergeKeyAppendFunction(ValueType valu return &mergeKeyAppendConstColumn; break; + case ValueType::Point: + throwUnlessKeyedAsItsOwnType(ValueType::Point, keyType); + return &mergeKeyAppendConstColumn; + break; + case ValueType::Map: throw IRException("a MERGE pattern cannot constrain a property to a map"); break; @@ -14516,6 +14590,10 @@ NLCompareFunction NLExecutor::selectCompareFunction(NLChunkKind kind) { return &compareColumn; break; + case NLChunkKind::Point: + return &compareColumn; + break; + case NLChunkKind::PathRef: throw IRException("A path column cannot be a sort key: expand it into its list first"); break; @@ -14567,6 +14645,10 @@ NLCompareFunction NLExecutor::selectOptCompareFunction(ValueType valueType) { return &compareOptColumn; break; + case ValueType::Point: + return &compareOptColumn; + break; + case ValueType::Map: return &compareOptContainerColumn; break; @@ -14658,6 +14740,7 @@ template void NLExecutor::runPropertyFetch(NLExecution template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); +template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); @@ -14668,6 +14751,7 @@ template void NLExecutor::runPropertyFetch(NLExecution template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); +template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); template void NLExecutor::runPropertyFetch(NLExecutionContext*, NLFunctionData*); void NLExecutor::runGetNodeLabelSet(NLExecutionContext* context, NLFunctionData* data) { diff --git a/query/ir/exec/NLProgram.h b/query/ir/exec/NLProgram.h index 3131caeb54..96199b8684 100644 --- a/query/ir/exec/NLProgram.h +++ b/query/ir/exec/NLProgram.h @@ -84,6 +84,7 @@ enum class NLChunkKind { Path, DateTime, Duration, + Point, PathRef, }; @@ -161,6 +162,10 @@ void dispatchChunkKind(NLChunkKind kind, Handler&& handler) { return handler.template operator()(); break; + case NLChunkKind::Point: + return handler.template operator()(); + break; + case NLChunkKind::PathRef: return handler.template operator()(); break; diff --git a/query/ir/exec/NLTranslator.cpp b/query/ir/exec/NLTranslator.cpp index 54d3d4d660..b18237eefa 100644 --- a/query/ir/exec/NLTranslator.cpp +++ b/query/ir/exec/NLTranslator.cpp @@ -378,6 +378,7 @@ const std::unordered_map unaryFunctio {"nl.to_string_or_null", &NLExecutor::selectOrNull<&NLExecutor::selectConversion, TaggedToStringOrNullFunction>}, {"nl.to_datetime", &NLExecutor::selectConversion}, {"nl.to_duration", &NLExecutor::selectConversion}, + {"nl.to_point", &NLExecutor::selectConversion}, {"nl.datetime_year", &NLExecutor::selectFunction>}, {"nl.datetime_month", &NLExecutor::selectFunction>}, @@ -6890,6 +6891,10 @@ Column* NLTranslator::allocValueColumnForValueType(ValueType valueType) { return _memory->alloc>(); break; + case ValueType::Point: + return _memory->alloc>(); + break; + default: throw IRException("collect does not support this value type"); break; @@ -8020,6 +8025,8 @@ NLChunkKind NLTranslator::chunkKindFromElementType(mlir::Type elementType) { return NLChunkKind::DateTime; } else if (mlir::isa(elementType)) { return NLChunkKind::Duration; + } else if (mlir::isa(elementType)) { + return NLChunkKind::Point; } else if (mlir::isa(elementType)) { return NLChunkKind::PathRef; } else if (mlir::isa(elementType)) { diff --git a/query/ir/exec/NLWriteProperties.cpp b/query/ir/exec/NLWriteProperties.cpp index 762dda7344..8433f81cbf 100644 --- a/query/ir/exec/NLWriteProperties.cpp +++ b/query/ir/exec/NLWriteProperties.cpp @@ -372,6 +372,9 @@ void stageCellAsItsType(const CellView cell, case ValueType::Duration: value = std::optional {cell.getAs()}; break; + case ValueType::Point: + value = std::optional {cell.getAs()}; + break; case ValueType::String: value = std::optional {std::in_place, cell.getAs()}; break; diff --git a/query/ir/exec/NLWrittenValues.cpp b/query/ir/exec/NLWrittenValues.cpp index 29429a02cd..710262085d 100644 --- a/query/ir/exec/NLWrittenValues.cpp +++ b/query/ir/exec/NLWrittenValues.cpp @@ -155,5 +155,6 @@ template std::optional NLWrittenValues::read NLWrittenValues::read(const Value& value, NLValueStore& values); template std::optional NLWrittenValues::read(const Value& value, NLValueStore& values); template std::optional NLWrittenValues::read(const Value& value, NLValueStore& values); +template std::optional NLWrittenValues::read(const Value& value, NLValueStore& values); } diff --git a/query/ir/lowering/DBLowering.cpp b/query/ir/lowering/DBLowering.cpp index b34a801cec..7f33aa9386 100644 --- a/query/ir/lowering/DBLowering.cpp +++ b/query/ir/lowering/DBLowering.cpp @@ -208,6 +208,7 @@ const std::unordered_map unaryFunctionL {"db.to_boolean", {&emitNLUnaryFunction, ResultNullability::AlwaysNullable}}, {"db.to_datetime", {&emitNLUnaryFunction, ResultNullability::AlwaysNullable}}, {"db.to_duration", {&emitNLUnaryFunction, ResultNullability::AlwaysNullable}}, + {"db.to_point", {&emitNLUnaryFunction, ResultNullability::AlwaysNullable}}, // A calendar field of an instant is always readable, so the field is null exactly // where the instant it was read off is diff --git a/samples/list-buffer/main.cpp b/samples/list-buffer/main.cpp index e9c94c5414..89294fe6f6 100644 --- a/samples/list-buffer/main.cpp +++ b/samples/list-buffer/main.cpp @@ -37,6 +37,11 @@ int main() { std::string formatted; Duration::format(formatted, value); + spdlog::info("element: {}", formatted); + } else if constexpr (std::is_same_v) { + std::string formatted; + Point::format(formatted, value); + spdlog::info("element: {}", formatted); } else { spdlog::info("element: {}", value); diff --git a/samples/mlir/main.cpp b/samples/mlir/main.cpp index 463c755600..de378adc1e 100644 --- a/samples/mlir/main.cpp +++ b/samples/mlir/main.cpp @@ -485,6 +485,14 @@ class PrintingSink : public NLOutputSink { } break; + case CellTag::Point: { + std::string formatted; + Point::format(formatted, element.getAs()); + + std::cout << formatted; + } + break; + case CellTag::MapView: case CellTag::Path: case CellTag::INVALID: diff --git a/storage/CMakeLists.txt b/storage/CMakeLists.txt index 25d5a7e0da..1e5a68350e 100644 --- a/storage/CMakeLists.txt +++ b/storage/CMakeLists.txt @@ -45,6 +45,7 @@ set(storage_sources metadata/PropertyTypeMap.cpp metadata/LabelMap.cpp metadata/LabelSetMap.cpp + metadata/Point.cpp views/EntityPropertyView.cpp views/GraphView.cpp diff --git a/storage/CellOrder.cpp b/storage/CellOrder.cpp index 2a48c33022..1994fbb7d1 100644 --- a/storage/CellOrder.cpp +++ b/storage/CellOrder.cpp @@ -30,6 +30,7 @@ enum class CellOrderClass { Edge, List, Path, + Point, DateTime, Duration, String, @@ -90,6 +91,10 @@ CellOrderClass orderClassOf(CellTag tag) { return CellOrderClass::Path; break; + case CellTag::Point: + return CellOrderClass::Point; + break; + case CellTag::INVALID: throw FatalException("Cannot order an untagged list element."); break; @@ -211,6 +216,10 @@ std::strong_ordering compareValues(const Lhs lhs, const Rhs rhs) { } break; + case CellOrderClass::Point: + return valueOf(lhs) <=> valueOf(rhs); + break; + case CellOrderClass::String: return valueOf(lhs) <=> valueOf(rhs); break; @@ -618,3 +627,12 @@ std::strong_ordering db::operator<=>(const CellView element, const types::Durati return element.getAs() <=> value; } + +std::strong_ordering db::operator<=>(const CellView element, const types::Point::Primitive value) { + const CellOrderClass elementClass = orderClassOf(element.getTag()); + if (elementClass != CellOrderClass::Point) { + return elementClass <=> CellOrderClass::Point; + } + + return element.getAs() <=> value; +} diff --git a/storage/CellOrder.h b/storage/CellOrder.h index 977d543979..ca6817dd8a 100644 --- a/storage/CellOrder.h +++ b/storage/CellOrder.h @@ -116,6 +116,11 @@ inline bool operator==(const CellView element, const types::Duration::Primitive && element.getAs() == value; } +inline bool operator==(const CellView element, const types::Point::Primitive value) { + return element.getTag() == CellTag::Point + && element.getAs() == value; +} + inline bool operator==(const CellView element, const types::Embedding::Primitive value) { return element.getTag() == CellTag::Embedding && EmbeddingEqual {}(element.getAs(), value); @@ -164,6 +169,7 @@ std::strong_ordering operator<=>(CellView element, types::String::Primitive valu std::strong_ordering operator<=>(CellView element, types::Bool::Primitive value); std::strong_ordering operator<=>(CellView element, types::DateTime::Primitive value); std::strong_ordering operator<=>(CellView element, types::Duration::Primitive value); +std::strong_ordering operator<=>(CellView element, types::Point::Primitive value); /** * @brief Compares an element of a @ref ListByteBuffer as <, <=, > and >= do. Unlike the order diff --git a/storage/CellTag.h b/storage/CellTag.h index b5df797a7e..49b4f5bbcb 100644 --- a/storage/CellTag.h +++ b/storage/CellTag.h @@ -21,6 +21,7 @@ enum class CellTag : uint8_t { MapView, Duration, Path, + Point, INVALID, }; @@ -39,5 +40,6 @@ static_assert(static_cast(CellTag::DateTime) == 10); static_assert(static_cast(CellTag::MapView) == 11); static_assert(static_cast(CellTag::Duration) == 12); static_assert(static_cast(CellTag::Path) == 13); +static_assert(static_cast(CellTag::Point) == 14); } diff --git a/storage/CellUtils.h b/storage/CellUtils.h index 852b059953..036903b6d7 100644 --- a/storage/CellUtils.h +++ b/storage/CellUtils.h @@ -61,6 +61,9 @@ struct CellTagDispatcher { case CellTag::Path: return executor.template operator()(view); break; + case CellTag::Point: + return executor.template operator()(view); + break; case CellTag::INVALID: break; @@ -143,4 +146,9 @@ struct TypeToCellTag { static constexpr CellTag Tag = CellTag::Path; }; +template <> +struct TypeToCellTag { + static constexpr CellTag Tag = CellTag::Point; +}; + } diff --git a/storage/DebugDump.cpp b/storage/DebugDump.cpp index d35bd812d7..d1d705922d 100644 --- a/storage/DebugDump.cpp +++ b/storage/DebugDump.cpp @@ -62,6 +62,13 @@ void DebugDump::dump(std::ostream& out, CellView view, bool isInList) { dumper.execute(dumpTyped, view); } +void DebugDump::dump(std::ostream& out, Point value) { + std::string text; + Point::format(text, value); + + out << text; +} + void DebugDump::dump(std::ostream& out, ListView list) { if (list.empty()) { out << "[]\n"; diff --git a/storage/DebugDump.h b/storage/DebugDump.h index 97a5db16ff..c7d2b26689 100644 --- a/storage/DebugDump.h +++ b/storage/DebugDump.h @@ -78,6 +78,8 @@ class DebugDump { static void dump(std::ostream& out, PathView path); + static void dump(std::ostream& out, Point value); + private: static void dumpImpl(std::ostream& out, uint64_t data); }; diff --git a/storage/QueryTypeName.cpp b/storage/QueryTypeName.cpp index c154077034..501f617ee8 100644 --- a/storage/QueryTypeName.cpp +++ b/storage/QueryTypeName.cpp @@ -46,6 +46,9 @@ std::string_view db::queryTypeName(CellTag tag) { case CellTag::Path: return "GraphPath"; break; + case CellTag::Point: + return "Point"; + break; case CellTag::INVALID: return "Invalid"; break; @@ -84,6 +87,9 @@ std::string_view db::queryTypeName(ValueType type) { case ValueType::Duration: return "Duration"; break; + case ValueType::Point: + return "Point"; + break; case ValueType::Invalid: case ValueType::_SIZE: return "Invalid"; diff --git a/storage/columns/AllowedKinds.h b/storage/columns/AllowedKinds.h index 408bd5800c..6072ef3f1d 100644 --- a/storage/columns/AllowedKinds.h +++ b/storage/columns/AllowedKinds.h @@ -106,6 +106,7 @@ struct SearchedValueKindPairs { typename SearchPairs::Pairs, typename SearchPairs::Pairs, typename SearchPairs::Pairs, + typename SearchPairs::Pairs, typename SearchPairs::Pairs, typename SearchPairs::Pairs, typename SearchPairs::Pairs, @@ -189,6 +190,7 @@ struct PairRestrictions { OptionalKindPairs::Pairs, OptionalKindPairs::Pairs, OptionalKindPairs::Pairs, + OptionalKindPairs::Pairs, OptionalKindPairs::Pairs, OptionalKindPairs::Pairs, @@ -200,6 +202,7 @@ struct PairRestrictions { CellKindPairs::Pairs, CellKindPairs::Pairs, CellKindPairs::Pairs, + CellKindPairs::Pairs, CellKindPairs::Pairs, OptionalKindPairs::Pairs, CellKindPairs::Pairs, @@ -730,6 +733,7 @@ struct OutputtedTypes { types::Embedding::Primitive, types::DateTime::Primitive, types::Duration::Primitive, + types::Point::Primitive, std::optional, std::optional, std::optional, @@ -739,6 +743,7 @@ struct OutputtedTypes { std::optional, std::optional, std::optional, + std::optional, PropertyNull, std::optional, @@ -823,6 +828,7 @@ struct WriteProcessorPropertyTypes { types::Embedding::Primitive, types::DateTime::Primitive, types::Duration::Primitive, + types::Point::Primitive, ListView, MapView, @@ -834,6 +840,7 @@ struct WriteProcessorPropertyTypes { std::optional, std::optional, std::optional, + std::optional, std::optional, std::optional, @@ -853,6 +860,7 @@ struct WriteProcessorPropertyTypes { types::Embedding::Primitive, types::DateTime::Primitive, types::Duration::Primitive, + types::Point::Primitive, ListView, MapView, @@ -864,6 +872,7 @@ struct WriteProcessorPropertyTypes { std::optional, std::optional, std::optional, + std::optional, std::optional, std::string // For LOAD CSV inputs @@ -1003,6 +1012,9 @@ struct CartesianProductKinds { types::Duration::Primitive, std::optional, + types::Point::Primitive, + std::optional, + // Entities and metadata NodeID, EdgeID, diff --git a/storage/columns/BinaryOperators.h b/storage/columns/BinaryOperators.h index 203e3ed405..25fd3c7d32 100644 --- a/storage/columns/BinaryOperators.h +++ b/storage/columns/BinaryOperators.h @@ -184,6 +184,7 @@ inline std::optional cellNumber(const CellView cell) { case CellTag::MapView: case CellTag::Duration: case CellTag::Path: + case CellTag::Point: case CellTag::INVALID: return std::nullopt; break; diff --git a/storage/columns/ColumnDispatcher.h b/storage/columns/ColumnDispatcher.h index 7760796c05..6b390496fb 100644 --- a/storage/columns/ColumnDispatcher.h +++ b/storage/columns/ColumnDispatcher.h @@ -39,6 +39,7 @@ using AllValueTypes = TypeList< types::Embedding::Primitive, types::DateTime::Primitive, types::Duration::Primitive, + types::Point::Primitive, std::optional, std::optional, std::optional, @@ -47,6 +48,7 @@ using AllValueTypes = TypeList< std::optional, std::optional, std::optional, + std::optional, std::optional, std::optional, std::string, diff --git a/storage/columns/Functions.h b/storage/columns/Functions.h index 666826a4e1..c0015e4cb3 100644 --- a/storage/columns/Functions.h +++ b/storage/columns/Functions.h @@ -419,6 +419,25 @@ class MapToDurationFunction { } }; +// point() over a map, the only value it reads. A map holding a null reads as null. +class ToPointFunction { +public: + using ArgType = MapView; + using ResultType = std::optional; + using TaggedCounterpart = TaggedConversionFunction; + + static constexpr std::string_view NAME = "point"; + + ResultType operator()(const MapView map) const { + types::Point::Primitive value; + if (!Point::fromMap(map, value)) { + return std::nullopt; + } + + return value; + } +}; + class MapToDateTimeFunction { public: using ArgType = MapView; @@ -600,20 +619,20 @@ class ToBoolFromValueFunction { } }; -template -class ToStringFromTemporalFunction { +template +class ToStringFromFormattedFunction { public: - using ArgType = Temporal; + using ArgType = Formatted; using ResultType = types::String::Primitive; - explicit ToStringFromTemporalFunction(StringBuffer* stringBuffer) + explicit ToStringFromFormattedFunction(StringBuffer* stringBuffer) : _stringBuffer(stringBuffer) { } - ResultType operator()(const Temporal value) { + ResultType operator()(const Formatted value) { _formatted.clear(); - Temporal::format(_formatted, value); + Formatted::format(_formatted, value); return _stringBuffer->insert(_formatted); } @@ -632,9 +651,11 @@ concept ConvertibleToBool = std::is_same_v || std::is_same_v || std::is_same_v; +// A value whose text is the one its type's format writes template -concept TemporalValue = std::is_same_v - || std::is_same_v; +concept FormattedValue = std::is_same_v + || std::is_same_v + || std::is_same_v; // The element a column of the given type converts, with any optional unwrapped: what a // conversion is applied to row by row. @@ -668,9 +689,9 @@ struct ConversionFunctorFor { using Type = ToStringFromValueFunction; }; -template +template struct ConversionFunctorFor { - using Type = ToStringFromTemporalFunction; + using Type = ToStringFromFormattedFunction; }; template <> @@ -769,7 +790,8 @@ class TaggedConversionFunction { _fromString(stringBuffer), _fromDateTime(stringBuffer), _fromDuration(stringBuffer), - _fromMap(stringBuffer) + _fromMap(stringBuffer), + _fromPoint(stringBuffer) { } @@ -801,6 +823,9 @@ class TaggedConversionFunction { case CellTag::MapView: return convert(cell, _fromMap); break; + case CellTag::Point: + return convert(cell, _fromPoint); + break; case CellTag::Null: return std::nullopt; break; @@ -832,6 +857,7 @@ class TaggedConversionFunction { ConversionRule> _fromDateTime; ConversionRule> _fromDuration; ConversionRule> _fromMap; + ConversionRule> _fromPoint; template ResultType convert(const ArgType cell, ConversionRule>& rule) { diff --git a/storage/columns/InternalKind.h b/storage/columns/InternalKind.h index b46d9705d6..fa2d2a9aab 100644 --- a/storage/columns/InternalKind.h +++ b/storage/columns/InternalKind.h @@ -75,6 +75,7 @@ class InternalKind { types::Embedding::Primitive, types::DateTime::Primitive, types::Duration::Primitive, + types::Point::Primitive, std::optional, std::optional, std::optional, @@ -91,6 +92,7 @@ class InternalKind { std::optional, std::optional, std::optional, + std::optional, std::optional, std::optional, std::optional, diff --git a/storage/comparators/PropertyContainerComparator.h b/storage/comparators/PropertyContainerComparator.h index f2d71f99e1..fff345cece 100644 --- a/storage/comparators/PropertyContainerComparator.h +++ b/storage/comparators/PropertyContainerComparator.h @@ -184,6 +184,15 @@ class PropertyContainerComparator { break; } + case ValueType::Point: { + if (!TypedPropertyContainerComparator::same( + a->cast(), + b->cast())) { + return false; + } + break; + } + case ValueType::Map: { if (!TypedPropertyContainerComparator::same(a->cast(), b->cast())) { return false; diff --git a/storage/dump/DataPartDumper.cpp b/storage/dump/DataPartDumper.cpp index c8e3ac9433..dba2f69f5e 100644 --- a/storage/dump/DataPartDumper.cpp +++ b/storage/dump/DataPartDumper.cpp @@ -89,6 +89,13 @@ DumpResult dumpProperties(fs::FilePageWriter& writer, PropertyContainer* c } break; } + case ValueType::Point: { + TrivialPropertyContainerDumper dumper(writer); + if (auto res = dumper.dump(container->cast()); !res) { + return res.get_unexpected(); + } + break; + } case ValueType::Map: { MapPropertyContainerDumper dumper(writer); if (auto res = dumper.dump(container->cast()); !res) { diff --git a/storage/dump/DataPartLoader.cpp b/storage/dump/DataPartLoader.cpp index bbcd4dc532..ff29b8d4b9 100644 --- a/storage/dump/DataPartLoader.cpp +++ b/storage/dump/DataPartLoader.cpp @@ -206,6 +206,8 @@ DumpResult> DataPartLoader::load(DataPartID partID, manager._dateTimes.emplace(pt->_id, static_cast(ptr)); } else if constexpr (std::is_same_v) { manager._durations.emplace(pt->_id, static_cast(ptr)); + } else if constexpr (std::is_same_v) { + manager._points.emplace(pt->_id, static_cast(ptr)); } else { bioassert(false, "Missing trivial property type"); } @@ -301,6 +303,12 @@ DumpResult> DataPartLoader::load(DataPartID partID, } break; } + case ValueType::Point: { + if (auto res = storeTrivialContainer.operator()(manager); !res) { + return res.get_unexpected(); + } + break; + } case ValueType::Map: { MapPropertyContainerLoader loader(reader.value()); diff --git a/storage/dump/PropertyContainerDumpConstants.h b/storage/dump/PropertyContainerDumpConstants.h index e517af3dc0..d1511e8340 100644 --- a/storage/dump/PropertyContainerDumpConstants.h +++ b/storage/dump/PropertyContainerDumpConstants.h @@ -5,6 +5,7 @@ #include #include "ID.h" +#include "metadata/Point.h" #include "metadata/SupportedType.h" #include "DumpConfig.h" #include "StringBucket.h" @@ -19,6 +20,9 @@ static_assert(std::is_trivially_copyable_v, "Dump format writes Entity static_assert(sizeof(StringBucket::StringLimits) == 2 * sizeof(uint32_t), "Dump format writes StringLimits object bytes as packed uint32 pairs"); static_assert(offsetof(StringBucket::StringLimits, _offset) == 0, "Loaders read StringLimits as offset then count"); static_assert(offsetof(StringBucket::StringLimits, _count) == sizeof(uint32_t), "Loaders read StringLimits as offset then count"); +static_assert(sizeof(Point) == 4 * sizeof(double), "Dump format writes a Point as x, y, z, then its CRS ordinal and padding"); +static_assert(std::is_trivially_copyable_v, "Dump format writes Point object bytes"); +static_assert(std::is_trivially_default_constructible_v, "Loaders read Point values straight out of the mapped page"); template class TrivialPropertyContainerDumpConstants { diff --git a/storage/indexes/IndexManager.cpp b/storage/indexes/IndexManager.cpp index 7f8115244d..2faf42474a 100644 --- a/storage/indexes/IndexManager.cpp +++ b/storage/indexes/IndexManager.cpp @@ -56,6 +56,7 @@ template WeakArc IndexManager::createNodeIndex(std::str template WeakArc IndexManager::createNodeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createNodeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createNodeIndex(std::string_view indexName, PropertyTypeID ptID); +template WeakArc IndexManager::createNodeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createNodeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createEdgeIndex(std::string_view indexName, PropertyTypeID ptID); @@ -67,5 +68,6 @@ template WeakArc IndexManager::createEdgeIndex(std::str template WeakArc IndexManager::createEdgeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createEdgeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createEdgeIndex(std::string_view indexName, PropertyTypeID ptID); +template WeakArc IndexManager::createEdgeIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc IndexManager::createEdgeIndex(std::string_view indexName, PropertyTypeID ptID); } diff --git a/storage/indexes/PropertyHashIndex.cpp b/storage/indexes/PropertyHashIndex.cpp index 9df3a6b99e..4080d26ffa 100644 --- a/storage/indexes/PropertyHashIndex.cpp +++ b/storage/indexes/PropertyHashIndex.cpp @@ -220,6 +220,7 @@ template class PropertyHashIndex; template class PropertyHashIndex; template class PropertyHashIndex; template class PropertyHashIndex; +template class PropertyHashIndex; template class PropertyHashIndex; template class PropertyHashIndex; @@ -231,5 +232,6 @@ template class PropertyHashIndex; template class PropertyHashIndex; template class PropertyHashIndex; template class PropertyHashIndex; +template class PropertyHashIndex; template class PropertyHashIndex; } diff --git a/storage/iterators/GetPropertiesWithNullIterator.cpp b/storage/iterators/GetPropertiesWithNullIterator.cpp index 973d2cfa57..b7956cae7a 100644 --- a/storage/iterators/GetPropertiesWithNullIterator.cpp +++ b/storage/iterators/GetPropertiesWithNullIterator.cpp @@ -118,6 +118,7 @@ template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; +template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; @@ -128,6 +129,7 @@ template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; +template class GetPropertiesIteratorWithNull; template class GetPropertiesIteratorWithNull; } @@ -165,6 +167,7 @@ template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; +template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; @@ -175,6 +178,7 @@ template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; +template class GetPropertiesWithNullChunkWriter; template class GetPropertiesWithNullChunkWriter; } diff --git a/storage/iterators/ScanEdgePropertiesIterator.cpp b/storage/iterators/ScanEdgePropertiesIterator.cpp index 4287655392..de5762b521 100644 --- a/storage/iterators/ScanEdgePropertiesIterator.cpp +++ b/storage/iterators/ScanEdgePropertiesIterator.cpp @@ -230,6 +230,7 @@ template class ScanEdgePropertiesIterator; template class ScanEdgePropertiesIterator; template class ScanEdgePropertiesIterator; template class ScanEdgePropertiesIterator; +template class ScanEdgePropertiesIterator; template class ScanEdgePropertiesIterator; template class ScanEdgePropertiesChunkWriter; @@ -241,6 +242,7 @@ template class ScanEdgePropertiesChunkWriter; template class ScanEdgePropertiesChunkWriter; template class ScanEdgePropertiesChunkWriter; template class ScanEdgePropertiesChunkWriter; +template class ScanEdgePropertiesChunkWriter; template class ScanEdgePropertiesChunkWriter; } diff --git a/storage/iterators/ScanNodePropertiesByLabelIterator.cpp b/storage/iterators/ScanNodePropertiesByLabelIterator.cpp index 68d2665bb4..41dff3cd7f 100644 --- a/storage/iterators/ScanNodePropertiesByLabelIterator.cpp +++ b/storage/iterators/ScanNodePropertiesByLabelIterator.cpp @@ -269,6 +269,7 @@ template class ScanNodePropertiesByLabelIterator; template class ScanNodePropertiesByLabelIterator; template class ScanNodePropertiesByLabelIterator; template class ScanNodePropertiesByLabelIterator; +template class ScanNodePropertiesByLabelIterator; template class ScanNodePropertiesByLabelIterator; template class ScanNodePropertiesByLabelChunkWriter; @@ -280,6 +281,7 @@ template class ScanNodePropertiesByLabelChunkWriter; template class ScanNodePropertiesByLabelChunkWriter; template class ScanNodePropertiesByLabelChunkWriter; template class ScanNodePropertiesByLabelChunkWriter; +template class ScanNodePropertiesByLabelChunkWriter; template class ScanNodePropertiesByLabelChunkWriter; } diff --git a/storage/iterators/ScanNodePropertiesIterator.cpp b/storage/iterators/ScanNodePropertiesIterator.cpp index 6e1e9f6cf6..9b5d536a5b 100644 --- a/storage/iterators/ScanNodePropertiesIterator.cpp +++ b/storage/iterators/ScanNodePropertiesIterator.cpp @@ -228,6 +228,7 @@ template class ScanNodePropertiesIterator; template class ScanNodePropertiesIterator; template class ScanNodePropertiesIterator; template class ScanNodePropertiesIterator; +template class ScanNodePropertiesIterator; template class ScanNodePropertiesIterator; template class ScanNodePropertiesChunkWriter; @@ -239,6 +240,7 @@ template class ScanNodePropertiesChunkWriter; template class ScanNodePropertiesChunkWriter; template class ScanNodePropertiesChunkWriter; template class ScanNodePropertiesChunkWriter; +template class ScanNodePropertiesChunkWriter; template class ScanNodePropertiesChunkWriter; } diff --git a/storage/list/EncodedList.cpp b/storage/list/EncodedList.cpp index dc4337b953..672db9723a 100644 --- a/storage/list/EncodedList.cpp +++ b/storage/list/EncodedList.cpp @@ -61,6 +61,9 @@ void encodeElement(CellView element, std::vector& out) { case CellTag::Duration: appendValue(out, element.getAs()); break; + case CellTag::Point: + appendValue(out, element.getAs()); + break; case CellTag::Null: break; case CellTag::String: { @@ -190,6 +193,9 @@ class ListDecoder { case CellTag::Duration: return read(); break; + case CellTag::Point: + return read(); + break; case CellTag::Null: return PropertyNull {}; break; diff --git a/storage/list/ListBuffer.h b/storage/list/ListBuffer.h index 5d63936ec9..d9ffade789 100644 --- a/storage/list/ListBuffer.h +++ b/storage/list/ListBuffer.h @@ -28,7 +28,7 @@ using ListableTypesImpl = db::types::Double::Primitive, db::types::String::Primitive, db::types::Bool::Primitive, db::types::Embedding::Primitive, db::ListView, db::PropertyNull, db::NodeID, db::EdgeID, db::types::DateTime::Primitive, db::MapView, - db::types::Duration::Primitive, db::PathView>; + db::types::Duration::Primitive, db::PathView, db::types::Point::Primitive>; } namespace db { diff --git a/storage/list/ListByteBuffer.cpp b/storage/list/ListByteBuffer.cpp index e78a832434..d4fafe1e19 100644 --- a/storage/list/ListByteBuffer.cpp +++ b/storage/list/ListByteBuffer.cpp @@ -127,4 +127,5 @@ template CellView ListByteBuffer<>::write(CellTag, const types::DateTime::Primit template CellView ListByteBuffer<>::write(CellTag, const types::Duration::Primitive&); template CellView ListByteBuffer<>::write(CellTag, const MapView&); template CellView ListByteBuffer<>::write(CellTag, const PathView&); +template CellView ListByteBuffer<>::write(CellTag, const types::Point::Primitive&); } diff --git a/storage/list/ListHash.cpp b/storage/list/ListHash.cpp index 22a01bc8fe..1cfc64006d 100644 --- a/storage/list/ListHash.cpp +++ b/storage/list/ListHash.cpp @@ -68,6 +68,9 @@ size_t db::hashCell(CellView element) { case CellTag::Duration: return combine(seed, hashValue(element.getAs())); break; + case CellTag::Point: + return combine(seed, hashValue(element.getAs())); + break; case CellTag::Embedding: { size_t hash = seed; for (const float value : element.getAs()) { diff --git a/storage/map/EncodedMap.cpp b/storage/map/EncodedMap.cpp index 7eca97a84a..6a35e8af71 100644 --- a/storage/map/EncodedMap.cpp +++ b/storage/map/EncodedMap.cpp @@ -61,6 +61,9 @@ void encodeValue(MapEntryView entry, std::vector& out) { case CellTag::Duration: appendValue(out, entry.getValue().getAs()); break; + case CellTag::Point: + appendValue(out, entry.getValue().getAs()); + break; case CellTag::Null: break; case CellTag::String: { @@ -209,6 +212,9 @@ class MapDecoder { case CellTag::Duration: return read(); break; + case CellTag::Point: + return read(); + break; case CellTag::Null: return PropertyNull {}; break; diff --git a/storage/map/MapBuffer.h b/storage/map/MapBuffer.h index ebe61de36f..75c800a2c4 100644 --- a/storage/map/MapBuffer.h +++ b/storage/map/MapBuffer.h @@ -23,7 +23,8 @@ using MappableTypesImpl = db::types::Double::Primitive, db::types::Bool::Primitive, db::types::String::Primitive, db::types::Embedding::Primitive, db::ListView, db::MapView, db::PropertyNull, db::NodeID, db::EdgeID, - db::types::DateTime::Primitive, db::types::Duration::Primitive, db::PathView>; + db::types::DateTime::Primitive, db::types::Duration::Primitive, db::PathView, + db::types::Point::Primitive>; } namespace db { diff --git a/storage/map/MapByteBuffer.cpp b/storage/map/MapByteBuffer.cpp index ff25e43e38..823ec23e77 100644 --- a/storage/map/MapByteBuffer.cpp +++ b/storage/map/MapByteBuffer.cpp @@ -134,4 +134,5 @@ template MapEntryView MapByteBuffer<>::write(std::string_view, CellTag, const Ed template MapEntryView MapByteBuffer<>::write(std::string_view, CellTag, const types::DateTime::Primitive&); template MapEntryView MapByteBuffer<>::write(std::string_view, CellTag, const types::Duration::Primitive&); template MapEntryView MapByteBuffer<>::write(std::string_view, CellTag, const PathView&); +template MapEntryView MapByteBuffer<>::write(std::string_view, CellTag, const types::Point::Primitive&); } diff --git a/storage/map/MapHash.cpp b/storage/map/MapHash.cpp index 23dc058176..f906967929 100644 --- a/storage/map/MapHash.cpp +++ b/storage/map/MapHash.cpp @@ -63,6 +63,9 @@ size_t hashMapValue(MapEntryView entry) { case CellTag::Duration: return combine(seed, hashValue(entry.getValue().getAs())); break; + case CellTag::Point: + return combine(seed, hashValue(entry.getValue().getAs())); + break; case CellTag::Embedding: { size_t hash = seed; for (const float value : entry.getValue().getAs()) { @@ -129,6 +132,9 @@ bool sameMapValue(MapEntryView lhs, MapEntryView rhs) { case CellTag::Duration: return lhs.getValue().getAs() == rhs.getValue().getAs(); break; + case CellTag::Point: + return lhs.getValue().getAs() == rhs.getValue().getAs(); + break; case CellTag::Embedding: { const types::Embedding::Primitive lhsValues = lhs.getValue().getAs(); const types::Embedding::Primitive rhsValues = rhs.getValue().getAs(); diff --git a/storage/map/MapWriteCursor.cpp b/storage/map/MapWriteCursor.cpp index 8a967391ab..fb5bf41fab 100644 --- a/storage/map/MapWriteCursor.cpp +++ b/storage/map/MapWriteCursor.cpp @@ -72,4 +72,5 @@ template MapEntryView MapWriteCursor::writeValue(CellTag, const NodeID&); template MapEntryView MapWriteCursor::writeValue(CellTag, const EdgeID&); template MapEntryView MapWriteCursor::writeValue(CellTag, const types::DateTime::Primitive&); template MapEntryView MapWriteCursor::writeValue(CellTag, const types::Duration::Primitive&); +template MapEntryView MapWriteCursor::writeValue(CellTag, const types::Point::Primitive&); } diff --git a/storage/metadata/Point.cpp b/storage/metadata/Point.cpp new file mode 100644 index 0000000000..85d7c8cb1f --- /dev/null +++ b/storage/metadata/Point.cpp @@ -0,0 +1,541 @@ +#include "Point.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "QueryTypeName.h" +#include "TuringException.h" +#include "columns/ValueText.h" +#include "map/MapView.h" + +using namespace db; + +namespace { + +enum class PointKey : uint8_t { + X = 0, + Y, + Z, + Longitude, + Latitude, + Height, + CRS, + SRID, + _SIZE, +}; + +struct PointKeys { + std::array(PointKey::_SIZE)> _coordinates {}; + std::array(PointKey::_SIZE)> _present {}; + std::string_view _crsName; + int64_t _srid {0}; + + bool has(PointKey key) const { return _present[static_cast(key)]; } + double get(PointKey key) const { return _coordinates[static_cast(key)]; } +}; + +bool equalsIgnoringCase(std::string_view lhs, std::string_view rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + + for (size_t index = 0; index < lhs.size(); index++) { + const int lhsCharacter = tolower(static_cast(lhs[index])); + const int rhsCharacter = tolower(static_cast(rhs[index])); + + if (lhsCharacter != rhsCharacter) { + return false; + } + } + + return true; +} + +bool keyNamed(std::string_view name, PointKey& key) { + static constexpr std::array, 8> keys = {{ + {"x", PointKey::X }, + {"y", PointKey::Y }, + {"z", PointKey::Z }, + {"longitude", PointKey::Longitude}, + {"latitude", PointKey::Latitude }, + {"height", PointKey::Height }, + {"crs", PointKey::CRS }, + {"srid", PointKey::SRID }, + }}; + + for (const auto& [keyName, namedKey] : keys) { + if (equalsIgnoringCase(keyName, name)) { + key = namedKey; + return true; + } + } + + return false; +} + +bool crsNamed(std::string_view name, PointCRS& crs) { + static constexpr std::array systems = { + PointCRS::Cartesian, + PointCRS::Cartesian3D, + PointCRS::WGS84, + PointCRS::WGS84_3D, + }; + + for (const PointCRS system : systems) { + if (equalsIgnoringCase(Point::getCRSName(system), name)) { + crs = system; + return true; + } + } + + return false; +} + +bool crsOfSRID(int64_t srid, PointCRS& crs) { + if (srid == 7203) { + crs = PointCRS::Cartesian; + } else if (srid == 9157) { + crs = PointCRS::Cartesian3D; + } else if (srid == 4326) { + crs = PointCRS::WGS84; + } else if (srid == 4979) { + crs = PointCRS::WGS84_3D; + } else { + return false; + } + + return true; +} + +double coordinateOf(std::string_view key, CellView value) { + const CellTag tag = value.getTag(); + + if (tag == CellTag::Int) { + return static_cast(value.getAs()); + } else if (tag == CellTag::UInt) { + return static_cast(value.getAs()); + } else if (tag == CellTag::Double) { + return value.getAs(); + } + + throw TuringException(fmt::format("point() reads a number for '{}', not '{}'", key, queryTypeName(tag))); +} + +// Reads the keys point() knows; it ignores any other, as Neo4j does. False where a value is null. +bool readKeys(const MapView& map, PointKeys& keys) { + bool holdsANull = false; + + for (const MapEntryView entry : map) { + const std::string_view name = entry.getKey(); + const CellView value = entry.getValue(); + const CellTag tag = value.getTag(); + + if (tag == CellTag::Null) { + holdsANull = true; + continue; + } + + PointKey key {PointKey::X}; + if (!keyNamed(name, key)) { + continue; + } + + const size_t index = static_cast(key); + if (keys._present[index]) { + throw TuringException(fmt::format("point() reads the key '{}' twice", name)); + } + + keys._present[index] = true; + + if (key == PointKey::CRS) { + if (tag != CellTag::String) { + throw TuringException(fmt::format("point() reads a string for 'crs', not '{}'", queryTypeName(tag))); + } + + keys._crsName = value.getAs(); + } else if (key == PointKey::SRID) { + if (tag == CellTag::Int) { + keys._srid = value.getAs(); + } else if (tag == CellTag::UInt) { + const uint64_t srid = value.getAs(); + keys._srid = srid > static_cast(INT64_MAX) ? -1 : static_cast(srid); + } else { + throw TuringException(fmt::format("point() reads an integer for 'srid', not '{}'", queryTypeName(tag))); + } + } else { + keys._coordinates[index] = coordinateOf(name, value); + } + } + + return !holdsANull; +} + +// The CRS the map names with 'crs' or 'srid', false where it names none +bool namedCRS(const PointKeys& keys, PointCRS& crs) { + const bool namesACRS = keys.has(PointKey::CRS); + const bool namesAnSRID = keys.has(PointKey::SRID); + + if (namesACRS && namesAnSRID) { + throw TuringException("point() takes a 'crs' or an 'srid', not both"); + } else if (namesACRS) { + if (!crsNamed(keys._crsName, crs)) { + throw TuringException(fmt::format("Unknown coordinate reference system: '{}'", keys._crsName)); + } + + return true; + } else if (namesAnSRID) { + if (!crsOfSRID(keys._srid, crs)) { + throw TuringException(fmt::format("Unknown coordinate reference system SRID: {}", keys._srid)); + } + + return true; + } + + return false; +} + +std::string_view requiredKeys(PointCRS crs) { + switch (crs) { + case PointCRS::Cartesian: + return "'x' and 'y'"; + break; + case PointCRS::Cartesian3D: + return "'x', 'y' and 'z'"; + break; + case PointCRS::WGS84: + return "'longitude' and 'latitude'"; + break; + case PointCRS::WGS84_3D: + return "'longitude', 'latitude' and 'height'"; + break; + } + + return ""; +} + +double finiteCoordinate(double coordinate) { + if (!isfinite(coordinate)) { + throw TuringException("A point cannot have a non-finite coordinate"); + } + + return coordinate == 0.0 ? 0.0 : coordinate; +} + +double wrappedLongitude(double longitude) { + if (longitude <= 180.0 && longitude >= -180.0) { + return longitude; + } + + double wrapped = fmod(longitude, 360.0); + if (wrapped > 180.0) { + wrapped -= 360.0; + } else if (wrapped < -180.0) { + wrapped += 360.0; + } + + return wrapped == 0.0 ? 0.0 : wrapped; +} + +std::strong_ordering compareCoordinates(double lhs, double rhs) { + if (lhs < rhs) { + return std::strong_ordering::less; + } else if (lhs > rhs) { + return std::strong_ordering::greater; + } + + return std::strong_ordering::equal; +} + +bool withinRange(double value, double lower, double upper) { + return value >= lower && value <= upper; +} + +} + +Point::Point(PointCRS crs, double x, double y, double z) + : _x(finiteCoordinate(x)), + _y(finiteCoordinate(y)), + _z(getDimension(crs) == 3 ? finiteCoordinate(z) : 0.0), + _crs(crs), + _padding {} +{ + if (!isGeographic(crs)) { + return; + } + + if (!withinRange(_y, -90.0, 90.0)) { + throw TuringException(fmt::format("The latitude of a {} point lies in [-90, 90], not {}", getCRSName(crs), _y)); + } + + _x = wrappedLongitude(_x); +} + +bool Point::operator==(const Point& other) const { + return _crs == other._crs && _x == other._x && _y == other._y && _z == other._z; +} + +std::strong_ordering Point::operator<=>(const Point& other) const { + const std::strong_ordering bySRID = getSRID(_crs) <=> getSRID(other._crs); + if (bySRID != 0) { + return bySRID; + } + + const std::strong_ordering byX = compareCoordinates(_x, other._x); + if (byX != 0) { + return byX; + } + + const std::strong_ordering byY = compareCoordinates(_y, other._y); + if (byY != 0) { + return byY; + } + + return compareCoordinates(_z, other._z); +} + +size_t Point::getDimension(PointCRS crs) { + switch (crs) { + case PointCRS::Cartesian: + case PointCRS::WGS84: + return 2; + break; + case PointCRS::Cartesian3D: + case PointCRS::WGS84_3D: + return 3; + break; + } + + return 2; +} + +bool Point::isGeographic(PointCRS crs) { + return crs == PointCRS::WGS84 || crs == PointCRS::WGS84_3D; +} + +int64_t Point::getSRID(PointCRS crs) { + switch (crs) { + case PointCRS::Cartesian: + return 7203; + break; + case PointCRS::Cartesian3D: + return 9157; + break; + case PointCRS::WGS84: + return 4326; + break; + case PointCRS::WGS84_3D: + return 4979; + break; + } + + return 0; +} + +std::string_view Point::getCRSName(PointCRS crs) { + switch (crs) { + case PointCRS::Cartesian: + return "cartesian"; + break; + case PointCRS::Cartesian3D: + return "cartesian-3d"; + break; + case PointCRS::WGS84: + return "wgs-84"; + break; + case PointCRS::WGS84_3D: + return "wgs-84-3d"; + break; + } + + return ""; +} + +bool Point::partNamed(std::string_view name, PointPart& part) { + static const std::unordered_map parts = { + {"x", PointPart::X }, + {"y", PointPart::Y }, + {"z", PointPart::Z }, + {"longitude", PointPart::Longitude}, + {"latitude", PointPart::Latitude }, + {"height", PointPart::Height }, + {"crs", PointPart::CRS }, + {"srid", PointPart::SRID }, + }; + + const auto it = parts.find(name); + if (it == parts.end()) { + return false; + } + + part = it->second; + + return true; +} + +std::string_view Point::getPartName(PointPart part) { + switch (part) { + case PointPart::X: + return "x"; + break; + case PointPart::Y: + return "y"; + break; + case PointPart::Z: + return "z"; + break; + case PointPart::Longitude: + return "longitude"; + break; + case PointPart::Latitude: + return "latitude"; + break; + case PointPart::Height: + return "height"; + break; + case PointPart::CRS: + return "crs"; + break; + case PointPart::SRID: + return "srid"; + break; + } + + return ""; +} + +double Point::coordinate(Point value, PointPart part) { + const PointCRS crs = value.getCRS(); + const bool isThreeDimensional = getDimension(crs) == 3; + const bool namesAGeographicPart = part == PointPart::Longitude + || part == PointPart::Latitude + || part == PointPart::Height; + const bool namesAThirdCoordinate = part == PointPart::Z || part == PointPart::Height; + + const bool missesThePart = (namesAGeographicPart && !isGeographic(crs)) + || (namesAThirdCoordinate && !isThreeDimensional); + + if (missesThePart) { + std::string text; + format(text, value); + throw TuringException(fmt::format("The point {} has no {}", text, getPartName(part))); + } + + switch (part) { + case PointPart::X: + case PointPart::Longitude: + return value.getX(); + break; + case PointPart::Y: + case PointPart::Latitude: + return value.getY(); + break; + case PointPart::Z: + case PointPart::Height: + return value.getZ(); + break; + case PointPart::CRS: + case PointPart::SRID: + throw TuringException("The crs and the srid of a point are not coordinates"); + break; + } + + return 0.0; +} + +bool Point::fromMap(const MapView& map, Point& value) { + PointKeys keys; + if (!readKeys(map, keys)) { + return false; + } + + PointCRS crs {PointCRS::Cartesian}; + const bool namesACRS = namedCRS(keys, crs); + + const bool hasXY = keys.has(PointKey::X) && keys.has(PointKey::Y); + const bool hasLongitudeLatitude = keys.has(PointKey::Longitude) && keys.has(PointKey::Latitude); + + double x = 0.0; + double y = 0.0; + double z = 0.0; + size_t dimension = 2; + + if (hasXY) { + x = keys.get(PointKey::X); + y = keys.get(PointKey::Y); + + if (keys.has(PointKey::Z)) { + z = keys.get(PointKey::Z); + dimension = 3; + } + + if (!namesACRS) { + crs = dimension == 3 ? PointCRS::Cartesian3D : PointCRS::Cartesian; + } + } else if (hasLongitudeLatitude) { + x = keys.get(PointKey::Longitude); + y = keys.get(PointKey::Latitude); + + if (keys.has(PointKey::Z)) { + z = keys.get(PointKey::Z); + dimension = 3; + } else if (keys.has(PointKey::Height)) { + z = keys.get(PointKey::Height); + dimension = 3; + } + + if (!namesACRS) { + crs = dimension == 3 ? PointCRS::WGS84_3D : PointCRS::WGS84; + } else if (!isGeographic(crs)) { + throw TuringException(fmt::format("A point given by longitude and latitude takes a geographic CRS, not '{}'", + getCRSName(crs))); + } + } else if (namesACRS) { + throw TuringException(fmt::format("A {} point needs {}", getCRSName(crs), requiredKeys(crs))); + } else { + throw TuringException("point() needs 'x' and 'y', or 'longitude' and 'latitude'"); + } + + if (getDimension(crs) != dimension) { + throw TuringException(fmt::format("A {} point has {} coordinates, not {}", + getCRSName(crs), + getDimension(crs), + dimension)); + } + + value = Point(crs, x, y, z); + + return true; +} + +void Point::format(std::string& out, Point value) { + ValueTextScratch scratch; + const auto output = std::back_inserter(out); + + fmt::format_to(output, "point({{x: {}", valueTextInto(value.getX(), scratch)); + fmt::format_to(output, ", y: {}", valueTextInto(value.getY(), scratch)); + + if (getDimension(value.getCRS()) == 3) { + fmt::format_to(output, ", z: {}", valueTextInto(value.getZ(), scratch)); + } + + fmt::format_to(output, ", crs: '{}'}})", getCRSName(value.getCRS())); +} + +size_t std::hash::operator()(const Point& value) const noexcept { + const std::hash hasher; + + size_t hash = static_cast(value.getCRS()); + hash = hash * 31 + hasher(value.getX()); + hash = hash * 31 + hasher(value.getY()); + hash = hash * 31 + hasher(value.getZ()); + + return hash; +} diff --git a/storage/metadata/Point.h b/storage/metadata/Point.h new file mode 100644 index 0000000000..c8ed24c3a2 --- /dev/null +++ b/storage/metadata/Point.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace db { + +class MapView; + +// A coordinate reference system, written to disk as its ordinal inside every point +enum class PointCRS : uint8_t { + Cartesian = 0, + Cartesian3D, + WGS84, + WGS84_3D, +}; + +// A field of a point. Longitude, latitude and height name x, y and z in a geographic CRS. +enum class PointPart : uint8_t { + X, + Y, + Z, + Longitude, + Latitude, + Height, + CRS, + SRID, +}; + +// A location in one of the coordinate reference systems Cypher names. A 2D point holds a z +// of 0, and no coordinate is -0.0, so two equal points are equal in every byte. +class Point { +public: + Point() = default; + + // Raises on a non-finite coordinate and on a latitude outside [-90, 90]. A longitude + // outside [-180, 180] is wrapped into it. + Point(PointCRS crs, double x, double y, double z); + + PointCRS getCRS() const { return _crs; } + double getX() const { return _x; } + double getY() const { return _y; } + double getZ() const { return _z; } + + bool operator==(const Point& other) const; + + // The order ORDER BY sorts points in: by SRID, then by coordinates + std::strong_ordering operator<=>(const Point& other) const; + + static size_t getDimension(PointCRS crs); + + static bool isGeographic(PointCRS crs); + + static int64_t getSRID(PointCRS crs); + + static std::string_view getCRSName(PointCRS crs); + + static bool partNamed(std::string_view name, PointPart& part); + + static std::string_view getPartName(PointPart part); + + // A coordinate the point has. Raises for z of a 2D point, and for longitude, latitude + // and height of a cartesian one. + static double coordinate(Point value, PointPart part); + + // point() of a map. False where the map holds a null. + static bool fromMap(const MapView& map, Point& value); + + static void format(std::string& out, Point value); + +private: + // No initializers: the datapart loader reads a trivial property's values straight out of + // the mapped page, which needs a trivial default constructor + double _x; + double _y; + double _z; + PointCRS _crs; + uint8_t _padding[7]; +}; + +} + +template <> +struct std::hash { + size_t operator()(const db::Point& value) const noexcept; +}; diff --git a/storage/metadata/PropertyType.h b/storage/metadata/PropertyType.h index 59d8dfde4a..51029bf467 100644 --- a/storage/metadata/PropertyType.h +++ b/storage/metadata/PropertyType.h @@ -10,6 +10,7 @@ #include "metadata/DateTime.h" #include "metadata/Duration.h" +#include "metadata/Point.h" #include "list/EncodedList.h" #include "list/ListHash.h" @@ -34,6 +35,7 @@ enum class ValueType : uint8_t { DateTime, Map, Duration, + Point, _SIZE, }; @@ -49,7 +51,8 @@ using ValueTypeName = EnumToString::Create< EnumStringPair, EnumStringPair, EnumStringPair, - EnumStringPair>; + EnumStringPair, + EnumStringPair>; struct CustomBool { CustomBool() = default; @@ -159,6 +162,13 @@ struct Duration : public PropertyType { static constexpr auto _valueType = ValueType::Duration; }; +struct Point : public PropertyType { + using Primitive = db::Point; + using MandatorySpan = std::span; + using OptionalSpan = std::span>; + static constexpr auto _valueType = ValueType::Point; +}; + struct Map : public PropertyType { using Primitive = MapView; using OwningPrimitive = EncodedMap; @@ -227,6 +237,9 @@ struct ValueTypeDispatcher { case ValueType::Duration: executor.template operator()(); break; + case ValueType::Point: + executor.template operator()(); + break; case ValueType::Map: executor.template operator()(); break; diff --git a/storage/metadata/PropertyTypeDispatcher.h b/storage/metadata/PropertyTypeDispatcher.h index f1058837a5..06fe8f7a55 100644 --- a/storage/metadata/PropertyTypeDispatcher.h +++ b/storage/metadata/PropertyTypeDispatcher.h @@ -33,6 +33,8 @@ struct PropertyTypeDispatcher { return executor.template operator()(); case ValueType::Duration: return executor.template operator()(); + case ValueType::Point: + return executor.template operator()(); case ValueType::Map: return executor.template operator()(); case ValueType::_SIZE: diff --git a/storage/properties/PropertyManager.cpp b/storage/properties/PropertyManager.cpp index 4d0f892f59..1aa76b55e4 100644 --- a/storage/properties/PropertyManager.cpp +++ b/storage/properties/PropertyManager.cpp @@ -84,6 +84,11 @@ void PropertyManager::fillEntityPropertyView(EntityID entityID, fill(container, ptID); } + for (const auto& [ptID, rawContainer] : _points) { + const auto& container = rawContainer->cast(); + fill(container, ptID); + } + for (const auto& [ptID, rawContainer] : _maps) { const auto& container = rawContainer->cast(); fill(container, ptID); diff --git a/storage/properties/PropertyManager.h b/storage/properties/PropertyManager.h index a3b5d79ff0..6f7517e01c 100644 --- a/storage/properties/PropertyManager.h +++ b/storage/properties/PropertyManager.h @@ -57,6 +57,8 @@ class PropertyManager { _dateTimes.emplace(ptID, static_cast(ptr)); } else if constexpr (std::is_same_v) { _durations.emplace(ptID, static_cast(ptr)); + } else if constexpr (std::is_same_v) { + _points.emplace(ptID, static_cast(ptr)); } else if constexpr (std::is_same_v) { _maps.emplace(ptID, static_cast(ptr)); } @@ -241,6 +243,7 @@ class PropertyManager { PropertyContainerReferences _lists; PropertyContainerReferences _dateTimes; PropertyContainerReferences _durations; + PropertyContainerReferences _points; PropertyContainerReferences _maps; PropertyIndexer _indexers; diff --git a/storage/reader/GraphReader.cpp b/storage/reader/GraphReader.cpp index a7708113c9..b0225ffa7d 100644 --- a/storage/reader/GraphReader.cpp +++ b/storage/reader/GraphReader.cpp @@ -580,6 +580,7 @@ template std::optional GraphReader::tryGetNo template std::optional GraphReader::tryGetNodeProperty(PropertyTypeID ptID, NodeID nodeID) const; template std::optional GraphReader::tryGetNodeProperty(PropertyTypeID ptID, NodeID nodeID) const; template std::optional GraphReader::tryGetNodeProperty(PropertyTypeID ptID, NodeID nodeID) const; +template std::optional GraphReader::tryGetNodeProperty(PropertyTypeID ptID, NodeID nodeID) const; template std::optional GraphReader::tryGetNodeProperty(PropertyTypeID ptID, NodeID nodeID) const; template std::optional GraphReader::tryGetEdgeProperty(PropertyTypeID ptID, EdgeID edgeID) const; @@ -591,4 +592,5 @@ template std::optional GraphReader::tryGetEd template std::optional GraphReader::tryGetEdgeProperty(PropertyTypeID ptID, EdgeID edgeID) const; template std::optional GraphReader::tryGetEdgeProperty(PropertyTypeID ptID, EdgeID edgeID) const; template std::optional GraphReader::tryGetEdgeProperty(PropertyTypeID ptID, EdgeID edgeID) const; +template std::optional GraphReader::tryGetEdgeProperty(PropertyTypeID ptID, EdgeID edgeID) const; template std::optional GraphReader::tryGetEdgeProperty(PropertyTypeID ptID, EdgeID edgeID) const; diff --git a/storage/versioning/CommitBuilder.cpp b/storage/versioning/CommitBuilder.cpp index 6935142969..f0f7027b14 100644 --- a/storage/versioning/CommitBuilder.cpp +++ b/storage/versioning/CommitBuilder.cpp @@ -283,6 +283,7 @@ template WeakArc CommitBuilder::newNodePropertyIndex(st template WeakArc CommitBuilder::newNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); +template WeakArc CommitBuilder::newNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); @@ -294,5 +295,6 @@ template WeakArc CommitBuilder::newEdgePropertyIndex(st template WeakArc CommitBuilder::newEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); +template WeakArc CommitBuilder::newEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc CommitBuilder::newEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); } diff --git a/storage/versioning/CommitWriteBuffer.cpp b/storage/versioning/CommitWriteBuffer.cpp index 966bd7f46e..c17585f837 100644 --- a/storage/versioning/CommitWriteBuffer.cpp +++ b/storage/versioning/CommitWriteBuffer.cpp @@ -60,6 +60,10 @@ struct PrimitiveToTag { using Type = types::Duration; }; template <> +struct PrimitiveToTag { + using Type = types::Point; +}; +template <> struct PrimitiveToTag { using Type = types::Embedding; }; diff --git a/storage/versioning/CommitWriteBuffer.h b/storage/versioning/CommitWriteBuffer.h index 12be520c6b..5651ec4ae3 100644 --- a/storage/versioning/CommitWriteBuffer.h +++ b/storage/versioning/CommitWriteBuffer.h @@ -46,6 +46,7 @@ class CommitWriteBuffer { std::optional, std::optional, std::optional, + std::optional, std::optional >; using UntypedProperties = std::vector; diff --git a/storage/versioning/DataPartRebaser.cpp b/storage/versioning/DataPartRebaser.cpp index 09b29a308b..ba6eeee958 100644 --- a/storage/versioning/DataPartRebaser.cpp +++ b/storage/versioning/DataPartRebaser.cpp @@ -149,6 +149,7 @@ bool DataPartRebaser::rebase(const MetadataRebaser& metadata, std::unordered_map lists; std::unordered_map dateTimes; std::unordered_map durations; + std::unordered_map points; std::unordered_map maps; for (auto& [ptID, container] : nodeProperties->_containers) { @@ -202,6 +203,11 @@ bool DataPartRebaser::rebase(const MetadataRebaser& metadata, durations[newPT._id] = container; } + for (const auto& [ptID, container] : nodeProperties->_points) { + const auto newPT = metadata.getPropertyTypeMapping(ptID); + points[newPT._id] = container; + } + for (const auto& [ptID, container] : nodeProperties->_maps) { const auto newPT = metadata.getPropertyTypeMapping(ptID); maps[newPT._id] = container; @@ -217,8 +223,9 @@ bool DataPartRebaser::rebase(const MetadataRebaser& metadata, nodeProperties->_lists = std::move(lists); nodeProperties->_dateTimes = std::move(dateTimes); nodeProperties->_durations = std::move(durations); + nodeProperties->_points = std::move(points); nodeProperties->_maps = std::move(maps); - static_assert((size_t)ValueType::_SIZE == 11 && "A value type was added"); + static_assert((size_t)ValueType::_SIZE == 12 && "A value type was added"); { PropertyIndexer newIndexers; @@ -258,6 +265,7 @@ bool DataPartRebaser::rebase(const MetadataRebaser& metadata, std::unordered_map lists; std::unordered_map dateTimes; std::unordered_map durations; + std::unordered_map points; std::unordered_map maps; for (auto& [ptID, container] : edgeProperties->_containers) { @@ -311,6 +319,11 @@ bool DataPartRebaser::rebase(const MetadataRebaser& metadata, durations[newPT._id] = container; } + for (const auto& [ptID, container] : edgeProperties->_points) { + const auto newPT = metadata.getPropertyTypeMapping(ptID); + points[newPT._id] = container; + } + for (const auto& [ptID, container] : edgeProperties->_maps) { const auto newPT = metadata.getPropertyTypeMapping(ptID); maps[newPT._id] = container; @@ -326,8 +339,9 @@ bool DataPartRebaser::rebase(const MetadataRebaser& metadata, edgeProperties->_lists = std::move(lists); edgeProperties->_dateTimes = std::move(dateTimes); edgeProperties->_durations = std::move(durations); + edgeProperties->_points = std::move(points); edgeProperties->_maps = std::move(maps); - static_assert((size_t)ValueType::_SIZE == 11 && "A value type was added"); + static_assert((size_t)ValueType::_SIZE == 12 && "A value type was added"); { // Update property indexers based on our new LabelSetMapping PropertyIndexer newIndexers; diff --git a/storage/versioning/VersionController.cpp b/storage/versioning/VersionController.cpp index 36e1b0416c..2e64d650f9 100644 --- a/storage/versioning/VersionController.cpp +++ b/storage/versioning/VersionController.cpp @@ -255,6 +255,7 @@ template WeakArc VersionController::createNodePropertyIndex VersionController::createNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); +template WeakArc VersionController::createNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createNodePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); @@ -266,5 +267,6 @@ template WeakArc VersionController::createEdgePropertyIndex VersionController::createEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); +template WeakArc VersionController::createEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); template WeakArc VersionController::createEdgePropertyIndex(std::string_view indexName, PropertyTypeID ptID); } diff --git a/storage/views/EntityPropertyView.h b/storage/views/EntityPropertyView.h index 63c1a0a710..1e5e7fbf46 100644 --- a/storage/views/EntityPropertyView.h +++ b/storage/views/EntityPropertyView.h @@ -97,6 +97,6 @@ class EntityPropertyView { void removeProperty(PropertyTypeID propTypeID); }; -static_assert((size_t)ValueType::_SIZE == 11); +static_assert((size_t)ValueType::_SIZE == 12); } diff --git a/storage/views/PropertyView.h b/storage/views/PropertyView.h index 29d399bbdc..2015984163 100644 --- a/storage/views/PropertyView.h +++ b/storage/views/PropertyView.h @@ -16,6 +16,7 @@ using PropertyVariant = std::variant; struct PropertyView { diff --git a/storage/writers/DataPartBuilder.cpp b/storage/writers/DataPartBuilder.cpp index c4f6fddbf2..71abf8409a 100644 --- a/storage/writers/DataPartBuilder.cpp +++ b/storage/writers/DataPartBuilder.cpp @@ -306,4 +306,5 @@ INSTANTIATE(types::Bool); INSTANTIATE(types::List); INSTANTIATE(types::DateTime); INSTANTIATE(types::Duration); +INSTANTIATE(types::Point); INSTANTIATE(types::Map); diff --git a/storage/writers/GraphWriter.cpp b/storage/writers/GraphWriter.cpp index e89e2b40c2..d5c2eee1a5 100644 --- a/storage/writers/GraphWriter.cpp +++ b/storage/writers/GraphWriter.cpp @@ -219,6 +219,7 @@ template void GraphWriter::addNodeProperty(NodeID, std::string template void GraphWriter::addNodeProperty(NodeID, std::string_view, types::List::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, std::string_view, types::DateTime::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, std::string_view, types::Duration::Primitive&&); +template void GraphWriter::addNodeProperty(NodeID, std::string_view, types::Point::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, std::string_view, types::Map::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, PropertyType, types::Int64::Primitive&&); @@ -230,6 +231,7 @@ template void GraphWriter::addNodeProperty(NodeID, PropertyTyp template void GraphWriter::addNodeProperty(NodeID, PropertyType, types::List::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, PropertyType, types::DateTime::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, PropertyType, types::Duration::Primitive&&); +template void GraphWriter::addNodeProperty(NodeID, PropertyType, types::Point::Primitive&&); template void GraphWriter::addNodeProperty(NodeID, PropertyType, types::Map::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::Int64::Primitive&&); @@ -240,6 +242,7 @@ template void GraphWriter::addEdgeProperty(const EdgeRecord&, std:: template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::Embedding::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::DateTime::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::Duration::Primitive&&); +template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::Point::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::List::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, std::string_view, types::Map::Primitive&&); @@ -252,4 +255,5 @@ template void GraphWriter::addEdgeProperty(const EdgeRecord&, template void GraphWriter::addEdgeProperty(const EdgeRecord&, PropertyType, types::List::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, PropertyType, types::DateTime::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, PropertyType, types::Duration::Primitive&&); +template void GraphWriter::addEdgeProperty(const EdgeRecord&, PropertyType, types::Point::Primitive&&); template void GraphWriter::addEdgeProperty(const EdgeRecord&, PropertyType, types::Map::Primitive&&); diff --git a/test/net/TuringProtoRoundTripTest.cpp b/test/net/TuringProtoRoundTripTest.cpp index 393293af8f..6ed9de42a9 100644 --- a/test/net/TuringProtoRoundTripTest.cpp +++ b/test/net/TuringProtoRoundTripTest.cpp @@ -45,6 +45,7 @@ using Bool = db::types::Bool::Primitive; using Embedding = db::types::Embedding::Primitive; using DateTime = db::types::DateTime::Primitive; using Duration = db::types::Duration::Primitive; +using Point = db::types::Point::Primitive; struct FramedPacket { net::proto::MessageTypes _type; @@ -493,6 +494,77 @@ TEST(TuringProtoRoundTripTest, RoundTripsDurationColumnsAcrossChunkSizes) { } } +TEST(TuringProtoRoundTripTest, RoundTripsPointColumnsAcrossChunkSizes) { + using OptionalPoint = std::optional; + + const Point cartesian(db::PointCRS::Cartesian, 1.5, -2.0, 0.0); + const Point cartesian3D(db::PointCRS::Cartesian3D, 1.5, -2.0, 3.25); + const Point wgs84(db::PointCRS::WGS84, 2.3522, 48.8566, 0.0); + const Point wgs84With3D(db::PointCRS::WGS84_3D, 2.3522, 48.8566, 35.0); + + for (const size_t chunkSize : std::array {48, 64, 97, 256}) { + SCOPED_TRACE(::testing::Message() << "chunkSize=" << chunkSize); + + db::LocalMemory localMem; + db::DataframeManager dfMan; + db::Dataframe source; + + auto* at = localMem.alloc>(); + at->push_back(cartesian); + at->push_back(cartesian3D); + at->push_back(wgs84); + at->push_back(wgs84With3D); + addColumn(&dfMan, &source, "at", at); + + auto* home = localMem.alloc>(); + home->push_back(wgs84); + home->push_back(std::nullopt); + home->push_back(cartesian3D); + home->push_back(std::nullopt); + addColumn(&dfMan, &source, "home", home); + + std::vector::ListItemVariant> items; + items.emplace_back(cartesian); + items.emplace_back(wgs84With3D); + + auto* route = localMem.alloc>(); + route->set(localMem.listBuffer().insert(items)); + addColumn(&dfMan, &source, "route", route); + + const auto packets = encodeDataframeWithChunkSize(source, chunkSize); + expectPacketSequence(packets, true); + + net::proto::ChunkedBuffer embeddingBuffer; + net::proto::ChunkedBuffer pathBuffer; + net::proto::ChunkedBuffer stringBuffer; + db::ListBuffer<> listBuffer; + db::MapBuffer<> mapBuffer; + db::Dataframe decoded; + std::vector schemas; + decodeChunkPackets(packets, &localMem, &embeddingBuffer, &pathBuffer, &stringBuffer, &listBuffer, &mapBuffer, &dfMan, &decoded, &schemas); + + ASSERT_EQ(decoded.cols().size(), 3u); + EXPECT_EQ(decoded.getLogicalRowCount(), 4u); + + const auto* decodedAt = decoded.cols().at(0)->as>(); + const auto* decodedHome = decoded.cols().at(1)->as>(); + const auto* decodedRoute = decoded.cols().at(2)->as>(); + ASSERT_NE(decodedAt, nullptr); + ASSERT_NE(decodedHome, nullptr); + ASSERT_NE(decodedRoute, nullptr); + + EXPECT_EQ(decodedAt->getRaw(), (std::vector {cartesian, cartesian3D, wgs84, wgs84With3D})); + EXPECT_EQ(decodedHome->getRaw(), (std::vector {wgs84, std::nullopt, cartesian3D, std::nullopt})); + + const std::span elements = decodedRoute->at(0).elements(); + ASSERT_EQ(elements.size(), 2u); + ASSERT_EQ(elements[0].getTag(), db::CellTag::Point); + EXPECT_EQ(elements[0].getAs(), cartesian); + ASSERT_EQ(elements[1].getTag(), db::CellTag::Point); + EXPECT_EQ(elements[1].getAs(), wgs84With3D); + } +} + // A label predicate answers with a mask, a boolean column that carries no nulls of its // own, so it goes on the wire as a plain BOOL vector. TEST(TuringProtoRoundTripTest, RoundTripsMaskColumns) { diff --git a/test/query-test-suite/QueryResultFormatter.cpp b/test/query-test-suite/QueryResultFormatter.cpp index 639c6e0e31..cfc70aa2c9 100644 --- a/test/query-test-suite/QueryResultFormatter.cpp +++ b/test/query-test-suite/QueryResultFormatter.cpp @@ -106,6 +106,13 @@ template return result; } +[[maybe_unused]] std::string valueToString(const db::Point& value) { + std::string result; + db::Point::format(result, value); + + return result; +} + [[maybe_unused]] std::string valueToString(const db::CommitBuilder* value) { return value ? "commit_builder_ptr" : "null"; } diff --git a/test/query/ir/CMakeLists.txt b/test/query/ir/CMakeLists.txt index 21d1aadb51..d29fdaf79d 100644 --- a/test/query/ir/CMakeLists.txt +++ b/test/query/ir/CMakeLists.txt @@ -81,6 +81,7 @@ add_subdirectory(optional_match) add_subdirectory(parameter) add_subdirectory(passes) add_subdirectory(path) +add_subdirectory(point) add_subdirectory(projection) add_subdirectory(set) add_subdirectory(subquery) diff --git a/test/query/ir/IRTestRows.cpp b/test/query/ir/IRTestRows.cpp index 587247ead5..bb7848c04f 100644 --- a/test/query/ir/IRTestRows.cpp +++ b/test/query/ir/IRTestRows.cpp @@ -140,6 +140,11 @@ void renderCellView(const CellView& element, std::string& out) { return; break; + case CellTag::Point: + Point::format(out, element.getAs()); + return; + break; + case CellTag::MapView: renderMap(element.getAs(), out); return; @@ -204,6 +209,9 @@ void renderValue(const T& value, std::string& out) { } else if constexpr (std::is_same_v) { out.clear(); Duration::format(out, value); + } else if constexpr (std::is_same_v) { + out.clear(); + Point::format(out, value); } else { out = std::to_string(value); } @@ -339,7 +347,8 @@ void turing::test::renderCell(const Column* column, size_t row, std::string& out || renderValueCell(column, row, out) || renderValueCell(column, row, out) || renderValueCell(column, row, out) - || renderValueCell(column, row, out)) { + || renderValueCell(column, row, out) + || renderValueCell(column, row, out)) { // Rendered by the helper for whichever value type matched } else { throw std::runtime_error("IRTestRows: unsupported output column type"); diff --git a/test/query/ir/StringRowSink.cpp b/test/query/ir/StringRowSink.cpp index de8411aefb..4331563dac 100644 --- a/test/query/ir/StringRowSink.cpp +++ b/test/query/ir/StringRowSink.cpp @@ -24,6 +24,7 @@ #include "map/MapView.h" #include "metadata/DateTime.h" #include "metadata/Duration.h" +#include "metadata/Point.h" #include "metadata/PropertyNull.h" #include "metadata/PropertyType.h" #include "versioning/CommitHash.h" @@ -92,6 +93,10 @@ void appendValueText(Duration value, std::string& text) { Duration::format(text, value); } +void appendValueText(Point value, std::string& text) { + Point::format(text, value); +} + void appendValueText(PropertyNull value, std::string& text) { text += "null"; } diff --git a/test/query/ir/engine/EquivalenceTest.cpp b/test/query/ir/engine/EquivalenceTest.cpp index 49946c4f6c..31570f58e4 100644 --- a/test/query/ir/engine/EquivalenceTest.cpp +++ b/test/query/ir/engine/EquivalenceTest.cpp @@ -122,6 +122,13 @@ std::string valueToString(const DateTime value) { return result; } +std::string valueToString(const Point value) { + std::string result; + Point::format(result, value); + + return result; +} + std::string valueToString(const Duration value) { std::string result; Duration::format(result, value); diff --git a/test/query/ir/point/CMakeLists.txt b/test/query/ir/point/CMakeLists.txt new file mode 100644 index 0000000000..0555372a2a --- /dev/null +++ b/test/query/ir/point/CMakeLists.txt @@ -0,0 +1,5 @@ +add_turing_test(test_query_ir_point_property PointPropertyTest.cpp) +target_include_directories(test_query_ir_point_property PRIVATE ${IR_TEST_DIR}) + +add_turing_test(test_query_ir_point_value_flow PointValueFlowTest.cpp) +target_include_directories(test_query_ir_point_value_flow PRIVATE ${IR_TEST_DIR}) diff --git a/test/query/ir/point/PointPropertyTest.cpp b/test/query/ir/point/PointPropertyTest.cpp new file mode 100644 index 0000000000..e1e0df949d --- /dev/null +++ b/test/query/ir/point/PointPropertyTest.cpp @@ -0,0 +1,347 @@ +#include + +#include +#include +#include +#include + +#include "QueryInterpreterV3.h" +#include "QueryStatus.h" + +#include "Graph.h" +#include "QueryConfig.h" +#include "SimpleGraph.h" +#include "SystemAccessor.h" +#include "SystemManager.h" +#include "TuringDB.h" +#include "versioning/ChangeID.h" +#include "versioning/CommitHash.h" + +#include "IRTestRows.h" +#include "TuringTest.h" +#include "TuringTestEnv.h" + +using namespace db; +using namespace turing::test; + +class PointPropertyTest : public TuringTest { +protected: + void initialize() override { + _env = TuringTestEnv::create(fs::Path {_outDir} / "turing"); + _interpreter = std::make_unique(&_env->getSystemManager(), &_env->getMem(), &_env->getCompilerContext()); + + SystemAccessor system = _env->getSystemManager().accessUnique(); + Graph* graph = system.createGraph(_graphName); + SimpleGraph::createSimpleGraph(graph); + } + + void openChange(ChangeID& changeID) { + SystemAccessor system = _env->getSystemManager().accessUnique(); + const auto res = system.newChange(_graphName); + ASSERT_TRUE(res); + + changeID = res.value()->id(); + } + + void submit(const ChangeID& changeID) { + const QueryState submitState(_graphName, + &_env->getMem(), + &_env->getCompilerContext(), + &_queryConfig, + nullptr, + CommitHash::head(), + changeID); + const QueryStatus status = _env->getDB().query("CHANGE SUBMIT", submitState); + ASSERT_TRUE(status.isOk()) << "CHANGE SUBMIT failed"; + } + + void write(std::string_view query) { + ChangeID changeID; + openChange(changeID); + + RowSink sink; + QueryStatus status; + _interpreter->execute(status, + query, + _graphName, + CommitHash::head(), + changeID, + &sink); + ASSERT_TRUE(status.isOk()) << "query: " << query << "\nerror: " << status.getError(); + + submit(changeID); + } + + void run(std::string_view query, RowSink& sink, QueryStatus& status) { + _interpreter->execute(status, + query, + _graphName, + CommitHash::head(), + ChangeID::head(), + &sink); + } + + void expectRows(std::string_view query, const Rows& expected) { + RowSink sink; + QueryStatus status; + run(query, sink, status); + ASSERT_TRUE(status.isOk()) << "query: " << query << "\nerror: " << status.getError(); + + Rows actual; + sink.sortedRows(actual); + + Rows sortedExpected = expected; + std::sort(sortedExpected.begin(), sortedExpected.end()); + + std::string actualText; + describeRows(actual, actualText); + + EXPECT_EQ(actual, sortedExpected) << "query: " << query << "\ngot:\n" << actualText; + } + + void expectOrderedRows(std::string_view query, const Rows& expected) { + RowSink sink; + QueryStatus status; + run(query, sink, status); + ASSERT_TRUE(status.isOk()) << "query: " << query << "\nerror: " << status.getError(); + + std::string actualText; + describeRows(sink.rows(), actualText); + + EXPECT_EQ(sink.rows(), expected) << "query: " << query << "\ngot:\n" << actualText; + } + + void expectError(std::string_view query, std::string_view expectedError) { + RowSink sink; + QueryStatus status; + run(query, sink, status); + + ASSERT_FALSE(status.isOk()) << "query: " << query << "\nexpected it to fail"; + EXPECT_NE(status.getError().find(expectedError), std::string::npos) + << "query: " << query << "\nerror: " << status.getError(); + } + + void createFourCities() { + write("CREATE (n:City {name: 'Paris', at: point({longitude: 2.3522, latitude: 48.8566})})"); + write("CREATE (n:City {name: 'London', at: point({longitude: -0.1276, latitude: 51.5072})})"); + write("CREATE (n:City {name: 'Grid', at: point({x: 3, y: 4})})"); + write("CREATE (n:City {name: 'Tower', at: point({x: 3, y: 4, z: 5})})"); + } + + const std::string _graphName = "simpledb"; + std::unique_ptr _env; + std::unique_ptr _interpreter; + QueryConfig _queryConfig; +}; + +TEST_F(PointPropertyTest, returnsACartesianPoint) { + expectRows("RETURN point({x: 1, y: 2})", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); + expectRows("RETURN point({x: 1.5, y: -2, z: 3})", {{"point({x: 1.5, y: -2.0, z: 3.0, crs: 'cartesian-3d'})"}}); +} + +TEST_F(PointPropertyTest, returnsAGeographicPoint) { + expectRows("RETURN point({longitude: 2.3522, latitude: 48.8566})", + {{"point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})"}}); + expectRows("RETURN point({longitude: 2.3522, latitude: 48.8566, height: 35})", + {{"point({x: 2.3522, y: 48.8566, z: 35.0, crs: 'wgs-84-3d'})"}}); + expectRows("RETURN point({longitude: 2.3522, latitude: 48.8566, z: 35})", + {{"point({x: 2.3522, y: 48.8566, z: 35.0, crs: 'wgs-84-3d'})"}}); +} + +TEST_F(PointPropertyTest, readsTheCRSANameOrAnSRIDGives) { + expectRows("RETURN point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})", + {{"point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})"}}); + expectRows("RETURN point({x: 1, y: 2, crs: 'WGS-84'})", {{"point({x: 1.0, y: 2.0, crs: 'wgs-84'})"}}); + expectRows("RETURN point({x: 1, y: 2, srid: 4326})", {{"point({x: 1.0, y: 2.0, crs: 'wgs-84'})"}}); + expectRows("RETURN point({x: 1, y: 2, z: 3, srid: 9157})", + {{"point({x: 1.0, y: 2.0, z: 3.0, crs: 'cartesian-3d'})"}}); + expectRows("RETURN point({longitude: 1, latitude: 2, height: 3, srid: 4979})", + {{"point({x: 1.0, y: 2.0, z: 3.0, crs: 'wgs-84-3d'})"}}); +} + +TEST_F(PointPropertyTest, readsItsKeysWhateverTheirCase) { + expectRows("RETURN point({X: 1, Y: 2})", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); + expectRows("RETURN point({Longitude: 1, LATITUDE: 2})", {{"point({x: 1.0, y: 2.0, crs: 'wgs-84'})"}}); +} + +TEST_F(PointPropertyTest, ignoresTheKeysItDoesNotKnow) { + expectRows("RETURN point({x: 1, y: 2, name: 'a'})", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); +} + +TEST_F(PointPropertyTest, wrapsALongitudeIntoTheRange) { + expectRows("RETURN point({longitude: 190, latitude: 0})", {{"point({x: -170.0, y: 0.0, crs: 'wgs-84'})"}}); + expectRows("RETURN point({longitude: -540, latitude: 0})", {{"point({x: -180.0, y: 0.0, crs: 'wgs-84'})"}}); + expectRows("RETURN point({longitude: 360, latitude: 0})", {{"point({x: 0.0, y: 0.0, crs: 'wgs-84'})"}}); +} + +TEST_F(PointPropertyTest, returnsNullForANullMapOrANullValue) { + expectRows("RETURN point(null)", {{"null"}}); + expectRows("RETURN point({x: null, y: 2})", {{"null"}}); + expectRows("RETURN point({x: 1, y: 2, crs: null})", {{"null"}}); +} + +TEST_F(PointPropertyTest, rejectsALatitudePastAPole) { + expectError("RETURN point({longitude: 0, latitude: 90.5})", "The latitude of a wgs-84 point lies in [-90, 90], not 90.5"); + expectError("RETURN point({x: 0, y: -91, crs: 'wgs-84'})", "lies in [-90, 90], not -91"); +} + +TEST_F(PointPropertyTest, rejectsANonFiniteCoordinate) { + expectError("RETURN point({x: 1e308 * 10, y: 0})", "A point cannot have a non-finite coordinate"); +} + +TEST_F(PointPropertyTest, rejectsAMapNamingBothACRSAndAnSRID) { + expectError("RETURN point({x: 1, y: 2, crs: 'cartesian', srid: 7203})", "point() takes a 'crs' or an 'srid', not both"); +} + +TEST_F(PointPropertyTest, rejectsAnUnknownCRS) { + expectError("RETURN point({x: 1, y: 2, crs: 'mercator'})", "Unknown coordinate reference system: 'mercator'"); + expectError("RETURN point({x: 1, y: 2, srid: 3857})", "Unknown coordinate reference system SRID: 3857"); +} + +TEST_F(PointPropertyTest, rejectsAGeographicPointInACartesianCRS) { + expectError("RETURN point({longitude: 1, latitude: 2, crs: 'cartesian'})", + "A point given by longitude and latitude takes a geographic CRS, not 'cartesian'"); +} + +TEST_F(PointPropertyTest, rejectsCoordinatesTheCRSDoesNotHave) { + expectError("RETURN point({x: 1, y: 2, crs: 'cartesian-3d'})", "A cartesian-3d point has 3 coordinates, not 2"); + expectError("RETURN point({x: 1, y: 2, z: 3, crs: 'wgs-84'})", "A wgs-84 point has 2 coordinates, not 3"); +} + +TEST_F(PointPropertyTest, rejectsAMapMissingACoordinate) { + expectError("RETURN point({x: 1})", "point() needs 'x' and 'y', or 'longitude' and 'latitude'"); + expectError("RETURN point({latitude: 1, crs: 'wgs-84'})", "A wgs-84 point needs 'longitude' and 'latitude'"); +} + +TEST_F(PointPropertyTest, rejectsACoordinateThatIsNoNumber) { + expectError("RETURN point({x: '1', y: 2})", "point() reads a number for 'x', not 'String'"); +} + +TEST_F(PointPropertyTest, rejectsAKeyGivenTwice) { + expectError("RETURN point({x: 1, X: 2, y: 3})", "point() reads the key 'x' twice"); +} + +TEST_F(PointPropertyTest, buildsAPointFromAMapItReadsPerRow) { + expectRows("UNWIND [{x: 1, y: 2}, {longitude: 3, latitude: 4}, null] AS m RETURN point(m)", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, + {"point({x: 3.0, y: 4.0, crs: 'wgs-84'})"}, + {"null"}}); +} + +TEST_F(PointPropertyTest, rejectsAValueThatIsNoMap) { + expectError("UNWIND [1] AS m RETURN point(m)", "point"); +} + +TEST_F(PointPropertyTest, writesAPointAsText) { + expectRows("RETURN toString(point({x: 1, y: 2}))", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); + expectRows("UNWIND [point({x: 1, y: 2})] AS p RETURN toString(p)", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); +} + +TEST_F(PointPropertyTest, storesAndReadsBackAPoint) { + write("CREATE (n:City {name: 'Paris', at: point({longitude: 2.3522, latitude: 48.8566})})"); + expectRows("MATCH (n:City) RETURN n.at", {{"point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})"}}); +} + +TEST_F(PointPropertyTest, setsThePointOfAnExistingNode) { + write("MATCH (n:Person {name: 'Remy'}) SET n.home = point({x: 1, y: 2, z: 3})"); + expectRows("MATCH (n:Person {name: 'Remy'}) RETURN n.home", {{"point({x: 1.0, y: 2.0, z: 3.0, crs: 'cartesian-3d'})"}}); +} + +TEST_F(PointPropertyTest, storesAPointOnAnEdge) { + write("MATCH (a:Person {name: 'Remy'}), (b:Person {name: 'Adam'}) " + "CREATE (a)-[:MET {at: point({x: 1, y: 2})}]->(b)"); + + expectRows("MATCH (:Person)-[e:MET]->(:Person) RETURN e.at", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); +} + +TEST_F(PointPropertyTest, readsNullFromANodeCarryingNoPoint) { + write("CREATE (a:City {name: 'a', at: point({x: 1, y: 2})})"); + write("CREATE (b:City {name: 'b'})"); + + expectRows("MATCH (n:City) RETURN n.name, n.at", {{"a", "point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"b", "null"}}); +} + +TEST_F(PointPropertyTest, holdsPointsOfDifferentCRSsInOneProperty) { + createFourCities(); + expectRows("MATCH (n:City) RETURN n.name, n.at", + {{"Paris", "point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})"}, + {"London", "point({x: -0.1276, y: 51.5072, crs: 'wgs-84'})"}, + {"Grid", "point({x: 3.0, y: 4.0, crs: 'cartesian'})"}, + {"Tower", "point({x: 3.0, y: 4.0, z: 5.0, crs: 'cartesian-3d'})"}}); +} + +TEST_F(PointPropertyTest, filtersOnEquality) { + createFourCities(); + expectRows("MATCH (n:City) WHERE n.at = point({x: 3, y: 4}) RETURN n.name", {{"Grid"}}); + expectRows("MATCH (n:City) WHERE n.at <> point({x: 3, y: 4}) RETURN n.name", {{"Paris"}, {"London"}, {"Tower"}}); +} + +TEST_F(PointPropertyTest, tellsPointsOfDifferentCRSsApart) { + expectRows("RETURN point({x: 1, y: 2}) = point({x: 1, y: 2, crs: 'wgs-84'})", {{"false"}}); + expectRows("RETURN point({x: 1, y: 2}) = point({x: 1, y: 2, z: 0})", {{"false"}}); + expectRows("RETURN point({x: 1, y: 2}) = point({x: 1.0, y: 2.0})", {{"true"}}); +} + +TEST_F(PointPropertyTest, equatesAZeroToANegativeZero) { + expectRows("RETURN point({x: -0.0, y: 0}) = point({x: 0.0, y: 0})", {{"true"}}); + expectRows("UNWIND [point({x: -0.0, y: 0}), point({x: 0.0, y: 0})] AS p RETURN DISTINCT p", + {{"point({x: 0.0, y: 0.0, crs: 'cartesian'})"}}); +} + +TEST_F(PointPropertyTest, ordersNoPointBeforeAnother) { + expectRows("RETURN point({x: 1, y: 2}) < point({x: 3, y: 4})", {{"null"}}); +} + +TEST_F(PointPropertyTest, ordersBySRIDThenByCoordinates) { + createFourCities(); + write("CREATE (n:City {name: 'Origin', at: point({x: 0, y: 9})})"); + + expectOrderedRows("MATCH (n:City) RETURN n.name ORDER BY n.at", + {{"London"}, {"Paris"}, {"Origin"}, {"Grid"}, {"Tower"}}); + expectOrderedRows("MATCH (n:City) RETURN n.name ORDER BY n.at DESC", + {{"Tower"}, {"Grid"}, {"Origin"}, {"Paris"}, {"London"}}); +} + +TEST_F(PointPropertyTest, sortsPointsAfterPathsAndBeforeDateTimes) { + expectOrderedRows("UNWIND [datetime('2026-01-01'), point({x: 1, y: 2}), [1]] AS v RETURN v ORDER BY v", + {{"[1]"}, {"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"2026-01-01T00:00:00Z"}}); +} + +TEST_F(PointPropertyTest, countsAndCollectsPoints) { + write("CREATE (a:City {name: 'a', at: point({x: 1, y: 2})})"); + write("CREATE (b:City {name: 'b', at: point({x: 1, y: 2})})"); + + expectRows("MATCH (n:City) RETURN count(n.at), count(DISTINCT n.at)", {{"2", "1"}}); + expectRows("MATCH (n:City) RETURN collect(n.at)", + {{"[point({x: 1.0, y: 2.0, crs: 'cartesian'}), point({x: 1.0, y: 2.0, crs: 'cartesian'})]"}}); +} + +TEST_F(PointPropertyTest, groupsByPoint) { + write("CREATE (a:City {name: 'a', at: point({x: 1, y: 2})})"); + write("CREATE (b:City {name: 'b', at: point({x: 1, y: 2})})"); + write("CREATE (c:City {name: 'c', at: point({x: 1, y: 2, crs: 'wgs-84'})})"); + + expectRows("MATCH (n:City) RETURN n.at, count(*)", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})", "2"}, {"point({x: 1.0, y: 2.0, crs: 'wgs-84'})", "1"}}); +} + +TEST_F(PointPropertyTest, storesPointsInAList) { + write("CREATE (n:City {name: 'a', route: [point({x: 1, y: 2}), point({longitude: 3, latitude: 4})]})"); + expectRows("MATCH (n:City) RETURN n.route", + {{"[point({x: 1.0, y: 2.0, crs: 'cartesian'}), point({x: 3.0, y: 4.0, crs: 'wgs-84'})]"}}); +} + +TEST_F(PointPropertyTest, storesAPointInAMap) { + write("CREATE (n:City {name: 'a', attrs: {at: point({x: 1, y: 2}), n: 1}})"); + expectRows("MATCH (n:City) RETURN n.attrs", {{"{at: point({x: 1.0, y: 2.0, crs: 'cartesian'}), n: 1}"}}); +} + +TEST_F(PointPropertyTest, findsAPointInAList) { + expectRows("RETURN point({x: 1, y: 2}) IN [point({x: 3, y: 4}), point({x: 1, y: 2})]", {{"true"}}); + expectRows("RETURN point({x: 1, y: 2}) IN [point({x: 1, y: 2, crs: 'wgs-84'})]", {{"false"}}); +} + +TEST_F(PointPropertyTest, unwindsAListOfPoints) { + expectRows("UNWIND [point({x: 3, y: 4}), point({x: 1, y: 2})] AS p RETURN p", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"point({x: 3.0, y: 4.0, crs: 'cartesian'})"}}); +} diff --git a/test/query/ir/point/PointValueFlowTest.cpp b/test/query/ir/point/PointValueFlowTest.cpp new file mode 100644 index 0000000000..dc4dc0c3e5 --- /dev/null +++ b/test/query/ir/point/PointValueFlowTest.cpp @@ -0,0 +1,238 @@ +#include + +#include +#include +#include +#include + +#include "QueryInterpreterV3.h" +#include "QueryStatus.h" + +#include "Graph.h" +#include "QueryConfig.h" +#include "SimpleGraph.h" +#include "SystemAccessor.h" +#include "SystemManager.h" +#include "TuringDB.h" +#include "versioning/ChangeID.h" +#include "versioning/CommitHash.h" + +#include "IRTestRows.h" +#include "TuringTest.h" +#include "TuringTestEnv.h" + +using namespace db; +using namespace turing::test; + +class PointValueFlowTest : public TuringTest { +protected: + void initialize() override { + _env = TuringTestEnv::create(fs::Path {_outDir} / "turing"); + _interpreter = std::make_unique(&_env->getSystemManager(), &_env->getMem(), &_env->getCompilerContext()); + + SystemAccessor system = _env->getSystemManager().accessUnique(); + Graph* graph = system.createGraph(_graphName); + SimpleGraph::createSimpleGraph(graph); + } + + void openChange(ChangeID& changeID) { + SystemAccessor system = _env->getSystemManager().accessUnique(); + const auto res = system.newChange(_graphName); + ASSERT_TRUE(res); + + changeID = res.value()->id(); + } + + void submit(const ChangeID& changeID) { + const QueryState submitState(_graphName, + &_env->getMem(), + &_env->getCompilerContext(), + &_queryConfig, + nullptr, + CommitHash::head(), + changeID); + const QueryStatus status = _env->getDB().query("CHANGE SUBMIT", submitState); + ASSERT_TRUE(status.isOk()) << "CHANGE SUBMIT failed"; + } + + void write(std::string_view query) { + ChangeID changeID; + openChange(changeID); + + RowSink sink; + QueryStatus status; + _interpreter->execute(status, + query, + _graphName, + CommitHash::head(), + changeID, + &sink); + ASSERT_TRUE(status.isOk()) << "query: " << query << "\nerror: " << status.getError(); + + submit(changeID); + } + + void run(std::string_view query, RowSink& sink, QueryStatus& status) { + _interpreter->execute(status, + query, + _graphName, + CommitHash::head(), + ChangeID::head(), + &sink); + } + + void expectRows(std::string_view query, const Rows& expected) { + RowSink sink; + QueryStatus status; + run(query, sink, status); + ASSERT_TRUE(status.isOk()) << "query: " << query << "\nerror: " << status.getError(); + + Rows actual; + sink.sortedRows(actual); + + Rows sortedExpected = expected; + std::sort(sortedExpected.begin(), sortedExpected.end()); + + std::string actualText; + describeRows(actual, actualText); + + EXPECT_EQ(actual, sortedExpected) << "query: " << query << "\ngot:\n" << actualText; + } + + void expectOrderedRows(std::string_view query, const Rows& expected) { + RowSink sink; + QueryStatus status; + run(query, sink, status); + ASSERT_TRUE(status.isOk()) << "query: " << query << "\nerror: " << status.getError(); + + std::string actualText; + describeRows(sink.rows(), actualText); + + EXPECT_EQ(sink.rows(), expected) << "query: " << query << "\ngot:\n" << actualText; + } + + void expectError(std::string_view query, std::string_view expectedError) { + RowSink sink; + QueryStatus status; + run(query, sink, status); + + ASSERT_FALSE(status.isOk()) << "query: " << query << "\nexpected it to fail"; + EXPECT_NE(status.getError().find(expectedError), std::string::npos) + << "query: " << query << "\nerror: " << status.getError(); + } + + void createFourCities() { + write("CREATE (n:City {name: 'Paris', at: point({longitude: 2.3522, latitude: 48.8566})})"); + write("CREATE (n:City {name: 'London', at: point({longitude: -0.1276, latitude: 51.5072})})"); + write("CREATE (n:City {name: 'Grid', at: point({x: 3, y: 4})})"); + write("CREATE (n:City {name: 'Tower', at: point({x: 3, y: 4, z: 5})})"); + } + + const std::string _graphName = "simpledb"; + std::unique_ptr _env; + std::unique_ptr _interpreter; + QueryConfig _queryConfig; +}; + +TEST_F(PointValueFlowTest, coalescesAPoint) { + expectRows("RETURN coalesce(null, point({x: 1, y: 2}))", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); +} + +TEST_F(PointValueFlowTest, selectsAPointInACase) { + expectRows("UNWIND [true, false] AS b RETURN CASE WHEN b THEN point({x: 1, y: 2}) ELSE null END", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"null"}}); + expectRows("UNWIND [true, false] AS b RETURN CASE WHEN b THEN point({x: 1, y: 2}) ELSE 1 END", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"1"}}); +} + +TEST_F(PointValueFlowTest, unitesPoints) { + expectRows("RETURN point({x: 1, y: 2}) AS p UNION RETURN point({x: 1, y: 2}) AS p", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); + expectRows("RETURN point({x: 1, y: 2}) AS p UNION ALL RETURN point({x: 1, y: 2, crs: 'wgs-84'}) AS p", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"point({x: 1.0, y: 2.0, crs: 'wgs-84'})"}}); +} + +TEST_F(PointValueFlowTest, comparesListsAndMapsHoldingPoints) { + expectRows("RETURN [point({x: 1, y: 2})] = [point({x: 1, y: 2})]", {{"true"}}); + expectRows("RETURN [point({x: 1, y: 2})] = [point({x: 1, y: 3})]", {{"false"}}); + expectRows("RETURN {a: point({x: 1, y: 2})} = {a: point({x: 1, y: 2})}", {{"true"}}); +} + +TEST_F(PointValueFlowTest, comparesAPointCarriedByAVariable) { + expectRows("WITH point({x: 1, y: 2}) AS p RETURN p = p, p <> p", {{"true", "false"}}); +} + +TEST_F(PointValueFlowTest, findsAPointAmongValuesOfOtherTypes) { + expectRows("RETURN point({x: 1, y: 2}) IN [1, 'a', point({x: 1, y: 2})]", {{"true"}}); + expectRows("RETURN coll.indexOf([1, point({x: 1, y: 2})], point({x: 1, y: 2}))", {{"1"}}); +} + +TEST_F(PointValueFlowTest, ordersPointsAmongValuesOfOtherTypes) { + expectOrderedRows("UNWIND [1, 'a', point({x: 1, y: 2}), true] AS v RETURN v ORDER BY v", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"a"}, {"true"}, {"1"}}); +} + +TEST_F(PointValueFlowTest, countsDistinctPointsOfAList) { + expectRows("UNWIND [point({x: 1, y: 2}), point({x: 1, y: 2}), point({x: 1, y: 2, crs: 'wgs-84'})] AS p " + "RETURN count(DISTINCT p)", + {{"2"}}); +} + +TEST_F(PointValueFlowTest, collectsDistinctPointProperties) { + createFourCities(); + write("CREATE (n:City {name: 'Twin', at: point({x: 3, y: 4})})"); + + expectRows("MATCH (n:City) WHERE n.name IN ['Grid', 'Twin'] RETURN collect(DISTINCT n.at)", + {{"[point({x: 3.0, y: 4.0, crs: 'cartesian'})]"}}); +} + +TEST_F(PointValueFlowTest, filtersOnAListOfPoints) { + createFourCities(); + expectRows("MATCH (n:City) WHERE n.at IN [point({x: 3, y: 4}), point({x: 3, y: 4, z: 5})] RETURN n.name", + {{"Grid"}, {"Tower"}}); +} + +TEST_F(PointValueFlowTest, joinsOnAPoint) { + createFourCities(); + write("CREATE (n:City {name: 'Twin', at: point({x: 3, y: 4})})"); + + expectRows("MATCH (a:City), (b:City) WHERE a.at = b.at AND a.name < b.name RETURN a.name, b.name", + {{"Grid", "Twin"}}); +} + +TEST_F(PointValueFlowTest, mergesOnAPoint) { + write("MERGE (n:City {at: point({x: 1, y: 2})})"); + write("MERGE (n:City {at: point({x: 1, y: 2})})"); + write("MERGE (n:City {at: point({x: 1, y: 2, crs: 'wgs-84'})})"); + + expectRows("MATCH (n:City) RETURN n.at", + {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}, {"point({x: 1.0, y: 2.0, crs: 'wgs-84'})"}}); +} + +TEST_F(PointValueFlowTest, matchesAPointGivenInline) { + createFourCities(); + expectRows("MATCH (n:City {at: point({x: 3, y: 4})}) RETURN n.name", {{"Grid"}}); +} + +TEST_F(PointValueFlowTest, projectsAPointPropertyIntoAMap) { + write("CREATE (n:City {name: 'Grid', at: point({x: 3, y: 4})})"); + + expectRows("MATCH (n:City) RETURN n {.at}", {{"{at: point({x: 3.0, y: 4.0, crs: 'cartesian'})}"}}); + expectRows("MATCH (n:City) RETURN properties(n)", {{"{at: point({x: 3.0, y: 4.0, crs: 'cartesian'}), name: Grid}"}}); +} + +TEST_F(PointValueFlowTest, buildsAPointFromAMapParameterOfARow) { + expectRows("WITH {longitude: 2.3522, latitude: 48.8566} AS m RETURN point(m)", + {{"point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})"}}); +} + +TEST_F(PointValueFlowTest, writesAPointReadOffAListElement) { + write("UNWIND [point({x: 1, y: 2})] AS p CREATE (n:City {at: p})"); + expectRows("MATCH (n:City) RETURN n.at", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); +} + +TEST_F(PointValueFlowTest, convertsAPointToNothingButAString) { + expectError("RETURN toInteger(point({x: 1, y: 2}))", "toInteger"); + expectRows("RETURN toIntegerOrNull(point({x: 1, y: 2}))", {{"null"}}); + expectRows("RETURN toStringOrNull(point({x: 1, y: 2}))", {{"point({x: 1.0, y: 2.0, crs: 'cartesian'})"}}); +} diff --git a/test/storage/CMakeLists.txt b/test/storage/CMakeLists.txt index 25fdab173c..be25e00b29 100644 --- a/test/storage/CMakeLists.txt +++ b/test/storage/CMakeLists.txt @@ -9,6 +9,7 @@ add_turing_test(test_storage_datetime_parse DateTimeParseTest.cpp) add_turing_test(test_storage_datetime_component DateTimeComponentTest.cpp) add_turing_test(test_storage_duration_format DurationFormatTest.cpp) add_turing_test(test_storage_duration_component DurationComponentTest.cpp) +add_turing_test(test_storage_point PointTest.cpp) add_turing_test(test_storage_mappropertycontainer MapPropertyContainerTest.cpp) add_turing_test(test_storage_property_hash_index_numeric_key PropertyHashIndexNumericKeyTest.cpp) @@ -126,6 +127,7 @@ add_turing_test(test_storage_dump_embedding_graph dump/EmbeddingGraphDumpLoadTes add_turing_test(test_storage_dump_list_graph dump/ListGraphDumpLoadTest.cpp) add_turing_test(test_storage_dump_datetime_graph dump/DateTimeGraphDumpLoadTest.cpp) add_turing_test(test_storage_dump_duration_graph dump/DurationGraphDumpLoadTest.cpp) +add_turing_test(test_storage_dump_point_graph dump/PointGraphDumpLoadTest.cpp) add_turing_test(test_storage_dump_map_graph dump/MapGraphDumpLoadTest.cpp) add_turing_test(test_storage_dump_map_datetime_graph dump/MapDateTimeGraphDumpLoadTest.cpp) add_turing_test(test_storage_dump_celltagdumpformat dump/CellTagDumpFormatTest.cpp) diff --git a/test/storage/PointTest.cpp b/test/storage/PointTest.cpp new file mode 100644 index 0000000000..96410b81d0 --- /dev/null +++ b/test/storage/PointTest.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include + +#include +#include +#include + +#include "metadata/Point.h" +#include "TuringException.h" + +using namespace db; + +namespace { + +std::string formatted(const Point& value) { + std::string out; + Point::format(out, value); + + return out; +} + +} + +TEST(PointTest, writesTheCoordinatesItsCRSHas) { + EXPECT_EQ(formatted(Point(PointCRS::Cartesian, 1.0, 2.5, 9.0)), "point({x: 1.0, y: 2.5, crs: 'cartesian'})"); + EXPECT_EQ(formatted(Point(PointCRS::Cartesian3D, 1.0, 2.5, 9.0)), "point({x: 1.0, y: 2.5, z: 9.0, crs: 'cartesian-3d'})"); + EXPECT_EQ(formatted(Point(PointCRS::WGS84, 2.3522, 48.8566, 0.0)), "point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})"); + EXPECT_EQ(formatted(Point(PointCRS::WGS84_3D, 2.3522, 48.8566, 35.0)), "point({x: 2.3522, y: 48.8566, z: 35.0, crs: 'wgs-84-3d'})"); +} + +TEST(PointTest, holdsAZOfZeroIn2D) { + const Point point(PointCRS::Cartesian, 1.0, 2.0, 9.0); + + EXPECT_EQ(point.getZ(), 0.0); + EXPECT_EQ(point, Point(PointCRS::Cartesian, 1.0, 2.0, 0.0)); +} + +TEST(PointTest, holdsNoNegativeZero) { + const Point negative(PointCRS::Cartesian3D, -0.0, -0.0, -0.0); + const Point positive(PointCRS::Cartesian3D, 0.0, 0.0, 0.0); + + EXPECT_EQ(memcmp(&negative, &positive, sizeof(Point)), 0); + EXPECT_EQ(std::hash {}(negative), std::hash {}(positive)); +} + +TEST(PointTest, wrapsALongitudeIntoTheRange) { + EXPECT_EQ(Point(PointCRS::WGS84, 190.0, 0.0, 0.0).getX(), -170.0); + EXPECT_EQ(Point(PointCRS::WGS84, -190.0, 0.0, 0.0).getX(), 170.0); + EXPECT_EQ(Point(PointCRS::WGS84, 180.0, 0.0, 0.0).getX(), 180.0); + EXPECT_EQ(Point(PointCRS::WGS84, -180.0, 0.0, 0.0).getX(), -180.0); + EXPECT_EQ(Point(PointCRS::WGS84, 720.0, 0.0, 0.0).getX(), 0.0); + EXPECT_EQ(Point(PointCRS::Cartesian, 720.0, 0.0, 0.0).getX(), 720.0); +} + +TEST(PointTest, rejectsALatitudePastAPole) { + EXPECT_THROW(Point(PointCRS::WGS84, 0.0, 90.5, 0.0), TuringException); + EXPECT_THROW(Point(PointCRS::WGS84_3D, 0.0, -90.5, 0.0), TuringException); + EXPECT_NO_THROW(Point(PointCRS::WGS84, 0.0, 90.0, 0.0)); + EXPECT_NO_THROW(Point(PointCRS::Cartesian, 0.0, 90.5, 0.0)); +} + +TEST(PointTest, rejectsANonFiniteCoordinate) { + EXPECT_THROW(Point(PointCRS::Cartesian, std::numeric_limits::infinity(), 0.0, 0.0), TuringException); + EXPECT_THROW(Point(PointCRS::Cartesian3D, 0.0, 0.0, std::numeric_limits::quiet_NaN()), TuringException); +} + +TEST(PointTest, ordersBySRIDThenByCoordinates) { + const Point wgs84(PointCRS::WGS84, 100.0, 0.0, 0.0); + const Point wgs84With3D(PointCRS::WGS84_3D, -100.0, 0.0, 0.0); + const Point cartesian(PointCRS::Cartesian, -100.0, 0.0, 0.0); + const Point cartesian3D(PointCRS::Cartesian3D, -100.0, 0.0, 0.0); + + EXPECT_LT(wgs84, wgs84With3D); + EXPECT_LT(wgs84With3D, cartesian); + EXPECT_LT(cartesian, cartesian3D); + + EXPECT_LT(Point(PointCRS::Cartesian, 1.0, 9.0, 0.0), Point(PointCRS::Cartesian, 2.0, 0.0, 0.0)); + EXPECT_LT(Point(PointCRS::Cartesian, 1.0, 1.0, 0.0), Point(PointCRS::Cartesian, 1.0, 2.0, 0.0)); + EXPECT_LT(Point(PointCRS::Cartesian3D, 1.0, 1.0, 1.0), Point(PointCRS::Cartesian3D, 1.0, 1.0, 2.0)); +} + +TEST(PointTest, namesTheCRSAndItsSRID) { + EXPECT_EQ(Point::getCRSName(PointCRS::Cartesian), "cartesian"); + EXPECT_EQ(Point::getSRID(PointCRS::Cartesian), 7203); + EXPECT_EQ(Point::getCRSName(PointCRS::Cartesian3D), "cartesian-3d"); + EXPECT_EQ(Point::getSRID(PointCRS::Cartesian3D), 9157); + EXPECT_EQ(Point::getCRSName(PointCRS::WGS84), "wgs-84"); + EXPECT_EQ(Point::getSRID(PointCRS::WGS84), 4326); + EXPECT_EQ(Point::getCRSName(PointCRS::WGS84_3D), "wgs-84-3d"); + EXPECT_EQ(Point::getSRID(PointCRS::WGS84_3D), 4979); +} + +TEST(PointTest, keepsTheOrdinalsOfItsCRSs) { + EXPECT_EQ(static_cast(PointCRS::Cartesian), 0); + EXPECT_EQ(static_cast(PointCRS::Cartesian3D), 1); + EXPECT_EQ(static_cast(PointCRS::WGS84), 2); + EXPECT_EQ(static_cast(PointCRS::WGS84_3D), 3); +} diff --git a/test/storage/QueryTypeNameTest.cpp b/test/storage/QueryTypeNameTest.cpp index 9fbb4ff7a5..95539d9cb0 100644 --- a/test/storage/QueryTypeNameTest.cpp +++ b/test/storage/QueryTypeNameTest.cpp @@ -33,6 +33,7 @@ constexpr std::array CELL_TAG_TYPES { CellTagType {CellTag::MapView, EvaluatedType::Map}, CellTagType {CellTag::Duration, EvaluatedType::Duration}, CellTagType {CellTag::Path, EvaluatedType::GraphPath}, + CellTagType {CellTag::Point, EvaluatedType::Point}, CellTagType {CellTag::INVALID, EvaluatedType::Invalid}, }; diff --git a/test/storage/dump/CellTagDumpFormatTest.cpp b/test/storage/dump/CellTagDumpFormatTest.cpp index ae2f15ad3b..2f9e1b1920 100644 --- a/test/storage/dump/CellTagDumpFormatTest.cpp +++ b/test/storage/dump/CellTagDumpFormatTest.cpp @@ -10,6 +10,7 @@ #include "map/MapContainer.h" #include "metadata/DateTime.h" #include "metadata/Duration.h" +#include "metadata/Point.h" #include "metadata/PropertyNull.h" #include "FileHash.h" #include "FilePageReader.h" @@ -132,6 +133,74 @@ TEST_F(CellTagDumpFormatTest, mapHoldingEveryCellTag) { } } +TEST_F(CellTagDumpFormatTest, listAndMapHoldingAPointOfEveryCRS) { + const fs::Path outDir(_outDir.c_str()); + const fs::Path listPath = outDir / "point_lists"; + const fs::Path mapPath = outDir / "point_maps"; + + ListContainer lists; + const std::vector items = {Point(PointCRS::Cartesian, 1.5, -2.0, 0.0), + Point(PointCRS::Cartesian3D, 1.5, -2.0, 3.25), + Point(PointCRS::WGS84, 2.3522, 48.8566, 0.0), + Point(PointCRS::WGS84_3D, 2.3522, 48.8566, 35.0)}; + + TypedPropertyContainer originalLists; + originalLists.add(EntityID(0), lists.insert(items)); + + MapContainer maps; + const std::vector entries = {{"a", Point(PointCRS::Cartesian, 1.5, -2.0, 0.0)}, + {"b", Point(PointCRS::Cartesian3D, 1.5, -2.0, 3.25)}, + {"c", Point(PointCRS::WGS84, 2.3522, 48.8566, 0.0)}, + {"d", Point(PointCRS::WGS84_3D, 2.3522, 48.8566, 35.0)}}; + + TypedPropertyContainer originalMaps; + originalMaps.add(EntityID(0), maps.insert(entries)); + + { + auto writer = fs::FilePageWriter::open(listPath); + ASSERT_TRUE(writer); + ListPropertyContainerDumper dumper(writer.value()); + ASSERT_TRUE(dumper.dump(originalLists)); + } + + { + auto writer = fs::FilePageWriter::open(mapPath); + ASSERT_TRUE(writer); + MapPropertyContainerDumper dumper(writer.value()); + ASSERT_TRUE(dumper.dump(originalMaps)); + } + + uint64_t listHash = 0; + hashDumpFileContent(listPath, listHash); + EXPECT_EQ(listHash, 1837138699016962021ull); + + uint64_t mapHash = 0; + hashDumpFileContent(mapPath, mapHash); + EXPECT_EQ(mapHash, 8917834194145396159ull); + + { + auto reader = fs::FilePageReader::open(listPath, DumpConfig::PAGE_SIZE); + ASSERT_TRUE(reader); + ListPropertyContainerLoader loader(reader.value()); + auto result = loader.load(); + ASSERT_TRUE(result); + + const auto& loaded = result.value()->cast(); + ASSERT_TRUE(PropertyContainerComparator::same(&originalLists, &loaded)); + } + + { + auto reader = fs::FilePageReader::open(mapPath, DumpConfig::PAGE_SIZE); + ASSERT_TRUE(reader); + MapPropertyContainerLoader loader(reader.value()); + auto result = loader.load(); + ASSERT_TRUE(result); + + const auto& loaded = result.value()->cast(); + ASSERT_TRUE(PropertyContainerComparator::same(&originalMaps, &loaded)); + } +} + int main(int argc, char** argv) { return turing::test::turingTestMain(argc, argv); } diff --git a/test/storage/dump/PointGraphDumpLoadTest.cpp b/test/storage/dump/PointGraphDumpLoadTest.cpp new file mode 100644 index 0000000000..0a82b9ca81 --- /dev/null +++ b/test/storage/dump/PointGraphDumpLoadTest.cpp @@ -0,0 +1,135 @@ +#include "TuringTest.h" + +#include + +#include + +#include "dump/GraphDumper.h" +#include "dump/GraphLoader.h" +#include "Path.h" +#include "TuringException.h" + +#include "Graph.h" +#include "comparators/GraphComparator.h" +#include "list/ListContainer.h" +#include "map/MapContainer.h" +#include "metadata/Point.h" +#include "writers/GraphWriter.h" + +#include "JobSystem.h" + +using namespace db; +using namespace turing::test; + +namespace { + +using ListItem = ListContainer::ListItemVariant; +using MapEntry = MapContainer::MapKeyValuePair; + +constexpr PointCRS CRS_CYCLE[] = {PointCRS::Cartesian, PointCRS::Cartesian3D, PointCRS::WGS84, PointCRS::WGS84_3D}; + +Point pointAt(size_t index) { + const double coordinate = static_cast(index) - 40.0; + const PointCRS crs = CRS_CYCLE[index % 4]; + + return Point(crs, coordinate, coordinate / 2.0, coordinate * 3.0); +} + +} + +class PointGraphDumpLoadTest : public TuringTest { +protected: + void initialize() override { + } + + void dumpAndCompare(Graph* graph) { + const fs::Path dumpPath = fs::Path {_outDir} / "dump"; + + const auto res = GraphDumper::dump(graph, dumpPath); + if (!res) { + throw TuringException("Failed to dump graph:\n" + res.error().fmtMessage()); + } + + auto loadedGraph = Graph::create(); + const auto loadRes = GraphLoader::load(loadedGraph.get(), dumpPath); + if (!loadRes) { + throw TuringException("Failed to load graph:\n" + loadRes.error().fmtMessage()); + } + + ASSERT_TRUE(GraphComparator::same(*graph, *loadedGraph)); + } +}; + +TEST_F(PointGraphDumpLoadTest, NodePoints) { + constexpr size_t nodeCount = 80; + + auto graph = Graph::create("pointgraph", fs::Path {_outDir} / "original"); + + { + JobSystem jobSystem; + jobSystem.init(); + GraphWriter writer(graph.get(), &jobSystem); + + for (size_t i = 0; i < nodeCount; i++) { + const NodeID node = writer.addNode({"City"}); + writer.addNodeProperty(node, "at", pointAt(i)); + } + + writer.submit(); + } + + dumpAndCompare(graph.get()); +} + +TEST_F(PointGraphDumpLoadTest, EdgePoints) { + constexpr size_t edgeCount = 40; + + auto graph = Graph::create("pointgraph", fs::Path {_outDir} / "original"); + + { + JobSystem jobSystem; + jobSystem.init(); + GraphWriter writer(graph.get(), &jobSystem); + + const NodeID source = writer.addNode({"City"}); + + for (size_t i = 0; i < edgeCount; i++) { + const NodeID target = writer.addNode({"City"}); + const EdgeRecord edge = writer.addEdge("ROAD", source, target); + + writer.addEdgeProperty(edge, "midpoint", pointAt(i)); + } + + writer.submit(); + } + + dumpAndCompare(graph.get()); +} + +TEST_F(PointGraphDumpLoadTest, ListsAndMapsHoldingPoints) { + constexpr size_t nodeCount = 20; + + auto graph = Graph::create("pointgraph", fs::Path {_outDir} / "original"); + ListContainer lists; + MapContainer maps; + + { + JobSystem jobSystem; + jobSystem.init(); + GraphWriter writer(graph.get(), &jobSystem); + + for (size_t i = 0; i < nodeCount; i++) { + const NodeID node = writer.addNode({"City"}); + + const std::vector route = {pointAt(i), pointAt(i + 1), static_cast(i)}; + writer.addNodeProperty(node, "route", lists.insert(route)); + + const std::vector entries = {{"at", pointAt(i)}, {"n", static_cast(i)}}; + writer.addNodeProperty(node, "attrs", maps.insert(entries)); + } + + writer.submit(); + } + + dumpAndCompare(graph.get()); +} diff --git a/test/storage/dump/PropertyDumpFormatTest.cpp b/test/storage/dump/PropertyDumpFormatTest.cpp index 743eac4eb3..168ab15509 100644 --- a/test/storage/dump/PropertyDumpFormatTest.cpp +++ b/test/storage/dump/PropertyDumpFormatTest.cpp @@ -5,6 +5,7 @@ #include "dump/PropertyContainerLoader.h" #include "comparators/PropertyContainerComparator.h" #include "embedding/EmbeddingBucket.h" +#include "metadata/Point.h" #include "FileHash.h" #include "FilePageReader.h" #include "FilePageWriter.h" @@ -144,6 +145,43 @@ TEST_F(PropertyDumpFormatTest, embeddingDumpBytes) { } } +TEST_F(PropertyDumpFormatTest, pointDumpBytes) { + const fs::Path outDir(_outDir.c_str()); + const fs::Path path = outDir / "points"; + + // 40'000 points of 32 bytes span several value pages, and cycle through every CRS so + // the ordinal each is written with shows in the bytes + constexpr PointCRS systems[] = {PointCRS::Cartesian, PointCRS::Cartesian3D, PointCRS::WGS84, PointCRS::WGS84_3D}; + + TypedPropertyContainer original; + for (size_t i = 0; i < 40'000; i++) { + const double coordinate = static_cast(i % 180) - 89.5; + original.add(EntityID(i), Point(systems[i % 4], coordinate, coordinate, static_cast(i))); + } + + { + auto writer = fs::FilePageWriter::open(path); + ASSERT_TRUE(writer); + TrivialPropertyContainerDumper dumper(writer.value()); + ASSERT_TRUE(dumper.dump(original)); + } + + uint64_t hash = 0; + hashDumpFileContent(path, hash); + EXPECT_EQ(hash, 934115674187105631ull); + + { + auto reader = fs::FilePageReader::open(path, DumpConfig::PAGE_SIZE); + ASSERT_TRUE(reader); + TrivialPropertyContainerLoader loader(reader.value()); + auto result = loader.load(); + ASSERT_TRUE(result); + + const auto& loaded = result.value()->cast(); + ASSERT_TRUE(PropertyContainerComparator::same(&original, &loaded)); + } +} + int main(int argc, char** argv) { return turing::test::turingTestMain(argc, argv); } diff --git a/tools/turingdb/TuringShell.cpp b/tools/turingdb/TuringShell.cpp index 0d52e0bee3..c27103538b 100644 --- a/tools/turingdb/TuringShell.cpp +++ b/tools/turingdb/TuringShell.cpp @@ -569,6 +569,10 @@ void asString(std::string& out, const db::Duration v) { db::Duration::format(out, v); } +void asString(std::string& out, const db::Point v) { + db::Point::format(out, v); +} + template void asString(std::string& out, const TemplateCommitHash& hash) { hash.appendString(out); diff --git a/wasm/WasmSink.h b/wasm/WasmSink.h index ebc657fd63..a60c1a3162 100644 --- a/wasm/WasmSink.h +++ b/wasm/WasmSink.h @@ -56,6 +56,8 @@ consteval ColumnType wireCodeOfElement() { return ColumnType::DATE_TIME; } else if constexpr (std::is_same_v) { return ColumnType::DURATION; + } else if constexpr (std::is_same_v) { + return ColumnType::POINT; } else if constexpr (std::is_same_v) { return ColumnType::GRAPH_PATH; } else if constexpr (std::is_same_v) { diff --git a/wasm/npm/TuringClient.d.ts b/wasm/npm/TuringClient.d.ts index 35485c3c94..8b3ae83d8f 100644 --- a/wasm/npm/TuringClient.d.ts +++ b/wasm/npm/TuringClient.d.ts @@ -24,6 +24,7 @@ export declare const ColumnType: Readonly<{ DATE_TIME: 19; DURATION: 20; GRAPH_PATH: 21; + POINT: 22; }>; export type ColumnTypeName = keyof typeof ColumnType; @@ -38,6 +39,16 @@ export declare const ColumnEncoding: Readonly<{ export type ColumnEncodingCode = (typeof ColumnEncoding)[keyof typeof ColumnEncoding]; +/** A point a query returns. z is undefined for a 2D point. */ +export declare class Point { + private constructor(); + readonly srid: 7203 | 9157 | 4326 | 4979; + readonly x: number; + readonly y: number; + readonly z: number | undefined; + readonly crs: "cartesian" | "cartesian-3d" | "wgs-84" | "wgs-84-3d"; +} + /** A row value as Column.get and Column.toArray return it. A path is its node IDs. */ export type Value = | bigint @@ -46,6 +57,7 @@ export type Value = | string | null | Float32Array + | Point | Value[] | { [key: string]: Value }; @@ -56,6 +68,7 @@ export type NumberValue = | string | null | Float32Array + | Point | NumberValue[] | { [key: string]: NumberValue }; diff --git a/wasm/npm/TuringClient.mjs b/wasm/npm/TuringClient.mjs index 3fcdb758fe..a561561b64 100644 --- a/wasm/npm/TuringClient.mjs +++ b/wasm/npm/TuringClient.mjs @@ -57,6 +57,7 @@ export const ColumnType = Object.freeze({ DATE_TIME: 19, DURATION: 20, GRAPH_PATH: 21, + POINT: 22, }); // The column types whose values the nested reader walks out of the flat nested bytes @@ -84,6 +85,12 @@ const CELL_TAG_DATETIME = 10; const CELL_TAG_MAP_VIEW = 11; const CELL_TAG_DURATION = 12; const CELL_TAG_PATH = 13; +const CELL_TAG_POINT = 14; + +// Mirrors db::PointCRS (storage/metadata/Point.h): the SRID of each CRS ordinal. +const POINT_SRIDS = [7203, 9157, 4326, 4979]; +const POINT_CRS_NAMES = { 7203: "cartesian", 9157: "cartesian-3d", 4326: "wgs-84", 4979: "wgs-84-3d" }; +const POINT_SIZE = 32; // Mirrors db::QueryStatus::Status (base/QueryStatus.h); the ERROR packet's first // payload byte indexes into this. @@ -269,6 +276,30 @@ export function double(value) { return new DoubleValue(value); } +// A point a query returns. z is undefined for a 2D point. +export class Point { + constructor(srid, x, y, z) { + this.srid = srid; + this.x = x; + this.y = y; + this.z = z; + Object.freeze(this); + } + + get crs() { + return POINT_CRS_NAMES[this.srid]; + } +} + +// A point's 32 bytes: x, y and z as doubles, then the ordinal of its CRS +function readPoint(view, offset) { + const srid = POINT_SRIDS[view.getUint8(offset + 24)]; + const isThreeDimensional = srid === 9157 || srid === 4979; + const z = isThreeDimensional ? view.getFloat64(offset + 16, true) : undefined; + + return new Point(srid, view.getFloat64(offset, true), view.getFloat64(offset + 8, true), z); +} + function throwParameterError(path, message) { throw new TuringQueryError("PARAMETER_ERROR", `Parameter ${path} ${message}`); } @@ -415,6 +446,7 @@ const FIXED_WIDTH_KINDS = { // count is handed over as it arrived so no precision is lost on the way [ColumnType.DATE_TIME]: { size: 8, wrap: (buffer) => new BigInt64Array(buffer) }, [ColumnType.DURATION]: { size: 8, wrap: (buffer) => new BigInt64Array(buffer) }, + [ColumnType.POINT]: { size: POINT_SIZE, wrap: (buffer) => new DataView(buffer), read: (view, index) => readPoint(view, index * POINT_SIZE) }, }; // Strings are decoded with one TextDecoder call over the whole column and sliced by @@ -509,6 +541,9 @@ export class NestedReader { case CELL_TAG_DOUBLE: this._cursor = payload + 8; return this._view.getFloat64(payload, true); + case CELL_TAG_POINT: + this._cursor = payload + POINT_SIZE; + return readPoint(this._view, payload); case CELL_TAG_BOOL: this._cursor = payload + 1; return this._bytes[payload] !== 0; diff --git a/wasm/npm/client_smoke.mjs b/wasm/npm/client_smoke.mjs index 52e44c1f16..4d68220f74 100644 --- a/wasm/npm/client_smoke.mjs +++ b/wasm/npm/client_smoke.mjs @@ -8,7 +8,7 @@ import assert from "assert"; -import { Column, TuringClient, TuringQueryError, double } from "./TuringClient.mjs"; +import { Column, Point, TuringClient, TuringQueryError, double } from "./TuringClient.mjs"; const MESSAGE_CHUNK_HEADER = 0; const MESSAGE_CHUNK = 1; @@ -26,6 +26,7 @@ const TYPE_NODE_ID = 9; const TYPE_PROPERTY_NULL = 17; const TYPE_MAP_VIEW = 18; const TYPE_GRAPH_PATH = 21; +const TYPE_POINT = 22; const ENCODING_VECTOR = 0; const ENCODING_OPTIONAL_VECTOR = 1; @@ -40,6 +41,10 @@ const TAG_LIST_VIEW = 6; const TAG_NULL = 7; const TAG_NODE_ID = 8; const TAG_MAP_VIEW = 11; +const TAG_POINT = 14; + +const CRS_CARTESIAN = 0; +const CRS_WGS84_3D = 3; const encoder = new TextEncoder(); @@ -96,6 +101,14 @@ class Writer { raw(byteArray) { this.bytes.push(...byteArray); } + // A point as the server stores it: x, y and z, then its CRS ordinal and 7 bytes of padding + point(crs, x, y, z) { + this.f64(x); + this.f64(y); + this.f64(z); + this.u8(crs); + this.raw(new Uint8Array(7)); + } toUint8Array() { return Uint8Array.from(this.bytes); } @@ -751,6 +764,52 @@ async function testNullGraphPath() { assert.deepStrictEqual(data, [[[[7], null]]]); } +// A point reads as a Point, in a column of its own and inside a list. A 2D point has no z. +async function testPoints() { + const header = makeHeader([ + { name: "at", typeCode: TYPE_POINT, encoding: ENCODING_VECTOR }, + { name: "route", typeCode: TYPE_LIST_VIEW, encoding: ENCODING_VECTOR }, + ]); + + const chunk = new Writer(); + chunk.u32(2); + chunk.point(CRS_CARTESIAN, 1.5, -2, 0); + chunk.point(CRS_WGS84_3D, 2.3522, 48.8566, 35); + + chunk.u32(2); + chunk.u32(1); + chunk.u32(32); + chunk.u8(TAG_POINT); + chunk.point(CRS_WGS84_3D, 2.3522, 48.8566, 35); + chunk.u32(0); + chunk.u32(0); + + const bytes = concatPackets([frame(MESSAGE_CHUNK_HEADER, header), frame(MESSAGE_CHUNK, chunk), makeEndChunk(2), makeEnd(1.0)]); + const { chunks } = await new TuringClient({ fetch: makeFakeFetch(bytes, {}) }).query("RETURN 1"); + const [at, route] = chunks[0].columns; + + const cartesian = at.get(0); + assert.ok(cartesian instanceof Point); + assert.strictEqual(cartesian.srid, 7203); + assert.strictEqual(cartesian.crs, "cartesian"); + assert.strictEqual(cartesian.x, 1.5); + assert.strictEqual(cartesian.y, -2); + assert.strictEqual(cartesian.z, undefined); + + const paris = at.get(1); + assert.strictEqual(paris.srid, 4979); + assert.strictEqual(paris.crs, "wgs-84-3d"); + assert.deepStrictEqual([paris.x, paris.y, paris.z], [2.3522, 48.8566, 35]); + + const [inList] = route.get(0); + assert.ok(inList instanceof Point); + assert.deepStrictEqual([inList.srid, inList.x, inList.y, inList.z], [4979, 2.3522, 48.8566, 35]); + assert.deepStrictEqual(route.get(1), []); + + const data = await new TuringClient({ fetch: makeFakeFetch(bytes, {}) }).queryData("RETURN 1"); + assert.ok(data[0][0][0] instanceof Point); +} + await testQuery(); await testParameters(); await testRejectsParametersAQueryCannotHold(); @@ -767,5 +826,6 @@ await testMaps(true); await testRaggedQueryData(); await testGraphPathIdsStayExact(); await testNullGraphPath(); +await testPoints(); -console.log("wasm client smoke test passed: query, parameters, buffer growth, constant nulls, optional constant lists, queryData, multi-byte body, error, rejected request, error without packets, maps, ragged, paths, null paths"); +console.log("wasm client smoke test passed: query, parameters, buffer growth, constant nulls, optional constant lists, queryData, multi-byte body, error, rejected request, error without packets, maps, ragged, paths, null paths, points");