fix: Cypher field-report round — reindex OOM, EXPLAIN, =~, IRI round-tripping, HTTP bulk import, arena sealing#1518
Open
bplatz wants to merge 11 commits into
Open
fix: Cypher field-report round — reindex OOM, EXPLAIN, =~, IRI round-tripping, HTTP bulk import, arena sealing#1518bplatz wants to merge 11 commits into
bplatz wants to merge 11 commits into
Conversation
…lopes An update with neither an insert nor a delete clause can never change data, yet it previously parsed to an empty Txn, committed zero flakes, and returned HTTP 200 with a real commit envelope. A Cypher write posted with Content-Type: application/json fell through to this path and looked committed while writing nothing. - parse_update now rejects bodies without an insert or delete clause, mirroring the existing empty-insert/upsert guards. Explicit "delete": [] keeps its documented no-op semantics. - The server transact choke point detects a Cypher envelope sent with a JSON Content-Type and returns a 400 pointing at application/cypher.
explain_cypher existed in fluree-db-api (sharing the SPARQL/JSON-LD plan
machinery) but nothing routed to it. Now:
- explain_cypher accepts the $param map and substitutes before lowering,
so the reported plan matches what execution would run; Fluree gains an
explain_cypher method mirroring explain_sparql.
- The ledger-scoped /explain endpoint handles Content-Type:
application/cypher (raw text or {cypher, params} envelope), with the
same bearer-scope and Min-T handling as the Cypher query path. The
connection-scoped /explain returns 400 pointing at the ledger-scoped
route, matching the query endpoints.
- fluree query --cypher --explain works locally and remotely (new
RemoteLedgerClient::explain_cypher); the --at rejection now also
covers remote explain.
Lexes =~, parses it at string-predicate precedence (STARTS WITH /
CONTAINS level), and lowers to the engine's REGEX — which already exists
for SPARQL — wrapping the pattern as CONCAT("^(?:", pat, ")$") so
Cypher's whole-string match semantics hold, alternations stay grouped,
and runtime (non-literal) patterns anchor too. Wired through both the
read path and the write-path filter lowering. Inline flags like (?i)
pass through to the regex engine.
cypher_name_from_iri blindly stripped everything before the last # or /, so a relationship typed http://other.example/kb#order came back as "order" — unparseable as an IRI on the client, and inconsistent with db.labels()/db.relationshipTypes(), which are @vocab-aware and keep foreign IRIs whole. Naming now follows the db.labels() rule everywhere: strip the ledger context's @vocab prefix when it applies, otherwise return the full IRI. The vocab rides from Cypher lowering on Query::cypher_vocab into ExecutionContext for scalar evaluation, and the typed formatter derives it from the view's default context, so labels()/type()/keys() and rendered node/relationship names agree. Default-mode (namespace-0 bare names) output is unchanged.
Reproduces the KB Elasticsearch-reindex query shape — UNWIND over a URI list feeding a property-equality match plus one-hop expansion against an n10s-style ledger — with knobs for ledger construction (reindex / bulk import / novelty-only), a mixed string/langString value predicate, and reified (annotated) edges. Timings at the reported 72k-node/190k-edge, 21k-URI scale: - reindex- and import-built roots, plain edges: ~0.4s (fast) - mixed value datatypes: point lookup degrades 0.1ms -> ~7ms (the untyped-string bound-object broad scan; UNWIND lane unaffected) - annotated edges: the query wedges at 100% CPU (reproduces the field report; per-row edge-annotation probe under investigation)
A Cypher relationship variable used only for its value surface
(RETURN p, type(p), ...) lowers to OPTIONAL { EdgeAnnotation } so
reified parallel edges bind per annotation. On a ledger with reified
edges that OPTIONAL evaluated its expanded chain per row, and with no
stats for the system f:reifies* predicates the chain drove from
f:reifiesPredicate — per row it enumerated ~(sidecar / #relationship
types) candidate reifiers and existence-checked each with its own scan.
Measured: 13s for a 520-row UNWIND over 13k reified edges (~25ms/row),
matching a field report of 35s-with-LIMIT-20 / OOM at 21k rows.
- AnnotationValueOptionalBuilder answers the whole required batch with
three set-wise f:reifies* scans (planned scans: overlay-merged,
policy-filtered) plus hash lookups per row; falls back to the generic
per-row path for history/dataset/multi-ledger contexts. 520-row probe:
13.2s -> 0.04s.
- The batched OPTIONAL hash-join lane now admits single-source
DefaultGraphSource chains (the edge-annotation expansion wrapper).
- The forward-arena probe accepts a per-row (variable) predicate, so
untyped -[p]-> also qualifies where the arena exists.
- estimate_triple_row_count gets structural selectivities for bound-
object f:reifies* lookups (stats never cover system predicates).
- cypher_unwind_probe example grows annotated/mixed/import modes and
query variants that isolate the relationship-binding cost.
The batched annotation OPTIONAL probe re-drained the three f:reifies* predicates for every required batch — ~55 sidecar passes on a 54k-row result, ~35s at 190k reified edges. The builder lives for exactly one query run against one snapshot/overlay/to_t, so the maps are built once on first use and reused.
…lementers fluree query --cypher --explain --remote now POSTs application/cypher to the ledger-scoped /explain endpoint (verbatim body, params substituted server-side); drop the stale 'explain not supported for Cypher' claim.
The negotiated /import-upload handshake now accepts raw source data — the same formats fluree create --from ingests locally — and runs the chunked bulk-import pipeline on complete instead of a .flpack restore. Mint takes source_kind: "source" plus filename (its extension drives the pipeline's format sniffing; only the extension is trusted). CSV and Cypher uploads are converted to JSON-LD shards server-side via new shared helpers in fluree-db-api::import_source, which the CLI's local import now also uses (one conversion implementation for both surfaces). - Discovery advertises "source-upload" + import.source_formats; refused (and unadvertised) on Raft-replicated servers, where the pipeline's direct nameservice publishing would bypass the log. - fluree create <ledger> --remote <name> --from data.ttl|dump.cypher|… uploads single files through the same mint→PUT→complete→poll flow and errors with a .flpack fallback hint when the server lacks the mode. - Import job registry carries the source kind; complete branches restore vs pipeline in the same background-task slot. - The import commit paths held tracing EnteredSpan guards across the blob-store awaits, making their futures !Send (invisible until a tokio::spawn needed them); store steps now use .instrument.
A bulk-imported root carries annotation_index: None; queries binding relationship variables then take the scan-fallback path. The CLI's local import already follows up with one reindex so the api attachment provider's bulk-import bootstrap scan seals an authoritative arena — the server's source-upload complete step now does the same when the import reports has_annotations. Verified end-to-end: an uploaded Cypher dump with relationship CREATEs finishes with a sealed arena and the relationship queryable through the arena probe. The probe example gains knobs used while verifying this (PROBE_REINDEX2 / PROBE_LOAD_FIRST / PROBE_CACHE_CFG) and property-bearing annotations.
An annotated insert followed by indexing left annotation_index=None
permanently. Three cooperating defects:
1. The api attachment provider returned None ('delta unknown') when the
ledger wasn't resident in the LedgerManager — the norm for a
write-only ingest, where the background indexer fires before any
read loads the ledger. The build defensively dropped the arena and
stamped had_annotation_arena, blocking the bootstrap scan forever.
The provider now falls back to a *transient* backend load (never
inserted into the cache — cache insertion from a background context
disturbs the running handle's novelty bookkeeping) whose replay of
un-indexed commits yields the complete event history.
2. A full rebuild under Augment coverage discarded a previously-sealed
arena: root assembly had no way to reach the base arena. Fir6Inputs
now carries the previous root's CID, and the Augment arm merges the
prior arena's events with the delta (the incremental Phase 3d
contract) before resealing. Prior-arena blobs follow the normal
old-generation GC lifecycle.
3. Fluree::reindex read the NsRecord before quiescing the background
indexer; a racing background publish left it rebuilding from a stale
record (index_head_id gone), losing the just-sealed arena. The
record is re-fetched after cancel + wait_for_idle.
Pinned by a four-cell seal matrix in it_edge_annotations_indexed,
including the background-race reproduction at probe scale. The
deliberate post-defensive-drop scan-fallback contract is unchanged and
its existing test still passes.
anh-h
approved these changes
Jul 18, 2026
anh-h
left a comment
There was a problem hiding this comment.
This addresses everything that was really problematic for me, and also saves me from testing a couple of KB-side workarounds I had half-drafted for the relationship-binding issue. Thanks for turning this around so fast.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the full KB-on-FlureeDB field report (Cypher/Bolt integration): the reindex-query OOM, interactive-latency tooling, surface gaps, the silent write failure, and the bulk-import sidecar.
Performance: reindex-query OOM / 35s (the headline)
UNWIND $uris MATCH (s:INDEXED {value: uri})-[p]->(o:INDEXED)against ~72k nodes / ~190k reified edges pegged CPU and OOM-killed a 16 GB server; LIMIT 20 still took 35s.Root cause: a value-only relationship binding lowers to
OPTIONAL { EdgeAnnotation }; on reified ledgers without a sealed annotation arena, the expanded chain evaluated per row and drove fromf:reifiesPredicate— enumerating ~(sidecar / #relationship-types) candidate reifiers per row and existence-checking each with its own scan (~25ms/row).f:reifies*planned scans (overlay-merged, policy-filtered) hash-indexed once per query run, then per-row lookups. Falls back to the per-row path for history/dataset/multi-ledger contexts.DefaultGraphSourcechains; the forward-arena probe accepts per-row (variable) predicates; structural selectivities forf:reifies*bound-object lookups; a reusablecypher_unwind_probeexample pins the shape.Annotation arena sealing (write-only ingest trap)
Annotated insert + index left
annotation_index = Nonepermanently — three cooperating defects:LedgerManager(the norm for write-only ingest); the build defensively dropped the arena and stamped the sticky bit, blocking the bootstrap scan forever. Fixed with a transient (never-cached) backend load whose commit replay yields the complete event history.Augmentcoverage discarded a previously-sealed arena; root assembly now receives the previous root's CID and merges the prior arena's events with the delta (the incremental Phase 3d contract).Fluree::reindexread the NsRecord before quiescing the background indexer, so a racing publish left it rebuilding from a stale record. Re-fetched after cancel + wait-for-idle.Pinned by a four-cell seal matrix including a background-race reproduction; the deliberate post-defensive-drop scan-fallback contract is unchanged.
Server-side bulk import over HTTP (drops the customer's sidecar)
The negotiated
/import-uploadhandshake now accepts raw source data — the same formats asfluree create --from(TTL/NT/NQ/TriG/JSON-LD/JSONL±gz/zst, plus CSV and Cypher dumps converted server-side via sharedfluree-db-api::import_sourcehelpers the CLI also uses). Discovery advertisessource-upload+ formats; refused on Raft-replicated servers (the pipeline publishes nameservice heads outside the replicated log).fluree create <ledger> --remote <name> --from data.ttl|dump.cypherrides mint→PUT→complete→poll. Imports with annotations get a follow-up reindex so the arena is sealed on arrival.Also surfaced and fixed: the import commit paths held tracing
EnteredSpanguards across store awaits, making the pipeline future!Send.Cypher surface
POST /explain/{ledger}withapplication/cypher(raw or{cypher, params}envelope;$paramsubstitution matches execution), plusfluree query --cypher --explainlocally and over--remote. Verified live end-to-end.=~regex operator: whole-string Neo4j semantics (non-capturing-group anchoring so alternations and runtime patterns behave; inline flags pass through), wired in read filters and write-path WHERE.labels()/type()/keys()and rendered node/relationship names now follow thedb.labels()rule — strip the configured@vocab, otherwise return the full IRI. A relationship typedhttp://…/kb#orderno longer comes back as"order".Correctness: silent no-op on wrong Content-Type
A Cypher envelope posted as
application/jsonfell through to the JSON-LD update parser, produced a zero-clause transaction, and returned HTTP 200 with a real commit envelope. Updates with neitherinsertnordeleteare now rejected (422), and a misrouted{"cypher": …}body gets a 400 pointing atContent-Type: application/cypher.Docs
docs/api/endpoints.md(Cypher explain, source uploads),docs/query/cypher.md(explain,=~, naming rule),docs/cli/server-integration.md(remote Cypher explain + source-upload contracts),docs/cli/create.md.Not addressed (deliberate)