# Binding Conformance Checklist
Every first-party graphdblite binding (Python, Go, Node, C/FFI, future) MUST
pass every scenario below. Third-party bindings should run the same checklist
to verify they preserve the core's soundness contract.
Each scenario has a stable ID (`BC-NN`). Per-binding test files include the ID
in the test name or a comment so coverage is grep-able across languages.
The Rust core enforces these guarantees regardless of binding behavior — the
worst a buggy binding can do is lose uncommitted writes. The checklist exists
so bindings expose that contract correctly to their host language.
---
## Transaction lifecycle
### BC-01 — implicit rollback on scope exit without commit
A write transaction that goes out of scope without an explicit commit MUST
NOT persist its writes. After scope exit, a fresh read on the same database
handle MUST observe the pre-transaction state.
In language-idiomatic terms:
- **Python**: `with db.begin_write() as tx: tx.execute(...)` and let the
block end via an unhandled exception (or explicit `tx.rollback()`).
- **Go**: `tx, _ := db.BeginWrite(); ...; tx.Rollback()`.
- **C**: `graphdb_tx_begin_write(...); ... ; graphdb_tx_rollback(...)`.
- **Node**: `db.withWriteTx(async tx => { throw ... })`.
### BC-02 — commit persists writes
A write transaction that calls commit MUST make its writes visible on
subsequent reads (same handle and any other handle on the same DB file).
### BC-03 — exception/panic mid-transaction discards writes
If the host language raises an exception or the user panics inside a write
transaction, the transaction MUST be rolled back (either by the binding's
context-manager/`defer` machinery or by the core's drop-time rollback) and
the next operation on the same handle MUST succeed.
### BC-04 — nested begin returns an error
Calling `begin_write` (or `begin_read`) while a transaction is already open
on the same handle MUST return an error. The first transaction MUST remain
usable after the rejected nested call.
### BC-05 — write inside a read transaction returns an error
Executing a write Cypher query (CREATE/SET/DELETE/MERGE/REMOVE) inside a
read-only transaction MUST return an error. The transaction MUST remain
usable for subsequent read queries.
### BC-06 — commit/rollback without an active transaction returns an error
Calling commit or rollback on a handle with no active transaction MUST
return an error rather than silently no-op.
### BC-07 — operations on a finished transaction return an error
For bindings that expose a per-transaction object (Python, Node, Go),
`execute`/`query`/`commit`/`rollback` on a tx object whose commit or
rollback has already returned MUST return an error.
For the C/FFI binding, where transaction state lives on the flat
`GraphDB` handle rather than a per-tx object, the contract is narrower:
`graphdb_tx_rollback` after commit MUST still return an error (no tx
to roll back). `graphdb_tx_execute` after commit, however, succeeds
via the core's `Database::execute` auto-tx semantics — it opens and
commits a fresh transaction implicitly. Bindings that need stricter
behavior should track tx state on their wrapper.
---
## Multi-process / cross-handle visibility
### BC-08 — concurrent reader sees pre-txn snapshot
While a writer holds an open write transaction, a reader opened on a
separate handle (or process) on the same DB file MUST observe the
pre-write-transaction state until the writer commits. After commit, the
reader MUST see the new state on its next query.
This validates that bindings do not bypass SQLite's WAL snapshot semantics.
---
## Resource hygiene
### BC-09 — closing the database while a transaction is open does not corrupt data
Closing the database handle (or letting it fall out of scope) while a write
transaction is open MUST roll back the transaction. Reopening the same DB
file MUST NOT observe the uncommitted writes and MUST NOT report
corruption.
### BC-10 — result handles freed independently of transactions
Query result handles (where the binding exposes them, e.g. C/Go
`GraphResult`) MUST remain freeable after the producing transaction has
been committed or rolled back. Freeing a result MUST NOT require an
active transaction.
---
## Index DDL
### BC-11 — fulltext index create + use + drop
Each binding that exposes secondary-index DDL (`create_index` /
`drop_index`) MUST also expose the parallel fulltext-index DDL.
The smoke test creates a fulltext index on a `(label, property)`
pair, runs a `CONTAINS` query that returns the expected row through
the planner-rewritten `FullTextLookup` path, then drops the index.
In language-idiomatic terms:
| Python | `tx.create_fulltext_index(label, property)` / `tx.drop_fulltext_index(label, property)` |
| Node | `tx.createFulltextIndex(label, property)` / `tx.dropFulltextIndex(label, property)` |
| Go | `tx.CreateFulltextIndex(label, property)` / `tx.DropFulltextIndex(label, property)` |
| C/FFI | `graphdb_create_fulltext_index(db, label, property)` / `graphdb_drop_fulltext_index(db, label, property)` |
The core enforces case-sensitive substring matching (matches the
openCypher spec exactly), skips non-string property values silently,
and falls back to a label scan for search terms shorter than 3
codepoints (FTS5 trigram tokenizer floor). Bindings need only wire
the two methods through — they don't enforce these semantics
themselves.
Each binding additionally exposes a case-insensitive variant —
`create_fulltext_index_ci` (Python / Rust) / `createFulltextIndexCi`
(Node) / `CreateFulltextIndexCI` (Go) /
`graphdb_create_fulltext_index_ci` (C/FFI). Behaviour mirrors the
case-sensitive variant except the FTS5 tokenizer is built with
`case_sensitive 0`, and the planner also rewrites the symmetric
`toLower(prop) <op> toLower(rhs)` idiom against a CI index. The
`db.indexes()` procedure reports `kind = "fulltext_ci"` for CI
indexes.
Each binding also exposes a word-tokenized variant —
`create_fulltext_index_word` (Python / Rust) /
`createFulltextIndexWord` (Node) / `CreateFulltextIndexWord` (Go) /
`graphdb_create_fulltext_index_word` (C/FFI). Backed by FTS5's
`unicode61` tokenizer (word-boundary, case-folded, diacritic-folded),
this variant is intended for use with the `fts.search(label,
property, query)` built-in procedure rather than `CONTAINS` /
`STARTS WITH` / `ENDS WITH` (which fall back to label scan against
a word index). `db.indexes()` reports `kind = "fulltext_word"` for
word indexes.
Each binding additionally exposes a multi-property word variant —
`create_fulltext_index_word_multi(label, properties)` (Python / Rust) /
`createFulltextIndexWordMulti(label, properties)` (Node) /
`CreateFulltextIndexWordMulti(label, properties)` (Go) /
`graphdb_create_fulltext_index_word_multi(db, label, props, n_props)`
(C/FFI). One FTS5 multi-column `unicode61` virtual table covers all
listed properties on the label, and BC-11 exercises this surface
through a `CALL fts.search(label, '*', query)` wildcard round-trip
that matches across every covered column. `db.indexes()` emits one
row per covered property (all with `kind = "fulltext_word"`).
---
## Coverage matrix
| BC-01 | ✓ | ✓ | ✓ | ✓ |
| BC-02 | ✓ | ✓ | ✓ | ✓ |
| BC-03 | ✓ | —¹ | —¹ | ✓ |
| BC-04 | ✓ | ✓ | ✓ | ✓ |
| BC-05 | ✓ | ✓ | ✓ | ✓ |
| BC-06 | ✓ | ✓ | ✓ | ✓ |
| BC-07 | ✓ | ✓ | ✓ | ✓ |
| BC-08 | ✓ | —² | —² | ✓ |
| BC-09 | ✓ | ✓ | ✓ | ✓ |
| BC-10 | —³ | ✓ | ✓ | —³ |
| BC-11 | ✓ | ✓ | ✓ | ✓ |
BC-05 enforcement lives in `cypher::execute_cypher` (`src/cypher/mod.rs`):
when `ExecContext::require_read_only` is set, the planner's output tree is
checked via `executor::is_read_only` before execution and any write operator
fails with `GraphError::Transaction`. The flag is wired automatically by
`Database::execute_with_params` (set when `tx_state == Read`) and by the
typed `ReadTransaction::query*` methods.
¹ BC-03 is panic-driven; idiomatic in Python only. Go/C exercise the
explicit-rollback equivalent under BC-01. The core's drop-time rollback is
covered there.
² BC-08 (multi-process) is exercised by the Rust integration suite
(`tests/multiprocess_tests.rs`) — running it again per binding adds little
value once the Python runner confirms the binding doesn't shortcut WAL.
³ Python and Node don't surface a separate result-handle object — query
results are plain lists/arrays. Not applicable.
---
## Running the suites
| Python | `cd bindings/python && uv run pytest tests/conformance/` |
| Go | `cd bindings/go && go test -run Conformance` |
| C/FFI | `cd bindings/ffi && make conformance && ./conformance` |
| Node | `cd bindings/node && npm test` |
Each runner prints `[BC-NN]` in test names so failures point at the
checklist row directly.