TCK Compliance

Graphersal's GQL is measured against the openCypher TCK (Technology Compatibility Kit): Gherkin scenarios that each set up a graph, run one query and state the exact expected result, side effects or error. They exercise the Cypher compatibility forms that gql() accepts (CREATE, MATCH ... RETURN, WITH, UNWIND, ...) with Neo4j's semantics, so the kit works as a ready-made differential test. It serves three purposes:

  1. Compliance measure: what passes, what does not, and why.
  2. Regression gate: a test case that passed must keep passing; cargo test fails otherwise.
  3. Backlog generator: the report ranks the missing constructs and lists every wrong behaviour.

Graphersal's GQL is not a Cypher implementation; the TCK measures how far its compatibility forms agree with the behaviour the kit specifies.

The kit

  • Pinned version: openCypher TCK release 2024.3 (commit 677cbafabb8c3c5eed458fd3b1ec0daec8d67d23), vendored unmodified under crates/graphersal/tests/opencypher/ (features/, the named graphs in graphs/, its licence and notice). The directory's README.md records the upgrade procedure. A test case is a scenario, or one Examples row of a scenario outline.
  • Runner: the integration test target crates/graphersal/tests/opencypher_tck.rs (feature gql), with its harness in crates/graphersal/tests/opencypher/harness/. Every case runs on a fresh graph through the Rust GQL API: the setup queries (having executed, the named graphs) run through gql()'s own writes, the query runs with its parameters, then its rows, its side effects (a state diff) and its error are compared. Every query runs under resource limits and a wall-clock guard.
  • Gate: crates/graphersal/tests/opencypher/baseline.toml lists every in-scope case that does not pass yet, with its class and a one-line reason.

Headline numbers

The numbers below are the kit's result on the current code. All totals and percentages count in-scope cases only (see Out of scope). The text between the opencypher:begin / opencypher:end markers on this page is generated by the run and checked by every test run (see Running and regenerating); never edit it by hand.

3887 in-scope test cases; 10 excluded (upstream-ignore 1, w21-property-kinds 1, parameter-at-compile-time 6, year-range 2).

Test casesShare
Passed3810 / 388798.0 %
Unsupported (not yet or refused)77 / 38872.0 %
Wrong behaviour of supported constructs0 / 38870.0 %
Passed without expressions/temporal2813 / 288597.5 %

Unsupported = missing 74, setup 3. Wrong behaviour = wrong_result 0, wrong_columns 0, wrong_side_effects 0, wrong_error 0, unexpected_error 0, timeout 0, panic 0.

The date and time functions (date(), datetime(), duration(), datetime.truncate(), ...) are supported with the differences listed on Semantics (zoned time() not yet), and expressions/temporal is about a quarter of the kit and its least covered part, so the headline also shows the share without it. The write clauses (CREATE, SET, REMOVE, DELETE, MERGE) pass, and so do the procedure calls of clauses/call: the kit declares each procedure it calls with its signature and a table of rows, and the harness saves it as a saved GQL query before the case runs (test.my.proc is the query proc of the folder test/my, its parameters typed from the signature, its body returning the table's rows that match the arguments), which the query then calls with CALL test.my.proc(..) (Calling saved queries). rand() is refused by design (Refused Constructs). The ranking with counts is in the report.

Per sub-directory of the kit:

DirectoryPass %PassedTotalUnsupportedWrong
clauses/call100.0 %5252··
clauses/create100.0 %7878··
clauses/delete100.0 %4141··
clauses/match99.2 %3783813·
clauses/match-where100.0 %3434··
clauses/merge100.0 %7575··
clauses/remove100.0 %3333··
clauses/return98.4 %62631·
clauses/return-orderby100.0 %3535··
clauses/return-skip-limit96.0 %24251·
clauses/set100.0 %5252··
clauses/union100.0 %1212··
clauses/unwind100.0 %1414··
clauses/with96.6 %28291·
clauses/with-orderBy100.0 %292292··
clauses/with-skip-limit77.8 %792·
clauses/with-where100.0 %1919··
expressions/aggregation100.0 %3535··
expressions/boolean100.0 %150150··
expressions/comparison100.0 %7272··
expressions/conditional100.0 %1313··
expressions/existentialSubqueries100.0 %1010··
expressions/graph100.0 %6060··
expressions/list100.0 %185185··
expressions/literals100.0 %131131··
expressions/map100.0 %4444··
expressions/mathematical100.0 %66··
expressions/null100.0 %4444··
expressions/path100.0 %77··
expressions/pattern98.0 %49501·
expressions/precedence100.0 %121121··
expressions/quantifier89.6 %54160463·
expressions/string100.0 %3232··
expressions/temporal99.5 %99710025·
expressions/typeConversion100.0 %4747··
useCases/countingSubgraphMatches100.0 %1111··
useCases/triadicSelection100.0 %1919··
all98.0 %3810388777·

Out of scope

Some cases are excluded from the scope: they are not run, get the class excluded, have no baseline entry and are in no total or percentage. They are listed by case id, grouped by reason, in crates/graphersal/tests/opencypher/harness/scope.rs (ids, never tags or content, so a case that a kit upgrade adds is never excluded silently). Merely unimplemented constructs are never excluded: they stay missing, the backlog.

CodeWhyDocsTest cases
upstream-ignoremarked @ignore by the TCK itself ("does not work in Cypher")gql/tck_compliance.md#out-of-scope1
w21-property-kindsa list of maps is a valid property value (maps and lists of any kinds are stored as given); Neo4j raises a TypeError (deliberate deviation)gql/semantics.md#writes1
parameter-at-compile-timeparameters are bound when the query is compiled: a negative or non-integer SKIP / LIMIT parameter is refused then (22G02 / 22G03), not at runtime (deliberate deviation)gql/semantics.md#parameters-are-bound-when-the-query-is-compiled6
year-rangetemporal values hold the years -9999 to 9999; a year beyond them is a data error (22008), Neo4j reads years up to 999999999 (deliberate deviation)gql/semantics.md#where-dates-and-times-differ-from-neo4j2
all10
  • upstream-ignore: the one scenario that the kit itself marks @ignore.
  • w21-property-kinds (a deliberate deviation, user decision): a list of maps is a valid property value in Graphersal, where Neo4j raises a TypeError (Semantics).
  • parameter-at-compile-time (a deliberate deviation, user decision): a negative or non-integer $parameter of SKIP / LIMIT is refused when the query is compiled, since parameters are bound then; the kit expects the SyntaxError at runtime. The user sees the same thing, an error before any row (Semantics).

Every difference from Neo4j, also those the kit has no case for, is listed on Semantics.

The deliberate deviations run in a guard test (deviation_cases_still_deviate): it fails when one of them passes, so it moves back into scope. id() (an integer in Neo4j, a string id here) does not occur in the pinned kit.

Classes

ClassMeaning
passevery expectation holds: rows (and their order where stated), column names, side effects, the error type and phase
missingthe query uses a construct that gql() refuses or does not support yet (GQLSTATUS 0A000); the report ranks the constructs
setupa setup step (having executed, a named graph, a parameter value) does not run, usually for the same reason
excludedout of the scope (above): not run, in no total
wrong_resultthe rows differ
wrong_columnsthe column names differ
wrong_side_effectsthe side effects differ, or a failed query changed the graph
wrong_erroran error was expected and another one (type or phase), or none, came
unexpected_erroran error where a result was expected
timeouta time or resource budget of the harness fired
panica panic was caught (a rule-1 violation, top priority)

The headline groups them: Unsupported = missing + setup; Wrong behaviour of supported constructs = the seven classes from wrong_result to panic. An unimplemented construct is fine; a supported construct must behave as the kit specifies, unless a user decision says otherwise (then the case is excluded with its reason and documented as a difference from Neo4j). The wrong-behaviour count is driven to 0.

Errors match by type and phase only, never by the detail text. The phase comes from where the error arose: compile time is an error of parsing, binding or planning, before the traversal runs; runtime is an error while it runs.

Running and regenerating

# Run the kit (also part of the plain `cargo test --package graphersal --features script,io,display,serde,persist,gql`)
cargo test --package graphersal --features script,io,display,serde,persist,gql --test opencypher_tck

# Rewrite the baseline AND the generated blocks of this page after intended changes; review the diff like code
OPENCYPHER_UPDATE_BASELINE=1 cargo test --package graphersal --features script,io,display,serde,persist,gql --test opencypher_tck

Every run prints a summary to stderr and writes the full report to target/opencypher-report.md: the headline, the per-directory table, the exclusions with their ids, the full wrong-behaviour list by class, the missing constructs ranked by the number of cases they block, the setup failures by reason and the baseline class changes. OPENCYPHER_DETAILS=1 prints one line per in-scope case that does not pass; OPENCYPHER_LANES=<n> sets the number of parallel lanes (default: one per core).

The test fails when:

  • a case that is not in the baseline does not pass (a regression);
  • a case in the baseline passes now, no longer exists or is excluded (the entry is stale; remove it in the same change);
  • an id of the scope table does not exist, an @ignore case is not in it, or a deliberate deviation passes now (scope_table_is_consistent, deviation_cases_still_deviate);
  • a generated block of this page (scope, headline, directories, excluded) differs from what the run computes. Only trailing whitespace is ignored.

A case still failing under another class than recorded is reported, not failed. GRAPHERSAL_TEST_STORAGE=forwarding runs the whole kit on a storage that is not TraversalGraph (the Forwarding wrapper of graphersal-storage-tests): the same gate against the same baseline and page, it additionally fails when a case changes class, never rewrites either and writes target/opencypher-report-forwarding.md.