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
10 changes: 5 additions & 5 deletions docs/maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions docs/points.md
Original file line number Diff line number Diff line change
@@ -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<types::Point>`.
`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.
4 changes: 2 additions & 2 deletions import/common/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 0 additions & 9 deletions import/common/DateTimeSpec.cpp

This file was deleted.

29 changes: 0 additions & 29 deletions import/common/DateTimeSpec.h

This file was deleted.

9 changes: 0 additions & 9 deletions import/common/DurationSpec.cpp

This file was deleted.

29 changes: 0 additions & 29 deletions import/common/DurationSpec.h

This file was deleted.

14 changes: 4 additions & 10 deletions import/common/JsonlImportSpecs.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

#include <utility>

#include "DateTimeSpec.h"
#include "DurationSpec.h"
#include "EmbeddingsSpec.h"
#include "TypedPropertySpec.h"

namespace db {

Expand All @@ -13,21 +12,16 @@ 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) {
for (const auto& [name, dimension] : other._embeddings) {
_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);
}
}
};
Expand Down
24 changes: 24 additions & 0 deletions import/common/TypedPropertySpec.cpp
Original file line number Diff line number Diff line change
@@ -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});
}
}
45 changes: 45 additions & 0 deletions import/common/TypedPropertySpec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once

#include <string_view>
#include <unordered_map>
#include <vector>

#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<std::string_view, ValueType>;
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<Clash>& 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<Clash> _clashes;
};

}
22 changes: 10 additions & 12 deletions import/jsonl/JsonlParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<std::optional<DateTime>> 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<DateTime> {};
}

Expand All @@ -179,9 +178,9 @@ JsonlImportResult<std::optional<DateTime>> tryGetDateTime(std::string_view name,

JsonlImportResult<std::optional<Duration>> 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<Duration> {};
}

Expand Down Expand Up @@ -215,8 +214,7 @@ std::optional<size_t> tryGetEmbDim(std::string_view name,
JsonlImportResult<void> 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;
Expand Down Expand Up @@ -303,13 +301,13 @@ JsonlImportResult<void> JsonlParser::parse(ChangeAccessor& change,
}

const JsonlImportResult<std::optional<DateTime>> instant =
tryGetDateTime(ptName, value, dateTimeSpecs, lineNumber);
tryGetDateTime(ptName, value, typedProperties, lineNumber);
if (!instant) {
return instant.get_unexpected();
}

const JsonlImportResult<std::optional<Duration>> duration =
tryGetDuration(ptName, value, durationSpecs, lineNumber);
tryGetDuration(ptName, value, typedProperties, lineNumber);
if (!duration) {
return duration.get_unexpected();
}
Expand Down Expand Up @@ -418,13 +416,13 @@ JsonlImportResult<void> JsonlParser::parse(ChangeAccessor& change,
}

const JsonlImportResult<std::optional<DateTime>> instant =
tryGetDateTime(ptName, value, dateTimeSpecs, lineNumber);
tryGetDateTime(ptName, value, typedProperties, lineNumber);
if (!instant) {
return instant.get_unexpected();
}

const JsonlImportResult<std::optional<Duration>> duration =
tryGetDuration(ptName, value, durationSpecs, lineNumber);
tryGetDuration(ptName, value, typedProperties, lineNumber);
if (!duration) {
return duration.get_unexpected();
}
Expand Down
Loading