Migrating 0.16.24 → 0.17.0

KGLite remains pre-1.0. This release deliberately tightens several invalid or ambiguous inputs, changes a few result shapes, and restricts operations whose old behavior could silently use the wrong graph value. Pin kglite==0.16.24 until you have checked the affected rows below.

Before upgrading

Back up the complete .kgl file or disk-graph directory together with its WAL. A CSV/blueprint export is useful for portable node, relationship, and property recovery, but it does not contain every engine feature. Finish or checkpoint important WAL-backed work with 0.16.24 before replacing the runtime.

If you embed the Rust crate, pin an exact version while migrating. KGLite’s pre-1.0 policy permits documented breaks in patch releases. The C ABI remains additive; this migration changes C query-parameter validation but removes no C function or status value.

Changed behavior and migration

Area

0.16.24

0.17.0

What to change

Persisted Python graph ownership

Closing an owner could leave writer/WAL authority reachable through retained handles.

Close/context exit ends publication authority. Retained data remains privately readable and mutable, but an earlier transaction cannot publish through the ended owner. A failed checkpoint retains authority for retry.

Commit or roll back transactions before closing. Do not use a retained snapshot as a route back to the closed file.

Derived-handle capture

Independent handles could publish changes into the source graph’s capture stream.

Independently derived handles do not publish into the source capture stream.

Write through the owner when changes belong in its CDC/WAL history; use copy() for independent data.

Query defaults and deadlines

Derived handles did not consistently preserve the source defaults. Transaction lifetime 0 could act as an immediate bound.

Snapshots, sessions, cursors, and transactions capture defaults when created. Omitted query options inherit them, including the built-in 180-second query timeout. timeout_ms=0 and transaction lifetime 0 mean unlimited. A positive transaction lifetime bounds each query, while completed work can still be committed or rolled back.

Pass timeout_ms=0 explicitly only when you intend no query deadline. Do not use zero as an immediate transaction expiry.

Context exit and later save

Save/exit behavior could leave unclear ownership.

A clean graph context checkpoints on exit. It does not undo WAL commits when the block raises. After close, save() needs an explicit path; save(path) is an unlocked snapshot save coordinated by the caller.

Use transactions for rollback. Coordinate concurrent writers before an explicit-path save; there is no universal atomic-save promise across bindings and storage modes.

Declared integer ingestion

Some decimal/scientific whole values and nullable frame integers rounded through float.

Declared integers preserve signed 64-bit values exactly; fractional or out-of-range inputs become NULL under the existing tolerant ingestion policy. Blueprint CSV and DataFrame scalar text share integer, Boolean, float, and date parsing.

Validate NULLs after tolerant ingestion if rejection is required. This policy is separate from strict query parameters.

DataFrame output

Nullable/mixed integers could round during pandas inference; a list of empty maps could lose rows.

Nullable integer columns use pandas Int64; mixed integer columns use object; empty-map rows are retained. Native pandas missing values remain NULL. Textual blueprint NaN follows CSV float NaN.

Remove float-based workarounds and expect pandas extension/object dtypes where exact integers require them.

Python query parameters

Oversized integers rounded to float and unsupported nested objects became NULL.

Every Python query handle rejects integers outside signed 64-bit range and unsupported objects at the exact nested path. Ordinary containers, NumPy signed values in range, and non-finite float parameters remain accepted.

Catch OverflowError/TypeError at query admission. Do not expect the tolerant declared-property policy here.

JSON query parameters

Rust, C, and MCP JSON routes could round oversized integer tokens. A process-wide arbitrary-precision representation could also reinterpret an ordinary object whose first key was $serde_json::private::Number.

Integer tokens must fit signed 64-bit; decimal/exponent tokens must fit finite f64. Validation reads the original query-parameter lexemes without changing ordinary JSON object decoding. Nested errors retain their path. C returns InvalidArgument and an owned error message when requested; MCP preserves correlation even when typed decoding refuses a protected number. Duplicate object keys follow serde’s last-key-wins behavior.

Encode intentional floating values with a decimal/exponent and keep them finite. Do not reserve or rewrite $serde_json::private::Number; it is an ordinary key. parse_json() and declared ingestion retain their separate tolerant policy. YAML recipe manifests use their typed YAML route and do not promise raw numeric lexemes.

Numeric casts and predicates

toInteger() saturated NaN, infinities, and out-of-range floats. Mixed integer/float comparisons near 2^53 could match or order the wrong rows.

Invalid casts return NULL; finite in-range floats truncate toward zero. Mixed predicates compare exactly, with conservative index fallback when needed. Structural Value equality/hash and direct statistics grouping remain type-sensitive.

Audit boundary-value filters and any code that expected cast saturation. Do not infer that integer and float values became one universal identity type.

Aware datetimes and timestamp text

Aware Python datetimes and some RDF/text/protocol paths lost offsets or fractional seconds.

Aware Python inputs normalize to UTC; explicit dates keep their calendar date. Supported timestamp constructors and text outputs retain fractions. Bolt refuses a timestamp it cannot represent.

Compare UTC instants, and stop relying on silently truncated protocol timestamps. Human table text remains compact.

Endpoint result values

Endpoint values resolved differently across eager/lazy, nested, and direct Python output paths.

Results resolve against the executing immutable view while preserving structural node/relationship IDs.

Keep raw native references within their originating view. Ordinary projected values are safe to consume; this does not create durable cross-graph physical IDs.

RDF loading

Rust load_rdf could append into a graph with declarations while bypassing constraints.

load_rdf accepts only a fresh empty in-memory graph without schema, aliases, indexes, constraints, publication authority, or mutation capture. Python and C already construct a fresh graph. N-Triples loading is unchanged.

Bootstrap RDF into DirGraph::new(), then declare and validate constraints. Do not use RDF load as constrained append.

Embedding services and transactions

Transactions could lose a registered model; text_score() in mutations failed. Session callbacks could deadlock on same-session writes.

begin()/begin_read() capture the model binding. Reads and mutations support text_score(), including supported nested scopes. Later replacement/unbind does not change the handle. Mutable callback/model state is shared, not deep-frozen. Callback-capable Session writes use an isolated working copy; callback reads see committed Session state, and synchronous same-thread writes back into that Session raise InvalidArgument.

Register runtime-only models again after load. Avoid same-thread reentrant writes from a Session callback. Do not assume cross-thread causal refusal or immutable model weights.

NetworkX import

Nullable node/edge property columns could round exact integers.

Signed 64-bit properties survive nullable columns exactly. Existing ID coercion, heterogeneous-object stringification, and same-type parallel-edge deduplication remain.

Remove property-rounding workarounds. Do not treat this as a new lossless policy for NetworkX IDs or arbitrary mixed objects.

Fluent/grouped dictionaries

Equal titles, suffix-like labels, and metadata keys could overwrite groups or endpoints.

Collision-safe presentation keys retain every group for collect_grouped, titles, get_properties, unique_values, connections, count, and calculate.

Iterate all returned entries. Treat keys as display labels, never stable graph identifiers; mutation or selection changes can change them.

Selection set algebra

Selections from unrelated graphs could combine equal physical slots and return plausible wrong nodes.

Union/intersection/difference/symmetric difference require the same exact immutable graph view. Diverged copy-on-write siblings are refused.

Derive both selections from the same snapshot. Map by application IDs yourself when combining independent graphs.

CALL outline and kglite.outline

Python keys collapsed IDs such as 1, 1.0, true, lists/maps, or NULL; missing/ambiguous roots and negative depth could misrender or cycle.

root_type disambiguates cross-type roots. The procedure exposes node, depth, parent_id, node_type, node_id_type, parent_type, parent_id_type, node_token, and parent_token. Tokens are per-result reconstruction keys, not persistent IDs. Ambiguous or missing roots fail explicitly; depth is non-negative; cycles/DAG joins emit each node once by first discovery.

Add root_type= where public IDs are ambiguous. Consumers using bare CALL outline must accept the expanded nine-column row; explicit old-column YIELD remains valid. Never persist the tokens.

New stored endpoint properties

A property assigned startNode()/endNode() could store a physical slot and follow a later rename, deletion, reuse, or transfer target.

Admission recursively snapshots the referenced node’s statement/source-view title before constraints, indexes, CDC, or WAL. Lists/maps, titles, CREATE, SET, repeated MERGE, tables, subsets, and transfers share the rule. Missing/cyclic chains become NULL.

Store a relationship when you need a changing association. Low-level Rust GraphWrite raw values retain a view-local normalization obligation.

Legacy stored references

Complete old snapshots could retain physical-slot behavior; WAL replay could guess a slot after allocation changed.

Recoverable values in complete portable/disk snapshots normalize against the unchanged complete source before constraints, restored indexes, or publication. A malformed persisted disk edge-property payload refuses load. Load does not rewrite the source until explicit save. Uncheckpointed raw-reference WAL frames refuse before replay or repair; checkpointed residue is skipped.

Load before slot-changing work, validate values against application truth, repair any already-retargeted value, then explicitly save a new checkpoint. KGLite cannot recover historical identity after an older deletion/reuse/compaction.

Load memory ceilings

The metadata estimate was the only portable-load budget check.

Affected legacy portable files receive a conservative post-decode normalization-overlay check before private mutation/publication. estimate_load_memory() remains metadata-only.

An affected file can pass the public estimate and still raise LoadMemoryLimitError; raise the ceiling or defer index rebuild. Disk-directory loads still have no max_load_bytes option.

Machine CSV

CLI CSV compacted floats/timestamps recursively; CR/LF record accounting could split MCP records.

CLI/core CSV preserves numeric/timestamp text precision and RFC quoting. MCP caps 200 complete logical records and reports logical CSV counts, including CSV-over-HTTP acknowledgement. Human tables stay compact; empty string and NULL remain the same empty CSV field.

Use JSON when empty string versus NULL matters. Parse MCP CSV as RFC CSV rather than physical lines.

Java timeout conversion

A positive duration below 1 ms narrowed to zero, disabling the timeout.

Positive fractional milliseconds round up to 1 ms; larger positive values round up and overflow saturates at Long.MAX_VALUE; null/nonpositive remains unlimited.

No workaround is needed after upgrade; keep using positive Duration values.

Bolt tx_timeout

A nonzero timeout was silently ignored.

Top-level nonzero or mistyped tx_timeout is refused before RUN/BEGIN state changes. Absent/NULL/zero stays unlimited; nested tx_metadata.tx_timeout remains user metadata.

Omit the key or send zero. Enforce a deadline in the client until KGLite implements it. WAL durability normal/full still protects acknowledged writes between checkpoints; off does not.

MCP skills

Unknown predicate keys could activate a typo-gated skill; docs misstated precedence, size, and live refresh.

Unknown nested applies_when keys skip the external skill with a diagnostic. Activation, prompts, and injected tool descriptions are fixed at boot. Project overrides first-declared operator pack, which overrides bundled defaults. description is required; a complete file may be 16,384 bytes, while 16,385 is rejected.

Restart after changing the graph or skill files. Correct predicate typos. Use graph_overview(cypher=['MATCH', 'WHERE']); use cypher_query for data queries.

Stored endpoint example

New writes follow this rule now. The legacy-file guidance after the example describes the complete-snapshot load contract:

g.cypher("CREATE (a:Item {id: 1}), (b:Item {id: 2, title: 'Beta'}), (a)-[:LINK]->(b)")
g.cypher("MATCH (a:Item)-[r:LINK]->() SET a.owner = endNode(r)")
g.cypher("MATCH (b:Item {id: 2}) SET b.title = 'Renamed'")
assert g.cypher("MATCH (a:Item {id: 1}) RETURN a.owner").scalar() == "Beta"

In 0.16.24 the final value could be "Renamed" because the property held a physical slot. For an older checkpoint, load and validate before mutation, fix from your own source of truth if necessary, then save to a new path. A successful load proves only what the current complete snapshot can resolve.

Performance costs and limits

Release measurements are part of the contract record. Comparisons against 0.16.24 include all intervening changes unless an isolated pair says otherwise.

  • Session writes that can invoke an embedding callback measured about 1.056–1.069× the no-model path for the reported 1/10,000-node cells, improved from 1.250–1.770× before callback-path isolation was narrowed. Snapshot and getter safety adds tens-of-nanoseconds costs.

  • Collision-safe grouped output improved the measured unique-label fast path: 100 groups by about 12.3% and 1,000 groups by about 10.2%. Collision cases return more data than 0.16.24 and are correctness-cost cells.

  • Strict JSON number parsing added about 5–6% in focused parse cells. A broader query comparison stayed within its declared limit; fixed-hop EXISTS was the largest observed control change at 15–16%, and a later cumulative comparison measured 17.8–19.0%.

  • The cumulative JSON transport comparison measured CLI ordinary output at +4.0%/-1.1%, CLI precision at +0.2%/-0.2%, and MCP ordinary output 15.6–16.6% faster across two pairs. Multiline MCP cells ranged from 5.6–9.8% slower for one record to 11.7–12.5% faster for 201 records. Removing process-wide arbitrary precision improved measured integer/float JSON-array cells about 2–4%. These end-to-end cells do not isolate validator throughput.

  • Ordinary 100–1,000-node outline rendering is 33–39% slower than 0.16.24. Without a current ID index, exact root ambiguity detection scans the complete primary-type bucket; the 10,000-node release probe measured about 0.09–0.11 ms. The extra identity projection/conversion is required for the corrected result and remains an explicit performance exception.

  • Stored endpoint-reference admission left ordinary SET controls within roughly 0–4.5% of their before captures. Endpoint-reference SET and transfer cells return corrected values, so their timing is a correctness cost. A later source-equivalent comparison kept the broader fixed-hop EXISTS watch cell at 16.9–19.0% over baseline.

  • Complete-snapshot normalization measured 0.402–0.448 ms for an ordinary disk fixture versus 0.243–0.249 ms before the change, a 61.4–84.4% increase. Ordinary portable loads measured 0.671–0.683 ms versus 0.593–0.598 ms, a 12.2–15.2% increase. An earlier implementation measured 0.619–0.624 ms for ordinary disk before the final checked decoder correction. Reference-bearing fixtures return corrected values, so their 1.316–1.373 ms disk and 0.828–0.849 ms portable means are correctness costs. The unchanged disk format has no persisted marker proving that an edge-property heap lacks legacy references, so every nonempty blob is visited on complete-snapshot load, including blobs written by 0.17.0; uncommon shapes use canonical decoding. This is load-time work, not query timing, and these small fixtures do not establish large-graph scaling.

  • A broader disk-directory reopen cell measured 0.817 ms before normalization and 3.134–3.314 ms after it (+283.8–305.8%), with a bounded retake at 3.197 ms (+291.5%). This is the same complete-snapshot load work at a different fixture boundary.

  • The immediate CSV/binding comparison also includes the preceding JSON and snapshot work. Equivalent CLI ordinary output was +1.44%/-0.05% across two pairs, while equivalent MCP ordinary output was 12.88–13.10% faster; that is not an isolated formatter speedup. Corrected CLI precision was +0.58%/-1.69%, one-record multiline MCP +5.84%/-10.94%, and 201-record multiline MCP -5.51%/-4.80%. Those changed-output cells are correctness costs. An earlier published-wheel baseline had MCP ordinary output 20.3–20.7% slower. These end-to-end measurements establish no isolated formatter or record-scanner speed claim.

No release-wide speedup is claimed. Direct statistics retain f64 results, string-normalized grouping, compact human text, representation-specific NULL and precision behavior, and nondeterministic ordering among ties unless a specific API documents otherwise.