Skip to content

feat(gfql): index DDL accepts the Cypher optional spellings - #2131

Merged
lmeyerov merged 5 commits into
masterfrom
feat/gfql-index-ddl-cypher-spellings
Oct 4, 2026
Merged

lmeyerov merged 5 commits into
masterfrom
feat/gfql-index-ddl-cypher-spellings

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Audit of the GFQL index DDL against current Cypher / GQL spellings (Neo4j Cypher manual; the GQL implementations that add index DDL use Neo4j's spelling; ISO/IEC 39075's own DDL list has no index statement; GSQL does it as ALTER VERTEX ... ADD INDEX name ON (attr) inside a schema-change job). Our FOR <kind> target and the mandatory GFQL token are deliberate and stay; three optional parts were spelled only our way:

Cypher spelling (now accepted) GFQL spelling (unchanged)
CREATE GFQL INDEX [name] IF NOT EXISTS FOR <kind> CREATE GFQL INDEX [name] FOR <kind> (a repeat CREATE was already a no-op)
... ON (col) ... ON col
DROP GFQL INDEX name IF EXISTS DROP GFQL INDEX IF EXISTS name

Both spellings parse to the same wire op. Misplaced options stay malformed: IF EXISTS on CREATE, IF NOT EXISTS on DROP, the option twice, unbalanced parentheses, the option before the name.

Docs: one sentence in indexing.rst lists the optional parts.
Docs: the indexing.rst paragraph that said building and querying are two calls and that a seed list takes the scan path / is not accepted in GRAPH { } is corrected (fused DDL landed in cdac877, #2119; WHERE m.id IN [...] is index-served in the row form at scale and accepted in the GRAPH { } form on the scan path). This was the residual of #2130, closed as superseded.

Test plan

  • graphistry/tests/compute/gfql/index/test_index_ddl_cypher_spellings.py (12): same-op pins for five spellings, option values land in the op, six malformed forms
  • graphistry/tests/compute/gfql/index: 1140 passed; docs/test_doc_examples.py -k indexing: passed; bin/lint.sh, mypy clean
  • CI green at the head

🤖 Generated with Claude Code

https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp

CREATE GFQL INDEX [name] [IF NOT EXISTS] FOR <kind> [ON (col)] and DROP GFQL INDEX name
[IF EXISTS] parse to the same ops as the existing forms; misplaced options stay malformed.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
@lmeyerov

lmeyerov commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Real-GPU receipt (dgx-spark, graphistry/test-rapids-official:26.02-gfql-polars, cudf 26.02.01 / cupy 13.6.0 / polars 1.35.2) at 96880e5: 327 passed, 2 xfailed (spelling pins + test_index).

…-cypher-spellings

# Conflicts:
#	CHANGELOG.md
…pelling; one grammar reference

The docs correction described in the PR body was left uncommitted: the page said building and
querying are two calls and that a seed list takes the scan path, while the same page documented
the fused form. The paragraph and the bullet now state what gfql_explain reports at scale. The
module docstring is the single statement of the grammar, `ON(col)` without a space parses as in
Cypher, and the Cypher spellings are driven through one gfql() call on every engine with a
route_engaged pin against the two-step form.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Read-only review (parallel session) — Cypher DDL spellings, head 96880e5

Nothing here touches the branch; findings only, fixes deferred.

Note: the colleague reports a local merge of master plus the indexing.rst paragraph fix NOT yet pushed;
this review covers the pushed head only. Re-review the docs paragraph after the push.

Evidence (parse-level probes on a detached worktree of the head, scratchpad probe):

  • Positive: create gfql index if not exists for node_id (lowercase), my_idx IF NOT EXISTS FOR node_prop ON (score);,
    ON ( id ), DROP GFQL INDEX if exists my_idx, DROP GFQL INDEX my_idx if exists; all parse to the same ops as
    the GFQL spellings. One-call interplay works: CREATE ... ON (score); MATCH (n) RETURN n splits into 1 op + query;
    CREATE ... IF NOT EXISTS ... ON (score); DROP GFQL INDEX my_idx IF EXISTS; MATCH ... into 2 ops + query.
  • Negative: ON (), ON (a, b), ON (n.score), IF FOR, FOR FOR, doubled IF NOT EXISTS, bare DROP ... IF EXISTS,
    doubled IF EXISTS, IF EXISTS trailing a FOR <kind> DROP — all Malformed GFQL INDEX DDL. Correct: Cypher's
    trailing IF EXISTS belongs to the name form only.
  • cdac877cb cited in the body is on master (it is the feature commit of feat(gfql/index): index DDL travels with the query that uses it, in one gfql() call (#2119) #2132).

Findings

  • SUGGESTION (testing): all 12 tests are parse_index_ddl-level. Add one end-to-end g.gfql("CREATE GFQL INDEX IF NOT EXISTS FOR node_prop ON (score); MATCH ...") pin and one DROP ... name IF EXISTS on a missing name
    (must be a no-op, not an error) so the option's SEMANTICS are pinned, not just its parse. The probe above shows
    both shapes work today.
  • SUGGESTION (docs): say explicitly that IF NOT EXISTS is accepted for compatibility and changes nothing, since
    a repeat CREATE is already idempotent (the body says it; the rst should too, or users will expect an error
    without it).
  • SUGGESTION (consistency, pre-existing): CREATE names are [A-Za-z_]\w* but DROP-by-name accepts [\w:]*
    (colon). A created name can never contain :, so the asymmetry is harmless, but one _NAME fragment shared by
    both patterns would remove the question.
  • CI at 96880e5: 57 pass, 17 pending (routes-off lanes still running at check time), 0 fail.

Recommendation: mergeable after the pending push + green CI; suggestions are optional.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ropeBMLJUuy6ViYwy15ud

@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

You reviewed the pushed head, and you were right that it was not the whole change: the indexing.rst correction was still sitting unstaged in my worktree when the merge commit went up. Fixed at c53b024.

That commit carries the paragraph (building and querying are one call since #2132; WHERE m.id IN [...] is index-served in the row form and accepted on the scan path inside GRAPH { }, both checked at 100k nodes), the stale bullet lower down that still said the seed list takes the scan path, the module docstring as the single statement of the grammar, ON(col) without a space since Cypher accepts it, and a fused one-call test over pandas, polars and cuDF with a route_engaged pin asserting the same served seams as the two-step form.

Gates at this head: bin/lint.sh green, the DDL plus cache-registry files 44 passed with 4 cuDF skips, routes-off replay clean.

@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

GPU receipt at the current head, clean first time.

dgx-spark, GB10, graphistry/test-rapids-official:26.02-gfql-polars, head c53b024:

graphistry/tests/compute/gfql/index/test_index_ddl_cypher_spellings.py
graphistry/tests/compute/gfql/index/test_index_ddl_in_one_call.py
  -> 35 passed, 0 failed   (EXIT=0)

That covers the cuDF arm of the fused one-call test added in this PR, which drives each new Cypher spelling through gfql() and compares the served seams against the two-step form.

…-cypher-spellings

# Conflicts:
#	CHANGELOG.md
@lmeyerov
lmeyerov merged commit bf06e6a into master Oct 4, 2026
82 checks passed
@lmeyerov lmeyerov mentioned this pull request Oct 4, 2026
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.

1 participant