From 7c9742da3e38503925b25183a53d4c669fa65e0c Mon Sep 17 00:00:00 2001 From: Remy Boutonnet Date: Thu, 5 Nov 2026 11:55:04 +0100 Subject: [PATCH 1/4] Storage: implement a point property type --- CLAUDE.md | 5 +- io/parquet/ParquetEdgeVisitor.cpp | 1 + io/parquet/ParquetNodeVisitor.cpp | 1 + memory/LocalMemory.h | 4 + net/decoders/ParameterMapSink.cpp | 3 + net/decoders/TuringProtoDecoder.h | 2 + net/decoders/TuringProtoDecoderConcepts.h | 1 + net/encoders/OutputValues.h | 5 + net/encoders/TuringProtoEncoder.h | 2 + net/turing_proto_common/TuringProtoHeaders.h | 1 + procedures/ProcUtils.cpp | 12 + python/turingdb/__init__.py | 3 +- python/turingdb/_common/NanobindUtils.cpp | 62 ++ python/turingdb/point.py | 16 + query/AST/FunctionDecls.cpp | 33 +- query/AST/decl/EvaluatedType.h | 6 +- query/analyzer/ExprAnalyzer.cpp | 2 + query/analyzer/WriteStmtAnalyzer.cpp | 2 + query/ir/codegen/DBProgramGenerator.cpp | 1 + query/ir/common/IRValueTypes.cpp | 6 + query/ir/dialect/db/DBColumnElement.cpp | 5 + query/ir/dialect/db/DBHeldTags.cpp | 2 + query/ir/dialect/db/DBOps.td | 4 + query/ir/dialect/nl/NLOps.td | 1 + query/ir/dialect/storage/StorageTypes.td | 9 + query/ir/exec/NLEntityProperties.cpp | 6 + query/ir/exec/NLExecutor.cpp | 84 +++ query/ir/exec/NLProgram.h | 5 + query/ir/exec/NLTranslator.cpp | 7 + query/ir/exec/NLWriteProperties.cpp | 3 + query/ir/exec/NLWrittenValues.cpp | 1 + query/ir/lowering/DBLowering.cpp | 1 + samples/list-buffer/main.cpp | 5 + samples/mlir/main.cpp | 8 + storage/CMakeLists.txt | 1 + storage/CellOrder.cpp | 18 + storage/CellOrder.h | 6 + storage/CellTag.h | 2 + storage/CellUtils.h | 8 + storage/DebugDump.cpp | 7 + storage/DebugDump.h | 2 + storage/QueryTypeName.cpp | 6 + storage/columns/AllowedKinds.h | 12 + storage/columns/BinaryOperators.h | 1 + storage/columns/ColumnDispatcher.h | 2 + storage/columns/Functions.h | 48 +- storage/columns/InternalKind.h | 2 + .../comparators/PropertyContainerComparator.h | 9 + storage/dump/DataPartDumper.cpp | 7 + storage/dump/DataPartLoader.cpp | 8 + storage/dump/PropertyContainerDumpConstants.h | 4 + storage/indexes/IndexManager.cpp | 2 + storage/indexes/PropertyHashIndex.cpp | 2 + .../GetPropertiesWithNullIterator.cpp | 4 + .../iterators/ScanEdgePropertiesIterator.cpp | 2 + .../ScanNodePropertiesByLabelIterator.cpp | 2 + .../iterators/ScanNodePropertiesIterator.cpp | 2 + storage/list/EncodedList.cpp | 6 + storage/list/ListBuffer.h | 2 +- storage/list/ListByteBuffer.cpp | 1 + storage/list/ListHash.cpp | 3 + storage/map/EncodedMap.cpp | 6 + storage/map/MapBuffer.h | 3 +- storage/map/MapByteBuffer.cpp | 1 + storage/map/MapHash.cpp | 6 + storage/map/MapWriteCursor.cpp | 1 + storage/metadata/Point.cpp | 541 ++++++++++++++++++ storage/metadata/Point.h | 90 +++ storage/metadata/PropertyType.h | 15 +- storage/metadata/PropertyTypeDispatcher.h | 2 + storage/properties/PropertyManager.cpp | 5 + storage/properties/PropertyManager.h | 3 + storage/reader/GraphReader.cpp | 2 + storage/versioning/CommitBuilder.cpp | 2 + storage/versioning/CommitWriteBuffer.cpp | 4 + storage/versioning/CommitWriteBuffer.h | 1 + storage/versioning/DataPartRebaser.cpp | 18 +- storage/versioning/VersionController.cpp | 2 + storage/views/EntityPropertyView.h | 2 +- storage/views/PropertyView.h | 1 + storage/writers/DataPartBuilder.cpp | 1 + storage/writers/GraphWriter.cpp | 4 + test/net/TuringProtoRoundTripTest.cpp | 72 +++ .../query-test-suite/QueryResultFormatter.cpp | 7 + test/query/ir/CMakeLists.txt | 1 + test/query/ir/IRTestRows.cpp | 11 +- test/query/ir/StringRowSink.cpp | 5 + test/query/ir/engine/EquivalenceTest.cpp | 7 + test/query/ir/point/CMakeLists.txt | 5 + test/query/ir/point/PointPropertyTest.cpp | 347 +++++++++++ test/query/ir/point/PointValueFlowTest.cpp | 238 ++++++++ test/storage/CMakeLists.txt | 2 + test/storage/PointTest.cpp | 100 ++++ test/storage/QueryTypeNameTest.cpp | 1 + test/storage/dump/CellTagDumpFormatTest.cpp | 69 +++ test/storage/dump/PointGraphDumpLoadTest.cpp | 135 +++++ test/storage/dump/PropertyDumpFormatTest.cpp | 38 ++ tools/turingdb/TuringShell.cpp | 4 + wasm/WasmSink.h | 2 + wasm/npm/TuringClient.d.ts | 13 + wasm/npm/TuringClient.mjs | 35 ++ wasm/npm/client_smoke.mjs | 64 ++- 102 files changed, 2315 insertions(+), 29 deletions(-) create mode 100644 python/turingdb/point.py create mode 100644 storage/metadata/Point.cpp create mode 100644 storage/metadata/Point.h create mode 100644 test/query/ir/point/CMakeLists.txt create mode 100644 test/query/ir/point/PointPropertyTest.cpp create mode 100644 test/query/ir/point/PointValueFlowTest.cpp create mode 100644 test/storage/PointTest.cpp create mode 100644 test/storage/dump/PointGraphDumpLoadTest.cpp 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"); From 27aab7374ac7b0652ce665cbd95fb81018d28332 Mon Sep 17 00:00:00 2001 From: Remy Boutonnet Date: Thu, 5 Nov 2026 12:01:52 +0100 Subject: [PATCH 2/4] Cypher: read the components of a point --- docs/maps.md | 10 +- query/AST/expr/PropertyExpr.cpp | 5 + query/AST/expr/PropertyExpr.h | 6 + query/AST/expr/PropertyLookupExpr.cpp | 5 + query/AST/expr/PropertyLookupExpr.h | 7 + .../expr/StructuralExpressionComparator.cpp | 15 +- query/analyzer/ExprAnalyzer.cpp | 65 +++++- query/ir/codegen/DBProgramGenerator.cpp | 50 +++- query/ir/codegen/DBProgramGenerator.h | 2 + query/ir/dialect/db/DBColumnElement.cpp | 9 + query/ir/dialect/db/DBOps.td | 12 + query/ir/dialect/nl/NLOps.td | 9 + query/ir/exec/NLExecutor.cpp | 42 +++- query/ir/exec/NLProgram.h | 15 +- query/ir/exec/NLTranslator.cpp | 10 + query/ir/lowering/DBLowering.cpp | 9 + storage/columns/Functions.h | 53 +++++ test/query/ir/map/MapProjectionTest.cpp | 2 +- .../ir/map/TemporalMapProjectionTest.cpp | 2 +- test/query/ir/point/CMakeLists.txt | 3 + test/query/ir/point/PointComponentTest.cpp | 218 ++++++++++++++++++ 21 files changed, 519 insertions(+), 30 deletions(-) create mode 100644 test/query/ir/point/PointComponentTest.cpp diff --git a/docs/maps.md b/docs/maps.md index a7a9400b89..13846c455b 100644 --- a/docs/maps.md +++ b/docs/maps.md @@ -128,11 +128,11 @@ as `[u64 length][EncodedMap bytes]`. # Map projection `v {.name, .*, rank: v.age + 1, x}` builds a map out of `v`, a node, a relationship, a map, a -datetime or a duration. `.key` holds `v.key`, null where `v` has no such property, and `.*` every -property of `v`. On a datetime or a duration `.key` holds the component `v.key` reads: -`WITH duration({hours: 1}) AS d RETURN d {.hours, x: 1}` is `{hours: 1, x: 1}`. A temporal has -no properties, so `.*` and a name that is no component are rejected by the analyzer where `v` is -typed a datetime or a duration, and raise on the row where a tagged cell holds one. +datetime, a duration or a point. `.key` holds `v.key`, null where `v` has no such property, and +`.*` every property of `v`. On a datetime, a duration or a point `.key` holds the component +`v.key` reads: `WITH duration({hours: 1}) AS d RETURN d {.hours, x: 1}` is `{hours: 1, x: 1}`. +These values have no properties, so `.*` and a name that is no component are rejected by the +analyzer where `v` is typed one of them, and raise on the row where a tagged cell holds one. `key: expr` holds the expression, and `x` the variable `x` under the key `x`. A later entry shadows an earlier one under the same key, as in a map literal: `v {x: 1 / 0, x: 2}` is `{x: 2}`, and `v {.name, name: 1 / 0}` raises. An entry wins over the property `.*` reads under diff --git a/query/AST/expr/PropertyExpr.cpp b/query/AST/expr/PropertyExpr.cpp index 44a4b4b257..8f3bfaa6d8 100644 --- a/query/AST/expr/PropertyExpr.cpp +++ b/query/AST/expr/PropertyExpr.cpp @@ -23,6 +23,11 @@ void PropertyExpr::setDurationPart(DurationPart part) { _readsADurationComponent = true; } +void PropertyExpr::setPointPart(PointPart part) { + _pointPart = part; + _readsAPointComponent = true; +} + void PropertyExpr::setMapKey(std::string_view key) { _mapKey = key; _readsAMapKey = true; diff --git a/query/AST/expr/PropertyExpr.h b/query/AST/expr/PropertyExpr.h index 8d7468349f..f03f79ed77 100644 --- a/query/AST/expr/PropertyExpr.h +++ b/query/AST/expr/PropertyExpr.h @@ -45,6 +45,10 @@ class PropertyExpr : public Expr { DurationPart getDurationPart() const { return _durationPart; } void setDurationPart(DurationPart part); + bool readsAPointComponent() const { return _readsAPointComponent; } + PointPart getPointPart() const { return _pointPart; } + void setPointPart(PointPart part); + // The key a map variable is read under, m.key. The key is meaningless unless this says so. bool readsAMapKey() const { return _readsAMapKey; } std::string_view getMapKey() const { return _mapKey; } @@ -66,9 +70,11 @@ class PropertyExpr : public Expr { ValueType _valueType {ValueType::Invalid}; DateTimePart _dateTimePart {DateTimePart::Year}; DurationPart _durationPart {DurationPart::Years}; + PointPart _pointPart {PointPart::X}; bool _stringTableHeaderAccess {false}; bool _readsADateTimeComponent {false}; bool _readsADurationComponent {false}; + bool _readsAPointComponent {false}; bool _readsAMapKey {false}; PropertyExpr(QualifiedName* name) diff --git a/query/AST/expr/PropertyLookupExpr.cpp b/query/AST/expr/PropertyLookupExpr.cpp index 2355c01006..9a7842747f 100644 --- a/query/AST/expr/PropertyLookupExpr.cpp +++ b/query/AST/expr/PropertyLookupExpr.cpp @@ -30,6 +30,11 @@ void PropertyLookupExpr::setDurationPart(DurationPart part) { _readsADurationComponent = true; } +void PropertyLookupExpr::setPointPart(PointPart part) { + _pointPart = part; + _readsAPointComponent = true; +} + void PropertyLookupExpr::setReadsAMapKey() { _readsAMapKey = true; } diff --git a/query/AST/expr/PropertyLookupExpr.h b/query/AST/expr/PropertyLookupExpr.h index 09df10d8e0..74ffc98cbb 100644 --- a/query/AST/expr/PropertyLookupExpr.h +++ b/query/AST/expr/PropertyLookupExpr.h @@ -4,6 +4,7 @@ #include "metadata/DateTime.h" #include "metadata/Duration.h" +#include "metadata/Point.h" #include "metadata/PropertyType.h" namespace db { @@ -30,6 +31,10 @@ class PropertyLookupExpr : public Expr { DurationPart getDurationPart() const { return _durationPart; } void setDurationPart(DurationPart part); + bool readsAPointComponent() const { return _readsAPointComponent; } + PointPart getPointPart() const { return _pointPart; } + void setPointPart(PointPart part); + bool readsAMapKey() const { return _readsAMapKey; } void setReadsAMapKey(); @@ -39,8 +44,10 @@ class PropertyLookupExpr : public Expr { ValueType _valueType {ValueType::Invalid}; DateTimePart _dateTimePart {DateTimePart::Year}; DurationPart _durationPart {DurationPart::Years}; + PointPart _pointPart {PointPart::X}; bool _readsADateTimeComponent {false}; bool _readsADurationComponent {false}; + bool _readsAPointComponent {false}; bool _readsAMapKey {false}; PropertyLookupExpr(Expr* base, std::string_view propName); diff --git a/query/AST/expr/StructuralExpressionComparator.cpp b/query/AST/expr/StructuralExpressionComparator.cpp index d3f5fb2131..e82af6ded3 100644 --- a/query/AST/expr/StructuralExpressionComparator.cpp +++ b/query/AST/expr/StructuralExpressionComparator.cpp @@ -122,11 +122,15 @@ bool StructuralExpressionComparator::equal(const Expr* lhs, const Expr* rhs) { const bool rhsReadsADateTime = rhsProperty->readsADateTimeComponent(); const bool lhsReadsADuration = lhsProperty->readsADurationComponent(); const bool rhsReadsADuration = rhsProperty->readsADurationComponent(); + const bool lhsReadsAPoint = lhsProperty->readsAPointComponent(); + const bool rhsReadsAPoint = rhsProperty->readsAPointComponent(); const bool neitherReadsAComponent = !lhsReadsADateTime && !rhsReadsADateTime && !lhsReadsADuration - && !rhsReadsADuration; + && !rhsReadsADuration + && !lhsReadsAPoint + && !rhsReadsAPoint; const bool sameDateTimeComponent = lhsReadsADateTime && rhsReadsADateTime @@ -136,7 +140,14 @@ bool StructuralExpressionComparator::equal(const Expr* lhs, const Expr* rhs) { && rhsReadsADuration && lhsProperty->getDurationPart() == rhsProperty->getDurationPart(); - const bool sameComponent = neitherReadsAComponent || sameDateTimeComponent || sameDurationComponent; + const bool samePointComponent = lhsReadsAPoint + && rhsReadsAPoint + && lhsProperty->getPointPart() == rhsProperty->getPointPart(); + + const bool sameComponent = neitherReadsAComponent + || sameDateTimeComponent + || sameDurationComponent + || samePointComponent; // And two keys of one map are two values for the same reason. The key is empty // on every read that is not one, so the comparison stands for all of them. diff --git a/query/analyzer/ExprAnalyzer.cpp b/query/analyzer/ExprAnalyzer.cpp index 76eda3f484..8457964b5f 100644 --- a/query/analyzer/ExprAnalyzer.cpp +++ b/query/analyzer/ExprAnalyzer.cpp @@ -42,6 +42,7 @@ #include "embedding/EmbeddingBucket.h" #include "metadata/DateTime.h" #include "metadata/Duration.h" +#include "metadata/Point.h" #include "StringBucket.h" #include "expr/All.h" @@ -64,6 +65,20 @@ constexpr std::string_view mapKeyNotAProperty = "A map key is a value inside a property, not a property of its own. " "Only a property can be set, removed or indexed."; +constexpr std::string_view pointComponentNotAProperty = + "A point component is computed from a property, not stored as one. " + "Only a property can be set, removed or indexed."; + +EvaluatedType pointComponentType(PointPart part) { + if (part == PointPart::CRS) { + return EvaluatedType::String; + } else if (part == PointPart::SRID) { + return EvaluatedType::Integer; + } + + return EvaluatedType::Double; +} + // The type a reduce's accumulator takes once it has held @param accumulator and then // @param value: their common type, an integer beside a double widening as the arithmetic // updating it does, and a tagged cell when the two share no column type @@ -579,12 +594,15 @@ ValueType ExprAnalyzer::analyzePropertyExpr(PropertyExpr* expr, bool allowCreate throwTargetInsideAProperty(expr, varDecl); } - // d.year or d.hours, where d was bound to an instant or a duration rather than to an entity + // d.year, d.hours or p.x, where d or p was bound to an instant, a duration or a point + // rather than to an entity const bool bindsAValueWithComponents = varType == EvaluatedType::DateTime - || varType == EvaluatedType::Duration; + || varType == EvaluatedType::Duration + || varType == EvaluatedType::Point; if (bindsAValueWithComponents) { const std::string_view name = propName->getName(); + EvaluatedType componentType = EvaluatedType::Integer; if (varType == EvaluatedType::DateTime) { DateTimePart part {DateTimePart::Year}; @@ -593,22 +611,30 @@ ValueType ExprAnalyzer::analyzePropertyExpr(PropertyExpr* expr, bool allowCreate } expr->setDateTimePart(part); - } else { + } else if (varType == EvaluatedType::Duration) { DurationPart part {DurationPart::Years}; if (!Duration::partNamed(name, part)) { throwError(fmt::format("'{}' is not a component of a duration", name), expr); } expr->setDurationPart(part); + } else { + PointPart part {PointPart::X}; + if (!Point::partNamed(name, part)) { + throwError(fmt::format("'{}' is not a component of a point", name), expr); + } + + expr->setPointPart(part); + componentType = pointComponentType(part); } expr->setEntityVarDecl(varDecl); - expr->setType(EvaluatedType::Integer); + expr->setType(componentType); expr->setDynamic(); - expr->setExprVarDecl(_ctxt->createUnnamedVariable(_ast, EvaluatedType::Integer)); + expr->setExprVarDecl(_ctxt->createUnnamedVariable(_ast, componentType)); - return ValueType::Int64; + return *toValueType(componentType); } if (varType == EvaluatedType::StringTable) { @@ -752,8 +778,10 @@ void ExprAnalyzer::throwTargetInsideAProperty(const PropertyExpr* expr, const Va DateTimePart part {DateTimePart::Year}; DurationPart durationPart {DurationPart::Years}; + PointPart pointPart {PointPart::X}; const bool namesADateTimePart = DateTime::partNamed(componentName, part); const bool namesADurationPart = Duration::partNamed(componentName, durationPart); + const bool namesAPointPart = Point::partNamed(componentName, pointPart); const EvaluatedType varType = varDecl->getType(); const bool readsAMapVariable = varType == EvaluatedType::Map || varType == EvaluatedType::Any; @@ -765,8 +793,10 @@ void ExprAnalyzer::throwTargetInsideAProperty(const PropertyExpr* expr, const Va throwError(datetimeComponentNotAProperty, expr); } else if (namesADurationPart) { throwError("A duration component cannot name a property.", expr); + } else if (namesAPointPart) { + throwError(pointComponentNotAProperty, expr); } else { - throwError(fmt::format("'{}' is not a component of a datetime or a duration", componentName), expr); + throwError(fmt::format("'{}' is not a component of a datetime, a duration or a point", componentName), expr); } } @@ -777,6 +807,8 @@ void ExprAnalyzer::throwIfReadsPartOfAProperty(const PropertyExpr* expr) { throwError(datetimeComponentNotAProperty, expr); } else if (expr->readsADurationComponent()) { throwError("A duration component cannot name a property.", expr); + } else if (expr->readsAPointComponent()) { + throwError(pointComponentNotAProperty, expr); } else if (expr->readsAMapKey()) { throwError(mapKeyNotAProperty, expr); } @@ -814,6 +846,14 @@ void ExprAnalyzer::analyzePropertyLookupExpr(PropertyLookupExpr* expr) { expr->setDurationPart(part); type = EvaluatedType::Integer; + } else if (baseType == EvaluatedType::Point) { + PointPart part {PointPart::X}; + if (!Point::partNamed(propName, part)) { + throwError(fmt::format("'{}' is not a component of a point", propName), expr); + } + + expr->setPointPart(part); + type = pointComponentType(part); } else if (readsAnEntity) { const ValueType valueType = resolvePropertyType(propName); @@ -2022,12 +2062,13 @@ void ExprAnalyzer::analyzeMapProjectionExpr(MapProjectionExpr* expr) { || baseType == EvaluatedType::EdgePattern; const bool projectsAMap = baseType == EvaluatedType::Map || baseType == EvaluatedType::Any; - const bool projectsATemporal = baseType == EvaluatedType::DateTime - || baseType == EvaluatedType::Duration; + const bool projectsComponents = baseType == EvaluatedType::DateTime + || baseType == EvaluatedType::Duration + || baseType == EvaluatedType::Point; const bool projectsNull = baseType == EvaluatedType::Null; - if (!projectsAnEntity && !projectsAMap && !projectsATemporal && !projectsNull) { - throwError(fmt::format("A map projection reads a node, a relationship, a map, a datetime or a duration, " + if (!projectsAnEntity && !projectsAMap && !projectsComponents && !projectsNull) { + throwError(fmt::format("A map projection reads a node, a relationship, a map, a datetime, a duration or a point, " "and '{}' is '{}'", baseSymbol->getName(), EvaluatedTypeName::value(baseType)), @@ -2040,7 +2081,7 @@ void ExprAnalyzer::analyzeMapProjectionExpr(MapProjectionExpr* expr) { const bool readsAProperty = item._kind == MapProjectionExpr::ItemKind::Property; const bool readsEveryProperty = item._kind == MapProjectionExpr::ItemKind::AllProperties; - if (readsEveryProperty && projectsATemporal) { + if (readsEveryProperty && projectsComponents) { throwError(fmt::format("A map projection reads .* off a node, a relationship or a map, and '{}' is '{}'", baseSymbol->getName(), EvaluatedTypeName::value(baseType)), diff --git a/query/ir/codegen/DBProgramGenerator.cpp b/query/ir/codegen/DBProgramGenerator.cpp index f8b906ff67..47c15cfeba 100644 --- a/query/ir/codegen/DBProgramGenerator.cpp +++ b/query/ir/codegen/DBProgramGenerator.cpp @@ -9858,6 +9858,8 @@ mlir::Value DBProgramGenerator::translatePropertyExpr(const PropertyExpr* propEx return emitDateTimeComponent(propExpr->getDateTimePart(), value); } else if (propExpr->readsADurationComponent()) { return emitDurationComponent(propExpr->getDurationPart(), value); + } else if (propExpr->readsAPointComponent()) { + return emitPointComponent(propExpr->getPointPart(), value); } return value; @@ -9884,6 +9886,8 @@ mlir::Value DBProgramGenerator::translatePropertyLookupExpr(const PropertyLookup return emitDateTimeComponent(lookupExpr->getDateTimePart(), baseColumn); } else if (lookupExpr->readsADurationComponent()) { return emitDurationComponent(lookupExpr->getDurationPart(), baseColumn); + } else if (lookupExpr->readsAPointComponent()) { + return emitPointComponent(lookupExpr->getPointPart(), baseColumn); } else if (readType == EvaluatedType::Any) { return emitStaticMapKey(baseColumn, lookupExpr->getPropName()); } @@ -9967,6 +9971,47 @@ mlir::Value DBProgramGenerator::emitDateTimeComponent(DateTimePart part, mlir::V return mlir::Value(); } +mlir::Value DBProgramGenerator::emitPointComponent(PointPart part, mlir::Value point) { + const mlir::Location loc = _opBuilder.getUnknownLoc(); + const mlir::db::ColumnType noneType = allocColumnType(mlir::NoneType::get(_mlirCtxt)); + + switch (part) { + case PointPart::X: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::Y: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::Z: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::Longitude: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::Latitude: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::Height: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::CRS: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + + case PointPart::SRID: + return _opBuilder.create(loc, noneType, point).getResult(); + break; + } + + return mlir::Value(); +} + mlir::Value DBProgramGenerator::emitDurationComponent(DurationPart part, mlir::Value duration) { const mlir::Location loc = _opBuilder.getUnknownLoc(); const mlir::db::ColumnType noneType = allocColumnType(mlir::NoneType::get(_mlirCtxt)); @@ -10053,12 +10098,13 @@ mlir::Value DBProgramGenerator::translatePropertyRead(const PropertyExpr* propEx const std::string_view varName = entityDecl->getName(); const std::string_view propName = propExpr->getPropName(); - // d.year, d.hours and m.key name no property: the value the component or the key is - // read off is the column the variable itself was bound to + // d.year, d.hours, p.x and m.key name no property: the value the component or the key + // is read off is the column the variable itself was bound to const EvaluatedType entityType = entityDecl->getType(); const bool readsOffTheVariableItself = entityType == EvaluatedType::DateTime || entityType == EvaluatedType::Duration + || entityType == EvaluatedType::Point || entityType == EvaluatedType::Map || entityType == EvaluatedType::Any; diff --git a/query/ir/codegen/DBProgramGenerator.h b/query/ir/codegen/DBProgramGenerator.h index 5ccfffd6de..dfddea64fd 100644 --- a/query/ir/codegen/DBProgramGenerator.h +++ b/query/ir/codegen/DBProgramGenerator.h @@ -23,6 +23,7 @@ #include "expr/CaseExpr.h" #include "stmt/SetItem.h" #include "metadata/DateTime.h" +#include "metadata/Point.h" #include "ExplainRequest.h" #include "InsertionOrderedMap.h" @@ -1413,6 +1414,7 @@ class DBProgramGenerator { mlir::Value emitDynamicMapKey(mlir::Value map, mlir::Value key); mlir::Value emitDateTimeComponent(DateTimePart part, mlir::Value instant); mlir::Value emitDurationComponent(DurationPart part, mlir::Value duration); + mlir::Value emitPointComponent(PointPart part, mlir::Value point); mlir::Value translateEntityTypeExpr(const EntityTypeExpr* typeExpr); // labels() / type() of an entity the CREATE wrote, as the constant string it reads: diff --git a/query/ir/dialect/db/DBColumnElement.cpp b/query/ir/dialect/db/DBColumnElement.cpp index 33bf79c321..190fae3061 100644 --- a/query/ir/dialect/db/DBColumnElement.cpp +++ b/query/ir/dialect/db/DBColumnElement.cpp @@ -176,6 +176,15 @@ const std::unordered_map elementRules = { {"db.duration_milliseconds_of_second", &integerElement}, {"db.duration_microseconds_of_second", &integerElement}, + {"db.point_x", &floatElement}, + {"db.point_y", &floatElement}, + {"db.point_z", &floatElement}, + {"db.point_longitude", &floatElement}, + {"db.point_latitude", &floatElement}, + {"db.point_height", &floatElement}, + {"db.point_crs", &stringElement}, + {"db.point_srid", &integerElement}, + {"db.to_string", &stringElement}, {"db.to_integer_or_null", &integerElement}, {"db.to_float_or_null", &floatElement}, diff --git a/query/ir/dialect/db/DBOps.td b/query/ir/dialect/db/DBOps.td index fc2294a22f..5695a48245 100644 --- a/query/ir/dialect/db/DBOps.td +++ b/query/ir/dialect/db/DBOps.td @@ -3048,6 +3048,18 @@ def DurationSecondsOfMinute : TypedUnaryFunctionOp<"duration_seconds_of_minute", def DurationMillisecondsOfSecond : TypedUnaryFunctionOp<"duration_milliseconds_of_second", "::mlir::storage::DurationType">; def DurationMicrosecondsOfSecond : TypedUnaryFunctionOp<"duration_microseconds_of_second", "::mlir::storage::DurationType">; +// The fields of a point: its coordinates as float columns, the name of its CRS as a string +// column and its SRID as an integer column. A 2D point has no z or height and a cartesian +// one no longitude, latitude or height, so those four raise over a column of points too. +def PointX : TypedUnaryFunctionOp<"point_x", "::mlir::storage::PointType">; +def PointY : TypedUnaryFunctionOp<"point_y", "::mlir::storage::PointType">; +def PointZ : UnaryFunctionOp<"point_z", [MayRaise]>; +def PointLongitude : UnaryFunctionOp<"point_longitude", [MayRaise]>; +def PointLatitude : UnaryFunctionOp<"point_latitude", [MayRaise]>; +def PointHeight : UnaryFunctionOp<"point_height", [MayRaise]>; +def PointCRSName : TypedUnaryFunctionOp<"point_crs", "::mlir::storage::PointType">; +def PointSRID : TypedUnaryFunctionOp<"point_srid", "::mlir::storage::PointType">; + def CurrentDateTime : TuringOp<"current_datetime", [Pure, ConstantThroughOperands]> { let summary = "The instant the query runs, as a constant column"; let description = [{ diff --git a/query/ir/dialect/nl/NLOps.td b/query/ir/dialect/nl/NLOps.td index ae2b1e7d5e..8fb90ece47 100644 --- a/query/ir/dialect/nl/NLOps.td +++ b/query/ir/dialect/nl/NLOps.td @@ -3163,6 +3163,15 @@ def DurationSecondsOfMinute : NLUnaryFunctionOp<"duration_seconds_of_minute", [C def DurationMillisecondsOfSecond : NLUnaryFunctionOp<"duration_milliseconds_of_second", [ConstantThroughOperands]>; def DurationMicrosecondsOfSecond : NLUnaryFunctionOp<"duration_microseconds_of_second", [ConstantThroughOperands]>; +def PointX : NLUnaryFunctionOp<"point_x", [ConstantThroughOperands]>; +def PointY : NLUnaryFunctionOp<"point_y", [ConstantThroughOperands]>; +def PointZ : NLUnaryFunctionOp<"point_z", [ConstantThroughOperands]>; +def PointLongitude : NLUnaryFunctionOp<"point_longitude", [ConstantThroughOperands]>; +def PointLatitude : NLUnaryFunctionOp<"point_latitude", [ConstantThroughOperands]>; +def PointHeight : NLUnaryFunctionOp<"point_height", [ConstantThroughOperands]>; +def PointCRSName : NLUnaryFunctionOp<"point_crs", [ConstantThroughOperands]>; +def PointSRID : NLUnaryFunctionOp<"point_srid", [ConstantThroughOperands]>; + def CurrentDateTime : NLOp<"current_datetime", [Pure, ConstantThroughOperands, RowAlignedThroughOperands]> { let summary = "The instant the query runs, as a single-value chunk"; let description = [{ diff --git a/query/ir/exec/NLExecutor.cpp b/query/ir/exec/NLExecutor.cpp index 02edb3ad7b..67e0b4ecd7 100644 --- a/query/ir/exec/NLExecutor.cpp +++ b/query/ir/exec/NLExecutor.cpp @@ -5568,6 +5568,9 @@ NLKeyedValue readKeyedCell(const CellView cell) { case CellTag::Duration: return cell.getAs(); break; + case CellTag::Point: + return cell.getAs(); + break; case CellTag::Null: return std::monostate {}; break; @@ -5579,7 +5582,6 @@ NLKeyedValue readKeyedCell(const CellView cell) { case CellTag::Embedding: case CellTag::ListView: case CellTag::Path: - case CellTag::Point: return NLKeylessValue {tag}; break; case CellTag::INVALID: @@ -5672,8 +5674,34 @@ CellView componentCell(Owner value, return map.front().getValue(); } +// componentCell's sibling for a point, whose components differ in type: a coordinate is a +// double, the CRS a name, the SRID an integer +CellView pointComponentCell(const Point value, + std::optional part, + std::string_view key, + MapBuffer<>& mapBuffer) { + if (!part) { + throw IRException(fmt::format("'{}' is not a component of a point", key)); + } + + MapBuffer<>::MapKeyValuePair component {.key = key, .value = PropertyNull {}}; + + if (*part == PointPart::CRS) { + component.value = Point::getCRSName(value.getCRS()); + } else if (*part == PointPart::SRID) { + component.value = Point::getSRID(value.getCRS()); + } else { + component.value = Point::coordinate(value, *part); + } + + const MapView map = mapBuffer.insert(std::span::MapKeyValuePair> {&component, 1}); + + return map.front().getValue(); +} + // The value a property read finds in @param keyed: a key of its map, a property of its node -// or edge, or a component of its instant or duration. Null where it holds null or no such key. +// or edge, or a component of its instant, duration or point. Null where it holds null or no +// such key. CellView readStaticKey(const NLKeyedValue& keyed, const NLStaticMapKeyData* mapKey, NLEntityProperties& properties, @@ -5684,6 +5712,8 @@ CellView readStaticKey(const NLKeyedValue& keyed, return componentCell(*instant, mapKey->getDateTimePart(), key, "datetime", mapBuffer); } else if (const Duration* duration = std::get_if(&keyed)) { return componentCell(*duration, mapKey->getDurationPart(), key, "duration", mapBuffer); + } else if (const Point* point = std::get_if(&keyed)) { + return pointComponentCell(*point, mapKey->getPointPart(), key, mapBuffer); } else if (const NLKeylessValue* keyless = std::get_if(&keyed)) { throw IRException(fmt::format("Type mismatch: a value of type '{}' has no property '{}'", queryTypeName(keyless->_tag), @@ -11418,6 +11448,14 @@ template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); +template NLUnaryFunctionKernel NLExecutor::selectFunction>(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectFunction(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectFunction(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); template NLUnaryFunctionKernel NLExecutor::selectFunction(const Column* input, bool inputNullable, LocalMemory* memory, Column*& result); diff --git a/query/ir/exec/NLProgram.h b/query/ir/exec/NLProgram.h index 96199b8684..e8bc4edb74 100644 --- a/query/ir/exec/NLProgram.h +++ b/query/ir/exec/NLProgram.h @@ -39,6 +39,7 @@ #include "list/PathTrie.h" #include "metadata/DateTime.h" #include "metadata/Duration.h" +#include "metadata/Point.h" #include "metadata/LabelSet.h" #include "metadata/LabelSetHandle.h" #include "metadata/PropertyType.h" @@ -4425,9 +4426,9 @@ struct NLKeylessValue { }; // What one row of an nl.static_map_key or an nl.dynamic_map_key reads its key in: a map, the -// node or the edge whose properties are its keys, or the instant or the duration whose -// components are. Nothing where the row holds null. -using NLKeyedValue = std::variant; +// node or the edge whose properties are its keys, or the instant, the duration or the point +// whose components are. Nothing where the row holds null. +using NLKeyedValue = std::variant; // The keyed value one column of a map key read holds at @param row. One per column shape, // selected during translation. @@ -4439,8 +4440,8 @@ using NLStringReadFunction = std::optional (*)(const C size_t row); // Row-wise map key read (nl.static_map_key): row r of the result views the value row r's map -// holds under the key, or a null cell where it holds none. The components of an instant and -// of a duration the key names are resolved during translation. +// holds under the key, or a null cell where it holds none. The components of an instant, a +// duration and a point the key names are resolved during translation. class NLStaticMapKeyData : public NLFunctionData { public: NLStaticMapKeyData(const Column* input, @@ -4449,6 +4450,7 @@ class NLStaticMapKeyData : public NLFunctionData { std::string_view key, std::optional dateTimePart, std::optional durationPart, + std::optional pointPart, NLStepBuffers* stepBuffers) : _input(input), _result(result), @@ -4456,6 +4458,7 @@ class NLStaticMapKeyData : public NLFunctionData { _key(key), _dateTimePart(dateTimePart), _durationPart(durationPart), + _pointPart(pointPart), _stepBuffers(stepBuffers) { } @@ -4466,6 +4469,7 @@ class NLStaticMapKeyData : public NLFunctionData { std::string_view getKey() const { return _key; } std::optional getDateTimePart() const { return _dateTimePart; } std::optional getDurationPart() const { return _durationPart; } + std::optional getPointPart() const { return _pointPart; } NLStepBuffers* getStepBuffers() const { return _stepBuffers; } private: @@ -4475,6 +4479,7 @@ class NLStaticMapKeyData : public NLFunctionData { std::string_view _key {}; std::optional _dateTimePart; std::optional _durationPart; + std::optional _pointPart; NLStepBuffers* _stepBuffers {nullptr}; }; diff --git a/query/ir/exec/NLTranslator.cpp b/query/ir/exec/NLTranslator.cpp index b18237eefa..a272ac8f79 100644 --- a/query/ir/exec/NLTranslator.cpp +++ b/query/ir/exec/NLTranslator.cpp @@ -408,6 +408,15 @@ const std::unordered_map unaryFunctio {"nl.duration_milliseconds_of_second", &NLExecutor::selectFunction>}, {"nl.duration_microseconds_of_second", &NLExecutor::selectFunction>}, + {"nl.point_x", &NLExecutor::selectFunction>}, + {"nl.point_y", &NLExecutor::selectFunction>}, + {"nl.point_z", &NLExecutor::selectFunction>}, + {"nl.point_longitude", &NLExecutor::selectFunction>}, + {"nl.point_latitude", &NLExecutor::selectFunction>}, + {"nl.point_height", &NLExecutor::selectFunction>}, + {"nl.point_crs", &NLExecutor::selectFunction>}, + {"nl.point_srid", &NLExecutor::selectFunction>}, + {"nl.element_id", &NLExecutor::selectId}, {"nl.size", &NLExecutor::selectSize}, {"nl.is_empty", &NLExecutor::selectIsEmpty}, @@ -4097,6 +4106,7 @@ void NLTranslator::translateStaticMapKey(nl::StaticMapKey mapKey, NLStmtContaine key, componentNamed(key), componentNamed(key), + componentNamed(key), _program->allocStepBuffers()); body->emplaceStmt(&NLExecutor::runStaticMapKey, data); diff --git a/query/ir/lowering/DBLowering.cpp b/query/ir/lowering/DBLowering.cpp index 7f33aa9386..9c26a89440 100644 --- a/query/ir/lowering/DBLowering.cpp +++ b/query/ir/lowering/DBLowering.cpp @@ -239,6 +239,15 @@ const std::unordered_map unaryFunctionL {"db.duration_seconds_of_minute", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, {"db.duration_milliseconds_of_second", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, {"db.duration_microseconds_of_second", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + + {"db.point_x", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_y", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_z", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_longitude", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_latitude", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_height", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_crs", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, + {"db.point_srid", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, {"db.to_string", {&emitNLUnaryFunction, ResultNullability::FollowsInput}}, {"db.to_integer_or_null", {&emitNLUnaryFunction, ResultNullability::AlwaysNullable}}, {"db.to_float_or_null", {&emitNLUnaryFunction, ResultNullability::AlwaysNullable}}, diff --git a/storage/columns/Functions.h b/storage/columns/Functions.h index c0015e4cb3..619900aeca 100644 --- a/storage/columns/Functions.h +++ b/storage/columns/Functions.h @@ -548,6 +548,59 @@ class DurationComponentFunction { } }; +template +using PointComponent = std::conditional_t>; + +template +PointComponent readPointComponent(const types::Point::Primitive value) { + if constexpr (part == PointPart::CRS) { + return Point::getCRSName(value.getCRS()); + } else if constexpr (part == PointPart::SRID) { + return Point::getSRID(value.getCRS()); + } else { + return Point::coordinate(value, part); + } +} + +template +class TaggedPointComponentFunction { +public: + using ArgType = CellView; + using ResultType = std::optional>; + + ResultType operator()(const ArgType cell) const { + const CellTag tag = cell.getTag(); + + if (tag == CellTag::Point) { + return readPointComponent(cell.getAs()); + } else if (tag == CellTag::Null) { + return std::nullopt; + } + + throw TuringException(fmt::format("a point component reads a point, not '{}'", queryTypeName(tag))); + } + + ResultType operator()(const std::optional& cell) const { + return cell.has_value() ? (*this)(*cell) : std::nullopt; + } +}; + +template +class PointComponentFunction { +public: + using ArgType = types::Point::Primitive; + using ResultType = PointComponent; + using TaggedCounterpart = TaggedPointComponentFunction; + + ResultType operator()(const types::Point::Primitive value) { + return readPointComponent(value); + } +}; + // toInteger() and toFloat() over a number rather than a string. Every conversion keeps an // optional result so one column type carries it whatever the argument was, and so a double // that no integer can represent reads as null. diff --git a/test/query/ir/map/MapProjectionTest.cpp b/test/query/ir/map/MapProjectionTest.cpp index 4d2dba4a0a..b0ca364781 100644 --- a/test/query/ir/map/MapProjectionTest.cpp +++ b/test/query/ir/map/MapProjectionTest.cpp @@ -184,5 +184,5 @@ TEST_F(MapProjectionTest, readsANodePatternWithPropertiesAsAPattern) { } TEST_F(MapProjectionTest, rejectsABaseThatIsNoEntityOrMap) { - expectError("WITH 1 AS x RETURN x {.a} AS m", "A map projection reads a node, a relationship, a map, a datetime or a duration"); + expectError("WITH 1 AS x RETURN x {.a} AS m", "A map projection reads a node, a relationship, a map, a datetime, a duration or a point"); } diff --git a/test/query/ir/map/TemporalMapProjectionTest.cpp b/test/query/ir/map/TemporalMapProjectionTest.cpp index 3bdd885395..90bebfedc8 100644 --- a/test/query/ir/map/TemporalMapProjectionTest.cpp +++ b/test/query/ir/map/TemporalMapProjectionTest.cpp @@ -184,7 +184,7 @@ TEST_F(TemporalMapProjectionTest, rejectsANameNoComponentOfATypedTemporalHas) { TEST_F(TemporalMapProjectionTest, rejectsAValueWithNoComponents) { expectRejected("WITH 1 AS d RETURN d {.hours}", - "A map projection reads a node, a relationship, a map, a datetime or a duration, and 'd' is 'Integer'"); + "A map projection reads a node, a relationship, a map, a datetime, a duration or a point, and 'd' is 'Integer'"); } TEST_F(TemporalMapProjectionTest, raisesOnEveryPropertyOfATemporalCell) { diff --git a/test/query/ir/point/CMakeLists.txt b/test/query/ir/point/CMakeLists.txt index 0555372a2a..9522e4a280 100644 --- a/test/query/ir/point/CMakeLists.txt +++ b/test/query/ir/point/CMakeLists.txt @@ -3,3 +3,6 @@ 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}) + +add_turing_test(test_query_ir_point_component PointComponentTest.cpp) +target_include_directories(test_query_ir_point_component PRIVATE ${IR_TEST_DIR}) diff --git a/test/query/ir/point/PointComponentTest.cpp b/test/query/ir/point/PointComponentTest.cpp new file mode 100644 index 0000000000..8173957ec0 --- /dev/null +++ b/test/query/ir/point/PointComponentTest.cpp @@ -0,0 +1,218 @@ +#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 PointComponentTest : 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(PointComponentTest, readsTheCoordinatesOfAPoint) { + expectRows("RETURN point({x: 1, y: 2}).x, point({x: 1, y: 2}).y", {{"1.000000", "2.000000"}}); + expectRows("WITH point({x: 1, y: 2, z: 3}) AS p RETURN p.x, p.y, p.z", {{"1.000000", "2.000000", "3.000000"}}); +} + +TEST_F(PointComponentTest, readsTheGeographicNamesOfTheCoordinates) { + expectRows("WITH point({longitude: 2.5, latitude: 48.5, height: 35}) AS p " + "RETURN p.longitude, p.latitude, p.height, p.x, p.y, p.z", + {{"2.500000", "48.500000", "35.000000", "2.500000", "48.500000", "35.000000"}}); +} + +TEST_F(PointComponentTest, readsTheCRSAndTheSRID) { + expectRows("UNWIND [{x: 1, y: 2}, {x: 1, y: 2, z: 3}, {longitude: 1, latitude: 2}, " + "{longitude: 1, latitude: 2, height: 3}] AS m " + "WITH point(m) AS p RETURN p.crs, p.srid", + {{"cartesian", "7203"}, {"cartesian-3d", "9157"}, {"wgs-84", "4326"}, {"wgs-84-3d", "4979"}}); +} + +TEST_F(PointComponentTest, readsTheComponentsOfAStoredPoint) { + write("CREATE (n:City {name: 'Paris', at: point({longitude: 2.5, latitude: 48.5})})"); + expectRows("MATCH (n:City) RETURN n.at.longitude, n.at.latitude, n.at.crs", + {{"2.500000", "48.500000", "wgs-84"}}); +} + +TEST_F(PointComponentTest, computesWithAComponent) { + expectRows("WITH point({x: 3, y: 4}) AS p RETURN sqrt(p.x * p.x + p.y * p.y)", {{"5.000000"}}); +} + +TEST_F(PointComponentTest, filtersAndOrdersOnAComponent) { + write("CREATE (n:City {name: 'a', at: point({x: 3, y: 1})})"); + write("CREATE (n:City {name: 'b', at: point({x: 1, y: 2})})"); + write("CREATE (n:City {name: 'c', at: point({x: 2, y: 3})})"); + + expectRows("MATCH (n:City) WHERE n.at.x >= 2 RETURN n.name", {{"a"}, {"c"}}); + expectOrderedRows("MATCH (n:City) RETURN n.name ORDER BY n.at.y DESC", {{"c"}, {"b"}, {"a"}}); +} + +TEST_F(PointComponentTest, readsNullOffANullPoint) { + write("CREATE (n:City {name: 'a'})"); + expectRows("MATCH (n:City) RETURN n.at.x, n.at.crs, n.at.srid", {{"null", "null", "null"}}); +} + +TEST_F(PointComponentTest, rejectsAThirdCoordinateOfA2DPoint) { + expectError("WITH point({x: 1, y: 2}) AS p RETURN p.z", + "The point point({x: 1.0, y: 2.0, crs: 'cartesian'}) has no z"); + expectError("WITH point({longitude: 1, latitude: 2}) AS p RETURN p.height", + "The point point({x: 1.0, y: 2.0, crs: 'wgs-84'}) has no height"); +} + +TEST_F(PointComponentTest, rejectsAGeographicNameOfACartesianPoint) { + expectError("WITH point({x: 1, y: 2}) AS p RETURN p.latitude", "has no latitude"); + expectError("WITH point({x: 1, y: 2, z: 3}) AS p RETURN p.height", "has no height"); +} + +TEST_F(PointComponentTest, rejectsANameThatIsNoComponent) { + expectError("WITH point({x: 1, y: 2}) AS p RETURN p.w", "'w' is not a component of a point"); + expectError("RETURN point({x: 1, y: 2}).w", "'w' is not a component of a point"); +} + +TEST_F(PointComponentTest, readsTheComponentOfAPointAListHolds) { + expectRows("UNWIND [point({x: 1, y: 2}), point({longitude: 3, latitude: 4})] AS p RETURN p.x, p.crs, p.srid", + {{"1.000000", "cartesian", "7203"}, {"3.000000", "wgs-84", "4326"}}); +} + +TEST_F(PointComponentTest, rejectsANameThatIsNoComponentOfAPointAListHolds) { + expectError("UNWIND [point({x: 1, y: 2})] AS p RETURN p.w", "'w' is not a component of a point"); + expectError("UNWIND [point({x: 1, y: 2})] AS p RETURN p.z", "has no z"); +} + +TEST_F(PointComponentTest, projectsTheComponentsOfAPoint) { + expectRows("WITH point({longitude: 1, latitude: 2}) AS p RETURN p {.longitude, .crs, n: 1}", + {{"{crs: wgs-84, longitude: 1.000000, n: 1}"}}); +} + +TEST_F(PointComponentTest, rejectsEveryPropertyOfAPoint) { + expectError("WITH point({x: 1, y: 2}) AS p RETURN p {.*}", "A map projection reads .* off a node, a relationship or a map"); +} + +TEST_F(PointComponentTest, rejectsAComponentAsTheTargetOfAWrite) { + write("CREATE (n:City {name: 'a', at: point({x: 1, y: 2})})"); + expectError("MATCH (n:City) SET n.at.x = 3", "component"); +} From ad4bc29b9ea8c2fdc2b352b6d9cfff9523ce1354 Mon Sep 17 00:00:00 2001 From: Remy Boutonnet Date: Thu, 5 Nov 2026 12:09:03 +0100 Subject: [PATCH 3/4] Cypher: implement point.distance and point.withinBBox --- docs/points.md | 76 +++++ query/AST/FunctionDecls.cpp | 21 ++ query/AST/FunctionDecls.h | 2 + query/ir/codegen/DBProgramGenerator.cpp | 42 +++ query/ir/codegen/DBProgramGenerator.h | 2 + query/ir/dialect/db/DBOps.cpp | 63 +++++ query/ir/dialect/db/DBOps.td | 41 +++ query/ir/dialect/nl/NLOps.td | 18 ++ query/ir/exec/NLExecutor.cpp | 102 +++++++ query/ir/exec/NLExecutor.h | 3 + query/ir/exec/NLProgram.h | 28 ++ query/ir/exec/NLTranslator.cpp | 20 ++ query/ir/exec/NLTranslator.h | 1 + query/ir/lowering/DBLowering.cpp | 21 ++ query/ir/lowering/DBLowering.h | 3 + storage/metadata/Point.cpp | 72 +++++ storage/metadata/Point.h | 7 + test/query/ir/point/CMakeLists.txt | 3 + .../ir/point/PointSpatialFunctionsTest.cpp | 264 ++++++++++++++++++ 19 files changed, 789 insertions(+) create mode 100644 docs/points.md create mode 100644 test/query/ir/point/PointSpatialFunctionsTest.cpp diff --git a/docs/points.md b/docs/points.md new file mode 100644 index 0000000000..199d122a4a --- /dev/null +++ b/docs/points.md @@ -0,0 +1,76 @@ +We support the Cypher point type as a stored property value, following Neo4j's spatial values: +https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/. + +# The value + +`ValueType::Point` is a scalar property type, held by `TypedPropertyContainer`. +`db::Point` (`storage/metadata/Point.h`) is 32 trivially copyable bytes: `x`, `y` and `z` as +doubles, then a one-byte `PointCRS` and 7 bytes of zero padding. The CRS is one of Neo4j's four: + +``` +PointCRS name SRID dimension +Cartesian cartesian 7203 2 +Cartesian3D cartesian-3d 9157 3 +WGS84 wgs-84 4326 2 +WGS84_3D wgs-84-3d 4979 3 +``` + +`PointCRS` is written to disk as its ordinal, inside every stored point, so it is append-only. + +The constructor makes two equal points equal in every byte, so a dump, a list encoding and a +DISTINCT key can all compare raw bytes: a 2D point holds a `z` of 0, `-0.0` becomes `0.0`, and +the padding is zero. It also rejects what no point holds: a non-finite coordinate, and a +geographic latitude outside [-90, 90]. A geographic longitude outside [-180, 180] is wrapped into +it, so `point({longitude: 190, latitude: 0})` has an x of -170. + +A point is a `CellTag::Point` inside a list or a map, written as its 32 bytes. + +# Building one + +`point(map)` reads the map's keys, ignoring case and ignoring any key it does not know: + +``` +point({x, y}) cartesian +point({x, y, z}) cartesian-3d +point({longitude, latitude}) wgs-84 +point({longitude, latitude, height}) wgs-84-3d; z may stand for height +point({..., crs: 'wgs-84'}) the CRS by name, ignoring case +point({..., srid: 4326}) the CRS by SRID +``` + +`x` and `y` win over `longitude` and `latitude` where a map holds both, and are read as longitude +and latitude in a geographic CRS. A null map, or a null value under any key, gives null. + +It raises on a map naming both a `crs` and an `srid`, on an unknown CRS, on longitude and latitude +in a cartesian CRS, on a number of coordinates the CRS does not have, and on a coordinate that is +no number. Neo4j parses a numeric string coordinate; TuringDB does not. + +# Reading one + +`p.x`, `p.y` and `p.z` are its coordinates, and `p.longitude`, `p.latitude` and `p.height` the +same ones in a geographic CRS; all six are doubles. `p.crs` is the CRS name and `p.srid` its SRID. +`p.z` and `p.height` raise on a 2D point, and the three geographic names raise on a cartesian +point. The CRS is a property of the value, not of the column, so a column of points mixing CRSs +raises on the row that holds the wrong one. + +A point renders as the text Neo4j's `toString` writes, in `toString(p)` and in the shell: +`point({x: 2.3522, y: 48.8566, crs: 'wgs-84'})`. The binary protocol carries it as its 32 bytes, +under the `POINT` column kind. + +# Comparing + +Two points are equal when they share a CRS and every coordinate. `<`, `<=`, `>` and `>=` between +points are null, as they are between maps. ORDER BY, DISTINCT and grouping order points by SRID, +then by x, y and z. Between types, points sort after paths and before datetimes, as in Neo4j. + +# Distance and bounding boxes + +`point.distance(a, b)` is the Euclidean distance between two cartesian points. Between two +wgs-84 points it is the great-circle distance in metres, by the haversine formula on a sphere of +the IUGG mean Earth radius, 6,371,008.8 m. Neo4j uses 6,378,140 m, so its distances are 0.11% +longer. Between two wgs-84-3d points the great circle is taken at the mean of the two heights and +combined with the height difference, as Neo4j does. It is null for points in different CRSs. + +`point.withinBBox(p, lowerLeft, upperRight)` is whether `p` lies in the box, bounds included. In a +geographic CRS a `lowerLeft` east of `upperRight` makes a box that crosses the 180th meridian, and +a `lowerLeft` north of `upperRight` an empty one. It is null unless the three points share a CRS. diff --git a/query/AST/FunctionDecls.cpp b/query/AST/FunctionDecls.cpp index 7e7fb6c2c8..8cdac4acd9 100644 --- a/query/AST/FunctionDecls.cpp +++ b/query/AST/FunctionDecls.cpp @@ -9,6 +9,7 @@ FunctionDecls::FunctionDecls() { initDefault(); initCollectionFunctions(); + initSpatialFunctions(); } FunctionDecls::~FunctionDecls() { @@ -941,6 +942,26 @@ void FunctionDecls::initCollectionFunctions() { } } +void FunctionDecls::initSpatialFunctions() { + FunctionNamespace* point = _root.createNamespace("point"); + + const PossibleArgTypes pointOrCell {EvaluatedType::Point, EvaluatedType::Any}; + + std::vector distances; + point->createOverloads("distance", {pointOrCell, pointOrCell}, distances); + + for (FunctionSignature* distance : distances) { + distance->setReturnTypes({{EvaluatedType::Double}}); + } + + std::vector boxes; + point->createOverloads("withinBBox", {pointOrCell, pointOrCell, pointOrCell}, boxes); + + for (FunctionSignature* box : boxes) { + box->setReturnTypes({{EvaluatedType::Bool}}); + } +} + FunctionSignature* FunctionDecls::createFunction(std::string_view name) { return _root.createFunction(name); } diff --git a/query/AST/FunctionDecls.h b/query/AST/FunctionDecls.h index 4376b02f02..4a570a7f16 100644 --- a/query/AST/FunctionDecls.h +++ b/query/AST/FunctionDecls.h @@ -33,6 +33,8 @@ class FunctionDecls : public FunctionResolver { void initCollectionFunctions(); + void initSpatialFunctions(); + FunctionSignature* createFunction(std::string_view name); void addAlias(std::string_view alias, std::string_view name); diff --git a/query/ir/codegen/DBProgramGenerator.cpp b/query/ir/codegen/DBProgramGenerator.cpp index 47c15cfeba..5e522db5ce 100644 --- a/query/ir/codegen/DBProgramGenerator.cpp +++ b/query/ir/codegen/DBProgramGenerator.cpp @@ -10443,6 +10443,12 @@ void DBProgramGenerator::translateFunctionExpr(const Expr* expr, } else if (funcName == "replace") { translateReplace(expr, args); return; + } else if (funcName == "point.distance") { + translatePointDistance(expr, args); + return; + } else if (funcName == "point.withinBBox") { + translatePointWithinBBox(expr, args); + return; } else if (funcName == "normalize") { translateNormalFormFunction(expr, args, mlir::storage::StringType::get(_mlirCtxt)); return; @@ -10535,6 +10541,42 @@ void DBProgramGenerator::translateFunctionExpr(const Expr* expr, throwError(fmt::format("Unsupported function: {}", funcName), expr); } +void DBProgramGenerator::translatePointDistance(const Expr* expr, const ExprChain* args) { + bioassert(args && args->size() == 2, "point.distance() takes 2 arguments."); + + const ExprChain::ExprVector& argExprs = args->getExprs(); + + const mlir::Value lhs = translateArg(argExprs[0]); + const mlir::Value rhs = translateArg(argExprs[1]); + + mlir::db::PointDistance distance = _opBuilder.create(_opBuilder.getUnknownLoc(), + allocColumnType(_opBuilder.getF64Type()), + lhs, + rhs); + + _part._exprMap[expr] = distance.getResult(); +} + +void DBProgramGenerator::translatePointWithinBBox(const Expr* expr, const ExprChain* args) { + bioassert(args && args->size() == 3, "point.withinBBox() takes 3 arguments."); + + const ExprChain::ExprVector& argExprs = args->getExprs(); + + const mlir::Value point = translateArg(argExprs[0]); + const mlir::Value lowerLeft = translateArg(argExprs[1]); + const mlir::Value upperRight = translateArg(argExprs[2]); + + const mlir::Type boolType = mlir::storage::BoolType::get(_mlirCtxt); + + mlir::db::PointWithinBBox within = _opBuilder.create(_opBuilder.getUnknownLoc(), + allocColumnType(boolType), + point, + lowerLeft, + upperRight); + + _part._exprMap[expr] = within.getResult(); +} + void DBProgramGenerator::translateRange(const Expr* expr, const ExprChain* args) { const size_t argCount = args ? args->size() : 0; const bool boundsGiven = argCount == 2 || argCount == 3; diff --git a/query/ir/codegen/DBProgramGenerator.h b/query/ir/codegen/DBProgramGenerator.h index dfddea64fd..9265762a3f 100644 --- a/query/ir/codegen/DBProgramGenerator.h +++ b/query/ir/codegen/DBProgramGenerator.h @@ -1392,6 +1392,8 @@ class DBProgramGenerator { void translateSubstring(const Expr* expr, const ExprChain* args); void translateSplit(const Expr* expr, const ExprChain* args); void translateReplace(const Expr* expr, const ExprChain* args); + void translatePointDistance(const Expr* expr, const ExprChain* args); + void translatePointWithinBBox(const Expr* expr, const ExprChain* args); template void translateStringEnd(const Expr* expr, const ExprChain* args); diff --git a/query/ir/dialect/db/DBOps.cpp b/query/ir/dialect/db/DBOps.cpp index 8be2005ed8..76261bade5 100644 --- a/query/ir/dialect/db/DBOps.cpp +++ b/query/ir/dialect/db/DBOps.cpp @@ -278,6 +278,19 @@ bool isStringColumn(Type type) { return isa(value); } +bool isPointColumn(Type type) { + const auto column = dyn_cast(type); + if (!column) { + return false; + } + + const Type element = column.getType(); + const auto nullable = dyn_cast(element); + const Type value = nullable ? nullable.getValueType() : element; + + return isa(value); +} + bool isIntegerColumn(Value column) { return isa(column.getType()) && isa(::db::declaredColumnElement(column)); @@ -2472,6 +2485,40 @@ LogicalResult Replace::verify() { return verifyStringResult(getOperation(), getResult()); } +LogicalResult PointDistance::verify() { + const bool readsPoints = llvm::all_of(getOperation()->getOperands(), [](Value operand) { + return isPointColumn(operand.getType()); + }); + + if (!readsPoints) { + return emitOpError("operands must be point columns"); + } + + const ColumnType resultColumn = dyn_cast(getResult().getType()); + if (!resultColumn || !isa(resultColumn.getType())) { + return emitOpError("result must be a column of f64"); + } + + return success(); +} + +LogicalResult PointWithinBBox::verify() { + const bool readsPoints = llvm::all_of(getOperation()->getOperands(), [](Value operand) { + return isPointColumn(operand.getType()); + }); + + if (!readsPoints) { + return emitOpError("operands must be point columns"); + } + + const ColumnType resultColumn = dyn_cast(getResult().getType()); + if (!resultColumn || !isa(resultColumn.getType())) { + return emitOpError("result must be a column of booleans"); + } + + return success(); +} + LogicalResult Split::verify() { const Value delimiter = getDelimiter(); @@ -3188,6 +3235,22 @@ Speculation::Speculatability Replace::getSpeculatability() { return speculatableWhen(readsStrings); } +Speculation::Speculatability PointDistance::getSpeculatability() { + const bool readsPoints = llvm::all_of(getOperation()->getOperands(), [](Value operand) { + return isa(columnElement(operand)); + }); + + return speculatableWhen(readsPoints); +} + +Speculation::Speculatability PointWithinBBox::getSpeculatability() { + const bool readsPoints = llvm::all_of(getOperation()->getOperands(), [](Value operand) { + return isa(columnElement(operand)); + }); + + return speculatableWhen(readsPoints); +} + Speculation::Speculatability Split::getSpeculatability() { const bool readsAString = isa(columnElement(getString())); diff --git a/query/ir/dialect/db/DBOps.td b/query/ir/dialect/db/DBOps.td index 5695a48245..d409526175 100644 --- a/query/ir/dialect/db/DBOps.td +++ b/query/ir/dialect/db/DBOps.td @@ -3220,6 +3220,47 @@ def Replace : TuringOp<"replace", [MayRaise, ConstantThroughOperands, DeclareOpI }]; } +def PointDistance : TuringOp<"point_distance", [MayRaise, ConstantThroughOperands, DeclareOpInterfaceMethods]> { + let summary = "The distance between the points of each row, null for two points in different CRSs"; + + let description = [{ + Euclidean between two cartesian points, and great-circle metres between two + geographic ones. It raises on a tagged cell holding no point. + + %d = db.point_distance(%a, %b) : (!db.column, !db.column) + -> !db.column + }]; + + let arguments = (ins Column:$lhs, Column:$rhs); + let results = (outs Column:$result); + let hasVerifier = 1; + + let assemblyFormat = [{ + `(` $lhs `,` $rhs `)` attr-dict `:` functional-type(operands, results) + }]; +} + +def PointWithinBBox : TuringOp<"point_within_bbox", [MayRaise, ConstantThroughOperands, DeclareOpInterfaceMethods]> { + let summary = "Whether each row's point lies in its box, null unless the three share a CRS"; + + let description = [{ + The box is bounded by its lower left and upper right corners, both included. It + raises on a tagged cell holding no point. + + %in = db.point_within_bbox(%p, %lowerLeft, %upperRight) + : (!db.column, !db.column, !db.column) + -> !db.column + }]; + + let arguments = (ins Column:$point, Column:$lowerLeft, Column:$upperRight); + let results = (outs Column:$result); + let hasVerifier = 1; + + let assemblyFormat = [{ + `(` $point `,` $lowerLeft `,` $upperRight `)` attr-dict `:` functional-type(operands, results) + }]; +} + class BinaryFunctionOp : TuringOp { let summary = "Apply a binary function row-wise to two value columns"; diff --git a/query/ir/dialect/nl/NLOps.td b/query/ir/dialect/nl/NLOps.td index 8fb90ece47..636f62d613 100644 --- a/query/ir/dialect/nl/NLOps.td +++ b/query/ir/dialect/nl/NLOps.td @@ -3278,6 +3278,24 @@ def Replace : NLOp<"replace", [Pure, RowAlignedThroughOperands]> { let assemblyFormat = "$string `,` $search `,` $replacement attr-dict `:` functional-type(operands, results)"; } +def PointDistance : NLOp<"point_distance", [Pure, RowAlignedThroughOperands]> { + let summary = "Row-wise distance between two points, null across CRSs"; + + let arguments = (ins NLChunk:$lhs, NLChunk:$rhs); + let results = (outs NLChunk:$result); + + let assemblyFormat = "$lhs `,` $rhs attr-dict `:` functional-type(operands, results)"; +} + +def PointWithinBBox : NLOp<"point_within_bbox", [Pure, RowAlignedThroughOperands]> { + let summary = "Row-wise test of a point against a box, null unless the three share a CRS"; + + let arguments = (ins NLChunk:$point, NLChunk:$lowerLeft, NLChunk:$upperRight); + let results = (outs NLChunk:$result); + + let assemblyFormat = "$point `,` $lowerLeft `,` $upperRight attr-dict `:` functional-type(operands, results)"; +} + class NLBinaryFunctionOp : NLOp { let summary = "Apply a binary scalar function row-wise to two value chunks"; diff --git a/query/ir/exec/NLExecutor.cpp b/query/ir/exec/NLExecutor.cpp index 67e0b4ecd7..a018b0ab9a 100644 --- a/query/ir/exec/NLExecutor.cpp +++ b/query/ir/exec/NLExecutor.cpp @@ -5833,6 +5833,28 @@ std::optional unsignedValueArgument(const Column* input return unsignedCharacterArgument(*cell, functionName); } +template +std::optional pointValueArgument(const Column* input, size_t row, std::string_view) { + return (*static_cast(input))[row]; +} + +template +std::optional cellPointArgument(const Column* input, size_t row, std::string_view functionName) { + const std::optional cell = (*static_cast(input))[row]; + if (!cell.has_value()) { + return std::nullopt; + } + + const CellTag tag = cell->getTag(); + if (tag == CellTag::Point) { + return cell->getAs(); + } else if (tag == CellTag::Null) { + return std::nullopt; + } + + throwCellTypeError(functionName, "a point", tag); +} + template std::optional cellIntegerArgument(const Column* input, size_t row, std::string_view functionName) { const std::optional cell = (*static_cast(input))[row]; @@ -10012,6 +10034,62 @@ void NLExecutor::runReplace(NLExecutionContext*, NLFunctionData* data) { template void NLExecutor::runReplace(NLExecutionContext* context, NLFunctionData* data); template void NLExecutor::runReplace>(NLExecutionContext* context, NLFunctionData* data); +void NLExecutor::runPointDistance(NLExecutionContext*, NLFunctionData* data) { + const NLPointFunctionData* function = static_cast(data); + + const NLPointFunctionData::PointArgument& lhs = function->getArguments()[0]; + const NLPointFunctionData::PointArgument& rhs = function->getArguments()[1]; + + const size_t rowCount = lhs._column->size(); + bioassert(rhs._column->size() == rowCount, "Columns of a point.distance are not row-aligned."); + + std::vector>& outputRaw = + static_cast*>(function->getResult())->getRaw(); + outputRaw.resize(rowCount); + + for (size_t rowIndex = 0; rowIndex < rowCount; rowIndex++) { + const std::optional from = lhs._read(lhs._column, rowIndex, "point.distance"); + const std::optional to = rhs._read(rhs._column, rowIndex, "point.distance"); + + if (from.has_value() && to.has_value()) { + outputRaw[rowIndex] = Point::distance(*from, *to); + } else { + outputRaw[rowIndex] = std::nullopt; + } + } +} + +void NLExecutor::runPointWithinBBox(NLExecutionContext*, NLFunctionData* data) { + const NLPointFunctionData* function = static_cast(data); + + const NLPointFunctionData::PointArgument& point = function->getArguments()[0]; + const NLPointFunctionData::PointArgument& lowerLeft = function->getArguments()[1]; + const NLPointFunctionData::PointArgument& upperRight = function->getArguments()[2]; + + const size_t rowCount = point._column->size(); + bioassert(lowerLeft._column->size() == rowCount, "Columns of a point.withinBBox are not row-aligned."); + bioassert(upperRight._column->size() == rowCount, "Columns of a point.withinBBox are not row-aligned."); + + std::vector>& outputRaw = + static_cast*>(function->getResult())->getRaw(); + outputRaw.resize(rowCount); + + for (size_t rowIndex = 0; rowIndex < rowCount; rowIndex++) { + const std::optional value = point._read(point._column, rowIndex, "point.withinBBox"); + const std::optional lower = lowerLeft._read(lowerLeft._column, rowIndex, "point.withinBBox"); + const std::optional upper = upperRight._read(upperRight._column, rowIndex, "point.withinBBox"); + + const bool readsANull = !value.has_value() || !lower.has_value() || !upper.has_value(); + const std::optional within = readsANull ? std::nullopt : Point::withinBBox(*value, *lower, *upper); + + if (within.has_value()) { + outputRaw[rowIndex] = types::Bool::Primitive {*within}; + } else { + outputRaw[rowIndex] = std::nullopt; + } + } +} + void NLExecutor::runRange(NLExecutionContext*, NLFunctionData* data) { const NLRangeData* range = static_cast(data); @@ -10434,6 +10512,30 @@ NLStringListArgumentRead NLExecutor::selectStringListArgumentRead(const Column* throw IRException("a string function reads a list of strings out of a nullable string column, a list column, or a column of tagged cells"); } +NLPointArgumentRead NLExecutor::selectPointArgumentRead(const Column* input) { + using PointValue = types::Point::Primitive; + + const ColumnKind::Code kind = input->getKind(); + + if (kind == ColumnOptVector::staticKind()) { + return &pointValueArgument>; + } else if (kind == ColumnConst::staticKind()) { + return &pointValueArgument>; + } else if (kind == ColumnConst>::staticKind()) { + return &pointValueArgument>>; + } else if (kind == ColumnVector::staticKind()) { + return &cellPointArgument>; + } else if (kind == ColumnOptVector::staticKind()) { + return &cellPointArgument>; + } else if (kind == ColumnConst::staticKind()) { + return &cellPointArgument>; + } else if (kind == ColumnConst>::staticKind()) { + return &cellPointArgument>>; + } + + throw IRException("a point function reads a point argument out of a nullable point column, or a column of tagged cells"); +} + NLIntegerArgumentRead NLExecutor::selectIntegerArgumentRead(const Column* input) { const ColumnKind::Code kind = input->getKind(); diff --git a/query/ir/exec/NLExecutor.h b/query/ir/exec/NLExecutor.h index 81cdced8bd..51ebc1eb9b 100644 --- a/query/ir/exec/NLExecutor.h +++ b/query/ir/exec/NLExecutor.h @@ -425,6 +425,8 @@ class NLExecutor { static void runStringEnd(NLExecutionContext* context, NLFunctionData* data); template static void runReplace(NLExecutionContext* context, NLFunctionData* data); + static void runPointDistance(NLExecutionContext* context, NLFunctionData* data); + static void runPointWithinBBox(NLExecutionContext* context, NLFunctionData* data); // Build one map per row (nl.make_map): row r of the result maps each key to what its // value column holds at r, as one contiguous run of the query's map buffer. @@ -473,6 +475,7 @@ class NLExecutor { static NLStringArgumentRead selectStringArgumentRead(const Column* input); static NLStringListArgumentRead selectStringListArgumentRead(const Column* input); static NLIntegerArgumentRead selectIntegerArgumentRead(const Column* input); + static NLPointArgumentRead selectPointArgumentRead(const Column* input); // Build one list per row (nl.list_comprehension): the body runs over the elements of // the step's cells, a chunkful at a time, and row r takes the ones it kept of its own diff --git a/query/ir/exec/NLProgram.h b/query/ir/exec/NLProgram.h index e8bc4edb74..5b4d8b1043 100644 --- a/query/ir/exec/NLProgram.h +++ b/query/ir/exec/NLProgram.h @@ -4254,6 +4254,34 @@ using NLIntegerArgumentRead = std::optional (*)(const C size_t row, std::string_view functionName); +using NLPointArgumentRead = std::optional (*)(const Column* input, + size_t row, + std::string_view functionName); + +// The arguments of point.distance or point.withinBBox, each a column read one point per row +class NLPointFunctionData : public NLFunctionData { +public: + struct PointArgument { + const Column* _column {nullptr}; + NLPointArgumentRead _read {nullptr}; + }; + + explicit NLPointFunctionData(Column* result) + : _result(result) + { + } + + Column* getResult() const { return _result; } + + const std::vector& getArguments() const { return _arguments; } + + void addArgument(const PointArgument& argument) { _arguments.push_back(argument); } + +private: + Column* _result {nullptr}; + std::vector _arguments; +}; + class NLSubstringData : public NLFunctionData { public: struct StringArgument { diff --git a/query/ir/exec/NLTranslator.cpp b/query/ir/exec/NLTranslator.cpp index a272ac8f79..c222994aeb 100644 --- a/query/ir/exec/NLTranslator.cpp +++ b/query/ir/exec/NLTranslator.cpp @@ -1329,6 +1329,10 @@ void NLTranslator::translateBlock(mlir::Block& block, NLStmtContainer* body) { translateStringEnd(right, body); } else if (nl::Replace replace = mlir::dyn_cast(operation)) { translateReplace(replace, body); + } else if (mlir::isa(operation)) { + translatePointFunction(&operation, &NLExecutor::runPointDistance, body); + } else if (mlir::isa(operation)) { + translatePointFunction(&operation, &NLExecutor::runPointWithinBBox, body); } else if (nl::ListComprehension listComprehension = mlir::dyn_cast(operation)) { translateListComprehension(listComprehension, body); } else if (nl::ListPredicate listPredicate = mlir::dyn_cast(operation)) { @@ -4382,6 +4386,22 @@ void NLTranslator::translateReplace(nl::Replace replace, NLStmtContainer* body) } } +void NLTranslator::translatePointFunction(mlir::Operation* operation, NLHandlerFunction run, NLStmtContainer* body) { + const mlir::Value resultValue = operation->getResult(0); + + Column* const result = allocColumnForChunkType(resultValue.getType()); + _valueSlots[resultValue] = result; + + NLPointFunctionData* data = _program->allocFunctionData(result); + + for (const mlir::Value operand : operation->getOperands()) { + const Column* const argument = getColumn(operand); + data->addArgument(NLPointFunctionData::PointArgument {argument, NLExecutor::selectPointArgumentRead(argument)}); + } + + body->emplaceStmt(run, data); +} + template Data* NLTranslator::allocElementData(Op op, Extra... extra) { const mlir::Value sourceValue = op.getSource(); diff --git a/query/ir/exec/NLTranslator.h b/query/ir/exec/NLTranslator.h index 573d4f79a5..fda9eeb866 100644 --- a/query/ir/exec/NLTranslator.h +++ b/query/ir/exec/NLTranslator.h @@ -1107,6 +1107,7 @@ class NLTranslator { void translateSubstring(mlir::nl::Substring substring, NLStmtContainer* body); void translateSplit(mlir::nl::Split split, NLStmtContainer* body); void translateReplace(mlir::nl::Replace replace, NLStmtContainer* body); + void translatePointFunction(mlir::Operation* operation, NLHandlerFunction run, NLStmtContainer* body); template void translateStringEnd(StringEndOp stringEnd, NLStmtContainer* body); diff --git a/query/ir/lowering/DBLowering.cpp b/query/ir/lowering/DBLowering.cpp index 9c26a89440..23c30b9ea4 100644 --- a/query/ir/lowering/DBLowering.cpp +++ b/query/ir/lowering/DBLowering.cpp @@ -1508,6 +1508,10 @@ void DBLowering::lowerOperationByKind(mlir::Operation& operation) { lowerStringEnd(right); } else if (mlir::db::Replace replace = mlir::dyn_cast(operation)) { lowerReplace(replace); + } else if (mlir::isa(operation)) { + lowerPointFunction(&operation, _builder.getF64Type()); + } else if (mlir::isa(operation)) { + lowerPointFunction(&operation, _builder.getI1Type()); } else if (mlir::db::ListSlice listSlice = mlir::dyn_cast(operation)) { lowerListSlice(listSlice); } else if (mlir::isa(operation)) { @@ -2935,6 +2939,23 @@ void DBLowering::lowerReplace(mlir::db::Replace replace) { _valueMap[replace.getResult()] = replaced.getResult(); } +// Two points in different CRSs have no distance and no box, so the result is nullable +// whatever its arguments are +template +void DBLowering::lowerPointFunction(mlir::Operation* operation, mlir::Type valueType) { + llvm::SmallVector arguments; + containerCellChunks(operation->getOperands(), arguments); + + mlir::MLIRContext* const context = _builder.getContext(); + const nl::ChunkType resultType = nl::ChunkType::get(context, storage::NullableType::get(context, valueType)); + + setInsertionForNaryOp(arguments); + + NLOp lowered = _builder.create(_builder.getUnknownLoc(), mlir::TypeRange {resultType}, arguments); + + _valueMap[operation->getResult(0)] = lowered.getResult(); +} + void DBLowering::lowerScanEdges(mlir::db::ScanEdges scanEdges) { // The edge sibling of lowerScanNodes: a scan reads no column, so its loop // sits at the top of the current root block. The nl.scan_edges iterator diff --git a/query/ir/lowering/DBLowering.h b/query/ir/lowering/DBLowering.h index b1857409ca..3d9597986d 100644 --- a/query/ir/lowering/DBLowering.h +++ b/query/ir/lowering/DBLowering.h @@ -304,6 +304,9 @@ class DBLowering { void lowerSplit(mlir::db::Split split); void lowerReplace(mlir::db::Replace replace); + template + void lowerPointFunction(mlir::Operation* operation, mlir::Type valueType); + template void lowerStringEnd(StringEndOp stringEnd); diff --git a/storage/metadata/Point.cpp b/storage/metadata/Point.cpp index 85d7c8cb1f..99108a970a 100644 --- a/storage/metadata/Point.cpp +++ b/storage/metadata/Point.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -20,6 +21,8 @@ using namespace db; namespace { +constexpr double EARTH_MEAN_RADIUS_METRES = 6371008.8; + enum class PointKey : uint8_t { X = 0, Y, @@ -258,6 +261,32 @@ bool withinRange(double value, double lower, double upper) { return value >= lower && value <= upper; } +// A longitude range whose lower end lies east of its upper end crosses the 180th meridian +bool withinLongitudes(double longitude, double west, double east) { + if (west <= east) { + return withinRange(longitude, west, east); + } + + return longitude >= west || longitude <= east; +} + +double haversineDistance(Point lhs, Point rhs, double radius) { + constexpr double radiansPerDegree = std::numbers::pi / 180.0; + + const double lhsLongitude = lhs.getX() * radiansPerDegree; + const double lhsLatitude = lhs.getY() * radiansPerDegree; + const double rhsLongitude = rhs.getX() * radiansPerDegree; + const double rhsLatitude = rhs.getY() * radiansPerDegree; + + const double halfLatitudeSine = sin((rhsLatitude - lhsLatitude) / 2.0); + const double halfLongitudeSine = sin((rhsLongitude - lhsLongitude) / 2.0); + + const double alpha = halfLatitudeSine * halfLatitudeSine + + cos(lhsLatitude) * cos(rhsLatitude) * halfLongitudeSine * halfLongitudeSine; + + return radius * 2.0 * atan2(sqrt(alpha), sqrt(1.0 - alpha)); +} + } Point::Point(PointCRS crs, double x, double y, double z) @@ -515,6 +544,49 @@ bool Point::fromMap(const MapView& map, Point& value) { return true; } +std::optional Point::distance(Point lhs, Point rhs) { + const PointCRS crs = lhs.getCRS(); + if (crs != rhs.getCRS()) { + return std::nullopt; + } + + const double heightDifference = lhs.getZ() - rhs.getZ(); + + if (!isGeographic(crs)) { + const double xDifference = lhs.getX() - rhs.getX(); + const double yDifference = lhs.getY() - rhs.getY(); + + return sqrt(xDifference * xDifference + yDifference * yDifference + heightDifference * heightDifference); + } else if (getDimension(crs) == 2) { + return haversineDistance(lhs, rhs, EARTH_MEAN_RADIUS_METRES); + } + + const double averageHeight = (lhs.getZ() + rhs.getZ()) / 2.0; + const double surfaceDistance = haversineDistance(lhs, rhs, EARTH_MEAN_RADIUS_METRES + averageHeight); + + return sqrt(surfaceDistance * surfaceDistance + heightDifference * heightDifference); +} + +std::optional Point::withinBBox(Point value, Point lowerLeft, Point upperRight) { + const PointCRS crs = value.getCRS(); + const bool sharesTheCRS = lowerLeft.getCRS() == crs && upperRight.getCRS() == crs; + if (!sharesTheCRS) { + return std::nullopt; + } + + const bool withinHeights = withinRange(value.getZ(), lowerLeft.getZ(), upperRight.getZ()); + + if (!isGeographic(crs)) { + return withinRange(value.getX(), lowerLeft.getX(), upperRight.getX()) + && withinRange(value.getY(), lowerLeft.getY(), upperRight.getY()) + && withinHeights; + } + + return withinLongitudes(value.getX(), lowerLeft.getX(), upperRight.getX()) + && withinRange(value.getY(), lowerLeft.getY(), upperRight.getY()) + && withinHeights; +} + void Point::format(std::string& out, Point value) { ValueTextScratch scratch; const auto output = std::back_inserter(out); diff --git a/storage/metadata/Point.h b/storage/metadata/Point.h index c8ed24c3a2..abab75f3e3 100644 --- a/storage/metadata/Point.h +++ b/storage/metadata/Point.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -70,6 +71,12 @@ class Point { // point() of a map. False where the map holds a null. static bool fromMap(const MapView& map, Point& value); + // Nullopt for two points in different CRSs + static std::optional distance(Point lhs, Point rhs); + + // Nullopt unless the three points share a CRS + static std::optional withinBBox(Point value, Point lowerLeft, Point upperRight); + static void format(std::string& out, Point value); private: diff --git a/test/query/ir/point/CMakeLists.txt b/test/query/ir/point/CMakeLists.txt index 9522e4a280..584f67e983 100644 --- a/test/query/ir/point/CMakeLists.txt +++ b/test/query/ir/point/CMakeLists.txt @@ -6,3 +6,6 @@ target_include_directories(test_query_ir_point_value_flow PRIVATE ${IR_TEST_DIR} add_turing_test(test_query_ir_point_component PointComponentTest.cpp) target_include_directories(test_query_ir_point_component PRIVATE ${IR_TEST_DIR}) + +add_turing_test(test_query_ir_point_spatial_functions PointSpatialFunctionsTest.cpp) +target_include_directories(test_query_ir_point_spatial_functions PRIVATE ${IR_TEST_DIR}) diff --git a/test/query/ir/point/PointSpatialFunctionsTest.cpp b/test/query/ir/point/PointSpatialFunctionsTest.cpp new file mode 100644 index 0000000000..6bfdd0543a --- /dev/null +++ b/test/query/ir/point/PointSpatialFunctionsTest.cpp @@ -0,0 +1,264 @@ +#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 PointSpatialFunctionsTest : 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(PointSpatialFunctionsTest, measuresACartesianDistance) { + expectRows("RETURN point.distance(point({x: 1, y: 2}), point({x: 4, y: 6}))", {{"5.000000"}}); + expectRows("RETURN point.distance(point({x: 1, y: 2, z: 3}), point({x: 4, y: 6, z: 15}))", {{"13.000000"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresAGreatCircleDistanceInMetres) { + expectRows("RETURN point.distance(point({longitude: 2.3522, latitude: 48.8566}), " + "point({longitude: -0.1276, latitude: 51.5072}))", + {{"343530.339999"}}); + expectRows("RETURN point.distance(point({longitude: 0, latitude: 0}), point({longitude: 1, latitude: 0}))", + {{"111195.080234"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresAcrossTheAntimeridian) { + expectRows("RETURN point.distance(point({longitude: 179.5, latitude: 0}), point({longitude: -179.5, latitude: 0}))", + {{"111195.080234"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresBetweenTwoHeights) { + expectRows("RETURN point.distance(point({longitude: 2.3522, latitude: 48.8566, height: 35}), " + "point({longitude: -0.1276, latitude: 51.5072, height: 100}))", + {{"343533.985807"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresNothingAcrossCRSs) { + expectRows("RETURN point.distance(point({x: 1, y: 2}), point({x: 1, y: 2, crs: 'wgs-84'}))", {{"null"}}); + expectRows("RETURN point.distance(point({x: 1, y: 2}), point({x: 1, y: 2, z: 0}))", {{"null"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresNothingFromANull) { + expectRows("RETURN point.distance(null, point({x: 1, y: 2}))", {{"null"}}); + expectRows("RETURN point.distance(point({x: 1, y: 2}), null)", {{"null"}}); +} + +TEST_F(PointSpatialFunctionsTest, filtersOnADistance) { + 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: 'Versailles', at: point({longitude: 2.1301, latitude: 48.8049})})"); + + expectRows("MATCH (a:City {name: 'Paris'}), (b:City) " + "WHERE point.distance(a.at, b.at) < 50000 " + "RETURN b.name", + {{"Paris"}, {"Versailles"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresBetweenPointsAListHolds) { + expectRows("UNWIND [[point({x: 0, y: 0}), point({x: 3, y: 4})], [point({x: 0, y: 0}), point({longitude: 0, latitude: 0})]] AS pair " + "RETURN point.distance(pair[0], pair[1])", + {{"5.000000"}, {"null"}}); +} + +TEST_F(PointSpatialFunctionsTest, rejectsADistanceToAValueThatIsNoPoint) { + expectError("RETURN point.distance(point({x: 1, y: 2}), 1)", "point.distance"); + expectError("UNWIND [1] AS v RETURN point.distance(point({x: 1, y: 2}), v)", "point.distance"); +} + +TEST_F(PointSpatialFunctionsTest, findsACartesianPointInABox) { + expectRows("RETURN point.withinBBox(point({x: 1, y: 2}), point({x: 0, y: 0}), point({x: 2, y: 2}))", {{"true"}}); + expectRows("RETURN point.withinBBox(point({x: 3, y: 2}), point({x: 0, y: 0}), point({x: 2, y: 2}))", {{"false"}}); + expectRows("RETURN point.withinBBox(point({x: 2, y: 0}), point({x: 0, y: 0}), point({x: 2, y: 2}))", {{"true"}}); + expectRows("RETURN point.withinBBox(point({x: 1, y: 1, z: 5}), point({x: 0, y: 0, z: 0}), point({x: 2, y: 2, z: 2}))", + {{"false"}}); +} + +TEST_F(PointSpatialFunctionsTest, findsNoPointInABoxWhoseCornersAreSwapped) { + expectRows("RETURN point.withinBBox(point({x: 1, y: 1}), point({x: 2, y: 2}), point({x: 0, y: 0}))", {{"false"}}); + expectRows("RETURN point.withinBBox(point({longitude: 0, latitude: 1}), " + "point({longitude: -1, latitude: 2}), point({longitude: 1, latitude: 0}))", + {{"false"}}); +} + +TEST_F(PointSpatialFunctionsTest, findsAGeographicPointInABoxAcrossTheAntimeridian) { + expectRows("RETURN point.withinBBox(point({longitude: 179.5, latitude: 0}), " + "point({longitude: 179, latitude: -1}), point({longitude: -179, latitude: 1}))", + {{"true"}}); + expectRows("RETURN point.withinBBox(point({longitude: -179.5, latitude: 0}), " + "point({longitude: 179, latitude: -1}), point({longitude: -179, latitude: 1}))", + {{"true"}}); + expectRows("RETURN point.withinBBox(point({longitude: 0, latitude: 0}), " + "point({longitude: 179, latitude: -1}), point({longitude: -179, latitude: 1}))", + {{"false"}}); +} + +TEST_F(PointSpatialFunctionsTest, findsNothingAcrossCRSsOrFromANull) { + expectRows("RETURN point.withinBBox(point({x: 1, y: 1}), point({x: 0, y: 0, crs: 'wgs-84'}), point({x: 2, y: 2}))", + {{"null"}}); + expectRows("RETURN point.withinBBox(null, point({x: 0, y: 0}), point({x: 2, y: 2}))", {{"null"}}); + expectRows("RETURN point.withinBBox(point({x: 1, y: 1}), null, point({x: 2, y: 2}))", {{"null"}}); +} + +TEST_F(PointSpatialFunctionsTest, filtersOnABox) { + 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})})"); + + expectRows("MATCH (n:City) " + "WHERE point.withinBBox(n.at, point({longitude: 0, latitude: 45}), point({longitude: 5, latitude: 50})) " + "RETURN n.name", + {{"Paris"}}); +} + +TEST_F(PointSpatialFunctionsTest, ordersByDistance) { + 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: 'Versailles', at: point({longitude: 2.1301, latitude: 48.8049})})"); + + expectOrderedRows("MATCH (n:City) RETURN n.name " + "ORDER BY point.distance(n.at, point({longitude: 2.3522, latitude: 48.8566}))", + {{"Paris"}, {"Versailles"}, {"London"}}); +} + +TEST_F(PointSpatialFunctionsTest, measuresNothingFromAMissingProperty) { + write("CREATE (n:City {name: 'Paris', at: point({longitude: 2.3522, latitude: 48.8566})})"); + write("CREATE (n:City {name: 'Nowhere'})"); + + expectRows("MATCH (n:City) RETURN n.name, point.distance(n.at, point({longitude: 2.3522, latitude: 48.8566}))", + {{"Paris", "0.000000"}, {"Nowhere", "null"}}); +} + +TEST_F(PointSpatialFunctionsTest, answersTheTicketQuery) { + write("CREATE (:City {name: 'London', location: point({latitude: 51.5072, longitude: -0.1276})})"); + write("CREATE (:City {name: 'Paris', location: point({latitude: 48.8566, longitude: 2.3522})})"); + write("CREATE (:City {name: 'Croydon', location: point({latitude: 51.3762, longitude: -0.0982})})"); + + expectRows("MATCH (a:City {name: 'London'}), (b:City) " + "WHERE point.distance(a.location, b.location) < 50000 " + "RETURN b.name, point.distance(a.location, b.location) AS metres", + {{"London", "0.000000"}, {"Croydon", "14708.388652"}}); +} From 1ada68fba14c643a76c050e9cb0fd49fbbbb8f12 Mon Sep 17 00:00:00 2001 From: Remy Boutonnet Date: Thu, 5 Nov 2026 12:28:11 +0100 Subject: [PATCH 4/4] Import: name the type of a property in one spec --- import/common/CMakeLists.txt | 4 +- import/common/DateTimeSpec.cpp | 9 ---- import/common/DateTimeSpec.h | 29 ------------ import/common/DurationSpec.cpp | 9 ---- import/common/DurationSpec.h | 29 ------------ import/common/JsonlImportSpecs.h | 14 ++---- import/common/TypedPropertySpec.cpp | 24 ++++++++++ import/common/TypedPropertySpec.h | 45 +++++++++++++++++++ import/jsonl/JsonlParser.cpp | 22 +++++---- import/jsonl/JsonlParser.h | 6 +-- import/parquet/ParquetImporter.cpp | 4 +- import/parquet/ParquetImporter.h | 8 ++-- io/parquet/ParquetImportVisitor.cpp | 2 +- io/parquet/ParquetImportVisitor.h | 8 ++-- query/AST/LoadJsonlQuery.h | 15 ++----- query/AST/LoadParquetQuery.h | 8 ++-- query/analyzer/CypherAnalyzer.cpp | 37 ++++++++++++--- query/ir/codegen/DBSystemProgramGenerator.cpp | 29 ++++++------ query/ir/codegen/DBSystemProgramGenerator.h | 3 +- query/ir/dialect/db/DBSystemOps.td | 21 ++++----- query/ir/dialect/nl/NLSystemOps.td | 5 +-- query/ir/exec/NLSystemData.h | 20 ++++----- query/ir/exec/NLSystemExecutor.cpp | 3 +- query/ir/exec/NLSystemTranslator.cpp | 26 +++++------ query/parser/CypherGrammar.h | 3 +- query/parser/CypherGrammarQueries.cpp | 30 ++++++------- system/GraphManager.cpp | 18 ++++---- system/GraphManager.h | 11 ++--- system/SystemAccessor.cpp | 5 +-- system/SystemAccessor.h | 6 +-- system/SystemManager.cpp | 5 +-- system/SystemManager.h | 6 +-- test/query/parser/CypherASTComparator.cpp | 27 +++++------ test/query/parser/CypherASTComparator.h | 6 ++- test/query/parser/CypherASTComparatorTest.cpp | 2 +- test/query/parser/KeywordObjectNameTest.cpp | 10 ++--- test/query/parser/ParseEdgeCaseTest.cpp | 21 ++++----- test/system/FailedImportClearsLoadingTest.cpp | 4 +- .../ListAvailableGraphsDuringImportTest.cpp | 2 +- 39 files changed, 251 insertions(+), 285 deletions(-) delete mode 100644 import/common/DateTimeSpec.cpp delete mode 100644 import/common/DateTimeSpec.h delete mode 100644 import/common/DurationSpec.cpp delete mode 100644 import/common/DurationSpec.h create mode 100644 import/common/TypedPropertySpec.cpp create mode 100644 import/common/TypedPropertySpec.h diff --git a/import/common/CMakeLists.txt b/import/common/CMakeLists.txt index dec338d67d..c37714bcca 100644 --- a/import/common/CMakeLists.txt +++ b/import/common/CMakeLists.txt @@ -1,8 +1,8 @@ add_library(turing_db_import_common_s STATIC - DateTimeSpec.cpp - DurationSpec.cpp EmbeddingsSpec.cpp + TypedPropertySpec.cpp ) target_include_directories(turing_db_import_common_s PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(turing_db_import_common_s PUBLIC turing_db_storage_s) diff --git a/import/common/DateTimeSpec.cpp b/import/common/DateTimeSpec.cpp deleted file mode 100644 index 2ab573e6fe..0000000000 --- a/import/common/DateTimeSpec.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "DateTimeSpec.h" - -#include - -using namespace db; - -bool DateTimeSpec::contains(std::string_view propName) const { - return _names.contains(propName); -} diff --git a/import/common/DateTimeSpec.h b/import/common/DateTimeSpec.h deleted file mode 100644 index ca63733481..0000000000 --- a/import/common/DateTimeSpec.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -namespace db { - -// The properties a JSONL import reads as instants. A datetime arrives as a plain JSON -// string, which a string property is spelled as too, so the import is told which names -// carry one rather than guessing from what the text looks like. -class DateTimeSpec { -public: - using SetType = std::unordered_set; - using ConstIterator = SetType::const_iterator; - - bool contains(std::string_view propName) const; - - ConstIterator begin() const { return _names.begin(); }; - ConstIterator end() const { return _names.end(); }; - - bool empty() const { return _names.empty(); } - - void emplace(std::string_view name) { _names.emplace(name); } - -private: - SetType _names; -}; - -} diff --git a/import/common/DurationSpec.cpp b/import/common/DurationSpec.cpp deleted file mode 100644 index fab65fbfc2..0000000000 --- a/import/common/DurationSpec.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "DurationSpec.h" - -#include - -using namespace db; - -bool DurationSpec::contains(std::string_view propertyName) const { - return _names.contains(propertyName); -} diff --git a/import/common/DurationSpec.h b/import/common/DurationSpec.h deleted file mode 100644 index e87a607087..0000000000 --- a/import/common/DurationSpec.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -namespace db { - -// The properties an import reads as durations. Neither JSONL nor Parquet has a duration -// type, so a duration arrives as a plain integer count of microseconds and the import is -// told which names carry one. -class DurationSpec { -public: - using SetType = std::unordered_set; - using ConstIterator = SetType::const_iterator; - - bool contains(std::string_view propertyName) const; - - ConstIterator begin() const { return _names.begin(); }; - ConstIterator end() const { return _names.end(); }; - - bool empty() const { return _names.empty(); } - - void emplace(std::string_view name) { _names.emplace(name); } - -private: - SetType _names; -}; - -} diff --git a/import/common/JsonlImportSpecs.h b/import/common/JsonlImportSpecs.h index 2abd779dd5..517395b147 100644 --- a/import/common/JsonlImportSpecs.h +++ b/import/common/JsonlImportSpecs.h @@ -2,9 +2,8 @@ #include -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" +#include "TypedPropertySpec.h" namespace db { @@ -13,8 +12,7 @@ namespace db { // text or an instant - so the query names the properties that are neither by default. struct JsonlImportSpecs { EmbeddingsSpec _embeddings; - DateTimeSpec _dateTimes; - DurationSpec _durations; + TypedPropertySpec _types; // Takes the hints another clause of the same statement gave void absorb(JsonlImportSpecs&& other) { @@ -22,12 +20,8 @@ struct JsonlImportSpecs { _embeddings.emplace(name, dimension); } - for (const std::string_view name : other._dateTimes) { - _dateTimes.emplace(name); - } - - for (const std::string_view name : other._durations) { - _durations.emplace(name); + for (const auto& [name, type] : other._types) { + _types.emplace(name, type); } } }; diff --git a/import/common/TypedPropertySpec.cpp b/import/common/TypedPropertySpec.cpp new file mode 100644 index 0000000000..337cf744b7 --- /dev/null +++ b/import/common/TypedPropertySpec.cpp @@ -0,0 +1,24 @@ +#include "TypedPropertySpec.h" + +using namespace db; + +ValueType TypedPropertySpec::getType(std::string_view propertyName) const { + const auto it = _types.find(propertyName); + if (it == _types.end()) { + return ValueType::Invalid; + } + + return it->second; +} + +bool TypedPropertySpec::contains(std::string_view propertyName) const { + return _types.contains(propertyName); +} + +void TypedPropertySpec::emplace(std::string_view propertyName, ValueType type) { + const auto [it, inserted] = _types.emplace(propertyName, type); + + if (!inserted && it->second != type) { + _clashes.push_back(Clash {._name = propertyName, ._first = it->second, ._second = type}); + } +} diff --git a/import/common/TypedPropertySpec.h b/import/common/TypedPropertySpec.h new file mode 100644 index 0000000000..bb26b2415d --- /dev/null +++ b/import/common/TypedPropertySpec.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include "metadata/PropertyType.h" + +namespace db { + +// The properties an import reads as a type its file format does not spell: a datetime is a +// JSON string, a duration an integer count of microseconds, a point a JSON object. The query +// names them, so the import does not guess from what a value looks like. +class TypedPropertySpec { +public: + // A name the query gave a second type. The first one stands. + struct Clash { + std::string_view _name; + ValueType _first {ValueType::Invalid}; + ValueType _second {ValueType::Invalid}; + }; + + using MapType = std::unordered_map; + using ConstIterator = MapType::const_iterator; + + // ValueType::Invalid where the query names no type for @param propertyName + ValueType getType(std::string_view propertyName) const; + + bool contains(std::string_view propertyName) const; + + void emplace(std::string_view propertyName, ValueType type); + + const std::vector& getClashes() const { return _clashes; } + + ConstIterator begin() const { return _types.begin(); } + ConstIterator end() const { return _types.end(); } + + bool empty() const { return _types.empty(); } + +private: + MapType _types; + std::vector _clashes; +}; + +} diff --git a/import/jsonl/JsonlParser.cpp b/import/jsonl/JsonlParser.cpp index 1d3bf8065a..2a8c56db9e 100644 --- a/import/jsonl/JsonlParser.cpp +++ b/import/jsonl/JsonlParser.cpp @@ -16,9 +16,8 @@ #include "versioning/CommitBuilder.h" #include "writers/DataPartBuilder.h" -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" +#include "TypedPropertySpec.h" #include "Profiler.h" #include "writers/MetadataBuilder.h" @@ -151,9 +150,9 @@ ListView fillList(const json& array, ListContainer& lists) { // alone, so a string that happens to read as a date stays the text it was written as. JsonlImportResult> tryGetDateTime(std::string_view name, const json& value, - const DateTimeSpec& dateTimeSpecs, + const TypedPropertySpec& typedProperties, size_t lineNo) { - if (!dateTimeSpecs.contains(name)) { + if (typedProperties.getType(name) != ValueType::DateTime) { return std::optional {}; } @@ -179,9 +178,9 @@ JsonlImportResult> tryGetDateTime(std::string_view name, JsonlImportResult> tryGetDuration(std::string_view name, const json& value, - const DurationSpec& durationSpecs, + const TypedPropertySpec& typedProperties, size_t lineNumber) { - if (!durationSpecs.contains(name)) { + if (typedProperties.getType(name) != ValueType::Duration) { return std::optional {}; } @@ -215,8 +214,7 @@ std::optional tryGetEmbDim(std::string_view name, JsonlImportResult JsonlParser::parse(ChangeAccessor& change, std::istream& stream, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs) { + const TypedPropertySpec& typedProperties) { Profile profile("JsonlParser::parse"); std::string line; @@ -303,13 +301,13 @@ JsonlImportResult JsonlParser::parse(ChangeAccessor& change, } const JsonlImportResult> instant = - tryGetDateTime(ptName, value, dateTimeSpecs, lineNumber); + tryGetDateTime(ptName, value, typedProperties, lineNumber); if (!instant) { return instant.get_unexpected(); } const JsonlImportResult> duration = - tryGetDuration(ptName, value, durationSpecs, lineNumber); + tryGetDuration(ptName, value, typedProperties, lineNumber); if (!duration) { return duration.get_unexpected(); } @@ -418,13 +416,13 @@ JsonlImportResult JsonlParser::parse(ChangeAccessor& change, } const JsonlImportResult> instant = - tryGetDateTime(ptName, value, dateTimeSpecs, lineNumber); + tryGetDateTime(ptName, value, typedProperties, lineNumber); if (!instant) { return instant.get_unexpected(); } const JsonlImportResult> duration = - tryGetDuration(ptName, value, durationSpecs, lineNumber); + tryGetDuration(ptName, value, typedProperties, lineNumber); if (!duration) { return duration.get_unexpected(); } diff --git a/import/jsonl/JsonlParser.h b/import/jsonl/JsonlParser.h index 36ecc61630..a722c62bb3 100644 --- a/import/jsonl/JsonlParser.h +++ b/import/jsonl/JsonlParser.h @@ -5,9 +5,8 @@ #include "JsonlImportResult.h" -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" +#include "TypedPropertySpec.h" namespace db { @@ -26,8 +25,7 @@ class JsonlParser { [[nodiscard]] static JsonlImportResult parse(ChangeAccessor& change, std::istream& stream, const EmbeddingsSpec& embeddingSpecs = {}, - const DateTimeSpec& dateTimeSpecs = {}, - const DurationSpec& durationSpecs = {}); + const TypedPropertySpec& typedProperties = {}); }; } diff --git a/import/parquet/ParquetImporter.cpp b/import/parquet/ParquetImporter.cpp index 0a05d02a76..f22ca65525 100644 --- a/import/parquet/ParquetImporter.cpp +++ b/import/parquet/ParquetImporter.cpp @@ -12,14 +12,14 @@ void ParquetImporter::import() { bioassert(_builder, "Null CommitBuilder."); { - ParquetNodeVisitor nodeVisitor(_builder, _nodeIDs, _durationSpecs); + ParquetNodeVisitor nodeVisitor(_builder, _nodeIDs, _typedProperties); ParquetReader reader(_nodeFile, nodeVisitor); reader.ensureFileOpen(); while (reader.nextChunk()) { } } { - ParquetEdgeVisitor edgeVisitor(_builder, _nodeIDs, _durationSpecs); + ParquetEdgeVisitor edgeVisitor(_builder, _nodeIDs, _typedProperties); ParquetReader reader(_edgeFile, edgeVisitor); reader.ensureFileOpen(); while (reader.nextChunk()) { } diff --git a/import/parquet/ParquetImporter.h b/import/parquet/ParquetImporter.h index c5e4dcb26d..67db8c2acf 100644 --- a/import/parquet/ParquetImporter.h +++ b/import/parquet/ParquetImporter.h @@ -4,8 +4,8 @@ #include "ParquetImportVisitor.h" -#include "DurationSpec.h" #include "Path.h" +#include "TypedPropertySpec.h" namespace db { @@ -33,11 +33,11 @@ class ParquetImporter { ParquetImporter(fs::Path nodeFile, fs::Path edgeFile, CommitBuilder* builder, - const DurationSpec& durationSpecs) + const TypedPropertySpec& typedProperties) : _nodeFile(std::move(nodeFile)), _edgeFile(std::move(edgeFile)), _builder(builder), - _durationSpecs(durationSpecs) + _typedProperties(typedProperties) { } @@ -49,7 +49,7 @@ class ParquetImporter { CommitBuilder* _builder {nullptr}; - const DurationSpec& _durationSpecs; + const TypedPropertySpec& _typedProperties; // Source ID -> NodeID, filled while importing nodes and consumed by the edges. ParquetImportVisitor::IDMap _nodeIDs; diff --git a/io/parquet/ParquetImportVisitor.cpp b/io/parquet/ParquetImportVisitor.cpp index 527c268f55..9e533ff45f 100644 --- a/io/parquet/ParquetImportVisitor.cpp +++ b/io/parquet/ParquetImportVisitor.cpp @@ -180,7 +180,7 @@ void ParquetImportVisitor::discoverPropertyColumn(size_t columnIndex, const bool isList = maxRepLevel > 0; const std::string name {isList ? listPropertyName(path) : std::string_view {path}}; - const bool holdsDurations = _durationSpecs.contains(name); + const bool holdsDurations = _typedProperties.getType(name) == ValueType::Duration; if (holdsDurations) { const bool hasNoLogicalType = !logicalType || logicalType->is_none(); const bool isSignedInt = logicalType && logicalType->is_int() diff --git a/io/parquet/ParquetImportVisitor.h b/io/parquet/ParquetImportVisitor.h index fb9442f8fd..9e99d51599 100644 --- a/io/parquet/ParquetImportVisitor.h +++ b/io/parquet/ParquetImportVisitor.h @@ -13,8 +13,8 @@ #include "ParquetReader.h" -#include "DurationSpec.h" #include "ID.h" +#include "TypedPropertySpec.h" #include "list/ListContainer.h" #include "list/ListView.h" #include "metadata/PropertyType.h" @@ -36,10 +36,10 @@ class ParquetImportVisitor : public ParquetSaxVisitor { using NodeIDs = std::span; using IDMap = std::unordered_map; - ParquetImportVisitor(CommitBuilder* builder, IDMap& nodeIDs, const DurationSpec& durationSpecs) + ParquetImportVisitor(CommitBuilder* builder, IDMap& nodeIDs, const TypedPropertySpec& typedProperties) : _builder(builder), _nodeIDs(nodeIDs), - _durationSpecs(durationSpecs) + _typedProperties(typedProperties) { } @@ -79,7 +79,7 @@ class ParquetImportVisitor : public ParquetSaxVisitor { // @ref DataPartBuilder used when importing IDMap& _nodeIDs; - const DurationSpec& _durationSpecs; + const TypedPropertySpec& _typedProperties; std::unordered_map _propertyColumns; diff --git a/query/AST/LoadJsonlQuery.h b/query/AST/LoadJsonlQuery.h index 56e4f43f87..714f85e969 100644 --- a/query/AST/LoadJsonlQuery.h +++ b/query/AST/LoadJsonlQuery.h @@ -5,9 +5,8 @@ #include "Path.h" #include "QueryCommand.h" -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" +#include "TypedPropertySpec.h" namespace db { @@ -23,8 +22,7 @@ class LoadJsonlQuery : public QueryCommand { void setGraphName(std::string_view graphName) { _graphName = graphName; } void setEmbeddingSpecs(EmbeddingsSpec&& specs) { _embeddingSpecs = std::move(specs); } - void setDateTimeSpecs(DateTimeSpec&& specs) { _dateTimeSpecs = std::move(specs); } - void setDurationSpecs(DurationSpec&& specs) { _durationSpecs = std::move(specs); } + void setTypedPropertySpec(TypedPropertySpec&& specs) { _typedPropertySpec = std::move(specs); } const fs::Path& getFilePath() const { return _path; } std::string_view getGraphName() const { return _graphName; } @@ -32,18 +30,13 @@ class LoadJsonlQuery : public QueryCommand { return _embeddingSpecs; } - const DateTimeSpec& getDateTimeSpecs() const { - return _dateTimeSpecs; - } - - const DurationSpec& getDurationSpecs() const { return _durationSpecs; } + const TypedPropertySpec& getTypedPropertySpec() const { return _typedPropertySpec; } private: fs::Path _path; std::string_view _graphName; EmbeddingsSpec _embeddingSpecs; - DateTimeSpec _dateTimeSpecs; - DurationSpec _durationSpecs; + TypedPropertySpec _typedPropertySpec; LoadJsonlQuery(DeclContext* declContext, fs::Path&& path); ~LoadJsonlQuery() override; diff --git a/query/AST/LoadParquetQuery.h b/query/AST/LoadParquetQuery.h index de6b4558fa..a1ad48cd57 100644 --- a/query/AST/LoadParquetQuery.h +++ b/query/AST/LoadParquetQuery.h @@ -4,7 +4,7 @@ #include "QueryCommand.h" -#include "DurationSpec.h" +#include "TypedPropertySpec.h" #include "Path.h" @@ -23,15 +23,15 @@ class LoadParquetQuery : public QueryCommand { std::string_view getGraphName() const { return _graphName; } void setGraphName(std::string_view name) { _graphName = name; } - void setDurationSpecs(DurationSpec&& specs) { _durationSpecs = std::move(specs); } + void setTypedPropertySpec(TypedPropertySpec&& specs) { _typedPropertySpec = std::move(specs); } const fs::Path& getFilePath() const { return _filePath; } - const DurationSpec& getDurationSpecs() const { return _durationSpecs; } + const TypedPropertySpec& getTypedPropertySpec() const { return _typedPropertySpec; } private: fs::Path _filePath; std::string_view _graphName; - DurationSpec _durationSpecs; + TypedPropertySpec _typedPropertySpec; LoadParquetQuery(DeclContext* declContext, fs::Path&& filePath); diff --git a/query/analyzer/CypherAnalyzer.cpp b/query/analyzer/CypherAnalyzer.cpp index c82cc3cdad..c769c69713 100644 --- a/query/analyzer/CypherAnalyzer.cpp +++ b/query/analyzer/CypherAnalyzer.cpp @@ -84,6 +84,23 @@ using namespace db; namespace { +std::string_view importedTypeName(ValueType type) { + switch (type) { + case ValueType::DateTime: + return "a datetime"; + break; + case ValueType::Duration: + return "a duration"; + break; + case ValueType::Point: + return "a point"; + break; + default: + return ValueTypeName::value(type); + break; + } +} + const FunctionSignature* aggregateSignatureOf(const Expr* expr) { if (expr->getKind() != Expr::Kind::FUNCTION_INVOCATION) { return nullptr; @@ -1874,13 +1891,19 @@ void CypherAnalyzer::analyze(LoadJsonlQuery* loadJsonl) { throwError("All embedding properties must have dimension greater than 1."); } - const DateTimeSpec& dateTimeSpecs = loadJsonl->getDateTimeSpecs(); - for (const std::string_view name : loadJsonl->getDurationSpecs()) { - if (dateTimeSpecs.contains(name)) { - throwError(fmt::format("Property '{}' is named both a datetime and a duration.", name), - loadJsonl); - } else if (embDims.find(name) != embDims.end()) { - throwError(fmt::format("Property '{}' is named both an embedding and a duration.", name), + const TypedPropertySpec& typedProperties = loadJsonl->getTypedPropertySpec(); + + for (const TypedPropertySpec::Clash& clash : typedProperties.getClashes()) { + throwError(fmt::format("Property '{}' is named both {} and {}.", + clash._name, + importedTypeName(clash._first), + importedTypeName(clash._second)), + loadJsonl); + } + + for (const auto& [name, type] : typedProperties) { + if (embDims.find(name) != embDims.end()) { + throwError(fmt::format("Property '{}' is named both an embedding and {}.", name, importedTypeName(type)), loadJsonl); } } diff --git a/query/ir/codegen/DBSystemProgramGenerator.cpp b/query/ir/codegen/DBSystemProgramGenerator.cpp index 7eb6bfc55e..45f08de5a7 100644 --- a/query/ir/codegen/DBSystemProgramGenerator.cpp +++ b/query/ir/codegen/DBSystemProgramGenerator.cpp @@ -7,6 +7,7 @@ #include "mlir/IR/MLIRContext.h" #include "DBOps.h" +#include "IRValueTypes.h" #include "StorageEnums.h" #include "StorageTypes.h" @@ -28,6 +29,7 @@ #include "QueryCommand.h" #include "S3ConnectQuery.h" #include "S3TransferQuery.h" +#include "TypedPropertySpec.h" #include "expr/PropertyExpr.h" #include "BioAssert.h" @@ -40,18 +42,17 @@ llvm::StringRef toStringRef(std::string_view text) { return llvm::StringRef(text.data(), text.size()); } -// A named property carries no value of its own, so each name is a unit entry. A dictionary -// rather than an array because it sorts and uniques its own names, which the hash set the -// spec holds does neither. -template -mlir::DictionaryAttr propertyNamesAttribute(mlir::OpBuilder* builder, const PropertyNameSpec& names) { - if (names.empty()) { +// Each named property maps to the element type it is read as. A dictionary rather than an +// array because it sorts its names itself, which the hash map the spec holds does not. +mlir::DictionaryAttr typedPropertiesAttribute(mlir::OpBuilder* builder, const TypedPropertySpec& specs) { + if (specs.empty()) { return mlir::DictionaryAttr(); } llvm::SmallVector entries; - for (const std::string_view propertyName : names) { - entries.emplace_back(builder->getStringAttr(toStringRef(propertyName)), builder->getUnitAttr()); + for (const auto& [propertyName, type] : specs) { + const mlir::Type elementType = valueTypeToElementType(*builder, type); + entries.emplace_back(builder->getStringAttr(toStringRef(propertyName)), mlir::TypeAttr::get(elementType)); } return builder->getDictionaryAttr(entries); @@ -323,8 +324,7 @@ void DBSystemProgramGenerator::generateLoadJsonl(const LoadJsonlQuery* query) { query->getGraphName(), mlir::storage::GraphImportFormat::Jsonl, embeddings, - propertyNamesAttribute(_opBuilder, query->getDateTimeSpecs()), - propertyNamesAttribute(_opBuilder, query->getDurationSpecs())); + typedPropertiesAttribute(_opBuilder, query->getTypedPropertySpec())); } void DBSystemProgramGenerator::generateLoadGML(const LoadGMLQuery* query) { @@ -339,24 +339,21 @@ void DBSystemProgramGenerator::generateLoadParquet(const LoadParquetQuery* query query->getGraphName(), mlir::storage::GraphImportFormat::Parquet, mlir::DictionaryAttr(), - mlir::DictionaryAttr(), - propertyNamesAttribute(_opBuilder, query->getDurationSpecs())); + typedPropertiesAttribute(_opBuilder, query->getTypedPropertySpec())); } void DBSystemProgramGenerator::generateImportGraph(std::string_view path, std::string_view graphName, mlir::storage::GraphImportFormat format, mlir::DictionaryAttr embeddings, - mlir::DictionaryAttr dateTimes, - mlir::DictionaryAttr durations) { + mlir::DictionaryAttr types) { mlir::db::ImportGraph op = _opBuilder->create(_opBuilder->getUnknownLoc(), stringColumnType(), toStringRef(path), toStringRef(graphName), format, embeddings, - dateTimes, - durations); + types); emitOutput(op.getGraph(), {"graphName"}); } diff --git a/query/ir/codegen/DBSystemProgramGenerator.h b/query/ir/codegen/DBSystemProgramGenerator.h index 02d0657302..5c23e86863 100644 --- a/query/ir/codegen/DBSystemProgramGenerator.h +++ b/query/ir/codegen/DBSystemProgramGenerator.h @@ -95,8 +95,7 @@ class DBSystemProgramGenerator { std::string_view graphName, mlir::storage::GraphImportFormat format, mlir::DictionaryAttr embeddings, - mlir::DictionaryAttr dateTimes = {}, - mlir::DictionaryAttr durations = {}); + mlir::DictionaryAttr types = {}); void generatePropertyIndex(std::string_view indexName, const PropertyExpr* propertyExpr, diff --git a/query/ir/dialect/db/DBSystemOps.td b/query/ir/dialect/db/DBSystemOps.td index aec9ff49e9..7f27617871 100644 --- a/query/ir/dialect/db/DBSystemOps.td +++ b/query/ir/dialect/db/DBSystemOps.td @@ -74,31 +74,28 @@ def ImportGraph : SystemOp<"import_graph"> { `LOAD JSONL`: one entry per property that holds a vector, mapping the property name to its dimension. The other two formats never carry it. - `date_times` is its `WITH DATETIMES [prop, ...]` clause: the properties whose - strings name an instant rather than text. A datetime is spelled as a string - in JSON, so the query says which names carry one. The names carry no value of - their own, so each is a unit entry - a dictionary rather than an array because - that sorts and uniques its names itself. - %g = db.import_graph jsonl("/data/social.jsonl") as "social" embeddings {vec = 128 : ui64} : !db.column - `durations` is the `WITH DURATIONS [prop, ...]` clause of `LOAD JSONL` and - `LOAD PARQUET`: the properties whose integers count microseconds. Neither - format has a duration type. Each name is a unit entry, as in `date_times`. + `types` maps each property the query names in a `WITH DATETIMES`, `WITH + DURATIONS` or `WITH POINTS` clause to the element type it is read as: a type + the format does not spell. A datetime is a string in JSON, a duration an + integer count of microseconds in JSON and Parquet, a point an object in JSON. + + %g = db.import_graph jsonl("/data/cities.jsonl") as "cities" + types {at = !storage.point, founded = !storage.datetime} : !db.column }]; let arguments = (ins StrAttr:$path, StrAttr:$graph_name, GraphImportFormat:$format, OptionalAttr:$embeddings, - OptionalAttr:$date_times, - OptionalAttr:$durations); + OptionalAttr:$types); let results = (outs ColumnStrings:$graph); let assemblyFormat = [{ $format `(` $path `)` `as` $graph_name (`embeddings` $embeddings^)? - (`datetimes` $date_times^)? (`durations` $durations^)? attr-dict `:` qualified(type($graph)) + (`types` $types^)? attr-dict `:` qualified(type($graph)) }]; } diff --git a/query/ir/dialect/nl/NLSystemOps.td b/query/ir/dialect/nl/NLSystemOps.td index 47d5c94056..85b6485e9b 100644 --- a/query/ir/dialect/nl/NLSystemOps.td +++ b/query/ir/dialect/nl/NLSystemOps.td @@ -70,13 +70,12 @@ def ImportGraph : NLSystemOp<"import_graph"> { StrAttr:$graph_name, GraphImportFormat:$format, OptionalAttr:$embeddings, - OptionalAttr:$date_times, - OptionalAttr:$durations); + OptionalAttr:$types); let results = (outs NLChunk:$graph); let assemblyFormat = [{ $format `(` $path `)` `as` $graph_name (`embeddings` $embeddings^)? - (`datetimes` $date_times^)? (`durations` $durations^)? attr-dict `:` qualified(type($graph)) + (`types` $types^)? attr-dict `:` qualified(type($graph)) }]; } diff --git a/query/ir/exec/NLSystemData.h b/query/ir/exec/NLSystemData.h index fcd4f62717..924c34757b 100644 --- a/query/ir/exec/NLSystemData.h +++ b/query/ir/exec/NLSystemData.h @@ -5,10 +5,9 @@ #include #include -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" #include "Path.h" +#include "TypedPropertySpec.h" #include "VecLibMetadata.h" #include "columns/ColumnVector.h" @@ -71,22 +70,21 @@ class NLGraphCommandData : public NLFunctionData { }; // LOAD JSONL / LOAD GML / LOAD PARQUET: the file to import, the graph to build -// from it, the per-property embedding dimensions (JSONL alone ever carries any) -// and the statement to blame when the import fails. +// from it, the per-property embedding dimensions (JSONL alone ever carries any), +// the properties it reads as a type the format does not spell, and the statement +// to blame when the import fails. class NLImportGraphData : public NLFunctionData { public: NLImportGraphData(const fs::Path& path, std::string_view graphName, const EmbeddingsSpec& embeddings, - const DateTimeSpec& dateTimes, - const DurationSpec& durations, + const TypedPropertySpec& typedProperties, std::string_view statement, NLViewColumn* graph) : _path(path), _graphName(graphName), _embeddings(embeddings), - _dateTimes(dateTimes), - _durations(durations), + _typedProperties(typedProperties), _statement(statement), _graph(graph) { @@ -95,8 +93,7 @@ class NLImportGraphData : public NLFunctionData { const fs::Path& getPath() const { return _path; } std::string_view getGraphName() const { return _graphName; } const EmbeddingsSpec& getEmbeddings() const { return _embeddings; } - const DateTimeSpec& getDateTimes() const { return _dateTimes; } - const DurationSpec& getDurations() const { return _durations; } + const TypedPropertySpec& getTypedProperties() const { return _typedProperties; } std::string_view getStatement() const { return _statement; } NLViewColumn* getGraph() const { return _graph; } @@ -104,8 +101,7 @@ class NLImportGraphData : public NLFunctionData { fs::Path _path; std::string_view _graphName; EmbeddingsSpec _embeddings; - DateTimeSpec _dateTimes; - DurationSpec _durations; + TypedPropertySpec _typedProperties; std::string_view _statement; NLViewColumn* _graph {nullptr}; }; diff --git a/query/ir/exec/NLSystemExecutor.cpp b/query/ir/exec/NLSystemExecutor.cpp index 25c6fa471e..c47c31499f 100644 --- a/query/ir/exec/NLSystemExecutor.cpp +++ b/query/ir/exec/NLSystemExecutor.cpp @@ -191,8 +191,7 @@ void NLSystemExecutor::runImportGraph(NLExecutionContext* context, NLFunctionDat const Graph* graph = accessor.importGraph(command->getPath(), graphName, command->getEmbeddings(), - command->getDateTimes(), - command->getDurations()); + command->getTypedProperties()); if (!graph) { throw IRException(fmt::format("{}: failed to import graph '{}' from '{}'", command->getStatement(), diff --git a/query/ir/exec/NLSystemTranslator.cpp b/query/ir/exec/NLSystemTranslator.cpp index f9b39f3bde..6d250469da 100644 --- a/query/ir/exec/NLSystemTranslator.cpp +++ b/query/ir/exec/NLSystemTranslator.cpp @@ -12,10 +12,10 @@ #include "NLOps.h" #include "StorageEnums.h" -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" +#include "IRValueTypes.h" #include "Path.h" +#include "TypedPropertySpec.h" #include "LocalMemory.h" @@ -172,17 +172,17 @@ void fillEmbeddingsSpec(mlir::DictionaryAttr attribute, EmbeddingsSpec& specs) { } } -// The properties an import named as instants or durations, one unit entry each - the name -// is the whole of it. A null attribute - an import with no such clause - leaves the spec -// empty. -template -void fillPropertyNameSpec(mlir::DictionaryAttr attribute, PropertyNameSpec& specs) { +// The properties an import reads as a type its format does not spell, each name mapped to +// that type's element type. A null attribute - an import with no such clause - leaves the +// spec empty. +void fillTypedPropertySpec(mlir::DictionaryAttr attribute, TypedPropertySpec& specs) { if (!attribute) { return; } for (const mlir::NamedAttribute& entry : attribute) { - specs.emplace(toStringView(entry.getName().getValue())); + const mlir::TypeAttr type = mlir::cast(entry.getValue()); + specs.emplace(toStringView(entry.getName().getValue()), elementTypeToValueType(type.getValue())); } } @@ -221,18 +221,14 @@ bool NLSystemTranslator::translate(mlir::Operation& operation, NLStmtContainer* EmbeddingsSpec embeddings; fillEmbeddingsSpec(importGraph.getEmbeddingsAttr(), embeddings); - DateTimeSpec dateTimes; - fillPropertyNameSpec(importGraph.getDateTimesAttr(), dateTimes); - - DurationSpec durations; - fillPropertyNameSpec(importGraph.getDurationsAttr(), durations); + TypedPropertySpec typedProperties; + fillTypedPropertySpec(importGraph.getTypesAttr(), typedProperties); NLImportGraphData* data = _program->allocFunctionData(fs::Path(importGraph.getPath().str()), toStringView(importGraph.getGraphName()), embeddings, - dateTimes, - durations, + typedProperties, importStatement(importGraph.getFormat()), allocResult(importGraph.getGraph())); body->emplaceStmt(&NLSystemExecutor::runImportGraph, data); diff --git a/query/parser/CypherGrammar.h b/query/parser/CypherGrammar.h index d9b08f573a..5c464fa2c7 100644 --- a/query/parser/CypherGrammar.h +++ b/query/parser/CypherGrammar.h @@ -364,8 +364,7 @@ class CypherGrammar { LoadJsonlQuery* parseLoadJsonl(); void parseJsonlOptions(JsonlImportSpecs& specs); void parseJsonlOption(JsonlImportSpecs& specs); - template - void parseSpecNames(Spec& specs); + void parseTypedNames(TypedPropertySpec& specs, ValueType type); void parseEmbeddingSpecs(EmbeddingsSpec& specs); std::pair parseEmbeddingSpec(); ListGraphQuery* parseListGraphQuery(); diff --git a/query/parser/CypherGrammarQueries.cpp b/query/parser/CypherGrammarQueries.cpp index 16a30b20b9..5c7aa7954d 100644 --- a/query/parser/CypherGrammarQueries.cpp +++ b/query/parser/CypherGrammarQueries.cpp @@ -556,8 +556,7 @@ LoadJsonlQuery* CypherGrammar::parseLoadJsonl() { if (hasOptions) { query->setEmbeddingSpecs(std::move(specs._embeddings)); - query->setDateTimeSpecs(std::move(specs._dateTimes)); - query->setDurationSpecs(std::move(specs._durations)); + query->setTypedPropertySpec(std::move(specs._types)); } locate(query, start); @@ -583,27 +582,26 @@ void CypherGrammar::parseJsonlOption(JsonlImportSpecs& specs) { parseEmbeddingSpecs(specs._embeddings); expect(CypherTokenKind::CBRACK); } else if (accept(CypherTokenKind::DATETIMES)) { - expect(CypherTokenKind::OBRACK); - parseSpecNames(specs._dateTimes); - expect(CypherTokenKind::CBRACK); + parseTypedNames(specs._types, ValueType::DateTime); } else if (accept(CypherTokenKind::DURATIONS)) { - expect(CypherTokenKind::OBRACK); - parseSpecNames(specs._durations); - expect(CypherTokenKind::CBRACK); + parseTypedNames(specs._types, ValueType::Duration); } else { throwUnexpectedToken(); } } -template -void CypherGrammar::parseSpecNames(Spec& specs) { +void CypherGrammar::parseTypedNames(TypedPropertySpec& specs, ValueType type) { + expect(CypherTokenKind::OBRACK); + do { if (accept(CypherTokenKind::STRING_LITERAL)) { - specs.emplace(_previous._text); + specs.emplace(_previous._text, type); } else { - specs.emplace(parseName()->getName()); + specs.emplace(parseName()->getName(), type); } } while (accept(CypherTokenKind::COMMA)); + + expect(CypherTokenKind::CBRACK); } void CypherGrammar::parseEmbeddingSpecs(EmbeddingsSpec& specs) { @@ -927,13 +925,11 @@ LoadParquetQuery* CypherGrammar::parseLoadParquet() { graphName = parseName(); } - DurationSpec durations; + TypedPropertySpec types; const bool hasDurations = accept(CypherTokenKind::WITH); if (hasDurations) { expect(CypherTokenKind::DURATIONS); - expect(CypherTokenKind::OBRACK); - parseSpecNames(durations); - expect(CypherTokenKind::CBRACK); + parseTypedNames(types, ValueType::Duration); } LoadParquetQuery* query = LoadParquetQuery::create(_ast, fs::Path(std::string(path))); @@ -943,7 +939,7 @@ LoadParquetQuery* CypherGrammar::parseLoadParquet() { } if (hasDurations) { - query->setDurationSpecs(std::move(durations)); + query->setTypedPropertySpec(std::move(types)); } locate(query, start); diff --git a/system/GraphManager.cpp b/system/GraphManager.cpp index 21b5fb9392..8a54e5098b 100644 --- a/system/GraphManager.cpp +++ b/system/GraphManager.cpp @@ -177,8 +177,7 @@ Graph* GraphManager::importGraph(std::string_view graphName, const fs::Path& filePath, JobSystem* jobSystem, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs) { + const TypedPropertySpec& typedProperties) { const fs::Path graphPath = _config->getGraphsDir() / filePath; // Step 1. Check if graph was already loaded || is already loading @@ -207,18 +206,18 @@ Graph* GraphManager::importGraph(std::string_view graphName, return loadGmlDB(graphName, absolute, jobSystem); break; case GraphFileType::JSONL: - return loadJsonlDB(graphName, absolute, jobSystem, embeddingSpecs, dateTimeSpecs, durationSpecs); + return loadJsonlDB(graphName, absolute, jobSystem, embeddingSpecs, typedProperties); break; case GraphFileType::BINARY: return loadBinaryDB(graphName, absolute, jobSystem); break; case GraphFileType::PARQUET: - return loadParquetDB(graphName, absolute, jobSystem, durationSpecs); + return loadParquetDB(graphName, absolute, jobSystem, typedProperties); break; case GraphFileType::UNKNOWN: // If we can not determine the file type, assume it is a JSONL graph // FIXME To be changed - return loadJsonlDB(graphName, absolute, jobSystem, embeddingSpecs, dateTimeSpecs, durationSpecs); + return loadJsonlDB(graphName, absolute, jobSystem, embeddingSpecs, typedProperties); break; case GraphFileType::_SIZE: throw TuringException("Unsupported graph type"); @@ -303,8 +302,7 @@ Graph* GraphManager::loadJsonlDB(std::string_view graphName, const fs::Path& dbPath, JobSystem* jobSystem, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs) { + const TypedPropertySpec& typedProperties) { fs::Path graphPath; getGraphPath(graphName, graphPath); if (graphPath == dbPath) { @@ -333,7 +331,7 @@ Graph* GraphManager::loadJsonlDB(std::string_view graphName, Change* change = _changes.createChange(graph.get(), CommitHash::head()); ChangeAccessor changeAccessor = change->access(); - const auto importRes = JsonlParser::parse(changeAccessor, file, embeddingSpecs, dateTimeSpecs, durationSpecs); + const auto importRes = JsonlParser::parse(changeAccessor, file, embeddingSpecs, typedProperties); if (!importRes) { _changes.deleteChange(changeAccessor, change->id()); @@ -415,7 +413,7 @@ Graph* GraphManager::loadGmlDB(std::string_view graphName, Graph* GraphManager::loadParquetDB(std::string_view graphName, const fs::Path& dbPath, JobSystem* jobSystem, - const DurationSpec& durationSpecs) { + const TypedPropertySpec& typedProperties) { fs::Path graphPath; getGraphPath(graphName, graphPath); if (graphPath == dbPath) { @@ -451,7 +449,7 @@ Graph* GraphManager::loadParquetDB(std::string_view graphName, // Unfortunately I think we need to have this try-catch due to the dependency on the // Parquet lib which throws. Removing would require a rewrite of the reader. // TODO: Attempt to remove this try-catch and make the importer/reader exception free - ParquetImporter importer(nodeFile, edgeFile, commitBuilder, durationSpecs); + ParquetImporter importer(nodeFile, edgeFile, commitBuilder, typedProperties); try { importer.import(); } catch (...) { diff --git a/system/GraphManager.h b/system/GraphManager.h index 5935104698..4eb8e50e16 100644 --- a/system/GraphManager.h +++ b/system/GraphManager.h @@ -7,13 +7,12 @@ #include #include "ChangeManager.h" -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" #include "GraphFileType.h" #include "GraphLoadStatus.h" #include "ObjectMap.h" #include "Path.h" +#include "TypedPropertySpec.h" #include "dump/LoadGraphResult.h" namespace db { @@ -52,8 +51,7 @@ class GraphManager { const fs::Path& filePath, JobSystem* jobSystem, const EmbeddingsSpec& embeddingSpecs = {}, - const DateTimeSpec& dateTimeSpecs = {}, - const DurationSpec& durationSpecs = {}); + const TypedPropertySpec& typedProperties = {}); GraphFileType getGraphFileType(const fs::Path& graphPath) const; @@ -86,8 +84,7 @@ class GraphManager { const fs::Path& dbPath, JobSystem* jobSystem, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs); + const TypedPropertySpec& typedProperties); Graph* loadGmlDB(std::string_view graphName, const fs::Path& dbPath, @@ -100,6 +97,6 @@ class GraphManager { Graph* loadParquetDB(std::string_view graphName, const fs::Path& dbPath, JobSystem* jobSystem, - const DurationSpec& durationSpecs); + const TypedPropertySpec& typedProperties); }; } diff --git a/system/SystemAccessor.cpp b/system/SystemAccessor.cpp index eee7c6ab06..d1e1416321 100644 --- a/system/SystemAccessor.cpp +++ b/system/SystemAccessor.cpp @@ -67,9 +67,8 @@ DumpResult SystemAccessor::loadCommit(std::string_view name, CommitHash ha Graph* SystemAccessor::importGraph(const fs::Path& path, std::string_view name, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs) { - return _sysMan->importGraph(path, name, embeddingSpecs, dateTimeSpecs, durationSpecs); + const TypedPropertySpec& typedProperties) { + return _sysMan->importGraph(path, name, embeddingSpecs, typedProperties); } GraphFileType SystemAccessor::getGraphFileType(const fs::Path& path) const { diff --git a/system/SystemAccessor.h b/system/SystemAccessor.h index 1801694955..e24032f63f 100644 --- a/system/SystemAccessor.h +++ b/system/SystemAccessor.h @@ -13,9 +13,8 @@ #include "dump/LoadGraphResult.h" #include "mergers/DataPartMergeResult.h" -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" +#include "TypedPropertySpec.h" #include "GraphFileType.h" #include "Path.h" @@ -73,8 +72,7 @@ class SystemAccessor : public Accessor { Graph* importGraph(const fs::Path& path, std::string_view name, const EmbeddingsSpec& embeddingSpecs = {}, - const DateTimeSpec& dateTimeSpecs = {}, - const DurationSpec& durationSpecs = {}); + const TypedPropertySpec& typedProperties = {}); GraphFileType getGraphFileType(const fs::Path& path) const; diff --git a/system/SystemManager.cpp b/system/SystemManager.cpp index f563c28585..31aae93e97 100644 --- a/system/SystemManager.cpp +++ b/system/SystemManager.cpp @@ -187,9 +187,8 @@ void SystemManager::listGraphs(std::vector& names) const { Graph* SystemManager::importGraph(const fs::Path& path, std::string_view graphName, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs) { - return _graphManager.importGraph(graphName, path, &_jobSystem, embeddingSpecs, dateTimeSpecs, durationSpecs); + const TypedPropertySpec& typedProperties) { + return _graphManager.importGraph(graphName, path, &_jobSystem, embeddingSpecs, typedProperties); } DumpResult SystemManager::dumpGraph(std::string_view graphName) { diff --git a/system/SystemManager.h b/system/SystemManager.h index d62d23e827..50a0992957 100644 --- a/system/SystemManager.h +++ b/system/SystemManager.h @@ -6,10 +6,9 @@ #include #include -#include "DateTimeSpec.h" -#include "DurationSpec.h" #include "EmbeddingsSpec.h" #include "GraphManager.h" +#include "TypedPropertySpec.h" #include "SystemAccessor.h" @@ -106,8 +105,7 @@ class SystemManager { Graph* importGraph(const fs::Path& path, std::string_view graphName, const EmbeddingsSpec& embeddingSpecs, - const DateTimeSpec& dateTimeSpecs, - const DurationSpec& durationSpecs); + const TypedPropertySpec& typedProperties); GraphFileType getGraphFileType(const fs::Path& graphPath) const; diff --git a/test/query/parser/CypherASTComparator.cpp b/test/query/parser/CypherASTComparator.cpp index 2e8d364d3b..ca2c056b91 100644 --- a/test/query/parser/CypherASTComparator.cpp +++ b/test/query/parser/CypherASTComparator.cpp @@ -15,12 +15,11 @@ #include "CreateNodePropertyIndexQuery.h" #include "CreateVectorIndexQuery.h" #include "CypherAST.h" -#include "DateTimeSpec.h" #include "DeleteVectorIndexQuery.h" #include "DropIndexQuery.h" -#include "DurationSpec.h" #include "EdgePattern.h" #include "EmbeddingsSpec.h" +#include "TypedPropertySpec.h" #include "ExplainRequest.h" #include "FunctionInvocation.h" #include "InstallExtensionQuery.h" @@ -648,7 +647,7 @@ void renderBound(int64_t bound, std::string& rendered) { fmt::format_to(std::back_inserter(rendered), "{}", bound); } -void renderNames(const std::vector& names, std::string& rendered) { +void renderTypedNames(const std::vector>& names, std::string& rendered) { rendered += '{'; for (size_t index = 0; index < names.size(); index++) { @@ -656,7 +655,9 @@ void renderNames(const std::vector& names, std::string& render rendered += ", "; } - renderQuoted(names[index], rendered); + renderQuoted(names[index].first, rendered); + rendered += ": "; + rendered += ValueTypeName::value(names[index].second); } rendered += '}'; @@ -809,10 +810,11 @@ bool CypherASTComparator::compareEnum(std::string_view field, Enum expected, Enu return false; } -template -bool CypherASTComparator::compareNameSpec(std::string_view field, const Spec& expected, const Spec& actual) { - std::vector expectedNames(expected.begin(), expected.end()); - std::vector actualNames(actual.begin(), actual.end()); +bool CypherASTComparator::compareTypedPropertySpec(std::string_view field, + const TypedPropertySpec& expected, + const TypedPropertySpec& actual) { + std::vector> expectedNames(expected.begin(), expected.end()); + std::vector> actualNames(actual.begin(), actual.end()); std::ranges::sort(expectedNames); std::ranges::sort(actualNames); @@ -822,10 +824,10 @@ bool CypherASTComparator::compareNameSpec(std::string_view field, const Spec& ex } std::string expectedText; - renderNames(expectedNames, expectedText); + renderTypedNames(expectedNames, expectedText); std::string actualText; - renderNames(actualNames, actualText); + renderTypedNames(actualNames, actualText); const PathScope scope(_path, field); reportDifference(expectedText, actualText); @@ -1183,8 +1185,7 @@ bool CypherASTComparator::compareLoadJsonlQuery(const LoadJsonlQuery* expected, return compareString("filePath", expected->getFilePath().get(), actual->getFilePath().get()) && compareString("graphName", expected->getGraphName(), actual->getGraphName()) && compareEmbeddingSpec("embeddingSpecs", expected->getEmbeddingSpecs(), actual->getEmbeddingSpecs()) - && compareNameSpec("dateTimeSpecs", expected->getDateTimeSpecs(), actual->getDateTimeSpecs()) - && compareNameSpec("durationSpecs", expected->getDurationSpecs(), actual->getDurationSpecs()); + && compareTypedPropertySpec("typedPropertySpec", expected->getTypedPropertySpec(), actual->getTypedPropertySpec()); } bool CypherASTComparator::compareChangeQuery(const ChangeQuery* expected, const ChangeQuery* actual) { @@ -1203,7 +1204,7 @@ bool CypherASTComparator::compareLoadGMLQuery(const LoadGMLQuery* expected, cons bool CypherASTComparator::compareLoadParquetQuery(const LoadParquetQuery* expected, const LoadParquetQuery* actual) { return compareString("filePath", expected->getFilePath().get(), actual->getFilePath().get()) && compareString("graphName", expected->getGraphName(), actual->getGraphName()) - && compareNameSpec("durationSpecs", expected->getDurationSpecs(), actual->getDurationSpecs()); + && compareTypedPropertySpec("typedPropertySpec", expected->getTypedPropertySpec(), actual->getTypedPropertySpec()); } bool CypherASTComparator::compareS3ConnectQuery(const S3ConnectQuery* expected, const S3ConnectQuery* actual) { diff --git a/test/query/parser/CypherASTComparator.h b/test/query/parser/CypherASTComparator.h index eeeb0b7503..2669951da5 100644 --- a/test/query/parser/CypherASTComparator.h +++ b/test/query/parser/CypherASTComparator.h @@ -91,6 +91,7 @@ class StringExpr; class Symbol; class SymbolChain; class SymbolExpr; +class TypedPropertySpec; class UnaryExpr; class UnwindStmt; class VectorSearchStmt; @@ -143,8 +144,9 @@ class CypherASTComparator { template bool compareEnum(std::string_view field, Enum expected, Enum actual); - template - bool compareNameSpec(std::string_view field, const Spec& expected, const Spec& actual); + bool compareTypedPropertySpec(std::string_view field, + const db::TypedPropertySpec& expected, + const db::TypedPropertySpec& actual); bool compareSize(std::string_view field, size_t expected, size_t actual); bool comparePresence(std::string_view field, const void* expected, const void* actual, std::string_view className); diff --git a/test/query/parser/CypherASTComparatorTest.cpp b/test/query/parser/CypherASTComparatorTest.cpp index cd69b91122..f7563b131a 100644 --- a/test/query/parser/CypherASTComparatorTest.cpp +++ b/test/query/parser/CypherASTComparatorTest.cpp @@ -157,7 +157,7 @@ const DifferenceCase DIFFERENCES[] = { "queries[0] CreateGraphQuery > graphName: expected \"alpha\", actual \"beta\""}, {"LOAD JSONL 'g.jsonl' WITH DURATIONS [a, b]", "LOAD JSONL 'g.jsonl' WITH DURATIONS [a]", - "queries[0] LoadJsonlQuery > durationSpecs: expected {\"a\", \"b\"}, actual {\"a\"}"}, + "queries[0] LoadJsonlQuery > typedPropertySpec: expected {\"a\": Duration, \"b\": Duration}, actual {\"a\": Duration}"}, {"RETURN 1; RETURN 2", "RETURN 1", "queries: expected size 2, actual size 1"}, diff --git a/test/query/parser/KeywordObjectNameTest.cpp b/test/query/parser/KeywordObjectNameTest.cpp index 2cff4f0381..9a710720c3 100644 --- a/test/query/parser/KeywordObjectNameTest.cpp +++ b/test/query/parser/KeywordObjectNameTest.cpp @@ -139,23 +139,23 @@ TEST_F(KeywordObjectNameTest, importsParquetAsAGraphMatch) { const LoadParquetQuery* load = static_cast(command); EXPECT_EQ(load->getGraphName(), "match"); - EXPECT_TRUE(load->getDurationSpecs().contains("commit")); + EXPECT_EQ(load->getTypedPropertySpec().getType("commit"), ValueType::Duration); } TEST_F(KeywordObjectNameTest, readsDatetimesCommit) { const QueryCommand* command = parseCommand("LOAD JSONL 'f.jsonl' WITH DATETIMES [commit, `created at`]", QueryCommand::Kind::LOAD_JSONL_QUERY); const LoadJsonlQuery* load = static_cast(command); - EXPECT_TRUE(load->getDateTimeSpecs().contains("commit")); - EXPECT_TRUE(load->getDateTimeSpecs().contains("created at")); + EXPECT_EQ(load->getTypedPropertySpec().getType("commit"), ValueType::DateTime); + EXPECT_EQ(load->getTypedPropertySpec().getType("created at"), ValueType::DateTime); } TEST_F(KeywordObjectNameTest, readsDurationsMatch) { const QueryCommand* command = parseCommand("LOAD JSONL 'f.jsonl' WITH DURATIONS [match, `time spent`]", QueryCommand::Kind::LOAD_JSONL_QUERY); const LoadJsonlQuery* load = static_cast(command); - EXPECT_TRUE(load->getDurationSpecs().contains("match")); - EXPECT_TRUE(load->getDurationSpecs().contains("time spent")); + EXPECT_EQ(load->getTypedPropertySpec().getType("match"), ValueType::Duration); + EXPECT_EQ(load->getTypedPropertySpec().getType("time spent"), ValueType::Duration); } TEST_F(KeywordObjectNameTest, readsEmbeddingsEmbedding) { diff --git a/test/query/parser/ParseEdgeCaseTest.cpp b/test/query/parser/ParseEdgeCaseTest.cpp index 79fbef6598..48e89a1f3d 100644 --- a/test/query/parser/ParseEdgeCaseTest.cpp +++ b/test/query/parser/ParseEdgeCaseTest.cpp @@ -374,14 +374,11 @@ TEST_F(ParseEdgeCaseTest, parsesAdministrativeQueries) { embeddings.emplace("e", 4); load->setEmbeddingSpecs(std::move(embeddings)); - DateTimeSpec dateTimes; - dateTimes.emplace("d"); - dateTimes.emplace("t"); - load->setDateTimeSpecs(std::move(dateTimes)); - - DurationSpec durations; - durations.emplace("u"); - load->setDurationSpecs(std::move(durations)); + TypedPropertySpec types; + types.emplace("d", ValueType::DateTime); + types.emplace("t", ValueType::DateTime); + types.emplace("u", ValueType::Duration); + load->setTypedPropertySpec(std::move(types)); expectParse("LOAD JSONL 'f' WITH EMBEDDINGS [{e, 4}] WITH DATETIMES ['d', t] WITH DURATIONS [u]", {load}); } @@ -404,10 +401,10 @@ TEST_F(ParseEdgeCaseTest, parsesAdministrativeQueries) { LoadParquetQuery* load = LoadParquetQuery::create(getExpectedAST(), fs::Path(std::string("f"))); load->setGraphName("g"); - DurationSpec durations; - durations.emplace("d"); - durations.emplace("e"); - load->setDurationSpecs(std::move(durations)); + TypedPropertySpec types; + types.emplace("d", ValueType::Duration); + types.emplace("e", ValueType::Duration); + load->setTypedPropertySpec(std::move(types)); expectParse("LOAD PARQUET 'f' AS g WITH DURATIONS [d, 'e']", {load}); } diff --git a/test/system/FailedImportClearsLoadingTest.cpp b/test/system/FailedImportClearsLoadingTest.cpp index c25349f9c2..43594b6bb0 100644 --- a/test/system/FailedImportClearsLoadingTest.cpp +++ b/test/system/FailedImportClearsLoadingTest.cpp @@ -37,7 +37,7 @@ class FailedImportClearsLoadingTest : public TuringTest { Graph* graph = nullptr; try { - graph = system.importGraph(fs::Path {std::string {fileName}}, "g", {}, {}, {}); + graph = system.importGraph(fs::Path {std::string {fileName}}, "g", {}, {}); } catch (const TuringException&) { } @@ -66,7 +66,7 @@ TEST_F(FailedImportClearsLoadingTest, aMalformedFileLeavesTheNameFree) { } SystemAccessor system = _env->getSystemManager().accessShared(); - EXPECT_NE(system.importGraph(fs::Path {"good.jsonl"}, "g", {}, {}, {}), nullptr); + EXPECT_NE(system.importGraph(fs::Path {"good.jsonl"}, "g", {}, {}), nullptr); } int main(int argc, char** argv) { diff --git a/test/system/ListAvailableGraphsDuringImportTest.cpp b/test/system/ListAvailableGraphsDuringImportTest.cpp index e26ac83f99..44c31ed34b 100644 --- a/test/system/ListAvailableGraphsDuringImportTest.cpp +++ b/test/system/ListAvailableGraphsDuringImportTest.cpp @@ -41,7 +41,7 @@ TEST_F(ListAvailableGraphsDuringImportTest, aListedGraphStaysListedWhileItFinish std::thread importer([&systemManager, &importing, &importedCount] { for (size_t graph = 0; graph < GRAPH_COUNT; graph++) { SystemAccessor system = systemManager.accessShared(); - if (system.importGraph(fs::Path {"tiny.jsonl"}, "g" + std::to_string(graph), {}, {}, {})) { + if (system.importGraph(fs::Path {"tiny.jsonl"}, "g" + std::to_string(graph), {}, {})) { importedCount++; } }