Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<types::List>` 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<types::Map>` 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<types::List>` 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<types::Map>` 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(<integer>)` 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).
Expand Down
1 change: 1 addition & 0 deletions io/parquet/ParquetEdgeVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions io/parquet/ParquetNodeVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions memory/LocalMemory.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class LocalMemory {
MakeMemoryPool<ColumnConst<types::Embedding::Primitive>>::type,
MakeMemoryPool<ColumnConst<types::DateTime::Primitive>>::type,
MakeMemoryPool<ColumnConst<types::Duration::Primitive>>::type,
MakeMemoryPool<ColumnConst<types::Point::Primitive>>::type,

MakeMemoryPool<ColumnConst<std::optional<types::Int64::Primitive>>>::type,
MakeMemoryPool<ColumnConst<std::optional<types::Int64::Primitive>>>::type,
Expand All @@ -91,6 +92,7 @@ class LocalMemory {
MakeMemoryPool<ColumnConst<std::optional<types::Embedding::Primitive>>>::type,
MakeMemoryPool<ColumnConst<std::optional<types::DateTime::Primitive>>>::type,
MakeMemoryPool<ColumnConst<std::optional<types::Duration::Primitive>>>::type,
MakeMemoryPool<ColumnConst<std::optional<types::Point::Primitive>>>::type,

MakeMemoryPool<ColumnConst<PropertyNull>>::type,

Expand All @@ -102,6 +104,7 @@ class LocalMemory {
MakeMemoryPool<ColumnVector<types::Embedding::Primitive>>::type,
MakeMemoryPool<ColumnVector<types::DateTime::Primitive>>::type,
MakeMemoryPool<ColumnVector<types::Duration::Primitive>>::type,
MakeMemoryPool<ColumnVector<types::Point::Primitive>>::type,

MakeMemoryPool<ColumnVector<const CommitBuilder*>>::type,
MakeMemoryPool<ColumnVector<const Change*>>::type,
Expand All @@ -114,6 +117,7 @@ class LocalMemory {
MakeMemoryPool<ColumnOptVector<types::Embedding::Primitive>>::type,
MakeMemoryPool<ColumnOptVector<types::DateTime::Primitive>>::type,
MakeMemoryPool<ColumnOptVector<types::Duration::Primitive>>::type,
MakeMemoryPool<ColumnOptVector<types::Point::Primitive>>::type,
MakeMemoryPool<ColumnOptVector<NodeID>>::type,
MakeMemoryPool<ColumnOptVector<EdgeID>>::type,
MakeMemoryPool<ColumnOptVector<EdgeTypeID>>::type,
Expand Down
3 changes: 3 additions & 0 deletions net/decoders/ParameterMapSink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ void readScalar(const char* bytes, db::ParameterValue& slot) {
case db::CellTag::Duration:
slot.setDuration(readValue<db::types::Duration::Primitive>(value));
break;
case db::CellTag::Point:
throwUnsupportedType("Point");
break;
default:
throw TuringException(fmt::format("Invalid value tag {} in a parameter", static_cast<uint32_t>(tag)));
break;
Expand Down
2 changes: 2 additions & 0 deletions net/decoders/TuringProtoDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ decltype(auto) TuringProtoDecoder<Sink>::dispatchColumnType(ColumnInternalKind t
return fn.template operator()<db::types::Duration::Primitive>(encoding);
case ColumnInternalKind::GRAPH_PATH:
return fn.template operator()<db::GraphPath>(encoding);
case ColumnInternalKind::POINT:
return fn.template operator()<db::types::Point::Primitive>(encoding);
}

throw TuringException("Unsupported incoming column type");
Expand Down
1 change: 1 addition & 0 deletions net/decoders/TuringProtoDecoderConcepts.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ concept TrivialInternalTypes = std::is_same_v<T, db::types::UInt64::Primitive>
|| std::is_same_v<T, db::types::Bool::Primitive>
|| std::is_same_v<T, db::types::DateTime::Primitive>
|| std::is_same_v<T, db::types::Duration::Primitive>
|| std::is_same_v<T, db::types::Point::Primitive>
|| std::is_same_v<T, db::ValueType>
|| std::is_same_v<T, db::NodeID>
|| std::is_same_v<T, db::EdgeID>
Expand Down
5 changes: 5 additions & 0 deletions net/encoders/OutputValues.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ concept IsDateTime = std::is_same_v<TypeUtils::unwrap_optional_t<T>, types::Date
template <typename T>
concept IsDuration = std::is_same_v<TypeUtils::unwrap_optional_t<T>, types::Duration::Primitive>;

template <typename T>
concept IsPoint = std::is_same_v<TypeUtils::unwrap_optional_t<T>, types::Point::Primitive>;

struct ColumnTypeGenerator {
std::string& _name;

Expand All @@ -96,6 +99,8 @@ struct ColumnTypeGenerator {
_name = fmt::format("DateTime");
} else if constexpr (IsDuration<T>) {
_name = fmt::format("Duration");
} else if constexpr (IsPoint<T>) {
_name = fmt::format("Point");
} else if constexpr (IsUInt64<T>) {
_name = fmt::format("UInt64");
} else if constexpr (IsInt64<T>) {
Expand Down
2 changes: 2 additions & 0 deletions net/encoders/TuringProtoEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ struct ColInternalKindToProtoEnum {
return Enum::DATE_TIME;
} else if constexpr (db::IsDuration<T>) {
return Enum::DURATION;
} else if constexpr (db::IsPoint<T>) {
return Enum::POINT;
} else if constexpr (db::IsListView<T>) {
return Enum::LIST_VIEW;
} else if constexpr (db::IsMap<T>) {
Expand Down
1 change: 1 addition & 0 deletions net/turing_proto_common/TuringProtoHeaders.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ enum class ColumnInternalKind : uint32_t {
DATE_TIME,
DURATION,
GRAPH_PATH,
POINT,
};

enum class ColumnKind : uint8_t {
Expand Down
12 changes: 12 additions & 0 deletions procedures/ProcUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<types::Point::Primitive>());

ProcUtils::appendJsonString(out, formatted);
}
break;
case CellTag::MapView:
appendMapValue(out, element.getAs<MapView>());
break;
Expand Down Expand Up @@ -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<V, Point>) {
std::string formatted;
Point::format(formatted, *ptr);

ProcUtils::appendJsonString(out, formatted);
} else if constexpr (std::is_same_v<V, MapView>) {
appendMapValue(out, *ptr);
Expand Down
3 changes: 2 additions & 1 deletion python/turingdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
62 changes: 62 additions & 0 deletions python/turingdb/_common/NanobindUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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<db::Point>& value) const {
return value.has_value() ? point(*value) : nb::none();
}

template <typename T>
nb::object pointColumn(const std::vector<T>& 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()) {
Expand Down Expand Up @@ -241,6 +266,8 @@ struct ValueToPyObject {
return duration(element.getAs<T>());
} else if constexpr (std::is_same_v<T, db::types::DateTime::Primitive>) {
return dateTime(element.getAs<T>());
} else if constexpr (std::is_same_v<T, db::types::Point::Primitive>) {
return point(element.getAs<T>());
} else if constexpr (std::is_same_v<T, db::PathView>) {
nb::list ids;
for (const uint64_t id : element.getAs<T>().getIDs()) {
Expand Down Expand Up @@ -367,6 +394,12 @@ nb::dict dataframeToNumpy(db::Dataframe* df) {
dtypeName = "Duration";
break;
}
case db::ColumnVector<db::types::Point::Primitive>::staticKind(): {
const auto& src = static_cast<const db::ColumnVector<db::types::Point::Primitive>*>(col)->getRaw();
value = listVisitor.pointColumn(src);
dtypeName = "Point";
break;
}
case db::ColumnVector<db::types::Embedding::Primitive>::staticKind(): {
const auto& src = static_cast<const db::ColumnVector<db::types::Embedding::Primitive>*>(col)->getRaw();
nb::list lst;
Expand Down Expand Up @@ -533,6 +566,13 @@ nb::dict dataframeToNumpy(db::Dataframe* df) {
break;
}

case db::ColumnOptVector<db::types::Point::Primitive>::staticKind(): {
const auto& src = static_cast<const db::ColumnOptVector<db::types::Point::Primitive>*>(col)->getRaw();
value = listVisitor.pointColumn(src);
dtypeName = "Point";
break;
}

case db::ColumnConst<db::types::UInt64::Primitive>::staticKind(): {
const auto& v = static_cast<const db::ColumnConst<db::types::UInt64::Primitive>*>(col)->getRaw();
value = repeatValueAsNdarray(v, rowCount);
Expand Down Expand Up @@ -614,6 +654,28 @@ nb::dict dataframeToNumpy(db::Dataframe* df) {
dtypeName = "Duration";
break;
}
case db::ColumnConst<db::types::Point::Primitive>::staticKind(): {
const auto& v = static_cast<const db::ColumnConst<db::types::Point::Primitive>*>(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<std::optional<db::types::Point::Primitive>>::staticKind(): {
const auto& v = static_cast<const db::ColumnConst<std::optional<db::types::Point::Primitive>>*>(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<db::types::Embedding::Primitive>::staticKind(): {
const auto& v = static_cast<const db::ColumnConst<db::types::Embedding::Primitive>*>(col)->getRaw();
nb::object py = embeddingToNdarray(v);
Expand Down
16 changes: 16 additions & 0 deletions python/turingdb/point.py
Original file line number Diff line number Diff line change
@@ -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]
Loading