Skip to content

Make a date or a moment from a week, an ordinal day or a quarter, and filter a grouping WITH by an aggregate in its WHERE, as drivine4j does, at Cypher spec v0.19.0 - #318

Merged
jasperblues merged 2 commits into
mainfrom
calendar-having
Oct 31, 2026
Merged

jasperblues merged 2 commits into
mainfrom
calendar-having

Conversation

@jasperblues

Copy link
Copy Markdown
Member

Ports drivine4j's #495 (liberation-data/drivine4j#498) and #497 (liberation-data/drivine4j#501), with liberation-data/drivine-cypher-spec#40 and #41. The compiler follows Drivine for TypeScript's port of the same two.

Calendar fields (#312)

  • date, localdatetime and datetime take {year, week[, dayOfWeek]}, {year, ordinalDay} and {year, quarter[, dayOfQuarter]}, with a time and a timezone beside them. localtime and time refuse these fields.
  • Where the year and the fields are written as numbers, the date is worked out when the statement is compiled and made as {year, month, day} is. That gives Neo4j's refusals at compile time: fields of two calendars, a day without its week or quarter, no year, a number past its range or not whole, and a year past ±999,999,999. Week 53 of a 52-week year and a day of the quarter up to 92 run on into the next. Ordinal day 366 of a common year fails.
  • Where an expression gives a field, Temporals.of_calendar works the date out in SQL. A field past its range gives day 0, and the store fails on it. A moment's date is read as a date first, because Oracle's JSON_VALUE reads a moment whose day is -00 as null.
  • dayOfQuarter can now be read off a temporal value.

HAVING (#313)

  • In a WITH that aggregates, or is DISTINCT, the WHERE filters the groups. An aggregate there is now allowed, except inside a comprehension's, a quantifier's, a reduce's or a pattern's own expressions. The refusal's message now reads "… unless its WITH aggregates, …".
  • _having makes each aggregate in that WHERE an item of the WITH:
    • Each new item is named #havingN by _hidden, which also names _anew_variable's variables, so the name is no text of the statement. Where an item already is that aggregate, its name is read instead.
    • A key written as its item is, before its name, is read as that item.
    • Keys and aggregates compare as grouping compares them. A function's name matches in any case, a property's name only as written, and count(DISTINCT i) is not count(i).
    • The made items aren't bound after the WITH.
  • The WHERE is still met after ORDER BY, SKIP and LIMIT. A CALL that imports the row reads it after the WITH too.

Spec at v0.19.0. HELD (tests/cypher/cypher_spec.py) holds the steps a later port owns. Each held step must still fail; one that passes fails the test, so its hold comes out when its port lands:

The golden statements are unchanged. drivine4j's CompilerRefusalTest, CypherQueryContract and CypherTemporalContract cases that the spec doesn't hold are ported, and the sync twins are regenerated.

Fixes #312
Fixes #313

… filter a grouping WITH by an aggregate in its WHERE, as drivine4j does, at Cypher spec v0.19.0

Ports drivine4j's #495 (merged as liberation-data/drivine4j#498) and #497 (merged as
liberation-data/drivine4j#501), with the spec's liberation-data/drivine-cypher-spec#40 and #41.

Calendar fields (#312):
- date, localdatetime and datetime take {year, week[, dayOfWeek]}, {year, ordinalDay} and
  {year, quarter[, dayOfQuarter]}, beside a time and a timezone; localtime and time refuse them
  ("localtime() takes no field week.").
- Written as numbers, the date is worked out when compiling (_calendar_date, proleptic Gregorian at
  any year) and made as {year, month, day} is, so it fails where Neo4j fails: two calendars, a day
  without its week or quarter, no year, a number past its range or not whole, a year past
  +-999,999,999. Week 53 of a 52-week year and a day of the quarter up to 92 run on into the next;
  ordinal day 366 of a common year fails.
- Given by an expression, Temporals.of_calendar works it out in SQL; a field past its range gives the
  day 0, which the store fails on. A moment's date is read as a date first, as Oracle's JSON_VALUE
  reads a moment with day 0 as null.
- dayOfQuarter is read off a temporal value.

HAVING (#313):
- check_aggregates takes the WHERE of a WITH that aggregates, or is DISTINCT, as filtering its groups;
  it refuses there only an aggregate in a comprehension's, a quantifier's, a reduce's or a pattern's own
  expressions. The message now says "unless its WITH aggregates".
- SqlCypherCompiler._having makes each aggregate in that WHERE an item of the WITH (#havingN, named by
  _hidden as no text of the statement is, or the item that is that aggregate already), and reads a key
  written as its item is before its name as that item, compared as grouping compares them (a
  function's name in any case, a property's name as written, count(DISTINCT i) apart from count(i)).
  The made items are no variables after the WITH. The WHERE is still met after ORDER BY, SKIP and
  LIMIT; a CALL that imports the row reads it after the WITH too.

The spec moves to v0.19.0. HELD in tests/cypher/cypher_spec.py holds the steps later ports own, each
checked to fail until its port lands: #314 (null grouping, v0.17.0), #316 (Implicit grouping key's
nested-body, order-term and any/none steps, v0.18.0), #317 (temporal from another, v0.19.0).

drivine4j's CompilerRefusalTest, CypherQueryContract and CypherTemporalContract cases not in the spec
are ported.

Fixes #312
Fixes #313
…isleap, and say when an aggregate is refused
@jasperblues
jasperblues merged commit f052b90 into main Oct 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant