Repository navigation
v4.3.0
Read-only Cypher answered inside IRIS by a native executor over ^KG (on by default; spec 253), ^KG derived from
the rows by a functional index (spec 252), and the three changes opsreview's event store asked for: temporal edges that
purge cleanly and report collisions (spec 254), a paged window reader with edge attributes (spec 255), and ledger
redaction of an erased tenant (spec 256). The executor passes the openCypher TCK, 3896 of 3896 scenarios (and did so with
every answer held to the SQL path's in shadow mode when spec 253 was completed). On the LDBC Social Network Benchmark at scale factor 10, IVG's median
time is 1.13x Neo4j's and 0.73x Memgraph's over 39 queries (docs/performance/alt-bench/REPORT_TEMPLATE.md).
Upgrading: run initialize_schema() once per namespace. It deploys the Graph.KG.* classes, declares the
adjacency index and builds it over every existing row (minutes on a large graph), converts rdf_props.val back to
EXACT collation where an earlier ensure_indexes altered it (IRIS rebuilds the val indexes: 254 s at 2.9M rows),
adds and backfills val_key, and runs backfill_temporal_rows(). The package and the classes deploy apart: until the
classes match, every read stays on the SQL path, with one warning. The engine asks the executor its plan version once,
every plan carries it, and the executor refuses another; an executor older than its planner had ignored plan fields
it predates and answered wrongly (BI01) or ran far past its time (BI11). Then read Changed: several defaults and
answers moved.
Added
engine.ledger.redact(graph=None, prefix=None, *, separator="|", actor="system:ledger-redact", batch_size=2000, max_batches=0)removes an erased target's content from the ledger's history and returns aRedactionReceipt
(spec 256): each of its records becomes a tombstone that keeps its seq, ordinal and op and holds the SHA-256 of the
record it replaced; each revision touched loses its message, source, correlation id and idempotency key; the run
ends with a content-free revision of kindredaction. A prefix matches with its separator (acme|, not
acme-health|). Refused while the target has live rows (RedactionTargetLiveError) or the ledger is disabled; runs
in batches ofbatch_size(max_batches > 0stops early) and resumes when re-run (redaction_in_progress();
readers raiseRedactionInProgressErrormeanwhile). The errors import fromiris_vector_graph.ledger. ADR-0013.iter_edges_in_window(start, end, ...)reads a window of any size, each event with its attributes (spec 255).
A cursor overGraph.KG.TemporalWindow(Open/Next/Close): pages of 1,000 events, the first one in the
opening call, so a window that fits a page is one round trip. Filters:source, asourceslist, a
source_prefix, a set ofpredicates,direction="in". A source filter is a lookup at each timestamp, not a walk
of every source at it. The server holds a resume key, not rows; each page is a live read from it. One tenant's
5-minute window with attributes went from 22–34 ms (QueryWindowplusGetEdgeAttrsper edge) to 0.8–1.5 ms on
the enterprise test container.get_latest_edges(sources | source_prefix, predicate=, before=, lookback=): each source's newest event (spec
255), from a new^KG("tlast", g, s, p) = tsindex that every temporal write keeps (about 3% more journal per
write) and every delete, purge and erase keeps exact. 0.15–0.2 ms for one tenant's five entities. On a graph written
before 4.3.0, named sources are answered by walking timestamps, and asource_prefixor all-sources read raises
TemporalWindowErroruntilrebuild_temporal_latest(graph)has run once.Graph.KG.GraphStores.Digest(graph): a canonical digest of one graph's content in every graph-scoped store, for
checking that a rebuild or a replay in other batch sizes or order lands the same graph (spec 254).- Lookups by a property value use an index.
rdf_props.valcannot be an index subscript, so since v1.4.8 every
n.prop = valuescanned that key's rows. A new computed columnval_key(the first 128 characters ofval,
recomputed on every write) and an index on(key, val_key)serve it; the translator adds theval_keypredicate
beside the exactvalcomparison only where the namespace has both. An SNB SF1 profile read went from 7 s to
0.6 ms.initialize_schema()adds the column, backfills it in batches and creates the index last, so the index's
presence means the column is complete. Inserts intordf_propsare about 14% slower. execute_cypher(..., raise_on_error=False). WithTrue, a statement whose SQL failed raisesRuntimeError
instead of coming back as zero rows withIVGResult.errorset. Checked on the main path, each part of a chained
query and a row-boundMERGE, not on the per-row statements of a count-onlyCREATEpipeline.ivg.shortestPath.weightedtakes an optional seventh argument, the relationship types (a list, one name, or a
$parameter). Without it the search walks every predicate out of every node it settles: from an SNB Person, each
post, comment and like, and the path could run through a Comment. SNB BIW at SF0.1 968 -> 5.1 ms (Neo4j 5.2). A
six-argument call is unchanged and still answers against older classes.- A supported read runs inside IRIS over
^KG(spec 253; on by default, see Changed). The planner
(iris_vector_graph.cypher.native_plan) turns a read it supports into a plan thatGraph.KG.Cypher.Execruns next
to the data: oneMATCHchain from a property lookup or the smallest of its labels, fixed and variable-length hops,
a chain that closes on its first node (a cycle),WHEREcomparisons (=,<>,<,<=,>,>=against
strings, or between two nodes' properties),RETURNof node properties withDISTINCT,ORDER BYandLIMIT,
groupedcount(*)/count(var)/count(DISTINCT var)keyed by node properties or
substring(var.key, start[, length]), and an aggregate over an aggregate
(WITH keys, count(..) AS n [WHERE n op number] RETURN ...). Anything else takes the SQL path unchanged, as does
every query when the adjacency index is not declared, insidebulk_load_session, or when the caller lacks SELECT on
the graph tables. Answers are named and typed by the same code as the SQL path's; an executor error falls back to
SQL. SNB SF0.1 BI01 554 -> 174 ms with substring keys, BI11 (triangles) 18.8 -> 1.3 s as a cycle (Neo4j 30 ms,
724 ms). - Large reads are split across worker processes (
%SYSTEM.WorkMgr, up to 8): a label scan by every n-th
anchor, a variable-length first hop by every n-th end, or an anchored first hop by every n-th neighbour, the
workers' groups or ordered rows merged by the caller. They split when the estimated work (anchors x the first two
hops' sampled fan-out) reachesnative_parallel_min(1,500;IVG_NATIVE_PARALLEL_MIN, 0 always, negative never). - A labelled node named by its id is an anchor.
MATCH (s:Entity {id: $x})-...(ornode_id) was declined, so
every DRKG query and every read keyed by node id took the SQL path. The executor now takes the id as the anchor's
one candidate in each graph that holds the label (^KG("label")), and checks it on a node bound later. A fixed
hop's relationship variable read only bycount(r)is planned too. As on the SQL path,{id: x}also needs an
idproperty equal to x (create_nodewrites one, a bulk load does not), and{node_id: x}needs only the
identity. A non-string id, two ids on one node, or an id without a label still go to SQL. The plan format is
version 2 (Exec.PlanVersion). - A variable-length read from an id-bound start runs natively. The translator hands
MATCH (s:L {id: $x})-[*1..2]->(m) RETURN count(DISTINCT m)to the SQL path's BFS routes, which answer from BFS
rather than the statement, and the native path declined every such translation. It now plans them like any other
chain (the planner still declines shortest paths, OPTIONAL, path functions and a variable on the hop). On DRKG,
D_Q2 went from 4.2 to 1.75 s. Where the BFS routes answer differently, the native answer is the right one (see
KNOWN_ISSUES, "The ID-bound variable-length route answers(id, hops, pred)rows"). - A variable-length walk splits every large level, not just its last, and a directed one splits from one hop.
A level past the first with 512 or more frontier nodes (the source's own end settled) is expanded by up to 8
workers; the ends come back in the order one process finds them. Each worker walks the levels before its own again,
unless they took 200,000 edge reads or more: then it reads the nodes reached so far from a shared global, since the
re-walk had made a deep split slower than none. A directed walk's workers report an edge back into the source, so
*1..nsplits without waiting for a known cycle. A query whose only hop is variable-length and whose RETURN
aggregates keeps its ends in one process, where every worker had walked the hop again; so does a lone tallied hop.
SNB SF1 IC1 139 -> 73-85 ms (the shared global changed no SF1 time); DRKG from its largest hub, server-side: D_Q1
26 -> 9 ms, D_Q2 1,934 -> 315-400 ms, D_Q3 3,450 -> 576 ms (Neo4j 6.7, 496, 706). - Native:
count(DISTINCT end)of a lone variable-length hop counts the ends that pass their checks without
emitting and grouping a row for each: the walk's ends are distinct already. Taken where each anchor is in a graph
of its own. DRKG D_Q2 324 -> 220 ms, D_Q3 582 -> 420 ms (Neo4j 496, 706); SF1 IC3 34.6 -> 32.8 ms, whose time is
the ends' label and property checks. - A variable-length
count(DISTINCT m)from a pinned start is counted inside IRIS on the SQL path. The count
route streamed every BFS row to Python and deduped there; it now asksGraph.KG.VarLength.EndsCount, the native
path's walk. DRKG D_Q2 3.5 s -> 222 ms, D_Q3 5.4 s -> 440 ms. Where the class is missing, the BFS rows are counted
as before. - An outgoing hop counted and nothing else reads the degree cache. Where the far node is checked for nothing
and every parallel edge is a match (MATCH (s {id: $x})-[r]->(n) RETURN count(r)), the count is the source's
cached out-degree (Graph.KG.Adj.Cached, which only reads: a stale entry walks the adjacency as before, and a read
never fills the cache). DRKG's D_Q1, 23,866 edges of one hub: 10.1 -> 0.6 to 1.4 ms (Neo4j 6.7, Memgraph 1.1). - The "more than one graph?" probe asked on every translation reads three index entries.
MIN/MAX(graph_id)
scanned the nodes of a namespace whose statistics were stale: 90 ms of DRKG's 97 ms shortest path. - A whole-label count (
MATCH (n:L) RETURN count(n)) takes the SQL path, and reads the label rows alone. The
SQL path counts the label's index (3.2 ms at SF1 on AWS against 10.9 natively; Neo4j 1.0); with a filter, a key or
a hop it stays native. It had joinednodestordf_labels, which is keyed(graph_id, s, label)with a foreign
key tonodes, so the join added nothing: SF1BI_count_persons4.7 -> 1.4 ms (Neo4j 0.8).count(DISTINCT p)
keeps its meaning; a label mapped to a SQL table still counts that table. - Exactly two variable-length hops (
*2..2) without the general trail walk. Their ends are a double loop over
^KG(the second edge may not be the first walked back), where the walk built and numbered the adjacency of every
first-hop node. IC3 8.4 -> 5.9 ms (Neo4j 2.5). - A chain that can start at either end starts where the work is least. Over one hop, each start's work is
estimated from its label's members and their relationships of the hop's type (one walk of the label, sampled past
2,048 members, remembered per process), the share of a spread sample passing the anchor's filters, the far node's
filters, and whether matches are counted per anchor; longer chains keep the smaller label. The choice is reported
("anchor"inGraph.KG.Cypher.Exec.Run's answer).BI_forum_members234 -> 93 ms (Neo4j 94), BI02 140 -> 97 ms. - A label scan's grouped counts in one loop, and split by key range. Counts over a label scan with no hop,
grouped by the scanned node's properties (BI01's shape), are counted in a local array and made into groups once per
key (bounded: past 50,000 distinct keys the counts are handed on and started again). Workers splitting any label scan
each read their share of eight key ranges per worker, dealt round robin (cut once per process, recut when the
graph's version moves), where each read the whole label for every n-th member. BI01 87 -> 46 ms (Neo4j 30),
BI_replies_per_post134 -> 101 ms. - Less work per node in the executor. A comparison reads its property inline (one
$Orderand one$ListGet,
not a call) and a grouping key on the same property reuses it; a label scan does not re-check the label it scans,
a zero-hop aggregate skips the walk, a node with no relationship of a hop's type costs no call, and the choice
between alternative anchors is remembered per process until the graph's version moves. Serial BI01 (a scan of
172k posts) 769 -> 452 ms, 199 -> 118 ms in parallel;BI_replies_per_post242 -> 204 ms. - Small reads go to whichever path is faster. A query shape (the query and its parameters' types) native answers
in undernative_route_ms(50;IVG_NATIVE_ROUTE_MS, 0 never) is also timed on the SQL path, three runs each after
a warm-up, and the path with the lower median answers it from then on. A shape whose SQL run fails, or takes over 4x
native's slowest timed run, stays native. Shadow mode times nothing. - Shadow mode (
IVG_CYPHER_SHADOW=1orengine.cypher_shadow = True) runs both paths and raises
iris_vector_graph.cypher.native_shadow.NativeMismatch(anAssertionError) where they differ. The full TCK is
3896/3896 in shadow mode, with and without the parallel path forced, and the 40 SNB queries at SF0.1 return the
same answers on both paths. - Speed (SNB SF0.1, same machine, 2026-10-21): median IVG/Neo4j 1.45 on the 39 queries Neo4j timed, within 3x on
37; the SQL path's median was 5.9, within 3x on 17 of 40. Seedocs/performance/ALT_VS_BASELINE.md. - A group's key JSON is built only for the groups a row or a worker reads. A short key's group stores its values
as a$List(Exec.KeysJSONmakes the JSON the old projection made, types kept), so a new group no longer reads
its properties twice and builds a JSON array it rarely needs. SF1 BI04 (34,054 forum titles,LIMIT 10) 628 ->
467 ms; grouping the same walk by title withcount(p)555 -> 405 ms. - An ordered native read with
LIMIT kkeeps only rows that can still be written. WithoutDISTINCT, the sort
tree is cut back to its first k rows each time it has filed 2k + 64, and a match whose first sort key falls after the
k-th row's is not filed, nor its other keys made. The answer is unchanged, ties included (TCK 3896/3896). SF1 IC2 in
one process 1,226 -> 1,065 ms; IC7 and IC8, with few matches, unchanged. - A variable-length hop whose far node has an equality filter reads only the nodes holding the value (SNB IC1:
(p {sid: $id})-[:KNOWS*1..3]-(f) WHERE f.firstName = $name). The executor walked out to every node in reach (9,900
of 10,600 people at SF1) and then filtered them; now the nodes holding the value come from the value index, and each
is checked against the start's two-hop neighbourhood, read once (VarLength.NamedEnds). Taken when at most 1,000
nodes hold the value, the hop's minimum is at most 1, the start is not one of them, and the answer cannot depend on
the ends' order. SF1 IC1 121 -> 12.6 ms (Neo4j 20), the same answers on every parameter set.
Changed
- Read-only Cypher goes to the native executor by default (spec 253). Before, every query ran as SQL. A
supported read (see Added) now runs over^KGwhere the adjacency index is declared, the classes match the
package (runinitialize_schema()), and the caller has SELECT on the graph tables; anything else takes the SQL
path. One known divergence: an unscopedcount(DISTINCT m)where graphs share a node ID (KNOWN_ISSUES, "Unscoped
count(DISTINCT m)disagrees between the native and SQL paths when graphs share a node ID").
IVG_NATIVE_CYPHER=0, orIRISGraphEngine(native_cypher=False), turns it off. - Degree is counted from
^KG("out"), not stored. Nothing writes^KG("deg")or^KG("degp")any more.
Graph.KG.Adj.Degreecounts a source'soutentries behind a cache (^KG("degc")) that holds while the source's
generation (^KG("gen")) is unchanged;Graph.KG.Adjacency, the only writer ofout/in, bumps it on every
change (ADR-0012). A rebuild andbulk_load_sessionleave the cache warm. Code that readsdegordegpgets
nothing on a new graph, and on an upgraded one stale counts until a rebuild or an erase removes them: call
Graph.KG.Adj.Degree(graph, src[, pred])instead.backfill_degp()is a deprecated no-op (DeprecationWarning);
a snapshot restore skipsdeg/degplines. - An anchored undirected hop is read as directed
UNION ALLbranches. IRIS materialized the wholeUNION ALL
CTE an undirected hop read and drove the join from it, so a lookup from a bound person walked everyKNOWSedge. A
statement with an equality anchor is now 2^k directed branches (k ≤ 3); an OPTIONAL hop and an unanchored statement
keep the CTE. SNB SF0.1 BI18 7.2 s -> 27 ms, IS3 12.6 -> 1.6 ms, BI14 29 -> 12 ms, the same answers. A shape's
first call compiles longer (BI14 1.7 -> 11 s, BI18 3.3 s), once per shape: warm up shapes that must answer fast on
their first call. shortestPathsearches between the endpoints the MATCH names, and returns one path per pair. A
{node_id: $x}endpoint is the search endpoint; before, only{id: ...}was read and otherwise the first string
parameter was taken, so SNB IC13 (personId first) searched from a sid and answered zero rows. Labelled and
property-map endpoints ((a:Person {sid: $x})) are found by running the translated MATCH; more than 1,000
(source, target) pairs raiseValueError.shortestPathreturned every path at the shortest depth (6 rows for one
pair); it returns one per endpoint pair. A caller that wants every shortest path usesallShortestPaths.betweenness_centrality(sample_size=n)with n at least the node count is exact Brandes. Such a call used 200
sources, whateversample_sizesaid (Pearson 0.77 against igraph's exact answer at 5,000 nodes); the store now
passes the caller'ssample_size. It costs a source per node: passsample_size=0, which still samples 200
sources, for the old cost.engine.capabilitiesis detected on first read, from%Dictionary. It stayedobjectscript_deployed=False
untilinitialize_schema()ran, so a fresh engine on a deployed namespace bulk-loaded edges through the per-row
Python fallback (1.5–2K edges/s); it now takesBulkIngestEdgesSQL(about 14K edges/s). The PageRank fast path, the
k-hop fallback's BFS and status see the real capabilities too. Nodes keep the Python path untilinitialize_schema()
(the server node loader writes through the iFind index, about 40 nodes/s on SNB data). Assigningcapabilities
works as before; assigningNonedetects again.bulk_load_sessiongathers table statistics (TUNE TABLE) after a load of 10,000 rows or more. Schema setup
tunes the tables while they are empty, so every plan after a bulk load was made for empty tables. Closing such a
session takes longer by the tuning.- A hop from a node named by its id joins
rdf_edgeson the SQL path, not theMatchEdgesscan. The scan (a
JSON_TABLE over^KG) cost about 77 us an edge once IRIS joined its rows: DRKG's
MATCH (s:Entity {id: $x})-[r]->(n) RETURN count(r)took 5.5 s for its hub's 23,866 edges, 10 ms joined. SNB SF1's 25
SQL-path queries that complete under the cap ran the same either way (median 0.99, every answer equal).
IVG_EDGESCAN=1restores the scan. BuildKG(and sosync()) builds the adjacency indexes with 8 parallel jobs.%BuildIndicesran every row's
callback in one process. SNB SF0.1 20.9 -> 5.8 s, with byte-identical^KG("out"/"in").get_edges_in_windowraisesTemporalWindowErrorwhen the server read fails, where it returned[](spec
255).QueryWindowbuilds its answer as one string, so a window past about 35 k edges raised<MAXSTRING>and read
as empty.execute_temporal_window_querysetserroron its result. CatchTemporalWindowError
(iris_vector_graph.exceptions), or useiter_edges_in_window.- A retention purge removes the structural rows only expired timestamps held up (spec 254).
purge_raw_before
andpurge_beforenow also delete therdf_edgesrow, and with it the^KG("out"/"in")entries, of every triple
none of whose timestamps survive, while the row carries the temporal marker (ADR-0009, amended). A rowcreate_edge
orbulk_create_edgesasserted stays; nodes stay; the aggregates stay. With one node per event, adjacency no longer
grows without bound under retention.bulk_create_edgesnow adopts a marked row ascreate_edgedoes, and so does
a CypherCREATEorMERGEof an edge between matched nodes (the marker goes; literal relationship properties
become the row's qualifiers). - An "insert" at coordinates that already hold an edge is a collision and changes no store (spec 254). It used
to overwrite the raw edge, merge attribute keys, and count a second time in the bucket aggregates and the sketch.
bulk_create_edges_temporalreturns aTemporalWriteCount(anint, the written count, with.collisionsand
.skipped);Graph.KG.TemporalIndex.BulkInsertCountsreturns"written:collisions:skipped", andBulkInsertits
integer as before.create_edge_temporalstill returnsTrue. The temporal stores keep a weight as a number
whichever path wrote it. To overwrite, passmode="update"(orupsert=True); check.collisionson a bulk write. - Interning an already-interned node or predicate takes no lock (
GraphIndex.InternNode,InternLabel): an
index once set never changes, so the read comes first.Adjacency.Putwith^NKGpresent went from 8.6 to 4.8 us a
row in one process; the lock pair per endpoint was most of a bulk adjacency build's cost per row, and contended
across the build's index jobs. - The purge's row retirement runs as embedded SQL (
Adjacency.RetractTemporalRows): 45–47 -> 34–41 us per
purged edge on 20,000 one-node-per-event edges. Spec 254's SC-004 (at most 2× the old purge, about 6 us) is still
not met: the walk costs 7 us and each row delete, with its index maintenance, about 29 us. - The Python mirror that re-inserted every temporal row after the write is skipped when the adjacency index is on.
- A Cypher read with no
USE GRAPHkeeps each node in its own graph (spec 253 Phase 0). Where a node ID lives in
more than one graph, the SQL path joined every alias onnode_idalone:MATCH (n {id: $x}) RETURN n.namereturned
each graph's value for each graph's row, labels were unioned across graphs, a label filter matched another graph's
node, and an edge of one graph reached another graph's node of the same ID (count(*)over one hop: 12 instead of
2). Joins between graph-owned tables are now correlated ongraph_id, variable-length walks run in the source's
graph, and the engine's variable-length routes answer per graph and return the union. Namespaces holding one graph
are unchanged (the correlation is added only whenGraph_KG.nodesholds more than one graph). Writes keep today's
matching. - The k-hop fast path answers only where the default graph alone holds edges; it read the default graph's
adjacency while the same query reads every graph, soMATCH (a {node_id: $s})-[:R]->(b) RETURN count(b)answered 0
for edges in named graphs. MATCH (n {id: $x})-[:R*1..2]->(m) RETURN m.namereturnsm.name. The inline-id spelling answered raw
traversal rows (id, hops, pred); theWHERE n.id = $xspelling was right.- A node pattern with several labels prepares in constant time. One join per label made IRIS's plan search
grow exponentially (12 labels: 18 s to prepare); the first label is a join and the rest one count (0.09 s). - The Adjacency Index is maintained from the rows (spec 252, ADR-0008 step 2, ADR-0009).
initialize_schema()/
ensure_indexesdeclare two functional indexes,idx_edges_adjonGraph_KG.rdf_edges(Graph.KG.AdjIndex) and
idx_labels_adjonGraph_KG.rdf_labels(Graph.KG.LabelIndex), which write^KG("out"/"in"/"label")in the same
transaction as the row. A SQLINSERT/UPDATE/DELETE, CypherCREATE/MERGE/DELETE, a ledger commit
and the bulk loaders now leave traversal current; before, only the engine's own writers andsync()did, and a
Cypher write followed by a Cypher traversal read missed the edge. Declaring the index builds it over every existing
row once (announced with a warning; minutes on a large graph), which also repairs a drifted install. With the index
declared, the engine's edge writers write rows only. - An edge's weight lives in its row.
create_edge(weight=...)stores{"weight": w}inqualifiersand
set_edge_weightupdates the row (it returnsFalse, writing nothing, when there is no such edge), so the
index, a rebuild andr.weightagree. - A temporal edge is visible structurally.
create_edge_temporal/bulk_create_edges_temporalassert a
rdf_edgesrow marked{"temporal": true}for the triple; one-hop Cypher,sync()and exports now see it, and
the row is removed with the triple's last timestamp only while it is still marked. An explicitcreate_edgeon
the same triple adopts the row and clears the mark. Existing temporal edges get their rows when the index is
declared (SELECT Graph_KG.backfill_temporal_rows()). Marked rows are outside ledger governance: canonical
reads skip them. bulk_delete_adjacencyis deprecated and deletes rows. It removed the adjacency and left the rows, so the
edges came back with the next rebuild. It now warns (DeprecationWarning) and deletes every edge row with one of
the given nodes as source or target, in every graph, with their reifications and embeddings; the nodes stay. Use
delete_node/bulk_delete_nodesto remove the nodes too.- A restore into an indexed namespace skips the archive's
out/in/labelentries: the restored rows
rebuild them, so an entry with no row behind it does not come back. kg_SUBGRAPHnode properties come fromrdf_props. They were read from^KG("prop"), which onlysync()
refilled, so a node created or a property changed since the last sync showed none or the old value. Nothing
writes^KG("prop")any more;sync()empties it.SQLUser.CY_VLP_ENDSreads^KGwhere the adjacency index is declared, instead of one SQL statement per node
reached, and for a lower bound of 0 or 1 takes each end at its BFS distance instead of enumerating every trail. SNB
SF0.1 IC1 (KNOWS*1..3) 425 to 150 ms, IC3 144 to 116 ms, same answers.- Removed
Graph.KG.Loader, which wrote adjacency with no rows and had no callers.
Fixed
- Property value equality is case-sensitive again. On an install that had run
ensure_indexes(every
initialize_schema()), itsALTER COLUMN val VARCHAR(64000)named no collation, which IRIS reads as its default,
SQLUPPER:n.name = 'JOHN'matched'John'. The ALTER now saysCOLLATE %EXACTand is skipped when the column is
already EXACT; a fresh install never runs it. An existing graph pays once: IRIS rebuilds the indexes onval(254 s
at 2.9M property rows, SNB SF0.1), announced by a warning. A caller that relied on the case-insensitive match
comparestoLower(...)values instead. n.prop >= $xandn.prop <= $xreturned no rows. The NaN-safe form repeated the operand, placeholder
included, while the parameter was bound once, so the statement failed ("Incorrect number of parameters").- A string-valued
$paramin<,<=,>,>=compares as a string. The property was cast to DOUBLE, so an
ISO date collapsed to its leading number. x.name < y.nameon string properties matched nothing. Both sides were cast to DOUBLE, making every
non-numeric string 0; aMATCH'sWHEREnow compares by Cypher orderability (SQLUser.CY_CMP). TheWHEREof
anOPTIONAL MATCH, which becomes an outer join'sONwhere IRIS refuses the function, keeps the cast.- A
$paramin a node's property map on an undirected relationship matched nothing.
MATCH (p:Person {sid: $id})-[:KNOWS]-(f:Person)bound the parameter's AST text instead of its value. - A
RETURNalias IRIS reserves (RETURN n.at AS at) failed at Prepare, andexecute_cypheranswered zero
rows. The 118 reserved words missing from the translator's list are quoted. sum()of floats came back rounded under intersystems-irispython 5.4.0 (2.25 as 2): the driver returns every
SUM as a float, where 5.2.1 returned an int, andsum(n.prop)was typed as an integer column. A sum whose value is
whole is INTEGER, any other FLOAT.ORDER BYunderLIMIT/SKIPfailed on builds that need theTOPworkaround: an aggregate or a bare alias
with SQLCODE -23, an alias of an earlier stage's column (WITH p, count(m) AS n RETURN n AS c ORDER BY c LIMIT 10)
with SQLCODE -29.ORDER BYa value shaped like a date that is not one (2021-02-30T10:00) failed the statement, which
execute_cypheranswered as zero rows; it orders as a string. The sort key (CY_SORT_KEY, called per row per sort
column) tests characters instead of building two regex matchers per call: 15x faster, SNB SF0.1 IC9 4.5 -> 1.2 s,
IC5 1.9 -> 1.3 s, the same answers. A value with non-ASCII digits orders as a string.ivg.shortestPath.weightedsearched for nodes named'a'and'b'instead of reading its$parameters.- Two regressions from 4.2.4's parameter isolation. It slowed every translation; it now costs nothing where no
parameter shares a variable's name, and a clashing query's renamed tree is built once per cached tree. AWITH
alias bound to a renamed parameter lost its value in a subscript lookup (TCK Map2 [1]). - The 4.0.0 schema migration no longer waits out IRIS's 120 s lock timeout. An
ALTER TABLEwaited behind an
open read of the child table in the single-graph backfill, and behindGraph.KG.Subgraph/TraversalBuild
compiled before the graph-scoped schema existed (they now use dynamic SQL). Wherenodesholds one graph, the child
rows'graph_idbackfill is one constant UPDATE per table;kg_node_storesreads each node in one native call
instead of two. - A BFS answer past IRIS's string limit no longer fails (
khop()'s server BFS):BFSFastStream/
ReadBFSResultsStreamstage it in chunks, used only when the one-string answer overflows. - The SQL path's variable-length counts keep every check, and the minimum hops. The BFS count routes answer from
the start's id and one hop alone:(s {id: $x})-[*1..2]->(m:B) RETURN count(DISTINCT m)counted every node reached,
Bor not; a WHERE on the far node, a relationship property on the hop, a label on a start pinned bynode_id, a
fixed hop after the far end and a second pattern were dropped the same way;*2..2counted every node within two
hops (BFSFastCountDistinct took only the maximum), the start too through a self-loop; and the second count route
passed no graph and answered 0 on an error. Such a query now expands in SQL (translator._vl_end_checked), and the
remaining count routes walk with both bounds in the query's graph (Graph.KG.VarLength.EndsCount). The native
path's answers were right. - A variable-length hop to a node named by its id ignored the id on the SQL path. The translator handed a pattern
with an id on either end to the engine's BFS routes, which read only the start:
(s {id: $a})-[:R*1..3]->(t {id: $b}) RETURN count(DISTINCT t)counted every node within three hops,
RETURN count(*)came back as raw(id, hops, pred)rows, and(s:L)-[*]->(t {node_id: $b})counted every
reachable node as a start. A pinned far end now goes toCY_VLP_PATHS, which runs the statement as SQL. A pinned
start keeps the BFS route only for a RETURN it answers as written (one query part, noORDER BY, items that are
count(DISTINCT x)or unaliased variables and properties): an aliased property and aWITHstage over the hop had
come back as raw(id, hops, pred)rows, andORDER BYwas not applied. - A variable-length count from
{id: $x}counted a node once per depth, and ignored the start's labels. DRKG's
MATCH (n:Entity {id: $x})-[*1..2]->(m) RETURN count(DISTINCT m)answered 61,331 (the nodes at one hop plus those
at two) where 45,993 are within two hops, and answered at all though no IVG DRKG node is labelledEntity. The
ID-bound route now counts distinct nodes. It walks from x only where x holds the pattern's labels and anid
property equal to x, as the translatedMATCHrequires.WHERE s.id = $xwith a label onswalked from every
node with the label; it now starts from the nodes holding thatid. The labeled route named an aliased count
countinstead of its alias. - An all-graphs variable-length read returned rows from the wrong graph where Arno was loaded. Its default-graph
pass took the Rust BFS, which walks^NKGacross every graph, and read those nodes' properties in the default graph
(an extranullrow). The default graph takes Arno only where it is the only graph with edges. USE GRAPHleft a node crossed in unscoped.USE GRAPH 'g' MATCH (a {node_id: $a}), (b {node_id: $b})
translates the second node to aCROSS JOIN, which has noONfor the graph predicate: where another graph held
b's id the read returned a row per such graph, and aCREATE (a)-[:R]->(b)after it made an edge per row (two
parallel edges in a multigraph). The predicate now goes inWHERE, as the first node's does.bulk_ingest_edges(and sobulk_load_session) dropped every self-loop without counting or reporting it.
Graph.KG.EdgeScan.BulkIngestEdgesSQLskipped an edge whose source is its target;create_edgeand the SQL
fallback store one. The full DRKG load kept 5,870,759 of its 5,874,258 distinct triples: the 3,499 missing were
its gene self-interactions. A load made through this path before the fix is missing its self-loops; re-ingesting
the source adds them (the rest are skipped as duplicates).ivg.shortestPath.weighted(...) YIELD totalCostbuilt the path it never returned. A call that yields only the
cost now asks for it alone (Graph.KG.TraversalPaths.DijkstraCost: Dijkstra from both ends, local arrays), where
DijkstraJsonsearched from the source with its queue in process-private globals; any otherYIELD, and classes
deployed before it, keepDijkstraJson. A failure of the cost search raises. BIW at SF1 145.6 -> 0.6 ms (Neo4j
15.5 on AWS), at SF0.1 15.9 -> 0.4 ms, the same costs on every parameter set.length(shortestPath(...))searched from one end and built every shortest path. A query that reads only the
length now asks for the distance alone (Graph.KG.TraversalPaths.ShortestLength: a breadth-first search from both
ends, expanding the smaller frontier, reading only the types asked for); one that reads the path, its nodes or its
relationships, or asks for all shortest paths, keeps the path routine. IC13 at SF1 14.8 -> 1.1 ms (Neo4j 1.3), at
SF0.1 4.4 -> 0.9 ms (Neo4j 1.6), the same answers on every parameter set.- Every Cypher query listed the graphs holding adjacency (two Native API round trips, about 0.2 ms) before the
k-hop shortcuts looked at it; only a query starting at a{node_id: $param}node, the shape they answer, does now. - An erase in a namespace that never created the FHIR stores failed on IRIS 2026.2.0AI with SQLCODE -400
(erase failed on Graph_KG.fhir_graphs): the eraser now asks whether each FHIR table exists instead of inferring
it from the SQLCODE. - The first
initialize_schema()on a fresh namespace did not declare the adjacency index: the index check ran
before the ObjectScript deploy compiled its classes. It is declared again after the deploy. - A variable-length expansion failed with SQLCODE -149 on a social graph (SNB IC1; at SF1 also IC3, IC5, IC6 and
IC9).SQLUser.CY_VLP_PATHSbuilds one JSON string holding every edge-distinct trail, which overflows IRIS's 3.6 MB
string limit at three hops. Where the result cannot observe how many trails reach an end, the translator calls the
newSQLUser.CY_VLP_ENDS, which returns each reachable end once with its minimum depth. That is: one typed, unnamed,
property-free variable-length hop (not a shortest path, in no named path orOPTIONAL MATCH), ending at a named
node other than its source; other fixed hops andMATCHes allowed where their types are disjoint from the hop's;
noWITH,UNWIND,UNIONor update;RETURNandORDER BYnot reading the source; and eitherRETURN DISTINCT
with no aggregate, or every aggregatecount(DISTINCT x),collect(DISTINCT x),min(x)ormax(x). Every other
shape keepsCY_VLP_PATHS.initialize_schema()installs the function.