Skip to content

Upgrading to 0.6

0.6 is a breaking release: it changes the database file format, a number of APIs and some query results. This page lists what to do, grouped by where it affects you: the files, the results of queries, the Rust API, each binding and the command-line tool. The CHANGELOG has every entry, with the issue behind it. What changed from 0.6.0-beta.1 to 0.6.0 is listed here too, for those upgrading from the beta.

Before You Upgrade

  1. Pin the new minor version: grafeo = "0.6" in Cargo, "@grafeo-db/js": "^0.6.0" in npm, grafeo~=0.6.0 for pip and uv (see Versioning).
  2. Stop every 0.5.x process that uses a database before 0.6 opens it. 0.5.x cannot open a migrated database.
  3. Plan for the migration: the first read-write open of a 0.5.x database migrates it to the 0.6 format and keeps the old files under .pre-0.6 names, which are your way back. The migration needs free disk space for a copy of the database, and write access to the directory that holds it. Read-only opens read a 0.5.x database without changing it, and so do the CLI commands that only read (grafeo info, stats, schema, validate, a query that does not write). See Upgrading from 0.5 for the details, encrypted databases and builds without the wal feature.
  4. Open every 0.5.x database for writing once with 0.6: 0.7 will no longer read 0.5.x databases.
  5. Take a full backup after the upgrade, before the next incremental one: incremental backups now copy WAL segments (backup file version 2). A restore still reads the incremental backups of 0.5.x and 0.6.0-beta.1.
  6. Check the data 0.5.x may have written wrongly (see Data Written by 0.5.x): left-pointing relationships created by Cypher or MERGE, names written in backticks through SET, and databases compacted by 0.5.40 to 0.5.44.

Database Files

  • Every database is a single file, whatever its extension, with its write-ahead log in <path>.wal/ while it is open. A 0.5.x WAL directory is migrated to a file at the same path, with its property, text and vector indexes. 0.5.x logged no distance or HNSW parameters for a vector index: the migrated one uses those of CREATE VECTOR INDEX without options (the cosine distance) and takes its dimensions from the vectors, and the open logs a warning that names it. Create a vector index that used other parameters again after the migration.
  • The write-ahead log has a new format: a WAL written by 0.5.x or 0.6.0-beta.1 that still holds commits is replayed on the first read-write open, checkpointed into the file and removed.
  • RDF triples are stored as term ids (from 0.6.0-beta.1): the first read-write open of a file that 0.6.0-beta.1 wrote with RDF triples keeps a copy of the file, and of its WAL, as <path>.pre-0.6.0, then rewrites its RDF and Ring index sections in the new version at once. A read-only open reads the beta file as it is. 0.6.0-beta.1 cannot open the rewritten file: the .pre-0.6.0 copy is the way back. Terms that 0.6.0-beta.1 stored keep the form it stored them in: it dropped a datatype it did not know, stored decimals as doubles and wrote numbers in canonical form.
  • A database file keeps the configuration it was created with: an open applies the stored graph model, backward edges, schema constraints, WAL and sync mode, memory limit, tier overrides and an in-directory spill path to the settings it does not set itself. An open that sets a different graph model or backward-edge setting fails. To change what a file stores, use GrafeoDB::set_stored_config. See Stored Configuration.
  • Opening a directory that is not a 0.5.x database fails, an empty one included. Use a path where nothing exists yet, such as a file inside that directory. Creating a database next to a non-empty <file>.wal/ left from another database fails and names the directory: remove it first.
  • save() fails if the path exists: save to a new path, or remove the old file first. save() and backup_full() write to <target>.partial and rename it once complete, so you may see that file while they run.
  • A damaged write-ahead log stops the open with an error that names the file and offset; 0.5.x skipped the damage and went on. To open such a database anyway, replay the commits before the damage with Config::with_wal_salvage() (Python wal_salvage=True, Node.js walSalvage); the rest of the log is kept in <path>.wal.damaged.
  • Backups: manifests and cursors are JSON. 0.6 restores 0.5.x backups and continues their chains, but 0.5.x cannot read a backup directory that 0.6 has written to. Once a backup has been taken, close() keeps the WAL written since the last backup, so a <path>.wal can remain after a clean close. backup_full() no longer checkpoints the database first.
  • Snapshots are version 5: export_snapshot() keeps the whole catalog (constraint and index names, index options, graph types, schemas, procedures). Version 4 snapshots from 0.5.x and 0.6.0-beta.1 still import, but earlier releases cannot import a version 5 snapshot. A snapshot does not carry the configuration a database file stores.
  • A build without a feature refuses the data only that feature reads: a build without triple-store, vector-index or text-index fails to open a database or snapshot holding RDF triples, vector or text indexes, where 0.5.x dropped them silently. The grafeo crate's defaults, the lpg and rdf profiles and the CLI lack some of these: add triple-store (or rdf) and ai to the features if your data has them.
  • Encryption at rest is enforced: an encrypted database with a spill path, with TierOverride::ForceDisk or without a path now fails to open, where 0.5.x wrote the data unencrypted. The .pre-0.6 copy of a migrated database stays unencrypted. In Rust, the encryption types come with the encryption feature of the grafeo crate, and restore_to_epoch refuses an encrypted backup with incremental segments: use restore_to_epoch_with and the key chain.
  • Vector and text indexes are built from the data when a database opens, so an open with large indexes takes longer.
  • Ids and epochs continue across reopens: a node or edge id is never reused after a reopen, and epochs continue after a reopen and after the migration, so they differ from what 0.5.x gave. Caches keyed on them may notice.
  • open_in_memory() leaves the source untouched: no lock and no checkpoint (it opened the file read-write).
  • Read-only databases refuse every write: SPARQL updates, restore_snapshot, imports and batch_insert_rdf now fail on them.
  • A write after close() fails with GRAFEO-T007 (Python DatabaseClosedError), where it was accepted and lost. Reopen the database instead.
  • With the default features, GrafeoDB::open reaches the file only at a checkpoint: add the wal feature for commits that survive a crash.

Data Written by 0.5.x

  • Properties stored as null are gone after the migration: 0.6 has no null-valued properties, so keys() and properties() no longer list them, GCounter and OnCounter values that 0.5.x wrote as null included.
  • Left-pointing relationships created by Cypher CREATE or MERGE, or GQL MERGE, ((a)<-[:T]-(b)) were stored from a to b, backwards. Find and reverse them; 0.6 stores them from b to a.
  • Keys and labels written through a backticked name in SET or REMOVE (SET n.`my key` = 1) were stored with the backticks in the name. Rename them.
  • Databases compacted by 0.5.40 to 0.5.44 lost data in the compacted store: indexes created before compact() were dropped (create them again), nodes without labels were dropped, missing properties were stored as '', 0, 0.0 or false, lists, maps and temporal values as text, and deleted items come back. See Compact Store.

Query Results

These changes apply in every binding.

Numbers

  • A number with a point is an exact decimal in GQL and SQL/PGQ: 0.1 + 0.2 = 0.3 holds, and 1.50 keeps its two digits. Write 1.5e0 or 1.5F for a float, and 2M for a decimal integer. A decimal with an integer gives a decimal, with a float a float; division and avg give at least 16 significant digits. A decimal assigned to a FLOAT64 property is stored as the nearest float. Cypher 1.5 stays a float. Write 1.0e0 / 0.0e0 for an infinity. Bindings return decimals as decimal.Decimal (Python), {"$decimal": "1.50"} (Node.js, WebAssembly, C) and a decimal type of their own (C#, Dart, Go); the Arrow export uses Decimal128.
  • SPARQL numbers follow the XPath type promotion: 1.5 and xsd:decimal literals are decimals, an integer divided by an integer is a decimal (5 / 2 is 2.5, it was 2), and integer arithmetic past the 64-bit range goes on as decimals. A stored numeric literal computes and compares as a number of its type, and the XPath casts (xsd:decimal(...), xsd:double, ...) convert (they gave no value).
  • Overflow and division by zero fail the query with GRAFEO-Q006 (GQLSTATUS 22003 or 22012) in GQL, Cypher and SQL/PGQ; they returned null. Guard a division with a condition (WHERE x <> 0 AND 10 / x > 1) if zero can occur. abs of the smallest integer fails the same way. A float literal past the float range (RETURN 1e400) is an error too: 22003 in GQL, a syntax error in Cypher.
  • An integer past 64 bits is an exact decimal wherever a value comes in: parameters and properties in every binding, LOAD DATA ... FORMAT JSONL and grafeo data load read it as a decimal with scale 0 up to 38 digits (it was a float that lost its last digits, a string, or an error, depending on the path); past 38 digits it is an error naming the parameter, property or line.
  • Float = and <> are exact (IEEE 754): 0.1e0 + 0.2e0 = 0.3e0 (Cypher 0.1 + 0.2 = 0.3) is false, and 9007199254740993 no longer equals 9007199254740992.0e0. <, <=, > and >= compare an integer with a float exactly, and NaN <= 1 is false. Use decimals for exact fractions.
  • CAST trims the spaces around a text in GQL: CAST(' 42 ' AS INT64) is 42 (it was null); Cypher toInteger(' 42 ') stays null. CAST converts booleans and numbers into each other: CAST(true AS FLOAT64) is 1.0 and CAST(0 AS BOOLEAN) is false (both were null).
  • SUM and AVG take numbers or durations: strings, booleans, lists and maps, or numbers mixed with durations, are a type error 22G03 (sum(['5', '7']) was 12). Durations now sum and average.
  • A unary sign reads numbers and durations only: +'Alix' and -true are a type error 22G03 in GQL, Cypher and SQL/PGQ, where +x passed any value through and -x of a non-number was null.
  • Grouping and DISTINCT take 3 and 3.0 as one value, and a float key no longer comes back as an integer. min() and max() over values of different types no longer depend on the input order (min(['19', '3']) is '19').

Dates, Times and Time Zones

  • SESSION SET TIME ZONE takes a UTC offset only: '+01:00', '-05:30', 'Z' or 'UTC'. A named zone ('Europe/Amsterdam') or any other text fails with 22009, where it was stored and never used. The time zone now carries over to the next one-shot execute.
  • A zoned datetime or time compared with a local one: GQL and SQL/PGQ read the local value at the session time zone (UTC without one, so nothing changes unless you set a time zone). Cypher keeps zoned and local values apart, as openCypher does: datetime('2024-03-15T10:00:00+01:00') = localdatetime('2024-03-15T09:00:00') is false and < between them is null, where both read the local value at UTC.
  • Cypher datetime() and time() are zoned and keep their offset: hour of datetime('2024-03-15T10:00+01:00') is 10 (it was 9), and stored values are zoned. Use localdatetime() and localtime() for local values. GQL zoned_datetime and zonedTime without an offset are at +00:00 (they were null).
  • toString of a local datetime has no zone: 2024-03-15T10:00:00, not 2024-03-15T10:00:00.000000Z.
  • A duration times or divided by a number keeps its value: P1D / 2 is PT12H (it was P0D); division by zero is 22012 and overflow 22003. date, time and datetime of a zoned value, and arithmetic on zoned values, give values where they gave null.

Patterns, Writes and Paths

  • A WHERE or FILTER right after INSERT, CREATE, MERGE or DELETE is an error in GQL: put the condition before the write (MATCH (n) WHERE n.age > 30 DETACH DELETE n).
  • A statement that ends with a write returns no rows and no columns: a GQL INSERT returned the node it created (in _anon_0) and a Cypher write returned internal columns. The write counters are unchanged.
  • Cypher reads arrows, types and predicates as openCypher does: <--> and <-[:T]-> match either direction, CREATE of a relationship without a type is an error (it got the type RELATED), an undirected CREATE is an error, and 1 = 2 IN [false] is false.
  • MERGE matches the whole pattern or creates all of it, and no longer reuses an unconnected node. Cypher SET n = {...} in ON CREATE or ON MATCH replaces the properties. A MERGE of an untyped, alternative-typed or variable-length relationship is an error.
  • Cypher matches a relationship at most once per MATCH, and an undirected pattern takes a self-loop once.
  • Paths: DIFFERENT EDGES, TRAIL, ACYCLIC and SIMPLE apply to the whole path; ANY keeps one path per pair and SHORTEST k keeps k paths; a path variable holds every edge. The 100-hop cap is gone: an unbounded variable-length pattern fails past a memory budget instead (as do SPARQL + and * paths). A shortest or ANY pattern over several edges is an error.
  • Nodes, edges and paths in lists and maps are nodes, edges and paths: RETURN [n, 1], {k: n} and RETURN p gave ids, and startNode(r) and endNode(r) returned node ids. A result's nodes() and edges() include the nested ones.
  • A list or map literal keeps missing values: [p.w, p.age] is [null, 35] and {w: p.w} is {w: null}, so size() counts them. size() of a string counts characters, not UTF-8 bytes.
  • Column names: an unaliased RETURN length(p) is named length(p) (it was _path_length_p), and an unaliased SQL/PGQ item is named after its text (count(*)).
  • CREATE GRAPH dst AS COPY OF src copies the graph (it created an empty one). USE GRAPH g followed by a statement in one string runs that statement on g, and SELECT ... FROM g MATCH ... reads graph g; both did nothing or read the current graph.
  • Snapshot isolation covers values, labels and deletes: other sessions and direct reads no longer see an open transaction's changes to them.

Functions, Types and New Errors

  • An unknown function, or a call with a number of arguments the function does not take, is an error (#570), where it returned null: among them the *OrNull and *List conversions (toStringOrNull), isEmpty, randomUUID, point, distance, btrim, mod, concat, substr, startsWith, endsWith, round(x, 2) and vector($v, 384). EXPLAIN of such a query fails too. path_length(p) now works.
  • These statements now fail, most of them returned null or wrong rows: an aggregate in WHERE, an aggregate with the wrong number of arguments, an aggregate item next to a non-key item, a SQL/PGQ SELECT of a non-key or an aggregate in COLUMNS, DISTINCT with ORDER BY a dropped variable, DELETE of an unbound variable, an invalid =~ pattern, -[:A:B]->, and CREATE with :A|B.
  • IS TYPED takes every type it can check: IS TYPED NODE, EDGE and ANY are true, a node is no longer INT64, and an ISO type Grafeo does not support is a 42000 error.
  • Type DDL refuses property types Grafeo does not support (INT32, a typo such as STIRNG), naming the supported ones; they declared a property of type ANY. It also refuses a default its property cannot hold.
  • Graph types are enforced: closed graph types and schema-scoped types check writes, graph-type defaults apply, and DROP GRAPH TYPE of a type in use is refused. Writes that used to pass now fail with G2000; a type created without a schema no longer checks writes in schema s1. A FLOAT64 property takes integers (stored as floats).
  • Vectors: a dimension mismatch, a NaN component or dimensions=0 is a GRAFEO-V001 error.
  • A statement nests at most 64 levels deep and its plan at most 128; a deeper statement fails before it runs. Property values nest at most 128 levels deep, and the migration names any value that is deeper.
  • A stored procedure's arguments are values: an undeclared parameter is an error, a float arrives as a float, and a wrong-typed or row-dependent argument in CALL is an error (parameters and expressions were ignored). A procedure call after other clauses runs once per row. CALL grafeo.dfs reports the tree depth as depth (it was the finishing order), with new discovery and finish columns: four columns without YIELD.
  • Other functions: normalize gives NFC, LIKE matches across newlines, cosine distance is exact, id() and properties() take any expression, and HOME_SCHEMA, HOME_GRAPH and SESSION RESET go to the session's home.

Aggregates and Ordering

  • GQL sorts nulls last in both directions; ORDER BY x DESC put them first. Write NULLS FIRST to keep the old order. Cypher keeps openCypher's order.
  • Grouped collect leaves nulls out, and grouped min, max and sample return values where they returned null. Cypher listagg and group_concat take their separator (the default is a comma; it was a space). Two-argument aggregates, DISTINCT in the statistical aggregates and GQL HAVING with GROUP BY give corrected results.
  • SQL/PGQ aggregates inside SELECT and HAVING expressions aggregate (0.5.44 gave one row per match with nulls), GRAPH_TABLE (g MATCH ...) reads graph g (it read the current graph; a missing graph is 42002), property maps after the first node and LIMIT after GROUP BY apply, and -[:T*]-> means one or more hops.
  • Deterministic order: changes_between returns changes in commit order, search results with equal scores in id order, and change data capture events carry their commit timestamps.

Errors, Codes and Conflicts

  • GQLSTATUS names the kind of error: semantic errors other than an unresolved name move from 42002 to 42000; a negative LIMIT or SKIP parameter is 22G02 and a non-integer one 22G03; Cypher DDL in a read-only transaction is 25G03. A schema or constraint violation is G2000 and a DELETE of a node with edges G1001 (both were 22000).
  • Codes: an unsupported feature is GRAFEO-Q004, a timeout always GRAFEO-Q003, and a damaged file GRAFEO-S002. A user mistake has a code of its own; GRAFEO-X001 now only means a bug in Grafeo. Every query error message starts with its code (GRAFEO-Q002: semantic error: ...): update code that matches messages.
  • More writes conflict, with the retryable GRAFEO-T001: commits are pipelined, so a write to something an in-progress commit wrote fails; a UNIQUE constraint holds between concurrent transactions (the second writer gets GRAFEO-T001, or G2000 once the value is committed); a direct write while a transaction is open is a transaction of its own and fails on what that transaction changed first; DROP GRAPH refuses a graph an open transaction changed; and SPARQL CLEAR, DROP, COPY, MOVE and ADD wait for open changes in their graphs. Retry these.
  • A commit whose WAL write fails is not reported as committed: GRAFEO-T009 can be retried, and GRAFEO-T010 means the outcome is unknown: reopen the database. After a commit that stopped midway, commits, checkpoints and saves fail with GRAFEO-T008 until the database is reopened; reads still work.
  • create_vector_index in a build without the vector-index feature returns an error.

SPARQL and Gremlin

  • SPARQL results hold RDF terms: each IRI, blank node and literal of a SELECT, CONSTRUCT or DESCRIBE row is the exact term, with its lexical form, language tag and datatype ("30"^^xsd:integer was the integer 30, "Pomme"@fr the string Pomme). See Bindings for the type each binding gives. Read a literal's native value where you need a number (Literal.to_python() in Python), and pass terms back as parameters unchanged.
  • SPARQL operators and ORDER BY follow SPARQL 1.1: a string and a number are never equal ("42" = 42 is false, "42" < 43 drops the FILTER row), two plain strings compare as strings ("9" < "10" is false), a tagged literal equals only a literal with its tag, STR gives the lexical form as written ("019"), and ORDER BY sorts numbers by value with unbound values first. Write a typed literal (42, "42"^^xsd:integer) where you meant a number.
  • SPARQL: ROUND(-2.5) is -2, a double divided by 0 is INF, isNumeric("1") is false, language tags survive expressions and DATATYPE of a tagged literal is rdf:langString, AVG of integers is an xsd:decimal, SUM and AVG of nothing are 0, ASK returns one row with the column boolean, COUNT in a transaction counts its own writes, and DROP ALL works.
  • Gremlin filters inside and(), or(), not() and where() apply, and a step Grafeo does not support is an error.

Algorithms and Query Metadata

  • Graph algorithms return their rows in node-id order, in CALL and in db.algorithms. Corrected results: k-core (core numbers), Louvain (merges level by level), strongly connected components (numbered in another order), label propagation communities and max-flow flows, as_networkx() PageRank on an undirected graph, clustering coefficients and triangle counts ignore self-loops, k-truss uses the simple graph, and create_projection reads the selected graph. Some betweenness scores differ in their last bits. Do not depend on component or community ids.
  • rows_scanned counts the rows a query reads from storage: it repeated the rows returned.
  • Every query reports execution_time_ms: Cypher, SQL/PGQ, Gremlin, GraphQL, SPARQL, EXPLAIN, SHOW and session commands reported none.
  • PROFILE and EXPLAIN show the operators that ran: an operator a fast path absorbed has no line of its own, and a variable-length expand prints [*1..2].
  • A statement without a result has GQLSTATUS 00001 (omitted result) in QueryResult::gql_status: a write without RETURN, FINISH, a schema or transaction command. It was 00000.
  • The Arrow export keeps lists, maps and durations typed (Python to_arrow() and the polars and pandas conversions, Node.js toArrowIpc(), the CLI's Arrow dump): they were text. A column of integers and floats is floats; other mixed columns stay text.
  • File imports keep every column and key: a CSV without headers names its columns column0, column1, ... (it imported nodes without properties), and headers with spaces or punctuation are kept.
  • A property whose name starts with _ (_created) is kept on result nodes and edges in every binding, where a name like a structural key replaced the node's identity or was dropped.

Rust

  • The minimum Rust version is 1.99.0.
  • Build Config with its constructors and methods: it is #[non_exhaustive], so a struct literal no longer compiles. Its public fields are deprecated: read a setting with its getter. A field written directly does not count as set, so a database file's stored value wins over it on open.
// 0.5
let config = Config { path: Some("data.grafeo".into()), wal_enabled: false, ..Config::default() };
// 0.6
let config = Config::persistent("data.grafeo").without_wal();
  • Option and result types are #[non_exhaustive]: build EncryptionConfig with EncryptionConfig::new, CdcRetentionConfig with CdcRetentionConfig::unlimited() and its with_* methods, EdgeUpsertOptions with EdgeUpsertOptions::new(), and DurabilityMode::batch(..) or DurabilityMode::adaptive(..). Match a result type (DatabaseInfo, DatabaseStats, WalStatus, IndexInfo, the schema infos, ValidationResult, ChangeEvent, IndexDefinition, the backup manifest, segment and cursor, CommitInfo, MetricsSnapshot, WriteCounters, UpsertSummary) or a Batch or Adaptive mode with .., and build MemoryUsage and its parts from Default::default(). admin::DumpMetadata is removed.
  • More calls return Result: drop_graph, create_property_index, drop_property_index, drop_vector_index, drop_text_index, batch_insert_rdf, create_projection, the index calls on Session, Session::set_time_zone, and get_node_at_epoch, get_edge_at_epoch and get_node_property_at_epoch (Ok(None) when it did not exist then, an error for an epoch older than the history kept). Add ?. A failed rebuild_vector_index or rebuild_text_index keeps the old index.
  • A read of an earlier epoch reads the database's history: execute_at_epoch, execute_as_of, Session::set_viewing_epoch and the *_at_epoch reads fail for an epoch outside the retention window. Without one, a read of an epoch before the last write fails, where a build without temporal answered it with the values of now. Set Config::with_history_retention(HistoryRetention::Duration(..)) or Forever to keep history.
  • text_search and hybrid_search take a filters argument: pass None to search as before.
  • compact() returns a CompactReport (admin::CompactionStats is replaced). The compacted store's API is gone: layered_store(), compact_tiered() and grafeo::database::compact_tiered, as compact() no longer builds a separate store.
  • Errors: a damaged file is Error::Corruption (Corruption { what, file, offset }; StorageError::Corruption is removed), TransactionState has a Committing state an exhaustive match needs an arm for, and Term::from_ntriples returns Result<Term, TermParseError> (RdfStore::load_ntriples refuses unknown escapes).
  • Removed: Config::wal_flush_interval_ms with ConfigError::ZeroWalFlushInterval (set the flush with with_wal_durability), GrafeoDB::adaptive_config(), the snapshot and section methods of GrafeoDB::file_manager() (it returns the new file manager), and the deprecated TieredStore trait (use Section and MemoryConsumer). wal() returns the new WAL writer, Option<&Arc<grafeo_storage::wal::Wal>>.

grafeo::Error Is 32 Bytes

Its large payloads are boxed. ?, to_string(), error_code(), the GQLSTATUS and the messages do not change; code that builds or matches these variants with their payloads does:

Before Now
Error::Query(query_error) Error::from(query_error) (or ?); a match binds a Box<QueryError>, whose fields read as before
Error::Storage(storage_error) Error::from(storage_error); match Error::Storage(e), then *e or &**e
Error::TypeMismatch { expected, found } Error::type_mismatch(expected, found); match Error::TypeMismatch(mismatch), then mismatch.expected and mismatch.found
Error::Constraint { kind, message: String } message is a Box<str> (message.into())

A Read the Store Cannot Do Is an Error

The direct reads of GrafeoDB (get_node, get_edge, get_node_labels, get_node_edges, the history reads, find_nodes_by_property), its counts (node_count, edge_count, label_count, property_key_count, edge_type_count) and reports (info, detailed_stats, schema, validate, AdminService), the direct reads of Session (get_node, get_edge, get_node_property, get_node_edges, the neighbor reads, node_exists, edge_exists, get_degree, get_nodes_batch, find_nodes_by_property, the history reads) and AsOfView::get_node, get_edge and get_node_property return a Result, around the Option where absence is an answer. A direct read of a selected graph that was dropped fails with "Graph '...' does not exist", and a store without history answers the history reads with the unsupported error. For the bindings, see Bindings.

Before Now
db.get_node(id) → Option<Node> (also get_edge, get_node_labels) db.get_node(id)? → Option<Node>
db.node_count() → usize (also edge_count, label_count, property_key_count, edge_type_count) db.node_count()?
db.get_node_edges(..), db.find_nodes_by_property(..), db.get_node_history(id) → Vec<_> the same call with ?
db.info(), db.detailed_stats(), db.schema(), db.validate() the same call with ?
session.node_exists(id) → bool, session.get_degree(id) → (usize, usize) the same call with ?

Reading a Graph: Snapshot

GrafeoDB::snapshot() (the graph set_current_graph selects), GraphHandle::snapshot(), AsOfView::snapshot() and GrafeoDB::projection(name) give a Snapshot: the graph's store, the ReadView its reads take, and a pin that keeps what that view reads while it lives, so several reads see one state across later commits.

Before Now
db.graph_store(), db.selected_graph_store()? db.snapshot()?, then snapshot.store() and snapshot.view()
db.graph("g")?.graph_store()? db.graph("g")?.snapshot()?
db.projection("p") → Option<Arc<dyn GraphStore>> db.projection("p") → Option<Snapshot> (snapshot.shared() for the store as a shared handle)

GrafeoDB::store() stays, marked experimental, for admin work. It returns Result<Arc<RowGroupStore>> (grafeo_core::graph::rowgroup), the store the database now holds its graphs in: LpgStore is gone, with LpgStoreSection, PropertyStorage, NodeRecord, EdgeRecord and LpgStore::copy_graph, and QueryProcessor::for_lpg, Planner::with_lpg_store and ProcedureContext::lpg_store take the row-group store. Write through sessions or the direct API: a write through the store's own methods is not logged and not undone by a rollback.

Reading Through the Store Contract

GraphStore has one family of reads, each taking the ReadView that names who reads (ReadView::committed(epoch), or a transaction's view) and returning a Result; GraphStoreSearch is merged into it, and the reads without a view, the _versioned and _at_epoch families and answers_at_view are gone. Calling these reads is stable; implementing the trait is experimental through 0.6.x (see API Stability).

Before Now
get_node(id), get_node_versioned(id, epoch, tx), get_node_at_epoch(id, epoch) (also edges) node(id, view)?
get_node_property(id, &key) (also edges, _batch forms) node_property(id, key, view)?, node_property_batch(ids, &key, view)?
edges_from(node, dir), edges_from_at(..), edges_of_type(..) edges(node, dir, &types, view)?
out_degree(node), in_degree(node) degree(node, dir, view)?
node_ids(), all_node_ids(), filter_visible_node_ids_at(..) node_ids(view)?, filter_visible_node_ids(ids, view)?
nodes_by_label(label), nodes_by_label_count_at_epoch(..) nodes_by_label(label, view)?, nodes_by_label_count(label, view)?
node_count(), edge_count() node_count(view)?, edge_count(view)?
all_labels(), all_edge_types(), all_property_keys() → Vec<String> the same call with ?
find_nodes_by_property(..), find_nodes_in_range(..), text_search(..), vector_search(..) the same call with a view last, and ?
estimate_label_cardinality(..), estimate_avg_degree(..) statistics()
current_epoch() committed_epoch()
get_node_history(id) node_history(id)?

A store of your own implements node, edge, edges, has_backward_adjacency, node_ids, all_labels, all_edge_types, all_property_keys, statistics and committed_epoch; every other read has an exact default. The graph algorithms (grafeo_adapters::plugins::algorithms) take the store and a view and return a Result, and GraphAlgorithm::execute takes the view too.

A Store of Your Own: GraphBackend

GrafeoDB::with_store takes an Arc<dyn GraphBackend>, the store contract's write side, whose transactions the engine opens, commits and rolls back. The database gives such a store rollbacks, savepoints and the rollback of a failed statement; what only the built-in store has (named graphs, indexes, search, the database file, imports, backups) is the unsupported error on it. With Config::persistent(path) it is durable: it writes <path>.wal and takes one process at a time, and GraphBackend::durable_epoch and flush say what it holds across a restart. grafeo::store holds the contract's types, and the conformance feature the suite a store runs. See Storage Backends.

Before Now
GrafeoDB::with_store(Arc<dyn GraphStoreMut>, config) GrafeoDB::with_store(Arc<dyn GraphBackend>, config)
impl GraphStoreMut for MyStore (create_node, set_node_property, the _versioned writes) impl GraphBackend for MyStore: reserve_node_ids, reserve_edge_ids, begin, apply(op, writer), commit, rollback, rollback_to, collect, durable_epoch, flush
ChangeTarget::stamp(transaction, entries, epoch) GraphBackend::commit(handle, changes, epoch)
ChangeTarget::undo(transaction, entries) GraphBackend::rollback(handle, changes), or rollback_to(handle, savepoint, changes)
store.create_node(..) through GraphStoreMut on a RowGroupStore the same call, a method of RowGroupStore (drop the trait import)
GraphWriter::new(store, view), .with_recording(recording) GraphWriter::new(store, recording); GraphWriter::immediate(store) for writes that commit at once
Recording { target: WriteTarget::Store(store), .. } Recording { target: store, .. } (an Arc<dyn GraphBackend>)
db.graph_store_mut() none: write through a session or the direct API
QueryProcessor::lpg_store() → &Arc<RowGroupStore> Option<&Arc<RowGroupStore>> (None on a backend of one's own)
db.store() → Arc<RowGroupStore> (a panic on a backend of one's own) db.store()? → Result<Arc<RowGroupStore>>: the unsupported error there
db.iter_nodes(), db.iter_edges() → an iterator db.iter_nodes()?, db.iter_edges()?: the unsupported error on a backend of one's own

GraphStoreMut, ExternalTarget, UndoSupport and WriteTarget are gone. A QueryProcessor outside a transaction runs a write in a transaction of its own, and set_current_graph refuses a graph such a database does not have.

Bindings

Every binding:

  • A read the store cannot do is an error (see above): Python raises GrafeoError, Node.js and WebAssembly throw with the error's gqlStatus.
  • An integer past 64 bits is an exact decimal (see Numbers).
  • SPARQL results hold RDF terms (see SPARQL and Gremlin): Python gives grafeo.IRI, grafeo.Literal (with .language, .datatype and .to_python()) and grafeo.BlankNode, all str subclasses; Node.js and WebAssembly give RDF/JS term objects ({ termType: 'Literal', value: '30', language: '', datatype: { termType: 'NamedNode', value: 'http://www.w3.org/2001/XMLSchema#integer' } }); C#, Go and Dart give an RdfTerm; the C library's JSON gives each term in a $term tag around its SPARQL 1.1 JSON form ({"$term": {"type": "uri", "value": "..."}}). Each result says what it holds (kind: rows, a boolean for ASK, or a graph for CONSTRUCT and DESCRIBE).
  • Writes after close() fail (GRAFEO-T007).
  • Direct single writes are slower than in 0.5.44 (about 0.1 to 0.2 microseconds more per call): write many nodes or edges with the batch calls (batch_create_nodes_with_props, batch_create_edges) or the upserts, which commit once.

Per binding:

  • Python:
  • db.algorithms.kcore() returns {"core_numbers": {...}, "max_core": n}, like louvain(), and compact() returns a dict.
  • get_node_at_epoch and the other *_at_epoch reads raise GrafeoError for an epoch older than the history kept (they returned None).
  • A damaged file raises GrafeoCorruptionError (a GrafeoError); the vector, text and hybrid searches raise GrafeoError (still a RuntimeError); a write after close() raises DatabaseClosedError.
  • bytes and bytearray come back as bytes (they were a list of integers), a time with an offset comes back as an aware datetime.time, and datetime.time and datetime.date can be stored and passed as parameters.
  • Long-running calls release the GIL: use a transaction from one thread at a time, since a commit() or rollback() while another thread runs one of its queries raises RuntimeError.
  • as_networkx(), as_solvor(), get_nodes_by_label, get_property_batch and db.algorithms read the graph set_graph() selects (they read the default graph), and the store reads list only committed data.
  • import_df imports all rows or none.
  • nodes_df() and edges_df() give the same values with pyarrow installed as without it.
  • grafeo.features() no longer lists temporal.
  • A SPARQL result's grafeo.IRI, grafeo.Literal and grafeo.BlankNode compare and hash as RDF terms (kind, text, language tag, datatype), as in rdflib: a term no longer equals the plain string of its text. Compare with a term (row["s"] == grafeo.IRI("http://ex.org/a")) or with str(term). Language tags are lowercase and compare case-insensitively.
  • Node.js:
  • compact() returns an object.
  • The package's entry module is grafeo.js (types in grafeo.d.ts): change deep imports of the old entry file. Errors carry gqlStatus and are typed as GrafeoError.
  • Parameters, batch and upsert rows, edge properties and search filters convert from the JavaScript value as properties do: a whole number past 2^32 is an INT64 (it was a float), a BigInt of any width follows the integer rule, and the strings "NaN" and "Infinity" stay strings (they became floats). Errors name the parameter, or the row and property.
  • ResultStream.next() returns Promise<Record<string, unknown> | null> (it was Promise<JsonValue | null>): update type annotations.
  • WebAssembly:
  • compact() returns an object, and importRows and importLpg import all rows or none.
  • Parameters, rows and filters convert from the JavaScript value as in Node.js, and a Date parameter is a timestamp.
  • C:
  • grafeo_node_count and grafeo_edge_count return a status and write the count to a new size_t* argument. grafeo_get_node_labels and grafeo_as_of_get_node_property return a status and write the JSON to a new char** argument (NULL when the node or property does not exist).
  • grafeo_get_node, grafeo_get_edge, grafeo_graph_get_node, grafeo_graph_get_edge, grafeo_as_of_get_node and grafeo_as_of_get_edge return GRAFEO_OK with NULL for a node or edge that does not exist (they returned GRAFEO_ERROR_DATABASE): check the pointer after GRAFEO_OK. grafeo_info returns NULL with the error.
  • grafeo_drop_property_index, grafeo_drop_vector_index, grafeo_create_projection and grafeo_drop_projection return -1 on an error; the two projection calls return an int32_t.
  • Invalid params or properties JSON is an error with its reason, where the call ran without them.
/* 0.5 */
size_t n = grafeo_node_count(db);
char* labels = grafeo_get_node_labels(db, id);
/* 0.6 */
size_t n;
if (grafeo_node_count(db, &n) != GRAFEO_OK) { /* the error */ }
char* labels;
GrafeoStatus status = grafeo_get_node_labels(db, id, &labels);
  • Go: NodeCount() and EdgeCount() return (int, error), an absent node or edge is the new ErrNotFound (which still matches ErrDatabase), and DropVectorIndex returns (bool, error), like DropPropertyIndex.
  • C#: NodeCount and EdgeCount throw for a failed read, GrafeoGraph.GetNode and GetEdge return null for an absent node or edge (they threw), and byte, short, uint, ulong, Int128, UInt128 and BigInteger parameters arrive as integers (they were strings).
  • Dart: getNode, getEdge, getNodeLabels and the graph handle's getNode and getEdge return null for an absent node or edge (they threw), and a BigInt parameter arrives as an integer (it was a string).
  • Metrics: the Block-STM metrics are removed, also the grafeo_block_stm_* counters on grafeo-server's /metrics; they always read 0.
  • Embeddings (embed): an application that builds its own ONNX Runtime environment before Grafeo loads its first embedding model cannot load Grafeo models in the same process.

Command-Line Tool

  • Standard output holds only results, and --quiet hides only status messages: progress and status lines (Creating full backup of ..., ✓ Full backup created, the row count of a query) go to standard error, and -q leaves them out. Results (query rows, info, stats, schema, validate, index list, backup status) always print, so a script that counted on -q printing nothing for a query now gets its rows. With --format json every command prints JSON; a command that acts (backup, import, data dump and load, wal checkpoint) prints a record of what it did.
  • Query results keep their values in JSON and CSV: --format json writes numbers, booleans, null, arrays and objects (it wrote "\"Alix\"" and "30"), and CSV no longer doubles quotes and writes a date as its value. Fix parsers that worked around the old output.
  • The commands that only read open the database read-only, so grafeo info on a 0.5.x database no longer migrates it: use a command that writes, or open it read-write once.
  • grafeo index list shows the name an index was created with (it made up idx_<label>_<property>), as GrafeoDB::list_indexes() does.
  • An error shows its whole chain of causes.
  • grafeo data load runs in one transaction: an edge to a node not in the file, a duplicate id or a value it cannot read is an error naming the line, and nothing is loaded. A dump writes NaN and the infinities, and a DECIMAL as {"Decimal": "1.50"}.
  • grafeo backup restore --force refuses a target that is another directory or is open in another process.

Deprecated in 0.6, Removed in 0.7

Each of these still works in 0.6 and warns where your compiler or tooling can. Move to the replacement before you upgrade to 0.7.

Deprecated Use instead
The public fields of Config its getters, and its constructors and with_* methods
Session::set_auto_commit, Session::auto_commit begin_transaction to group writes; a write outside a transaction always commits on its own
Config::adaptive, with_adaptive, without_adaptive, AdaptiveConfig nothing: adaptive execution was never wired in
StorageFormat::SingleFile, StorageFormat::WalDirectory StorageFormat::Auto, the default: every database is a single file
C grafeo_open_single_file, Go OpenSingleFile, Dart GrafeoDB.openSingleFile grafeo_open, Open, GrafeoDB.open
recompact() compact()
BackupCursor::log_sequence wal_lsn
InvertedIndex::with_tokenizer InvertedIndex::with_options with a TokenizerKind
RdfStore::insert_in_transaction, remove_in_transaction, commit_transaction, rollback_transaction, has_pending_ops, find_with_pending, contains_with_pending, find_in_graphs_with_pending a session transaction
The compact-store feature nothing: every build that opens files reads a database compacted by 0.5.x
The temporal feature nothing: history is the per-database setting Config::with_history_retention, in every build
The tiered-storage feature of grafeo-core and grafeo-common, and the lpg feature of grafeo-core, grafeo-adapters, grafeo-engine and grafeo-cli nothing: every build has the labeled property graph store, and the store tiered-storage shaped is gone