Skip to content

Make a date, a time or a moment from another temporal value, with the map's fields laid over its own, as drivine4j does - #320

Merged
jasperblues merged 2 commits into
mainfrom
temporal-from-temporal
Oct 31, 2026
Merged

jasperblues merged 2 commits into
mainfrom
temporal-from-temporal

Conversation

@jasperblues

Copy link
Copy Markdown
Member

Port of liberation-data/drivine4j#509 (drivine4j's #508), with liberation-data/drivine-cypher-spec#44, which the spec at v0.19.0 already holds. The compiler follows Drivine for TypeScript's port, liberation-data/drivine#503.

What changes

  • date(), localtime(), time(), localdatetime() and datetime() take a map with date, time or datetime, which is another temporal value. The map's other fields are laid over that value's own: SqlCypherCompiler._from_temporal checks the map, and Temporals.over builds the SQL.
    • A year, month and day replace the value's together. The date is read as a date first, so the store fails where they make no date (Oracle included).
    • A week, ordinalDay or dayOfQuarter goes through of_calendar. A quarter alone or a dayOfWeek alone moves the date. Beside a dayOfWeek, a year moves the date by that many years.
    • A fraction given makes the whole fraction.
    • A zoned value keeps the offset of whatever gives its time. A timezone given moves a zoned value there and names a local value's offset.
  • Where the statement doesn't tell the value's kind (a property, an unwound item), a runtime CASE over the possible kinds reads it. A kind known to be wrong is refused when compiling.
  • A value or zone from an expression that is null or of the wrong kind fails when the statement runs, on day 0, as Neo4j fails.
  • The compiler refuses, with drivine4j's messages:
    • a field the kind doesn't have;
    • a datetime beside a date or a time;
    • a value that isn't a temporal value, or is of the wrong kind;
    • fields of two calendars;
    • a date that isn't whole beside a value that gives only the time;
    • a field of the time without the one above it;
    • a number that isn't whole or is past its range (Java's year bounds for a whole date written beside a time);
    • a time zone that is null or isn't an offset.
  • A zone given by name, 'UTC' included, is now refused in any temporal map, not only one with calendar fields. That includes the constructor of a value given to another, as drivine4j does.
  • Shared helpers rather than parallel ones:

Tests

SQL comparison. Every statement of the spec at v0.19.0 (1,494) and of the openCypher TCK's features (4,119) was compiled for Postgres and Oracle, on main and on this branch:

  • 176 statements per store that main refused as "… of a map with date/time/datetime … not compiled to SQL yet" now compile.
  • 84 per store go from that refusal to one of the new refusals.
  • 19 per store go from compiled to "The time zone 'Europe/Stockholm' is Cypher, and is not compiled to SQL yet." These are maps with a named zone, which gave null before (datetime({…, timezone: 'Europe/Stockholm'}), TCK Temporal1 [10], Temporal3, Temporal5, Temporal6 and Temporal10 [8]). drivine4j#509 refuses them the same way.
  • Nothing else changed.

The 176 newly compiled statements were also compiled with drivine #503:

  • Postgres: the SQL is identical on all 176.
  • Oracle: the SQL is identical apart from this library's existing outer JSON_TRANSFORM(…, KEEP '$').

Against Neo4j. drivine #503's reviewer built a grid of 3,015 statements: 10 source values × 5 targets × each variant, run as a WITH, a stored property, an UNWIND, and a filtered UNWIND, plus the null cases. It was run here on Postgres and Oracle and compared with that grid's Neo4j answers (errors as errors, fractions to the microsecond): 0 differences on either store.

Fixes #317

… map's fields laid over its own, as drivine4j does

Ports drivine4j's #508 (merged as liberation-data/drivine4j#509), with the spec's
liberation-data/drivine-cypher-spec#44, which v0.19.0 already holds. The compiler follows Drivine for
TypeScript's port (liberation-data/drivine#503).

- date(), localtime(), time(), localdatetime() and datetime() take a map with date, time or datetime,
  another temporal value, and lay the map's other fields over its own (SqlCypherCompiler._from_temporal,
  Temporals.over). A year, a month and a day replace the value's together, the date read as a date first
  so the store fails where they make none; a week, an ordinal day or a day of the quarter go through
  of_calendar; a quarter or a day of the week alone moves the date; a fraction given makes the whole
  fraction. A zoned value keeps the offset of what gives its time; a timezone given moves a zoned value
  there and names a local one's offset.
- A value whose kind the statement doesn't tell is read by a CASE over the kinds that can give it; a known
  wrong kind is refused when compiling. A null or wrong-kind value or zone given by an expression fails as
  the statement runs, on the day 0, as Neo4j fails.
- Refused as Neo4j refuses them: a field the kind hasn't, a datetime beside a date or a time, a value that
  is no temporal value, two calendars, a date that isn't whole beside a value that gives the time alone, a
  field of the time without the one above it, a number not whole or past its range, a time zone that is
  null or no offset. A zone by name (UTC among them) is refused as not compiled, now in any temporal map.
- The whole-number check is shared with truncation (_whole_field), and the zone check (_zone_text) and
  the calendars (_calendars_of) with the calendar fields.

The #317 holds come out of HELD. Fixes #317.
…her, and test calendar fields beside a time and Python temporal parameters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cypher on SQL stores: make a date, a time or a moment from another temporal value, as {date: d, week: 2} or {datetime: dt, timezone: '+05:00'}

1 participant