# InvariantDB full reference Version: 2026-10-17 (releases up to 1.4.3.3) Last updated: 2026-10-17 Canonical URL: https://invariantdb.com/llms-full.txt > InvariantDB is a Cypher graph database that stores valid time and recorded time on each node and relationship version, keeps belief revisions linked to their evidence, and records the credential that made each write (from 1.4.2 for every write of an authenticated request except import and other background jobs and the legacy /write route; on 1.4.1 and earlier when the request includes provenance). Agents use it through Cypher and REST over HTTPS, or through a Model Context Protocol (MCP) endpoint with 28 tools that works with MCP clients that support the HTTP transport. This document describes the hosted service's API: Cypher dialect, procedures, MCP tools and REST endpoints. For the procedure list on the server you are connected to, run `CALL db.listProcedures() YIELD name, signature, status`. If you are a person, the individual pages linked from `/llms.txt` may be easier to read. Ids, names and values in the examples are fictional. Contents: 1. What InvariantDB is 2. Cypher surface (clauses, bitemporal clauses, indexes and constraints) 3. Procedure catalog 4. MCP tool catalog (28 tools, input schemas, examples, error codes) 5. REST endpoints (including write-side provenance and errors) 6. Bitemporal queries (`AT VALID` and `AT RECORDED`) 7. Belief revision flow 8. Crypto-shred (in development) 9. Authentication (API key, OAuth sign-in for MCP clients, capability tokens, Cognito JWT) 10. What not to do (common mistakes) --- ## 1. What InvariantDB is InvariantDB stores a graph with two time axes and keeps the history of node and relationship versions. - **Valid time** is when a fact was true in the world. Query it with `AT VALID`. - **Recorded time** is when the database stored the fact. Query it with `AT RECORDED`. - **Belief revision**: beliefs are nodes linked by `REVISES` relationships to earlier versions and by `DERIVED_FROM` relationships to evidence. `db.beliefLineage` walks the chain. - **Provenance**: writes can carry an actor, source, reason, request id and confidence, and the engine adds the authenticated principal. `db.propertyProvenance` returns, for each change written with provenance, the actor, source, reason, request id and confidence supplied by the caller and the principal of the credential that made the write. From 1.4.2 the principal is recorded for every write of an authenticated request, with or without provenance. - **Access policy** (from 1.4.2): a graph can carry a policy that says, per role, which labels and relationship types may be read and written (`db.access.*`, §2). For a credential the policy restricts, hidden nodes and relationships are absent from Cypher reads. On this release such a credential is refused search, other procedures, temporal reads and the HTTP routes outside Cypher. Other capabilities: - **Export and erasure**: `db.subjectExport` exports one subject's node, its relationships and its property versions (GDPR Article 20). On a graph with no declared encrypted property it matches only the node whose string id equals the argument. A subject id held only in a property is not matched, and nodes created with Cypher `CREATE` or `MERGE` get a generated string id. An empty result does not show that the graph holds nothing about the subject. Crypto-shred (per-subject encryption keys and key destruction, `db.destroySubjectKey`) is in development; on the current release, declaring an encrypted property is refused by default, and the call is refused without recording a receipt. Contact hello@invariantdb.com before relying on it. See §8. - **MCP**: JSON-RPC 2.0 over HTTPS at `POST /graphs/{name}/mcp` (on the hosted service, `https://invariantdb.com/graphs/{name}/mcp`). The server negotiates protocol version 2025-06-18 (default), 2025-03-26 or 2024-11-05. 28 tools (26 before 1.4.2.4). No local install is needed. - **Cypher**: the query language is Cypher with clauses for valid time and recorded time. - **Search**: BM25 fulltext (`gds.fulltextSearch`; declared full-text indexes with `db.fulltext.query`), vector search (`gds.vectorSearch`), reciprocal-rank or score fusion of both (`db.hybridSearch`), and vector seeds with graph expansion (`gds.semanticMatchExpand`). The signatures in §3 are copied from `CALL db.listProcedures()`. ### What changed in 1.4.3.3 - **Counts.** `count` / `sum` / `avg` / `min` / `max` (with or without `DISTINCT`, with grouping keys) after the last `MATCH` are computed as the `MATCH` produces its rows when it starts from a scan of a label or of the whole graph (with or without `WHERE`, `AT RECORDED`, `AT VALID`, `AS OF`), so they are no longer refused with `BUDGET_EXCEEDED` however many rows they count. The statement's time limit still applies. Relationship counts at a time without a type are much faster. See §2, "Counts". - **Full-text search.** `db.fulltext.query` takes a list of properties or a map of property to weight (each with a declared index; column `fieldScores`; not paged by `cursor`), and the options `filter` / `nodeIds` (before ranking; `filter_statistics` `'collection'` default or `'filtered'`), `matched_terms` (`matchedWords`, `matchedTerms`), `excerpt` (best passage with the matches marked) and `group_by` (one row per value, `group`, `groupHits`). Same options on `POST /graphs/{name}/search/fulltext` and MCP `search_fulltext` (`fields`, `node_ids`; hit fields `field_scores`, `matched_words`, `matched_terms`, `excerpt`, `group`, `group_hits`). Reads at a past time no longer index again the text of nodes whose text did not change. - **Hybrid search.** `db.hybridSearch` options `fusion` (`'rrf'` default, `'score'`), `weights` (`{bm25, vector}`), `normalize` (`'minmax'`, `'max'`), `at_valid`, `at_recorded`; `filter` / `nodeIds` at a past time are answered (through 1.4.3.2 refused with `UNSUPPORTED_FEATURE`). - **`CREATE INDEX` on a label with 100,000 nodes or more** returns at once and builds in the background (info warning `INDEX_POPULATING`; `db.listIndexes()` shows `populating`). `db.listIndexes()` yields a new last column `value_type`. - **Declared column files (opt-in):** `db.createColumn(kind, name, property)`, `db.listColumns()`, `db.dropColumn(...)`. - **New graphs store their history compressed** (same answers, less disk). Self-hosted: such graphs open on 1.4.3 and later only. - **Retired routes:** `POST /graphs/{name}/nlq`, `/investigate`, `/agent` answer 410 `FEATURE_RETIRED` (no model called). The cases routes stay for reading, deprecated. - **Security fix (`/cases`):** recorded agent runs are kept per tenant and served only to the credential that recorded them (or a global administrator holding the same roles); others get 404. Recording is off by default. A body `userId` other than the credential's subject: 400 `AGENT_USER_ID_FROM_CREDENTIAL`. `db.aclEvents` / MCP `acl_events` without a principal filter return every event the caller may see (administrators: all; others: their own). - **Keyed reads at a time** keep up with a steady writer; info warning `TEMPORAL_KEYED_INDEX_DECLINED` and `TEMPORAL_READ_LIMIT` `details.keyed_decline` when the key-history index cannot answer. - **Backups and clones** include files a write created for the first time during the cut. - Known issues: a full-text query over several fields or with `group_by` ranks every match before paging; an undirected walk between two labels at an `AT VALID` cut over a large graph is slow and can reach the time limit (directed or typed walks are much faster). ### What changed in 1.4.3.2 - **Query planner on by default.** Read statements are planned from per-graph statistics (start node and index, order of consecutive `MATCH` clauses, which end of a relationship between two bound nodes is checked). The same statement returns the same rows. What can change is anything that depends on row order when the statement has no `ORDER BY`: the order of rows, which rows a `LIMIT` without `ORDER BY` keeps (or which rows tied under the `ORDER BY` keys), the order of a `collect()` list and so `head()`, `last()`, `[i]` and slices, and which of two equal grouping keys (`2` and `2.0`) names a group. Say `ORDER BY` on a unique key when order matters. Statements that write are planned as before. - **Error code change.** `size(x)` where `x` is not a string, a list or null, and `properties(x)` / `keys(x)` where `x` is not a node, a relationship, a map or null, answer 400 `ARGUMENT_TYPE_MISMATCH` with `details.expected` and `details.got` when the value's type is known only when the statement runs, for example `size(n.age)` (1.4.3.1: 400 `UNSUPPORTED_FEATURE`, "not in the standard library"); a value whose type is known before it runs (`size(1)`, a node variable) is refused with `SEMANTIC_ERROR`, as before. `properties()`, `keys()` and `x {.*}` never list the reserved names, also when a record stored a value under one before the write guards. - **Paging contract** (already the behaviour of 1.4.3.1, now tested and documented): `MATCH (n:L) RETURN ... ORDER BY n.p SKIP s LIMIT k`, with or without `WHERE`, returns the rows a sort of the whole label returns, at every moment. - **Vector search.** Large-k exact searches over vector files read far fewer full vectors, with the same answers bit for bit; `YIELD node ... RETURN node.id` reads only `id` from each hit. Opt-in `gds.vectorSearch(..., {rescore: r})`: the first `r` rows exact, the rest by their 8-bit score with the new column `exact` false and the warning `VECTOR_PARTIAL_RESCORE`. During bulk vector writes a search that cannot finish before its time limit waits for the running rewrite when that ends sooner, or is refused at once with the retryable 503 `VECTOR_FILE_REWRITE_PENDING` (`details.reason` `overlay_over_deadline`) instead of timing out. - **Relationship property indexes:** `CREATE INDEX FOR ()-[r:TYPE]-() ON (r.prop)`. - **`FOR PORTION OF VALID FROM [TO ]`**: a correction over a valid interval that keeps later versions; new subcode `PORTION_WRITE_UNSUPPORTED`. - **Multi-statement transactions** exist but are off by default and not enabled on the hosted service (routes answer 404 `TRANSACTIONS_DISABLED`). ### What changed in 1.4.3.1 - **Linked memories in `recall` and `recall_as_of` (on by default).** When a search names a person, place or thing that saved facts are about, the facts are followed one or two steps and the memories that state related facts are added after the others with `match: "linked"` and `via` (at most 12, within `linkBudget` tokens, default 2000). `search.links` says when this happened. `links: "off"` leaves them out. See §4, `recall`. - **Group commit (on by default).** Writes to one graph that arrive together share one disk sync; a write is acknowledged after the sync that covers it. Concurrent reads do not see a write statement's writes before it has committed (on 1.4.3 a multi-clause write statement was visible to concurrent reads while it ran). When a write that shared a disk sync cannot be completed (for example, the disk is full), it and the writes that shared its sync are refused together, and nothing of them is kept. - **Filtered vector search on large indexed labels (on by default).** A label searched through an approximate (HNSW) vector index that also has an integer, string or boolean property index gets a vector index per value of that property (values with at least 1,000 vectors, within limits on their number and share of the label), built in the background. A filtered search whose eligible nodes are mostly one value's nodes searches that value's index when cheaper; such a search is approximate, with the recall target of other filtered approximate searches (about 0.99 recall@10 by default). A filtered search that 1.4.3 answered exactly by scoring every eligible node may now be approximate. Cypher searches with at most 5,000 eligible nodes, exact plans and flat vector indexes stay exact. - **Embedding model of new graphs.** A graph records its embedding model when it is created; existing graphs keep theirs. On the hosted service, graphs created from 1.4.3.1 on embed with Cohere Embed v4 (1,536 dimensions; a precomputed query vector of another dimension is refused there); the text being embedded is sent to Cohere's API, and Amazon Bedrock may also be used to run the same model. Queries are embedded as queries for Cohere and Voyage models. `db.memory.reembed(model)` (graph administrators only) moves a graph's memories to another model in a background job; `db.memory.reembedStatus()` reports it. - **Administrator keys bound to their tenant (hosted service).** A credential with role `admin` or `operator` issued to a customer tenant acts as `tenant_admin` of that tenant. - **30-day retention.** Request traces, copies of running statements written at shutdown and the records of ended jobs are deleted after at most 30 days; `GET /jobs/{job_id}` answers 404 for a job that ended more than 30 days ago. - **As-of reads on a large history.** A read at a time cut pinned to an indexed key answers from the property index and the label's key-history index instead of being refused with `TEMPORAL_READ_LIMIT` (info warning `TEMPORAL_KEY_HISTORY_INDEX_USED`). `db.buildBitemporalLabelIndex` runs as a background job (`db.bitemporalLabelIndexStatus()`). - **Bitemporal fixes.** An entity deleted with a future `AT VALID` time is returned by reads without a time until then (the `MATCH` of a write statement does not find it before then); `db.labelCounts()`, `db.graphCounts()` and the schema counts count what is valid now; `MERGE ... ON CREATE SET` writes one version in most cases (two when an `ON CREATE` value reads the node itself or the graph, assigns to another variable, or the `MERGE` is of a path); a backdated write is not refused because of a stored null. - **Full-text search.** Refusals of a declared-index option on the full-text search routes and the MCP tool `search_fulltext` carry `FULLTEXT_INDEX_NOT_FOUND` or `FULLTEXT_OPTION_INVALID` as `subcode` / `data.subcode`. Messages and procedure descriptions no longer name server settings. - **Faster reads** with the same answers: more statements on the query engine, property reads limited to the keys a statement uses, pattern counts from relationship lists. --- ## 2. Cypher surface InvariantDB implements a Cypher dialect with the standard read and write clauses and clauses for valid time and recorded time. ### Read clauses - `MATCH (n:Label)`: match nodes by label - `MATCH (n)-[e:TYPE]->(m)`: match a relationship - `WHERE expr`: filter. Operators: `=`, `<>`, `<`, `<=`, `>`, `>=`, `IN`, `IS NULL`, `IS NOT NULL`, `AND`, `OR`, `NOT`, `STARTS WITH`, `ENDS WITH`, `CONTAINS`, `=~` (regular expression). From 1.4.1 a comparison with null, as a literal or a null parameter (`n.p = null`, `n.p <> $v`, `n.p < null`), matches no node; use `IS NULL` / `IS NOT NULL` (1.4 treated `= null` as `IS NULL` and `<> null` as `IS NOT NULL` on `MATCH ... WHERE ... RETURN ... LIMIT k`) - `RETURN expr [AS alias]`: project columns - `WITH expr [AS alias]`: chain query stages - `ORDER BY expr [ASC|DESC] [NULLS FIRST|NULLS LAST]`: sort. Strings sort by byte order, so upper-case ASCII letters come before lower-case ones and non-ASCII letters come after both (`Eclair`, `Zebra`, `apple`, `zoo`, `Éclair`). - `SKIP n` / `LIMIT n`: pagination - `UNWIND list AS x`: expand a list into rows - `OPTIONAL MATCH`: left outer match - `UNION` / `UNION ALL`: combine query results ### Write clauses - `CREATE (n:Label {prop: value})`: create a node - `CREATE (a)-[:TYPE {prop: value}]->(b)`: create a relationship - `MERGE (n:Label {key: value})`: match the node, or create it if none matches - `MERGE ... ON CREATE SET ... ON MATCH SET ...`: set properties depending on whether MERGE created or matched - `SET n.prop = value`: set a property - `SET n += {map}`: add or update properties from a map; `SET n = {map}` replaces the properties - `SET n:NewLabel`: add a label - `REMOVE n.prop`: remove a property - `REMOVE n:Label`: remove a label - `DELETE n`: delete a node that has no relationships - `DETACH DELETE n`: delete a node and its relationships - `FOREACH (x IN list | ...)`: run write clauses once per list element - `FOR PORTION OF VALID FROM [TO ] MATCH ... SET ...` (from 1.4.3.2): apply `SET` / `REMOVE` to the valid interval `[, )` only, keeping the values outside it and the other values of versions that start inside it. Never refused with `WRITE_WOULD_REPLACE_LATER_VERSIONS`. Only `MATCH`, `OPTIONAL MATCH`, `WITH`, `UNWIND`, `SET`, `REMOVE` and `RETURN`; `SET` values must be literals or parameters (not read from the graph); no `SET n = {...}`, no `DELETE`. Other statements: 400 `PORTION_WRITE_UNSUPPORTED` (MCP -32006), nothing written. A node in MERGE matches on its labels and every listed property. `MERGE (n:L)` binds every `L` node or creates one; a node without a label is matched against every node in the graph (a full scan). MERGE on a relationship needs one type and a direction. MERGE does not need an index: without a property index on the label and the first listed property it scans the label and returns warning `UNINDEXED_MERGE`. When more than one node matches, Cypher MERGE binds every match and creates nothing. ### Subqueries and expressions - `CALL { ... }`: subquery that runs once per input row. From 1.4.2.3 the body sees an outer variable only through a `WITH` as its first clause (`CALL { WITH p MATCH (p)-->(c) RETURN count(c) AS n }`); a body that reads an outer variable without one is refused with 400 `UNRESOLVED_VARIABLE`. A name the body declares itself (`UNWIND ... AS t`, `WITH x AS t`, `YIELD t`, a list comprehension variable) is a new variable (from 1.4.2.5). A body without an importing `WITH` that returns a column under the name of an outer variable is refused with `SEMANTIC_ERROR` (`VariableAlreadyBound`), as on Neo4j. From 1.4.2.5 a body may contain a procedure call and further `CALL { }` bodies, nested up to 8 levels; through 1.4.2.4 both were refused. Subqueries of every kind nest at most 32 levels (`EXPRESSION_TOO_DEEP`). A body without `RETURN` keeps the outer rows. `CALL (a, b) { }` and `CALL { } IN TRANSACTIONS` are not supported. - `EXISTS { MATCH ... }` and `COUNT { MATCH ... }` subqueries. - Pattern comprehensions. - Map projections `n{.a, k: expr, .*}`. An explicit key wins over `.*`. From 1.4.2.3 a projection of null is null. From 1.4.2.5 an unaliased projection is named after its variable (`RETURN d{.id}` has the column `d`; it was `col_N`), unless another item already has that name or the item list holds `*`. ### Bitemporal clauses - `MATCH (n:Label) AT VALID '2026-01-15T00:00:00Z'`: read the graph as of a valid-time instant. - `MATCH (n:Label) AT RECORDED '2026-01-15T00:00:00Z'`: read the graph as the database had recorded it at that instant. - `MATCH (n:Label) AT VALID $vt AT RECORDED $rt`: both axes in one query. - `MATCH (n:Label) AS OF ''` is the same as `AT RECORDED ''`. - `MATCH (n:Label) OVER VALID BETWEEN '' AND ''` returns one row per version whose valid span overlaps the window. `OVER VALID COVERING BETWEEN ...` requires the span to contain the window. `OVER RECORDED ...` works on recorded time. - Temporal modifiers come after the pattern list and before WHERE: `MATCH (n:X) AT VALID '2025-01-01' WHERE n.p = 1 RETURN n`. - Bitemporal clauses attach to the `MATCH` pattern, not to `WITH`. See §10. - Time values: ISO 8601 strings (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`, optional fraction up to 9 digits, `Z` or `+HH:MM`) or integers in nanoseconds since the Unix epoch. `timestamp()` returns milliseconds, so do not pass it directly as a cut. From 1.4 an integer time with a magnitude below 10^15 (other than 0) is refused with subcode `TEMPORAL_UNIT_AMBIGUOUS` (HTTP 400). - A read without a time answers what is valid now (from 1.4.2.4): a value written with a future `AT VALID` time is returned from that time on, not at once. While a label or relationship type holds such a version, a read of it without a time carries the info warning `CURRENT_READ_VALID_NOW` and does not use the count and index fast paths (slower until that time has passed). The `MATCH` of a write statement binds the current record. A credential that the graph's access policy restricts still reads the current records. - Backdated writes: `AT VALID '2025-07-01' CREATE (p:Policy {id: 'p-1'})` sets the valid-from time of the statement's writes. Recorded time is always the server time. Closed intervals (`AT VALID FROM ... TO ...`, `AT VALID BETWEEN ...`) are refused for writes with subcode `CLOSED_INTERVAL_WRITE_UNSUPPORTED` (from 1.4.2.4; `SYNTAX_ERROR` before). On a read-only statement the prefix applies as the valid-time cut for each MATCH that has none. - A write that would replace later-dated versions is refused (from 1.4.2.5). A property or label write (`SET`, `REMOVE`, `MERGE ... ON MATCH SET`, a relationship `SET`) that starts before the current version of its node or relationship would replace every version recorded for a later valid time from its start on. When that would change such a version (lose one of its property values or labels, or bring back a property or label a later write removed), the statement is refused with HTTP 400 `WRITE_WOULD_REPLACE_LATER_VERSIONS` (MCP -32006) and nothing is written; the message names the entity, each later version's start and what it would lose. This covers a correction `AT VALID t MATCH (n) AT VALID t SET n.p = x` under a version recorded for a later time, and a write without `AT VALID` after a scheduled change (`AT VALID '' ... SET`). A write that changes nothing of them is applied; write the change at or after the latest later version's start. A write without `AT VALID` is refused only because of a version that was dated in the future when it was written (a scheduled change); concurrent writers, retries and clock differences between writers never cause the refusal. On the hosted service such writes are refused; a self-hosted operator can choose to apply them (the later versions are then replaced and the statement warns `WRITE_REPLACES_LATER_VERSIONS`). A write at or after the current version's start is never refused for this. A correction that keeps the later versions is not available. - `DELETE` ends the entity's valid time at the time of the delete, or at the `AT VALID` prefix time when the statement has one. A time before the version's start is refused with `VALID_END_BEFORE_VALID_FROM` (HTTP 400, MCP -32006; from 1.4.2.4, `RUNTIME_ERROR` before) and nothing is deleted; from 1.4.2.5 this also applies to a delete without `AT VALID` when the current version (or a relationship a `DETACH DELETE` would remove) starts after the time the statement runs. Withdraw a future-dated version with `AT VALID '' ... DELETE`. Recorded time is the time of the delete. The entity leaves the current graph when the statement runs. - A version's end (`_valid_to`, `validTo` in the history procedures, the windows of `OVER VALID`) is the earliest valid start among the writes and deletes recorded after it (from 1.4.2.5); a version that a later-recorded write covers from its own start holds no valid time. From 1.4.2.4 a version replaced by a write without `AT VALID` reports its end (it read null), and `OVER VALID` / `OVER RECORDED BETWEEN` return each version once, with its end. - Read-only span properties on nodes and relationships: `_valid_from`, `_valid_to`, `_txn_from`, `_txn_to`, `_version`, in milliseconds since the Unix epoch, and `_seq` (internal id). `_valid_to` and `_txn_to` are null while the span is open. Writing them fails with `RESERVED_BITEMPORAL_PROPERTY_WRITE` (`_seq` from 1.4.2.5). From 1.4.2.4 they also work in a node pattern map and on entities held in lists, maps and paths. - `DIFF GRAPH [RECORDED|VALID] FROM '' TO ''` lists changes in a window (columns `seq`, `txn_at`, `op_kind`, `entity_id`). - From 1.4.1 a read at a time cut (`AT VALID`, `AT RECORDED`, `AS OF`, `atTime`, with or without `LIMIT`) is complete or refused: a read that would have to look at more of the graph's history than the server allows fails with HTTP 400, subcode `TEMPORAL_READ_LIMIT` (`details.limit` is `history_ids` or `time_ms`; `details.label`, `details.bound`), and returns nothing. 1.4 answered 200 with warning `TEMPORAL_AUGMENT_BUDGET_EXCEEDED` or `TEMPORAL_AUGMENT_DEADLINE` and without the nodes deleted after the cut. Name one label in the read (the cost of `MATCH (n:L) AT RECORDED t` follows the history of `L`, not of the whole graph), add a property filter, or use a later cut. An as-of read of a label first written since the graph's last full maintenance reads the whole graph's history and can be refused on a graph with a long history. - From 1.4.1 `MATCH (n:L) AT VALID t RETURN ... LIMIT k` (and the same read with `atTime`) returns nodes deleted or relabelled after `t` when the live nodes do not fill the `LIMIT`. ### Query plans - `EXPLAIN ` returns a text plan without running the query. - `EXPLAIN ANALYZE ` runs the query, including any writes, and adds `plan_stats` to the response. From 1.4.1 `PROFILE ` is accepted and is the same; 1.4 and earlier accept `EXPLAIN ANALYZE` only. - From 1.4.1 the `EXPLAIN` text names the index a labelled read without a time clause starts from for `WHERE` equalities, `IN` lists and ranges; a read with no usable index prints `label scan` (or `full scan` without a label). Tests that compare `EXPLAIN` text can change. - From 1.4.1 every response's `stats` also carries `rows_materialized` (rows counted against the statement's row budget; a row is counted at each step that builds it, so this can exceed `rows_returned`), `row_budget` (the row budget that applied; 0 means none) and `nodes_read` (node records the `MATCH` clauses read as start candidates, before `WHERE`; nodes reached through a relationship are not counted). ### How read statements run (from 1.4.3) - Read statements run on a planned query engine where it plans them, and on the interpreter otherwise; nothing in a statement chooses the engine. Among the statements the engine does not plan, which run on the interpreter as on 1.4.2.5: statements that write, `EXPLAIN ANALYZE` and `PROFILE`, reads with a time clause, `UNION`, subqueries, and the statements of a credential that an access policy or a field read rule restricts. - Where both answer, the rows are the same. The rows of a statement without `ORDER BY` come in no defined order and can come in another order than on 1.4.2.5; add `ORDER BY` when the order matters. - From 1.4.3.2 the query planner is on by default: read statements are planned from per-graph statistics, built in the background after a graph opens (until then the graph plans as on 1.4.3.1). The same rows; row order, the rows a `LIMIT` without `ORDER BY` keeps, `collect()` order and the name of a group whose keys are equal (`2` and `2.0`) can change. Statements that write are planned as before. - Paging contract: `MATCH (n:L) RETURN ... ORDER BY n.p [DESC] SKIP s LIMIT k`, with or without `WHERE`, returns at every moment the rows a sort of the whole label returns, including nodes written since the last maintenance pass, and at a time cut the label as it stood then. Sort on a unique key (or add one as a second key) so pages do not overlap. - `properties(x)` / `keys(x)`: node, relationship or map (null for null); `size(x)`: string (code points) or list (elements, nulls included; a stored float list and a variable-length relationship variable are lists); null for null. Any other value: 400 `ARGUMENT_TYPE_MISMATCH` from 1.4.3.2 (`UNSUPPORTED_FEATURE` before) when its type is known only at run time; `SEMANTIC_ERROR` (unchanged) when it is known before the statement runs. A pattern is not a list: count it with `COUNT { (a)-->() }`. - Multi-statement transactions (from 1.4.3.2): off by default and not on the hosted service; `/graphs/{name}/tx` routes answer 404 `TRANSACTIONS_DISABLED` there. Each statement is all-or-nothing on its own. - The engine holds fewer rows at once, so some reads that 1.4.2.5 refused with `BUDGET_EXCEEDED` (row budget) or `QUERY_MEMORY_BUDGET` now answer. - `ORDER BY` over a key that mixes integers above 2^53 with floats compares the exact values (on 1.4.2.5 the order could differ between runs or the statement could fail with an internal error). - Field read rules and indexes: for a caller that a field read rule denies a property, a statement does not use that property's index to find the nodes of a pattern, so `EXPLAIN`, `EXPLAIN ANALYZE` (`rows_out`, `index_used`) and `stats.nodes_read` do not depend on values the caller may not read; `db.listIndexes()` returns `entity_count` null on that property's rows; a statement that calls `vector.*` gets no `stats.vector_profile.eligibility_size_estimate` from a predicate on it. A statement that filters only on such a property reads every node of the label. ### Indexes and constraints - `CREATE INDEX ON :Label(prop)` or `CREATE INDEX [name] FOR (n:Label) ON (n.prop)` creates a single-property index. The name is not stored; `db.listIndexes()` reports it as `Label.prop`. Optional `OPTIONS {value_type: 'string'|'int'|'float'|'bool'}`. Without it the type is inferred from existing values. When no node has the property yet, from 1.4.1 the index takes the type of the first value written and the statement returns warning `INDEX_TYPE_PENDING` (until then reads of the property scan the label); on 1.4 and earlier the index is a String index, with warning `INDEX_TYPE_DEFAULTED`. From 1.4.1, `CREATE INDEX` again without `OPTIONS` on an index that holds no value because every stored value has another type rebuilds it with that type (warning `INDEX_RETYPED`). Running the same statement again has no effect. - `DROP INDEX ON :Label(prop)` or `DROP INDEX FOR (n:Label) ON (n.prop)` drops it. - From 1.4.3.3, `CREATE INDEX` on a label with 100,000 nodes or more returns at once and builds the index in the background: info warning `INDEX_POPULATING` (`details.build_id`, `details.value_type`); `db.listIndexes()` shows `populating` until it is `online`; reads scan meanwhile, with the same answers; a `CREATE INDEX` sent during another build is queued. `db.listIndexes()` yields a new last column `value_type` (`int`, `string`, `float`, `bool`). - Declared column files (from 1.4.3.3, opt-in): `CALL db.createColumn(kind, name, property) YIELD kind, name, property, state, created` (`kind` `'node'` or `'relationship'`), `db.listColumns() YIELD kind, name, property, state, rows, skipped`, `db.dropColumn(kind, name, property) YIELD dropped`. The next full maintenance pass builds the file (`populating`, then `online`); statements the planned query engine runs then read declared properties of many records from it; records written since are read from their records; the answers are the same; reads at a time cut never use it. Refused: reserved names, a property declared for encryption, read-only servers. - Relationship property indexes (from 1.4.3.2): `CREATE INDEX [name] FOR ()-[r:TYPE]-() ON (r.prop) [OPTIONS {value_type: ...}]`, `DROP INDEX FOR ()-[r:TYPE]-() ON (r.prop)`. `CREATE INDEX` returns when the index is built (on a large graph, as long as a full maintenance pass). `db.listIndexes()` lists it with `type` `relationship_range`. A `MATCH ... WHERE r.prop value` on that type (`=`, `<`, `<=`, `>`, `>=` with an integer; `=` with a string) is answered from it, with the same answers; not used with a time clause, for a caller a field read rule may deny the property, or in a statement the planned query engine does not run. - Constraints (1.4): `CREATE CONSTRAINT [name] [IF NOT EXISTS] FOR (n:Label) REQUIRE ...` with `n.p IS UNIQUE`, `(n.a, n.b) IS UNIQUE`, `n.p IS NOT NULL`, `(n.a, n.b) IS NODE KEY`, or `n.p IS :: INTEGER` (property type). `IF NOT EXISTS` may come before or after the name. Default names: `uniq_