Skip to content

Commit a5d8e85

Browse files
authored
feat: extend parameterized queries and close out dialect parity (#20)
feat: extend parameterized queries to generic transform path and Python bindings Add parameter binding for OPTIONAL MATCH, WITH, and UNWIND execution paths. Support array/object JSON values in parameter parsing and binding. Add optional params argument to Python Graph.query() method. Refs #15 Refs #13
1 parent 564f1a5 commit a5d8e85

File tree

27 files changed

+5510
-164
lines changed

27 files changed

+5510
-164
lines changed

.angreal/task_test.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,33 @@ def test_rust(verbose: bool = False) -> int:
132132
@angreal.argument(
133133
name="python",
134134
long="python",
135-
default_value="python3.11",
136-
help="Python interpreter to use (default: python3.11)"
135+
default_value="",
136+
help="Python interpreter to use (auto-detects uv, falls back to python3)"
137137
)
138-
def test_python(verbose: bool = False, python: str = "python3.11") -> int:
138+
def test_python(verbose: bool = False, python: str = "") -> int:
139139
"""Run Python binding tests."""
140140
if not ensure_extension_built():
141141
return 1
142142

143-
print(f"Running Python binding tests (using {python})...")
144-
return run_make("test-python", verbose=verbose, PYTHON=python)
143+
root = get_project_root()
144+
bindings_dir = os.path.join(root, "bindings", "python")
145+
146+
# Auto-detect: if uv.lock exists in the bindings dir, use uv run
147+
use_uv = not python and os.path.exists(os.path.join(bindings_dir, "uv.lock"))
148+
149+
if use_uv:
150+
print("Running Python binding tests (using uv)...")
151+
cmd = ["uv", "run", "python", "-m", "pytest", "tests/", "-v"]
152+
else:
153+
interpreter = python or "python3"
154+
print(f"Running Python binding tests (using {interpreter})...")
155+
cmd = [interpreter, "-m", "pytest", "tests/", "-v"]
156+
157+
if verbose:
158+
print(f"Running: {' '.join(cmd)} in {bindings_dir}")
159+
160+
result = subprocess.run(cmd, cwd=bindings_dir)
161+
return result.returncode
145162

146163

147164
@test()

.metis/strategies/NULL/initiatives/GQLITE-I-0027/tasks/GQLITE-T-0090.md renamed to .metis/archived/strategies/NULL/initiatives/GQLITE-I-0027/tasks/GQLITE-T-0090.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ created_at: 2026-01-08T14:40:36.764584+00:00
77
updated_at: 2026-01-08T14:40:36.764584+00:00
88
parent: GQLITE-I-0027
99
blocked_by: []
10-
archived: false
10+
archived: true
1111

1212
tags:
1313
- "#task"
@@ -31,6 +31,8 @@ Ensure the build system supports two distinct profiles: a slim CPU-only build (d
3131

3232
## Acceptance Criteria
3333

34+
## Acceptance Criteria
35+
3436
- [ ] `make extension` produces CPU-only binary (~200KB, no Rust dependency)
3537
- [ ] `make extension GPU=1` produces GPU-enabled binary (~3-5MB, includes wgpu)
3638
- [ ] CPU-only build works without Rust toolchain installed

.metis/backlog/features/GQLITE-T-0093.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ level: task
44
title: "Bulk Insert Operations for Nodes and Edges"
55
short_code: "GQLITE-T-0093"
66
created_at: 2026-01-10T04:16:05.119817+00:00
7-
updated_at: 2026-01-10T04:16:05.119817+00:00
7+
updated_at: 2026-02-07T13:22:24.868891+00:00
88
parent:
99
blocked_by: []
1010
archived: false
1111

1212
tags:
1313
- "#task"
14-
- "#phase/backlog"
1514
- "#feature"
15+
- "#phase/completed"
1616

1717

1818
exit_criteria_met: false
@@ -185,6 +185,12 @@ MATCH (s0 {id: 'x'}), (t0 {id: 'y'}) CREATE (s0)-[:CALLS]->(t0)
185185

186186
## Acceptance Criteria
187187

188+
## Acceptance Criteria
189+
190+
## Acceptance Criteria
191+
192+
## Acceptance Criteria
193+
188194
- [ ] `insert_nodes_bulk` method implemented with batch INSERT operations
189195
- [ ] `insert_edges_bulk` method implemented using in-memory ID mapping
190196
- [ ] Both methods wrapped in transactions for atomicity
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
---
2+
id: cypher-dialect-parity-for-common
3+
level: task
4+
title: "Cypher dialect parity for common queries"
5+
short_code: "GQLITE-T-0096"
6+
created_at: 2026-02-07T02:09:56.178299+00:00
7+
updated_at: 2026-02-07T16:08:59.284721+00:00
8+
parent:
9+
blocked_by: []
10+
archived: false
11+
12+
tags:
13+
- "#task"
14+
- "#feature"
15+
- "#phase/completed"
16+
17+
18+
exit_criteria_met: false
19+
strategy_id: NULL
20+
initiative_id: NULL
21+
---
22+
23+
# Cypher dialect parity for common queries
24+
25+
**GitHub Issue**: [#13](https://github.com/colliery-io/graphqlite/issues/13)
26+
27+
## Objective
28+
29+
Expand the Cypher parser to accept common Memgraph/Neo4j-style syntax, eliminating the need for client-side string rewrites for bracket access, backtick identifiers, nested dot access, and trailing semicolons. (IN list literals already work — verified with 47+ passing tests.)
30+
31+
## Backlog Item Details
32+
33+
### Type
34+
- [x] Feature - New functionality or enhancement
35+
36+
### Priority
37+
- [ ] P1 - High (important for user experience)
38+
39+
### Business Justification
40+
- **User Value**: Users can write natural Cypher queries without client-side rewriting hacks
41+
- **Business Value**: Reduces friction for adoption by Neo4j/Memgraph users; removes a class of client bugs
42+
- **Effort Estimate**: M - Parser/grammar changes (phases 1-2 trivial, phases 3-4 medium — shared transform work). IN lists already done.
43+
44+
## Acceptance Criteria
45+
46+
## Acceptance Criteria
47+
48+
## Acceptance Criteria
49+
50+
## Acceptance Criteria
51+
52+
- [ ] Trailing semicolons are accepted without error
53+
- [ ] Backtick identifiers work in property access: `n.\`special-key\``
54+
- [ ] Nested dot access parses and executes: `p.metadata.name`
55+
- [ ] Bracket property access parses and executes: `p['status']['phase']`
56+
- [x] ~~List literals in expressions work: `IN ['Failed','Unknown']`~~ (already implemented — 47+ tests passing in `23_in_operator.sql`)
57+
- [ ] Existing accepted syntax continues to work (no regressions)
58+
59+
## Implementation Plan
60+
61+
### Approach
62+
63+
Two commits:
64+
65+
1. **Commit A — Trailing semicolons** (grammar only, trivial)
66+
2. **Commit B — Backtick + nested dot + bracket chaining** (grammar refactor + transform layer, batched)
67+
68+
---
69+
70+
### Commit A: Trailing Semicolons
71+
72+
**File:** `src/backend/parser/cypher_gram.y``stmt` rule (line 159)
73+
74+
Add two productions:
75+
76+
```
77+
| union_query ';'
78+
{ $$ = $1; context->result = $1; }
79+
| EXPLAIN union_query ';'
80+
{ if ($2->type == AST_NODE_QUERY) { ((cypher_query*)$2)->explain = true; }
81+
$$ = $2; context->result = $2; }
82+
```
83+
84+
Scanner already tokenizes `;` as `CYPHER_TOKEN_CHAR` (line 243 of `cypher_scanner.l`).
85+
86+
**Test:** New `tests/functional/32_dialect_parity.sql` — Section 1 with `RETURN 1;`, `MATCH (n) RETURN n;`, `EXPLAIN MATCH (n) RETURN n;`.
87+
88+
Build, test, commit.
89+
90+
---
91+
92+
### Commit B: Backtick + Nested Dot + Bracket Chaining
93+
94+
#### Grammar changes (`src/backend/parser/cypher_gram.y`)
95+
96+
**B1. Add 3 productions to `expr` rule** (after line 987):
97+
98+
```
99+
| expr '.' IDENTIFIER
100+
{ $$ = (ast_node*)make_property($1, $3, @3.first_line); free($3); }
101+
| expr '.' BQIDENT
102+
{ $$ = (ast_node*)make_property($1, $3, @3.first_line); free($3); }
103+
| expr '[' expr ']'
104+
{ $$ = (ast_node*)make_subscript($1, $3, @2.first_line); }
105+
```
106+
107+
Enables: `n.\`special-key\``, `p.metadata.name`, `p['status']['phase']`.
108+
109+
**B2. Remove redundant rules from `primary_expr`:**
110+
111+
- `IDENTIFIER '.' IDENTIFIER` (line 1002)
112+
- `END_P '.' IDENTIFIER` (line 1009) — replaced by adding END_P to `identifier` rule
113+
- `IDENTIFIER '[' expr ']'` (line 1023)
114+
- `'(' expr ')' '[' expr ']'` (line 1030)
115+
- `list_literal '[' expr ']'` (line 1035)
116+
117+
All subsumed by the new `expr` productions.
118+
119+
**B3. Add `END_P` to `identifier` rule:**
120+
121+
```
122+
| END_P { $$ = make_identifier(strdup("end"), @1.first_line); }
123+
```
124+
125+
**B4. Add BQIDENT variants to `remove_item`** (line 577):
126+
127+
```
128+
| IDENTIFIER '.' BQIDENT { ... same as IDENTIFIER '.' IDENTIFIER ... }
129+
| BQIDENT '.' IDENTIFIER { ... }
130+
| BQIDENT '.' BQIDENT { ... }
131+
```
132+
133+
(SET uses `expr '=' expr` so gets backtick support for free.)
134+
135+
**B5. Update `%expect` / `%expect-rr`** (lines 35-36):
136+
137+
Run `bison` and adjust conflict counts.
138+
139+
#### Transform changes
140+
141+
**B6. `transform_property_access()` in `transform_expr_ops.c`** (line 314):
142+
143+
Replace hard rejection at line 319 with recursive handling:
144+
145+
```c
146+
if (prop->expr->type == AST_NODE_PROPERTY) {
147+
/* Nested: p.metadata.name → json_extract(p.metadata_sql, '$.name') */
148+
append_sql(ctx, "json_extract(");
149+
if (transform_property_access(ctx, (cypher_property*)prop->expr) < 0) return -1;
150+
append_sql(ctx, ", '$."); append_sql(ctx, prop->property_name); append_sql(ctx, "')");
151+
return 0;
152+
}
153+
if (prop->expr->type == AST_NODE_SUBSCRIPT) {
154+
/* list[0].name → json_extract(list_subscript_sql, '$.name') */
155+
append_sql(ctx, "json_extract(");
156+
if (transform_expression(ctx, prop->expr) < 0) return -1;
157+
append_sql(ctx, ", '$."); append_sql(ctx, prop->property_name); append_sql(ctx, "')");
158+
return 0;
159+
}
160+
/* Existing AST_NODE_IDENTIFIER check remains below */
161+
```
162+
163+
**B7. Subscript string-key normalization in `transform_return.c`** (line 661):
164+
165+
At top of `AST_NODE_SUBSCRIPT` case, normalize `p['key']` → property access:
166+
167+
```c
168+
cypher_subscript *subscript = (cypher_subscript*)expr;
169+
if (subscript->index->type == AST_NODE_LITERAL) {
170+
cypher_literal *idx_lit = (cypher_literal*)subscript->index;
171+
if (idx_lit->literal_type == LITERAL_STRING) {
172+
if (subscript->expr->type == AST_NODE_IDENTIFIER ||
173+
subscript->expr->type == AST_NODE_PROPERTY) {
174+
cypher_property temp_prop;
175+
memset(&temp_prop, 0, sizeof(temp_prop));
176+
temp_prop.base.type = AST_NODE_PROPERTY;
177+
temp_prop.expr = subscript->expr;
178+
temp_prop.property_name = idx_lit->value.string;
179+
return transform_property_access(ctx, &temp_prop);
180+
}
181+
}
182+
}
183+
/* ... existing json_extract logic below unchanged ... */
184+
```
185+
186+
#### Tests
187+
188+
New `tests/functional/32_dialect_parity.sql` sections:
189+
- **Section 2:** Backtick property access (`n.\`special-key\``)
190+
- **Section 3:** Nested dot access (JSON-valued property + `n.metadata.name`)
191+
- **Section 4:** Bracket chaining (`n['status']`, `n['status']['phase']`)
192+
- **Section 5:** Mixed (`n['metadata'].name`, `n.items[0]`)
193+
194+
Also un-skip `tests/functional/09_edge_cases.sql` line 199-202.
195+
196+
---
197+
198+
### Files Modified
199+
200+
| File | Change |
201+
|------|--------|
202+
| `src/backend/parser/cypher_gram.y` | `stmt` + `;`, `expr '.' IDENT/BQIDENT`, `expr '[' expr ']'`, remove redundant `primary_expr` rules, END_P in `identifier`, BQIDENT in `remove_item`, `%expect` update |
203+
| `src/backend/transform/transform_expr_ops.c` | `transform_property_access()` handles nested `AST_NODE_PROPERTY` and `AST_NODE_SUBSCRIPT` base |
204+
| `src/backend/transform/transform_return.c` | String-key-to-property normalization in `AST_NODE_SUBSCRIPT` case |
205+
| `tests/functional/32_dialect_parity.sql` | **New** — tests for all 4 features |
206+
| `tests/functional/09_edge_cases.sql` | Un-skip nested property test (line 199) |
207+
208+
### Out of Scope
209+
210+
- SET/REMOVE of nested properties (`SET n.metadata.name = 'foo'`) — requires JSON path updates, much more complex
211+
- Nested/bracket access is read-only (RETURN, WHERE, WITH contexts)
212+
213+
### Verification
214+
215+
1. `angreal build extension` — bison/flex compile clean
216+
2. `angreal test unit` — CUnit tests pass
217+
3. `angreal test functional` — all SQL tests pass including new `32_dialect_parity.sql`
218+
4. `angreal test all` — no regressions
219+
220+
## Status Updates
221+
222+
### 2026-02-07: Code Review & Triage
223+
224+
**Code areas reviewed:**
225+
- `src/backend/parser/cypher_gram.y``stmt` rule (line 159), property access (lines 1002-1015), subscript (lines 1023-1039), BQIDENT usage (25 sites), `%expect 4`/`%expect-rr 3` (line 35-36), `%left '.'` (line 152), `%dprec` on list_literal/list_comprehension (lines 996-997)
226+
- `src/backend/transform/transform_expr_ops.c``transform_property_access()` (line 314), hard rejects non-IDENTIFIER base at line 319 with "Complex property access not yet supported"
227+
- `src/backend/transform/transform_return.c``AST_NODE_SUBSCRIPT` handler (lines 661-691), generates `json_extract()` with negative-index support
228+
229+
**Feature status after review:**
230+
231+
| Feature | Status | Evidence |
232+
|---------|--------|----------|
233+
| Trailing semicolons | NOT SUPPORTED | `stmt` rule has no `;` production |
234+
| Backtick property access | NOT SUPPORTED | BQIDENT accepted in labels, variables, rel types, map keys — but NOT in property access (`IDENTIFIER '.' IDENTIFIER` only at line 1002) |
235+
| Nested dot access | NOT SUPPORTED | `transform_property_access()` rejects non-IDENTIFIER base at line 319 |
236+
| Bracket property access | PARTIAL | `IDENTIFIER '[' expr ']'` works (line 1023) but no chaining (`expr '[' expr ']'` missing) |
237+
| IN list literals | ALREADY WORKS | 47+ tests passing in `tests/functional/23_in_operator.sql` using `["Alice", "Bob"]` syntax |
238+
239+
**Test harness findings:**
240+
- `tests/functional/09_edge_cases.sql:199-202` — Nested dot access test **explicitly SKIPPED** (`n.level1.level2.level3`)
241+
- `tests/functional/23_in_operator.sql` — IN with list literals extensively tested and **passing** (strings, ints, NOT IN, edge cases)
242+
- No existing tests for trailing semicolons, bracket access chaining, or backtick property access
243+
- No tests are erroneously failing for these features — they're either skipped or not tested
244+
245+
**Revised scope — IN list literals should be removed from acceptance criteria (already done). Remaining work:**
246+
1. Trailing semicolons — grammar-only, trivial
247+
2. Backtick property access — add `BQIDENT` variants to property access rules in `primary_expr`
248+
3. Nested dot access — move property access to `expr` rule + update transform layer
249+
4. Bracket access chaining — move subscript to `expr` rule + transform normalization for string keys

0 commit comments

Comments
 (0)