SQLite
SQLite 3.45 or later, over JDBC with xerial’s driver: a graph in a file, with no server to run. Drivine
keeps it in the same two tables as on Oracle and Postgres, so models, the query DSL and
StatelessGraphObjectManager are the same as on the other stores. Add the driver yourself:
org.xerial:sqlite-jdbc.
database: datasources: graph: type: SQLITE database-name: /var/data/app.db # the file, made if it is not there default-graph-path: drivine # names the graph's tables; this is the defaultdatabase-name is required, and names a file: an in-memory database is refused, as each pooled
connection would open one of its own. A host, a protocol or a cypher-dialect means nothing here and is
ignored with a warning; a port, a user and a password are not read. The pool is HikariCP with
pool-max. Each connection is opened with a write-ahead log, foreign keys enforced, a wait of 30
seconds for a locked file, and the functions Drivine’s statements call registered on it, so make the
provider with JdbcConnectionProvider.sqlite, not over a DataSource of your own:
val provider = JdbcConnectionProvider.sqlite("graph", "/var/data/app.db", graphName = "drivine")How a graph is stored. <graph>_node (nid, labels, props, stamp) and
<graph>_edge (eid, src, dst, type, props), made, where they are not there yet, before the datasource’s
first connection, with labels and props as JSON text (not SQLite’s binary
jsonb, so Drivine for TypeScript and for Python read the same file), and <graph>_schema for what
schema management made. A save is the plan of steps the other stores carry out, carried out by the
function drivine_write that Drivine registers on each connection: applied whole or not at all, and
stamped and checked as elsewhere. Deleting a node’s row deletes its relationships with it.
Notes:
- A statement written by hand is SQL. SQLite has no JSON type, so JSON a statement of your own returns is
text; one that opens with
/* drivine:json */has each of its text values read as the JSON it is. - A
CypherStatementis compiled to SQL over the graph’s tables, as on Oracle and Postgres (see Cypher on a relational database), and gives the rows a graph engine gives. A read is a query; a write is a procedure of steps, as on the other stores, which the functiondrivine_cyphercarries out on the statement’s connection under a savepoint, so it is applied whole or not at all. A value is JSON text: two compare as Cypher compares them (1equal to1.0, a map to a map of the same entries in any order) by functions Drivine registers, which also add lists and texts and work out temporal values withjava.time. A query that reads the row it is in (OPTIONAL MATCH,CALL { … }, a variable-length walk, a shortest path) is gathered as JSON and joined withjson_each, as SQLite joins no lateral query. Text compares and sorts by SQLite’sBINARYcollation;toLowerandtoUpperchange the case of any letter, where SQLite’s ownlowerandupperchange ASCII alone. sum()andavg()add floats as doubles, as Neo4j does, so0.1 + 0.2sums to0.30000000000000004here too. SQLite also corrects for rounding as it adds (Kahan–Babuška summation), so a long sum of floats, or one that mixes an integer past 2^53 with a float, can be more accurate than Neo4j’s in its last digit:9007199254740993and0.5sum to9.007199254740994E15, where Neo4j gives9.007199254740992E15. A result still comes back as a float. Asum()of integers that passes a long’s range fails withinteger overflow, where Neo4j gives a float.- An infinity is the number SQLite’s JSON writes for one, and is stored. Its JSON holds no NaN: a statement given one as
a parameter writes it as
{"$numberDouble": "NaN"}, works with it as the float it is, as Neo4j does, and gives it back as NaN. A statement that writes and is given NaN is refused, rather than store that map, and one that writes NaN it works out from an infinity ($i - $i,n.inf + n.negative) fails. A statement that may work NaN or an infinity out, as on Postgres (dividing or taking%by anything but a number other than 0,toFloatof a text), is compiled as one given one: a number divided by a float zero is NaN or an infinity, as on Neo4j, and so is a float by an integer zero where both are written in the statement (1.0 / 0); an infinity it works out is stored. A number divided by any other integer zero is null, where Neo4j fails. NaN worked out from a stored infinity by any other statement (n.inf - n.inf,n.inf * 0) is null, where Neo4j gives NaN, but for the two infinities added with+, summed withsum()or averaged withavg(), which give NaN. - A raw
whereClauseis a SQL predicate, as on Postgres:n.note = 'unverified', or SQL overn.props,n.labelsandn.stamp. SQLite’s->>gives a JSON value as the SQL value it is (trueas 1), where Postgres gives its text. - Temporal values are their ISO 8601 text, and a moment is compared and ordered as nanoseconds since
the epoch, whatever offset it was written with. A year before 1 is numbered as Neo4j numbers it:
0000is 1 BC and-0044is 45 BC. - SQLite has one writer at a time, and transactions are serializable:
- A transaction takes the file’s write lock as it begins (
BEGIN IMMEDIATE), so no write in it is turned away part of the way through. Another writer, and another transaction even if it only reads, waits for it to end: a Drivine connection for up tobusyTimeoutMillis(30 seconds unless you set it), and then fails as busy. Keep transactions short: one that holds the lock for more than 5 seconds is logged as a warning when it ends. - A read outside a transaction does not wait for a writer. The file is in WAL mode: a read sees what was committed when it started, and nothing of a transaction still open.
- Outside a transaction, each statement is a transaction of its own. A save or a delete the file stays locked against for longer than the wait, or that read before another connection wrote, is run again, as a contended save is on the other stores.
- Each statement Drivine writes applies whole or not at all, in a transaction or outside one: a statement that fails part of the way undoes what it wrote.
- A
DataSourceof your own is opened with none of this:JdbcConnectionProvider.sqlite(…)makes the connections, with their write lock, WAL mode and the functions Drivine’s statements call.
- A transaction takes the file’s write lock as it begins (
- Index the id of each node type you save, with a uniqueness constraint or a range index: each is a partial index over the property for the nodes of one label.
matchestakes a regular expression in Java’s dialect, matched against the whole value. A leading(?i)ignores case.- A vector index and a full-text index are recorded and nothing is made for them:
loadNearestandloadMatchingmeasure every node of the label, and are exact.loadMatchingscores by BM25, as Postgres does. - The test kit’s Cypher fixtures (
SqlCypherFixtures) work here as on Oracle and Postgres, and so doesTestCleanup.beforeEach. SQLite’s tests need no container.

