EvaluatedApplications.HoloDb
10.4.0
dotnet add package EvaluatedApplications.HoloDb --version 10.4.0
NuGet\Install-Package EvaluatedApplications.HoloDb -Version 10.4.0
<PackageReference Include="EvaluatedApplications.HoloDb" Version="10.4.0" />
<PackageVersion Include="EvaluatedApplications.HoloDb" Version="10.4.0" />
<PackageReference Include="EvaluatedApplications.HoloDb" />
paket add EvaluatedApplications.HoloDb --version 10.4.0
#r "nuget: EvaluatedApplications.HoloDb, 10.4.0"
#:package EvaluatedApplications.HoloDb@10.4.0
#addin nuget:?package=EvaluatedApplications.HoloDb&version=10.4.0
#tool nuget:?package=EvaluatedApplications.HoloDb&version=10.4.0
HoloDb
An embedded SQL database for .NET. Tables are typed columns, held in memory or on disk, and the engine builds the structures that make repeated queries fast from the queries it sees: summaries, sorted and keyed indexes, a tree of exact sums, per-block records of what earlier reads found, and links between blocks. You write SQL. The engine chooses and maintains the access paths, so there is no index design to do, and every answer is exact.
Tables live in typed columns. Everything the engine builds beside them is derived from those columns, so losing a derived structure costs time, never data. Integer sums are held in 128 bits and a statement that would overflow 64 bits throws instead of wrapping. The package adds ACID transactions with a write-ahead log, a NEAREST operator that ranks rows by similarity of their holographic encoding, ClickBench-compatible analytic SQL, and an optional exact aggregate plane built on Phasor's Pleroma stores.
This file documents the public surface of the package, with a working example for each group.
Contents
- Quick start
- What it does
- Storage format and upgrading
- Core concepts
- Opening a database
- SQL reference
- History: AS OF and sealed sums
- Loading data and change batches
- Summary tables
- Durable, larger-than-memory tables
- What the engine builds for itself
- The aggregate plane (Pleroma)
- The Pleroma page store
- Measured
- Costs and limits
- Dependency injection
- EvalApp pipeline integration
- Usage metering
- Parallel fan-out (HoloPar)
- Engine building blocks
- Exceptions
- File layout
- License
Quick start
dotnet add package EvaluatedApplications.HoloDb
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE users (id INT PRIMARY KEY, age INT, name TEXT)");
db.Execute("INSERT INTO users VALUES (1, 30, 'Ada')");
var result = await db.ExecuteAsync("SELECT name FROM users WHERE age >= 18");
foreach (var row in result.Rows) Console.WriteLine(row["name"]);
// Ada
With no WalPath the database is in memory. Set WalPath to make it durable (see Opening a database).
What it does
- SQL over typed columns.
CREATE,ALTER ... ADD COLUMN,INSERT(includingON CONFLICT ... DO UPDATE),UPDATE,DELETE,SELECTwith joins, predicates, grouping, ordering, limits, transactions, andEXPLAIN. - Exact aggregates.
COUNT,SUM,AVG,MIN,MAXandCOUNT(DISTINCT ...). Whole-table totals and single-column groups read maintained totals instead of scanning. - Self-tuning access paths. Summaries, sorted and keyed indexes, a holographic tree of exact sums, per-block records and learned block-to-block routes are built when the workload has paid for them, and dropped when it stops using them or when writes make them stale. No index is declared, and none has to be.
- Placement that follows use. Rows that the same filters read are written together, by a regroup that runs as
upkeep, and an
UPDATEorINSERTmoves a row to a block the repeated filters already read. - Deletes that cost what they delete. A
DELETEmarks rows and moves nothing. Row ids stay stable, and consolidation later fills the holes and gives the freed pages back, so the data file shrinks. - Durable transactions. A write-ahead log of signed deltas in a fixed ring, group commit, checksums on every log block and every data page, and crash recovery that replays the log.
- Larger than memory. Paged tables read their pages through a buffer pool. A statement larger than the pool spills to a scratch file, and a rollback or a power cut leaves the table as it was.
- History. Every committed change of a paged table is kept as a signed record.
SELECT ... AS OF nreads the table as it was after commitn, andSeal(table, asOf: n)returns its seal then. What cannot be answered exactly is refused. - ClickBench-compatible analytic SQL. Narrow column types, dictionary-coded text,
CASE,CAST,EXTRACT,DATE_TRUNC, aliases, positionalGROUP BYandORDER BY, andCOUNT(DISTINCT)on large cardinalities. - Similarity search.
NEAREST (col = value, ...)ranks rows by graded closeness, with ascorecolumn. - Change feeds.
ApplyChangesapplies upserts and deletes atomically, one write lock and one log commit per batch, andKeyDigestcompares two key sets inmodulusnumbers. - Summary tables. A table that keeps no rows, only an exact aggregate plane, for dashboards over feeds that are too large to keep.
Storage format and upgrading
Paged tables. The write-ahead log of a paged table, wal.log, is a fixed-size ring of checksummed blocks holding
signed deltas. It does not grow with history. Its tail is reclaimed as its deltas are written into data.pages, and a
writer that finds it full writes some of them first. The table record is table.meta9, which carries the history log's
root, sequence and floor. A table with a NULL column is recorded as table.meta10.
Older tables are refused. A paged table written before the history log (format 8 or older) is refused when it is opened. The message names the directory and says to create the table again. There is no in-place conversion. An older binary refuses the newer table record by name.
Commits. Commits that wait for the same fsync are written as one batch. A statement that has returned is on disk.
Derived files. Sorted indexes, keyed summaries, summary sidecars and learned records are trusted at open only when they match the table. A stale or damaged file is discarded and rebuilt from the columns when it is needed. No step is required.
Behaviour you will notice:
- Row order after a
DELETE, and after any write that moves a row, is unspecified. AddORDER BYwhere order matters. - Deletes take time in proportion to the rows they delete, not to the table.
- Upkeep runs by default: a paged table consolidates its deleted rows after a statement once the scans that read them
have paid for it, and the planner builds and drops structures on its own.
Database.AutoConsolidate = falseandSET summaries = declared(oroff) turn these off. HoloDbOptions.CheckpointThresholdBytesgoverns only the text log of a database that is not in paged mode.
COUNT(*) and AVG count the rows that exist. Deleted places are not rows.
Core concepts
- Columns are the source of truth. A table is typed columns. The planner's structures, the aggregate plane and the similarity encodings are derived from them, and each is trusted only when it matches the table's change counter and the exact totals.
- Exact answers.
INTsums are exact 128-bit running totals, and a statement throws if the true total leaves 64 bits. Counts, sums and extremes served from a structure equal the scan's answer. - Two storage shapes, one SQL surface. In-memory tables (
CREATE TABLEon a database without a paged default) and paged tables (CreatePaged/OpenPaged, or whole-database paged mode) take the sameExecutecalls. Where a capability differs, the relevant section says so. - Locking. One reader-writer lock per database.
SELECTtakes it shared. Every mutation, DDL statement and transaction takes it exclusive. Whole-table counts and sums and single-column group-bys are also served from a snapshot republished after each write, without the lock. A structure built in the background is checked under the shared lock; the exclusive lock covers only the O(1) swap that publishes it. - Transactions.
BEGINcopies nothing. Each change is journaled as it happens andROLLBACKreplays the journal. A transaction is a pending delta: a reader sees the base plus its own changes, and commit adds the changes to the log. A multi-statement transaction is a series ofExecutecalls on the same database. On a paged table, a statement that fails insideBEGINundoes only itself: the statements before it stay, andCOMMITkeeps them. Outside a transaction a failed statement changes nothing. - Constraints on write. Types,
NOT NULL(the default for every column not declaredNULL) and primary-key uniqueness are checked before a row is written.
Opening a database
public sealed class HoloDbOptions
{
public string? WalPath { get; set; } // null = in memory
public long CheckpointThresholdBytes { get; set; } // text log (not paged mode): fold into a snapshot past this size; 0 = never
public bool Paged { get; set; } // whole-database paged mode for a new database (see below)
}
HoloDbServiceis the entry point:Execute(sql[, args]),ExecuteAsync(sql[, args], ct),BulkLoad,BulkLoadColumns,ApplyChangesandApplyChangesAsync(single table and several tables),ApplyPlaneChangesand its async form,KeyDigest,PlanePosition,PlanePositionBytes,PlaneTotals,ExportPlane,ImportPlane,CreatePlaneOnly,CreatePaged,OpenPaged,PagedBulkLoad,ClosePaged,Consolidate(table),Regroup(table),Checkpoint(),Close(), and.Database, the engine itself.Databaseis the engine. Use it directly for the members not wrapped by the service:Database.Open(walPath = null, checkpointThresholdBytes, paged = false)(a static factory),Execute,Bind,BulkLoad,Checkpoint,Close,WalPath,IsPagedDefault,TableColumns(table),Consolidate(table),AutoConsolidate,PlaceOnWrite,Regroup(table),SummariesandIndexes(the planner modes),StructureBudgetBytes,SetStructureBudget(bytes),RouteTrace, the aggregate plane members (EnablePleroma,DisablePleroma,PleromaMode,PleromaStats,PleromaVerify,PleromaSumExact), and the static per-thread usage countersLastRowsScanned,LastTableRows,LastRowBytesandLastLogicalBytes.
using HoloDb;
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".wal");
var db = new HoloDbService(new HoloDbOptions { WalPath = path });
db.Execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)");
db.Execute("INSERT INTO t VALUES (1, 100)");
db.Checkpoint();
db.Close();
var reopened = new HoloDbService(new HoloDbOptions { WalPath = path });
var r = reopened.Execute("SELECT v FROM t WHERE id = 1");
Console.WriteLine(r.GetLong(0, 0));
// 100
reopened.Close();
Close() releases the file handles of every open paged table. Call it before you open the same WalPath again in
the same process, and on shutdown. Close() also cancels the background builds and waits for them to stop. A build
that was cancelled publishes nothing.
Diagnostics. Database.RouteTrace is a thread-static list. Set it to a new List<string> and the engine
records the access path and the route each statement on that thread takes. EXPLAIN reports the same decision
without running the statement.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE s (id INT PRIMARY KEY, n INT)");
db.Execute("INSERT INTO s VALUES (1, 1), (2, 2)");
Database.RouteTrace = new List<string>();
db.Execute("SELECT count(*) FROM s WHERE n > 1");
foreach (var step in Database.RouteTrace) Console.WriteLine(step);
Database.RouteTrace = null;
foreach (var r in db.Execute("EXPLAIN SELECT count(*) FROM s WHERE n > 1").Rows)
Console.WriteLine($"{r["kind"]}: {r["route"]}");
SQL reference
Types and tables
| Statement | Purpose |
|---|---|
CREATE TABLE [IF NOT EXISTS] t (col TYPE [NULL \| NOT NULL] [PRIMARY KEY], ...) |
Defines a table. An ordinary table has one PRIMARY KEY, of type INT or TEXT. A GUID is a TEXT value. |
CREATE TABLE t (col TYPE, ...) without a primary key, or with analytic types |
Defines an analytic table (see Analytic tables). Paged databases only. |
ALTER TABLE t ADD [COLUMN] col TYPE [NULL] [DEFAULT <literal> \| NULL] |
Adds one column. There is no DROP, rename or type change. A NOT NULL column needs a literal DEFAULT. The default fills the rows that existed when the column was added. |
CREATE SUMMARY TABLE [IF NOT EXISTS] t (col TYPE DIMENSION \| MEASURE, ...) [WITH (max_dim_bits = n, max_joint_bits = n)] |
Defines a table that keeps no rows (see Summary tables). |
Type names: INT (also INTEGER), TEXT (also VARCHAR, STRING), REAL (also FLOAT, DOUBLE, DECIMAL,
NUMERIC). Analytic tables also take SMALLINT, INTEGER, BIGINT, DATE, TIMESTAMP, CHAR(n) and
VARCHAR(n), each stored at its declared width.
Values are stored exactly or refused by name. An INT column takes a whole number in the 64-bit range (3.0 is
stored as 3; 2.5, a number past 64 bits or a text that is not a number throws). A REAL column takes any finite
number; NaN and Infinity throw, from SQL, parameters and bulk loads alike. Numeric literals take a point or an
exponent (1.5, 1e20, 2.5E-7). Integer arithmetic in a SELECT that would pass the 64-bit range throws for the
rows the query keeps.
Indexes, summaries and planner settings
| Statement | Purpose |
|---|---|
CREATE INDEX [name] ON t (col) |
A sorted index on one INT or TEXT column (a TEXT column gets an equality index). CREATE INDEX [name] ON t (textCol, intCol) is a composite index for WHERE textCol = 'v' ORDER BY intCol LIMIT k. USING SORTED is the same as no USING. |
CREATE INDEX [name] ON t (dims ...) [INCLUDE (measures ...)] USING SUMMARY [WITH (max_dim_bits = n, max_joint_bits = n)] |
Builds the aggregate plane from SQL. Dimensions are INT or TEXT columns, measures are INT columns. A dimension past its cap does not fail the statement: the row in SHOW INDEXES lists it as over the cap, and the queries that need it go to the engine. |
CREATE INDEX [name] ON t (intKey [, splitCol]) INCLUDE (measures) USING KEYED |
A keyed summary on a paged table: the exact count and sums for each value of an INT key, optionally split by a second column with few distinct values. |
CREATE INDEX ... USING NEAREST |
Refused. Similarity search needs no index: see NEAREST. |
DROP INDEX name [ON t] |
Removes an index or summary. |
SHOW INDEXES [ON t] |
One row per index: table, name, kind, columns, include, rows, bytes, served, declined, over_cap. |
SHOW SUMMARIES [ON t] |
One row per summary and index the planner knows about, with its origin (declared or automatic), state, size, counts served and declined, and the reason for its state. |
SET summaries = auto \| declared \| off |
auto (the default) lets the engine build and drop aggregate summaries. declared keeps only the ones you declared. off uses none. |
SET indexes = auto \| declared \| off |
The same for sorted and text indexes, and for the paged sorted and keyed structures. |
EXPLAIN [PLAN [FOR]] SELECT ... |
How the statement would be answered, without running it. One row per step: step, kind, route, detail, est_rows. |
EXPLAIN and SHOW SUMMARIES say what the planner is waiting for: how much scan time the shape has paid and what
the build would cost, and when a structure was built and why.
Writes
| Statement | Purpose |
|---|---|
INSERT INTO t [(col, ...)] VALUES (v, ...)[, (v, ...)] |
Inserts one or more rows. An omitted column is NULL when declared NULL, otherwise its type's zero (0, 0.0 or ''). |
INSERT INTO t [(col, ...)] VALUES (...) ON CONFLICT (pk) DO UPDATE SET col = EXCLUDED.col[, ...] |
Upsert. An existing key changes only the SET columns. EXCLUDED.col must name the same column. In-memory tables only. |
UPDATE t SET col = literal[, ...] [WHERE ...] |
Updates matching rows. The right-hand side is a literal (NULL for a column declared NULL). The primary key can not be updated. |
DELETE FROM t [WHERE ...] |
Deletes matching rows. |
A plain INSERT of an existing primary key throws HoloException. UPDATE validates every SET value before it
changes a row, so a bad value leaves the table as it was.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE p (id INT PRIMARY KEY, name TEXT)");
db.Execute("INSERT INTO p VALUES (1, 'Ada')");
try { db.Execute("INSERT INTO p VALUES (1, 'Dup')"); }
catch (HoloException ex) { Console.WriteLine(ex.Message); }
// duplicate primary key 1
try { db.Execute("UPDATE p SET id = 2 WHERE id = 1"); }
catch (HoloException ex) { Console.WriteLine(ex.Message); }
// cannot UPDATE the primary key
Parameterized queries
Every Execute has an overload that takes ? placeholders followed by arguments. The arguments are bound as
correctly quoted SQL literals before the statement is parsed. A string argument has its quotes doubled. A ?
inside a string literal is left alone. Supported argument types are string, bool, any integer type, and any
floating-point type; a floating-point argument binds with all its digits and reads back exactly. A null or DBNull
argument is the SQL literal NULL. An argument-count mismatch, an unsupported type, a ulong past the 64-bit range,
NaN or Infinity throws. Database.Bind(sql, args) returns the bound text without running it.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE p (id INT PRIMARY KEY, name TEXT)");
db.Execute("INSERT INTO p VALUES (?, ?)", 1L, "O'Brien");
var r = db.Execute("SELECT name FROM p WHERE id = ?", 1L);
Console.WriteLine(r.Rows[0]["name"]);
// O'Brien
string text = Database.Bind("SELECT * FROM t WHERE id = ? AND name = ?", 1L, "Widget");
Console.WriteLine(text);
// SELECT * FROM t WHERE id = 1 AND name = 'Widget'
NULL
A column holds SQL NULL only when it is declared NULL: CREATE TABLE orders (id INT PRIMARY KEY, region TEXT NULL, amount INT NULL, qty INT NOT NULL).
A primary key is never NULL.
On a paged table a column can be declared NULL, and its NULL cells are stored with the table. In this build a
statement that reads such a column (a SELECT, a WHERE on it, or an index on it) is refused by name, so read a paged
table's nullable columns in memory until that support lands.
- Three-valued logic.
WHEREandHAVINGkeep a row only when the predicate is true. A comparison,LIKE,INorBETWEENon aNULLis unknown.IS NULLandIS NOT NULLare always true or false. - Aggregates skip
NULL.COUNT(*)counts rows.COUNT(col),COUNT(DISTINCT col),SUM,AVG,MINandMAXuse the non-NULLvalues, and areNULLwhen there are none. - Grouping and ordering.
GROUP BYandDISTINCTmake one group of all theNULLs.ORDER BYputsNULLfirst when ascending and last when descending. AnINNER JOINnever matches aNULLkey. - Results. Read a
NULLcell withRowsorGetValue, where it isDBNull.Value, or testQueryResult.IsNull.GetLong,GetDoubleandGetTextthrow aHoloExceptionon aNULL. - Where a
NULLis refused. A comparison written with=,!=,<,>,<=,>=,IN,BETWEEN,LIKE,NEARESTor a joinONagainstNULLthrows, with a message that says to writeIS NULL.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE orders (id INT PRIMARY KEY, region TEXT NULL, amount INT NULL, qty INT NOT NULL)");
db.Execute("INSERT INTO orders VALUES (1, 'north', 10, 1), (2, NULL, 20, 1), (3, 'north', NULL, 2)");
var r = db.Execute("SELECT COUNT(*), COUNT(amount), SUM(amount), AVG(amount) FROM orders").Rows[0];
Console.WriteLine($"{r["count(*)"]} rows, {r["count(amount)"]} amounts, sum {r["sum(amount)"]}, avg {r["avg(amount)"]}");
// 3 rows, 2 amounts, sum 30, avg 15
Console.WriteLine(db.Execute("SELECT COUNT(*) FROM orders WHERE region IS NULL").Rows[0]["count(*)"]);
// 1
Console.WriteLine(db.Execute("SELECT COUNT(*) FROM orders WHERE region != 'north'").Rows[0]["count(*)"]);
// 0
foreach (var g in db.Execute("SELECT region, COUNT(*) FROM orders GROUP BY region ORDER BY region").Rows)
Console.WriteLine($"{(g["region"] is DBNull ? "(null)" : g["region"])}: {g["count(*)"]}");
// (null): 1
// north: 2
SELECT
SELECT [DISTINCT] item [, item ...] FROM t
[INNER JOIN t2 ON a.col = b.col [...]]
[NEAREST (col = value [, ...])]
[WHERE ...] [GROUP BY col [, ...]] [HAVING ...]
[ORDER BY col | position | alias | expression [ASC|DESC] [, ...]]
[LIMIT n] [OFFSET n]
An item is a column, *, an aggregate (COUNT(*), COUNT(col), COUNT(DISTINCT col), SUM, AVG, MIN,
MAX), or on an analytic-capable SELECT an expression with an AS alias. Aggregate result columns are named
agg(col) in lower case, for example count(*) and sum(amount), and are read by that name or by position. AVG
over an INT column returns a double.
- Row order. Without
ORDER BYthe order of the rows, and so which rows aLIMITkeeps, is unspecified. The count ismin(n, matching rows), every row satisfies theWHERE, and no row repeats.nis a non-negative integer;LIMIT 0returns no rows (an aggregate withoutGROUP BYrefuses it). - Empty selections.
SUM,AVG,MINandMAXof no rows areNULL.COUNTis0. An aggregate withoutGROUP BYalways returns one row. - Refused, not reinterpreted. A statement that would otherwise drop one of its clauses throws
HoloException:DISTINCTtogether with an aggregate (useGROUP BY), andNEARESTcombined with a join,GROUP BY,HAVING,ORDER BY,DISTINCTor an aggregate.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE sales (id INT PRIMARY KEY, region TEXT, amount INT)");
db.Execute("INSERT INTO sales VALUES (1,'EU',100),(2,'EU',50),(3,'US',10)");
var agg = db.Execute("SELECT region, sum(amount) FROM sales GROUP BY region HAVING sum(amount) > 20 ORDER BY region LIMIT 10");
foreach (var row in agg.Rows) Console.WriteLine($"{row["region"]}={row["sum(amount)"]}");
// EU=150
var distinctRegions = db.Execute("SELECT DISTINCT region FROM sales ORDER BY region");
foreach (var row in distinctRegions.Rows) Console.WriteLine(row["region"]);
// EU
// US
Joins
INNER JOIN t2 ON a = b (JOIN is the same). An in-memory database accepts several joins in one statement. A paged
table accepts one. Qualify a column with table.column when both sides share its name.
Joins with LEFT, RIGHT, FULL, CROSS or NATURAL, USING, comma joins, table aliases in a FROM or JOIN,
UNION, INTERSECT, EXCEPT, ROLLBACK TO, and a second statement after ; are refused with a HoloException
that names what is not supported. A clause the dialect does not have is never silently dropped.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)");
db.Execute("INSERT INTO users VALUES (1, 'Ada')");
db.Execute("CREATE TABLE orders (id INT PRIMARY KEY, userId INT)");
db.Execute("INSERT INTO orders VALUES (1, 1)");
var j = db.Execute("SELECT users.name FROM orders INNER JOIN users ON orders.userId = users.id");
foreach (var row in j.Rows) Console.WriteLine(row["users.name"]);
// Ada
Predicates
AND, OR, NOT and parentheses. Comparisons =, != (or <>), <, >, <=, >=. LIKE (% for any run,
_ for one character, case-insensitive). IN (v, ...) and IN (SELECT ...) (a single-column subquery).
BETWEEN lo AND hi. IS [NOT] NULL on a declared NULL column. Each of LIKE, IN and BETWEEN takes an
optional leading NOT.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE p (id INT PRIMARY KEY, name TEXT, price INT)");
db.Execute("INSERT INTO p VALUES (1,'Widget',25),(2,'Gadget',75),(3,'Widgetpro',150)");
Console.WriteLine(db.Execute("SELECT name FROM p WHERE name LIKE 'Widget%'").Rows.Count); // 2
Console.WriteLine(db.Execute("SELECT name FROM p WHERE id IN (1,2)").Rows.Count); // 2
Console.WriteLine(db.Execute("SELECT name FROM p WHERE price BETWEEN 50 AND 100").Rows[0]["name"]); // Gadget
Console.WriteLine(db.Execute("SELECT name FROM p WHERE id IN (SELECT id FROM p WHERE price > 50)").Rows.Count); // 2
Similarity search: NEAREST
SELECT ... FROM t NEAREST (col = value [, col = value ...]) [WHERE ...] [LIMIT k]
NEAREST ranks rows by the cosine similarity of their holographic encoding to a partial key: any subset of columns,
of any type. The encoding is derived from the columns, so a row is ranked by the values it holds now: an UPDATE
changes its rank. A WHERE filters the rows before they are ranked.
Similarity is graded. A number or date close to the key ranks ahead of a far one, and a text value that is nearly
the same string ranks ahead of an unrelated one, by character trigrams. Add score to the projection to read the
ranking value (1.0 for an exact match). score is a pseudo-column: SELECT * does not return it, and it can not
appear in a WHERE. Above a size threshold, the ranking uses an approximate candidate search (locality-sensitive
hashing) instead of comparing every row, so its order can differ from an exact scan.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE products (id INT PRIMARY KEY, category TEXT, price INT)");
db.Execute("INSERT INTO products VALUES (1,'electronics',100),(2,'electronics',120),(3,'furniture',300)");
var near = db.Execute("SELECT id, category, price, score FROM products NEAREST (category = 'electronics', price = 110) LIMIT 2");
foreach (var row in near.Rows) Console.WriteLine($"id={row["id"]} category={row["category"]} price={row["price"]} score={row["score"]:F3}");
// id=2 category=electronics price=120 score=0.989
// id=1 category=electronics price=100 score=0.989
Transactions
BEGIN, COMMIT and ROLLBACK. A rolled-back transaction leaves the same rows, though their order may differ.
DDL inside a transaction is allowed. BulkLoad is not.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE p (id INT PRIMARY KEY, name TEXT)");
db.Execute("BEGIN");
db.Execute("INSERT INTO p VALUES (1, 'Temp')");
db.Execute("ROLLBACK");
Console.WriteLine(db.Execute("SELECT count(*) FROM p").Rows[0]["count(*)"]);
// 0
db.Execute("BEGIN");
db.Execute("INSERT INTO p VALUES (1, 'Kept')");
db.Execute("COMMIT");
Console.WriteLine(db.Execute("SELECT count(*) FROM p").Rows[0]["count(*)"]);
// 1
Analytic tables and the extended SELECT
An analytic table is for wide, append-mostly data. Declare SMALLINT, INTEGER, BIGINT, DATE, TIMESTAMP,
CHAR(n) or VARCHAR(n) columns, or leave out the primary key, and the table is analytic. An analytic table exists
only in a paged database, and its columns can not be NULL. Each column is stored at its declared width. Text is
dictionary-coded. Load it column by column with BulkLoadColumns (one transaction per call).
A single-table SELECT that the older parser refuses runs on the extended executor, which also runs on in-memory
tables. It accepts AS aliases, expressions, CASE, CAST, EXTRACT, DATE_TRUNC, REGEXP_REPLACE, the string
and math functions, OFFSET, HAVING without GROUP BY, and GROUP BY or ORDER BY by alias, position or
expression. DATE and TIMESTAMP results come back as strings. Its aggregates run on morsels of rows across all
cores, with dense per-worker arrays for small key ranges, partitioned hash tables for large ones, and exact COUNT(DISTINCT).
using HoloDb;
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(dir);
var db = new HoloDbService(new HoloDbOptions { WalPath = Path.Combine(dir, "db.wal"), Paged = true });
db.Execute("CREATE TABLE hits (watch SMALLINT NOT NULL, day DATE NOT NULL, url VARCHAR(200) NOT NULL, n BIGINT NOT NULL)");
db.BulkLoadColumns("hits", new[] { "watch", "day", "url", "n" },
new object[] { new short[] { 1, 2, 3 }, new int[] { 19000, 19000, 19001 }, new[] { "a", "b", "a" }, new long[] { 10, 20, 30 } }, 3);
var r = db.Execute("SELECT day, COUNT(*) AS visits, SUM(n) AS total FROM hits GROUP BY day ORDER BY day");
for (int i = 0; i < r.RowCount; i++) Console.WriteLine($"{r.GetValue(i, 0)} {r.GetValue(i, 1)} {r.GetValue(i, 2)}");
// 2022-01-08 2 30
// 2022-01-09 1 30
db.Close();
History: AS OF and sealed sums
Paged tables keep each committed change as a signed record, so a past state is the present state with the later changes taken back out. No copy of the table is kept.
using HoloDb;
var dir = Path.Combine(Path.GetTempPath(), "holodb-asof-demo");
var db = Database.Open(null);
db.CreatePaged("CREATE TABLE orders (id INT PRIMARY KEY, amount INT)", dir);
db.Execute("INSERT INTO orders (id, amount) VALUES (1, 10), (2, 20)");
long before = db.CommitSequence; // the sequence after the insert
db.Execute("UPDATE orders SET amount = 99 WHERE id = 2");
var now = db.Execute("SELECT COUNT(*), SUM(amount) FROM orders");
var past = db.Execute($"SELECT COUNT(*), SUM(amount) FROM orders AS OF {before}");
Console.WriteLine($"now: {now.GetValue(0, 1)}, as of {before}: {past.GetValue(0, 1)}");
var sealPast = db.Seal("orders", asOf: before); // the table's seal as it was after commit `before`
Console.WriteLine($"seal as of {before}: {sealPast.Rows} rows");
db.Close();
SELECT ... FROM t AS OF nreads the table as it was after commitn.AS OF LATESTis the present state.SHOW SEQUENCEreturns the current sequence and the floor;Database.CommitSequenceandDatabase.HistoryFloor(table)return the same numbers.- Exact at every
nat or above the floor: COUNT, SUM, MIN, MAX, GROUP BY, DISTINCT and row reads. COUNT and SUM without a WHERE, GROUP BY on one INT key and MIN/MAX with a sorted index on the column start from the present answer and apply the overlay's signed correction. Other shapes scan the table and apply the same correction to the rows that changed.EXPLAINnames the route and says why a shape scans. Seal(table, asOf: n)returns the table's seal as it was after commitn. For an insert-only history it reads no data page.- Refused by name, never answered approximately: JOIN, NEAREST, an in-memory table, a plane-only table,
nabove the current sequence,nbelow the floor,nstrictly inside a retained segment, and a read inside a transaction that has written the table. - Retention is bounded by the structure budget: older history collapses into segments. An
ALTER TABLE ... ADD COLUMNis a barrier: it moves the floor, and anAS OFread below the floor is refused by name. Seal(table)is maintained as rows change, for paged and in-memory tables, so reading it is O(1).Seal(table, verify: true)re-derives it from the rows and repairs any drift. A key range scans the rows.- [GATE: the per-commit cost of keeping history, and the page cost of an AS OF read on large tables.]
Loading data and change batches
BulkLoad and BulkLoadColumns
int BulkLoad(string table, IReadOnlyList<string> cols, IReadOnlyList<Array> data, int count[, IReadOnlyList<bool[]?>? nulls])
int BulkLoadColumns(string table, IReadOnlyList<string> cols, IReadOnlyList<object> data, int count) // analytic and paged
long PagedBulkLoad(string table, IEnumerable<object?[]> rows, int commitEvery) // paged
BulkLoad appends typed arrays (long[] for INT, double[] for REAL, string[] for TEXT) without the SQL
parser. It validates every key first, so a duplicate primary key leaves the table unchanged. A column left out of
cols takes its type's zero, or NULL if declared NULL. The nulls argument gives one mask per column, true
for NULL. BulkLoadColumns takes long[], int[], short[], double[], string[] or Utf8Column for analytic
tables.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE users (id INT PRIMARY KEY, age INT, name TEXT)");
var cols = new[] { "id", "age", "name" };
var data = new Array[] { new long[] { 10, 11 }, new long[] { 40, 22 }, new[] { "Bo", "Cy" } };
int n = db.BulkLoad("users", cols, data, 2);
Console.WriteLine(n);
// 2
Change batches: ApplyChanges
A replica or a feed sends an ordered stream of upserts and deletes. ApplyChanges(table, changes) applies a batch
atomically: one write lock, one log commit, and a reader sees the table before the batch or after it, never part of
it. RowChange.Upsert(row) carries a full row, with every column, and a null for a NULL column. RowChange.Delete(key)
deletes by primary key, and a missing key is a counted no-op. Changes count in order, the last change of a key wins,
and only the net effect is applied. ChangeResult has Inserted, Updated, Deleted, MissingDeletes and Applied.
A failure leaves the table as it was.
The SQL form INSERT ... ON CONFLICT (pk) DO UPDATE SET ... changes only the listed columns of an existing row. It is
the way to keep the other columns of a row.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE orders (id INT PRIMARY KEY, region TEXT, total INT)");
var r = db.ApplyChanges("orders", new[]
{
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 1L, ["region"] = "north", ["total"] = 40L }),
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 2L, ["region"] = "south", ["total"] = 15L }),
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 1L, ["region"] = "north", ["total"] = 45L }),
RowChange.Delete(2L),
RowChange.Delete(99L),
});
Console.WriteLine($"{r.Inserted} inserted, {r.Updated} updated, {r.Deleted} deleted, {r.MissingDeletes} missing");
// 2 inserted, 1 updated, 1 deleted, 1 missing
db.Execute("INSERT INTO orders VALUES (1, 'west', 50), (3, 'east', 7) ON CONFLICT (id) DO UPDATE SET total = EXCLUDED.total");
foreach (var row in db.Execute("SELECT id, region, total FROM orders ORDER BY id").Rows)
Console.WriteLine($"{row["id"]} {row["region"]} {row["total"]}");
// 1 north 50
// 3 east 7
Text keeps every character it was given, including line breaks and backslashes. A batch is one log commit group, so one call with several changes is one fsync.
Several tables in one step
ApplyChanges(IReadOnlyList<TableChanges>) applies the changes of several in-memory tables as one atomic step, with
one write lock and one log commit. A reader sees every table before the batch or every table after it. Every table
and value is checked before the first change, and a failure leaves all of them unchanged. You get one ChangeResult
per table, in list order. It works inside BEGIN and COMMIT.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE accounts (id INT PRIMARY KEY, balance INT)");
db.Execute("CREATE TABLE ledger (id INT PRIMARY KEY, account INT, amount INT)");
db.Execute("INSERT INTO accounts VALUES (1, 100), (2, 100)");
var results = db.ApplyChanges(new[]
{
new TableChanges("accounts", new[]
{
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 1L, ["balance"] = 70L }),
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 2L, ["balance"] = 130L }),
}),
new TableChanges("ledger", new[]
{
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 1L, ["account"] = 1L, ["amount"] = -30L }),
RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 2L, ["account"] = 2L, ["amount"] = 30L }),
}),
});
Console.WriteLine($"{results[0].Updated} updated, {results[1].Inserted} inserted");
// 2 updated, 2 inserted
var both = db.Execute("SELECT SUM(accounts.balance), SUM(ledger.amount) FROM accounts JOIN ledger ON accounts.id = ledger.account");
Console.WriteLine($"{both.GetValue(0, 0)} {both.GetValue(0, 1)}");
// 200 0
Across two statements a batch can still be seen half applied. Only one statement, such as a join, reads one consistent state of several tables.
Comparing key sets: KeyDigest
KeyDigest(table, modulus) returns one KeyBucket (Count and an exact Int128 Sum) per bucket of an INT
primary key, in one pass. Bucket b holds the keys whose value modulo modulus is b. Two systems that compute the
same buckets can compare modulus numbers instead of every key, and list the keys of the buckets that differ.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE accounts (id INT PRIMARY KEY, balance INT)");
db.Execute("INSERT INTO accounts VALUES (1, 100), (2, 100), (6, 5)");
KeyBucket[] d = db.KeyDigest("accounts", 4);
Console.WriteLine($"bucket 1: {d[1].Count} key, sum {d[1].Sum}; bucket 2: {d[2].Count} keys, sum {d[2].Sum}");
// bucket 1: 1 key, sum 1; bucket 2: 2 keys, sum 8
Summary tables
A summary table keeps no rows. It stores only the aggregate plane, the value dictionaries of its dimensions and the position each feed has reached, so its size depends on the distinct values of its dimensions, not on how many rows went through it.
CREATE SUMMARY TABLE sales (region TEXT DIMENSION, channel TEXT DIMENSION, units INT MEASURE, revenue INT MEASURE)
A dimension is an INT or TEXT column you filter, group or count distinct on. A measure is an INT column
you SUM or AVG. The columns are fixed when the table is created. Database.CreatePlaneOnly(...) is the same
thing from code.
- Writing.
ApplyPlaneChanges(table, source, position, changes)takesPlaneChange.Add(row),PlaneChange.Remove(row)andPlaneChange.Replace(oldRow, newRow), as one atomic batch. A row is an array in the declared column order, or a dictionary with every column. ARemovecarries the row's values, because the table has no rows to look them up in. - Exactly once. Each batch carries the position its
sourcehas reached. A batch at or below the position already applied for that source is skipped, andSkippedistrue. The position is a number, or opaque bytes compared as unsigned numbers compared lexicographically (for example a 10-byte, big-endian log sequence number). It is saved in the same log commit as the counts.PlanePositionandPlanePositionBytesread it back. - No negative counts. A
Removeof a row that was never added refuses the whole batch, and nothing changes. - Reading.
COUNT,SUMandAVGof the measures.MIN,MAXandCOUNT(DISTINCT)of a dimension.WHEREover dimensions.GROUP BYandHAVINGoverCOUNTandSUM.ORDER BYandLIMIT. Anything that needs a row (SELECT *, a filter on a measure, a join, anUPDATE) is refused with the reason in the message. - Totals and export.
PlaneTotals(table)returns the row count and the exact sum of each measure.ExportPlane(table)returns the whole state as one blob with a checksum.ImportPlane(table, blob, replace)checks the blob before it changes anything.
The dimension caps (a maximum number of distinct values per dimension, and a maximum number of flag bits across all dimensions) are named in the refusal when a batch would pass them.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE SUMMARY TABLE sales (region TEXT DIMENSION, channel TEXT DIMENSION, units INT MEASURE, revenue INT MEASURE)");
var batch = new[]
{
PlaneChange.Add(new object?[] { "north", "web", 3L, 30L }),
PlaneChange.Add(new object?[] { "south", "shop", 5L, 50L }),
PlaneChange.Add(new object?[] { "south", "web", 1L, 10L }),
};
Console.WriteLine(db.ApplyPlaneChanges("sales", "orders-feed", 100, batch).Skipped); // False
Console.WriteLine(db.ApplyPlaneChanges("sales", "orders-feed", 100, batch).Skipped); // True
db.ApplyPlaneChanges("sales", "orders-feed", 101, new[]
{
PlaneChange.Replace(new object?[] { "south", "shop", 5L, 50L }, new object?[] { "south", "shop", 6L, 60L }),
});
foreach (var row in db.Execute("SELECT region, SUM(units), SUM(revenue) FROM sales GROUP BY region ORDER BY region").Rows)
Console.WriteLine($"{row["region"]} {row["sum(units)"]} {row["sum(revenue)"]}");
// north 3 30
// south 7 70
var t = db.PlaneTotals("sales");
Console.WriteLine($"{t.Rows} rows, {t.Sums["units"]} units, position {db.PlanePosition("sales", "orders-feed")}");
// 3 rows, 10 units, position 101
try { db.Execute("SELECT * FROM sales"); }
catch (HoloException e) { Console.WriteLine(e.Message); }
Durable, larger-than-memory tables
A paged table keeps its pages in a buffer pool, and its changes in a write-ahead log. It is durable, and it can be
larger than memory: pages that are not in the pool are read from data.pages, through a read-only memory map.
Paged tables take the SQL reference with these exceptions: one INNER JOIN per statement, ApplyChanges (for one table
or several) and INSERT ... ON CONFLICT are in-memory only, an analytic table has no NULL columns, and a read of a
paged NULL column is refused by name (see NULL).
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
db.CreatePaged("CREATE TABLE big (id INT PRIMARY KEY, v INT)", dir);
db.PagedBulkLoad("big", new object?[][] { new object?[] { 1L, 100L }, new object?[] { 2L, 200L } });
Console.WriteLine(db.Execute("SELECT sum(v) FROM big").Rows[0]["sum(v)"]);
// 300
db.ClosePaged("big");
var reopened = db.OpenPaged(dir);
Console.WriteLine(db.Execute($"SELECT count(*) FROM {reopened}").Rows[0]["count(*)"]);
// 2
db.ClosePaged(reopened);
CreatePaged(createTableSql, dir, bufferFrames)creates a paged table indir.bufferFramesis the pool size in pages.OpenPaged(dir)reopens it and recovers committed state from its log. It returns the table name.PagedBulkLoad(table, rows, commitEvery)streams rows and commits everycommitEveryrows.ClosePaged(table)checkpoints and closes one table.
Integrity
Every page of data.pages has a CRC-32C in data.crc, written at each checkpoint. A page that fails its check
throws a HoloException that names the page and the file, so a flipped bit or a bad sector is an error and never a
different answer. A database that predates the checksums gets them the first time it is opened.
A statement may change more pages than the pool holds. The pool then writes the open transaction's least recently
used pages to a scratch file, txn.spill, and deletes it when the transaction ends. A large UPDATE or DELETE
completes, and a ROLLBACK or a power cut leaves the table as it was.
UPDATE and DELETE find their rows through the primary key, a sorted index or a group index when one proves the
answer, and otherwise through the block scan. They adjust the stored totals by the difference.
Deletes and consolidation
A DELETE marks its rows and moves nothing. The stored totals, group indexes and learned block records are adjusted by
the deleted values, so the cost follows the rows deleted, not the table. Every scan, index and join skips a deleted row.
COUNT(*) and AVG count the rows that exist. A deleted key can be inserted again.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
db.CreatePaged("CREATE TABLE events (id INT PRIMARY KEY, g INT, v INT)", dir);
var rows = Enumerable.Range(1, 20000).Select(i => new object?[] { (long)i, (long)(i % 10), (long)i }).ToList();
db.PagedBulkLoad("events", rows);
db.Execute("DELETE FROM events WHERE g = 7");
Console.WriteLine(db.Execute("SELECT count(*) FROM events").Rows[0]["count(*)"]);
// 18000
long slots = db.Consolidate("events");
Console.WriteLine(slots > 0);
// True
Console.WriteLine(db.Execute("SELECT count(*) FROM events WHERE g = 7").Rows[0]["count(*)"]);
// 0
db.ClosePaged("events");
Deleted rows keep their place until the table is consolidated. The engine consolidates by itself, after a statement,
once the scans that read the dead places have paid for the rewrite (Database.AutoConsolidate, on by default), or you
call Consolidate(table), which returns the number of places removed. A consolidation moves rows into the holes and
cuts the end off. A moved row keeps its id, and no sorted index or keyed summary is made stale by it. Rows come back
in a different order afterwards unless you ORDER BY.
Pages that a consolidation, a regroup or an emptied table frees go to a free map, and new rows take the lowest free
page first. Free pages at the end of data.pages are cut off at the same checkpoint, and their checksums with them.
The free map is derived from the table's pages when the table opens, so a crash leaves nothing to repair.
Whole-database paged mode
HoloDbOptions.Paged = true (or Database.Open(walPath, checkpointBytes, paged: true)) puts a new database on the
paged engine. A plain CREATE TABLE then creates a paged table, and no CreatePaged call is needed. The option is for a database
with no existing files at WalPath. Opening over an existing text log with paged: true throws.
Database.IsPagedDefault reports the mode.
using HoloDb;
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(dir);
var walPath = Path.Combine(dir, "db.wal");
var pd = new HoloDbService(new HoloDbOptions { WalPath = walPath, Paged = true });
pd.Execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)");
pd.Execute("INSERT INTO t VALUES (1, 42)");
Console.WriteLine(pd.Database.IsPagedDefault);
// True
Console.WriteLine(pd.Execute("SELECT v FROM t WHERE id = 1").Rows[0]["v"]);
// 42
pd.Close();
What the engine builds for itself
Nothing here is declared. The engine builds a structure when the scans it would answer have paid for its build and for the writes that would keep it current, and it drops a structure when its maintenance costs more than the scans it saves, or when a write makes it stale. Every structure takes its bytes from one budget that scales with the data's own size. The cap is hard. A candidate that does not fit evicts the structures that save the fewest reads per byte. A declared structure that still does not fit is refused, with the amount it needs. Declared structures are never evicted. Answers are the same with any mix of structures, or none.
- Aggregate summaries. A table's small-domain columns, summarised so that filters, grouping,
COUNT(DISTINCT)and equi-joins are answered from the summary. The planner picks the dimensions and measures from the statements it sees. A summary on a paged table is built in the background and saved beside the table. - Sorted index on a paged table. An ordered list of the live rows' values, read in checksummed blocks. A range
COUNT,MINandMAX, andORDER BY col DESC LIMIT k, are answered from it without reading the table's pages.SUMover a range is not served from it. - Keyed summary. The exact count and sums for each value of a high-cardinality
INTkey, optionally split by a second column. It answersGROUP BY keywithSUMandCOUNT,ORDER BY ... LIMIT, and an equality filter on the split column, and a single join with a dimension table on its primary key. For each join shape the planner has paid for, it keeps pre-grouped totals, which appended rows and new dimension rows update. - Holographic tree. A tree of exact counts and sums over key ranges, for a
SUMover a key range or aGROUP BYon a key whose values are too spread for a keyed summary. It is built when a range sum or a group-by on that key has paid for it. - Row indexes in memory. A sorted index on an
INTcolumn, or an equality index on aTEXTcolumn, for a selective point or range predicate on a large in-memory table. - Learned block records. For each
WHEREclause the engine has seen, the number of matching live rows in each block. A block with no match is not read. Records are exact: anUPDATEorDELETEchanges them by the rows it touches, and a move of rows takes the counts of the blocks it changed out of them. Each block also keeps the bounds of its columns, so a block outside a range is skipped. - Learned routes. A link is the set of blocks that held a match for a conjunction of filters on an earlier read. A later query follows the links whose filters it repeats, and reads only the blocks they name. Every link is checked when it is followed: a block whose first row id or checksum does not match is a stale hop, and the statement reruns without routing. A query that has no link is an ordinary scan.
- Gravity. Rows that satisfy the same repeatedly-scanned predicates are written into the same blocks, by a regroup. Rows keep their ids. A regroup runs as upkeep, when the reads it would save are worth more than the rewrite.
- Placement on write. An
UPDATEorINSERTthat gives a row new values moves it into a free slot of a block that the repeated filters already read, when that saves more reads than the move costs. The row keeps its id, and the learned records follow it exactly.
Control and inspection. SET summaries and SET indexes take auto, declared or off. declared keeps
the structures you created and drops the ones the planner built. off keeps every structure but uses none. EXPLAIN
names the structure that answered a statement, or the reason it did not, and SHOW SUMMARIES reports the state of each
structure and what it is waiting for. Database.Regroup(table) runs a regroup now. Database.PlaceOnWrite turns
placement on writes off.
Keyed summary, sorted index and summary files on a paged table are saved beside the table, and the learned records
(routes, predicate counts and block zones) are saved in learned.idx. A saved file is trusted only when it matches the
table's change counter and exact totals. learned.idx is trusted at open only when the table has not changed since it
was written. A stale or damaged file is discarded, or for a declared structure
rebuilt from its saved definition, and SHOW SUMMARIES gives the reason.
using HoloDb;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE sales (id INT PRIMARY KEY, region TEXT, tier INT, qty INT)");
db.Execute("INSERT INTO sales VALUES (1,'north',1,10),(2,'north',2,-4),(3,'south',1,7),(4,'east',3,5),(5,'south',2,9)");
db.Execute("SET summaries = declared");
db.Execute("CREATE INDEX by_region ON sales (region, tier) INCLUDE (qty) USING SUMMARY");
foreach (var r in db.Execute("EXPLAIN SELECT SUM(qty) FROM sales WHERE region = 'north' OR tier = 3").Rows)
Console.WriteLine($"{r["kind"]}: {r["route"]}");
foreach (var r in db.Execute("SHOW SUMMARIES ON sales").Rows)
Console.WriteLine($"{r["name"]} {r["origin"]} {r["state"]}");
db.Execute("DROP INDEX by_region");
The aggregate plane (Pleroma)
An optional aggregate plane, held in Phasor Pleroma stores, summarises the small-domain columns of one table. It is
derived from the columns, updated by every write, and answers WHERE aggregates over any combination of AND, OR,
NOT, LIKE, IN and BETWEEN on its dimensions, multi-column GROUP BY with HAVING, COUNT(DISTINCT dimension),
MIN and MAX of a dimension, and one equi-join COUNT or SUM between two tables that both have a plane.
Sums are exact past 64 bits: the plane raises an error rather than wrap, and PleromaSumExact returns the exact
128-bit total.
A plane that is attached with EnablePleroma, or built by the planner, answers only what its dimensions and measures
cover. Anything else goes to the engine, so a statement returns the same rows with the plane on or off.
using HoloDb;
var db = Database.Open();
db.Execute("CREATE TABLE sales (id INT PRIMARY KEY, region TEXT, tier INT, qty INT)");
db.Execute("INSERT INTO sales (id, region, tier, qty) VALUES (1,'north',1,10),(2,'north',2,-4),(3,'south',1,7),(4,'east',3,5),(5,'south',2,9)");
db.EnablePleroma("sales", new PleromaPlaneOptions(new[] { "region", "tier" }, new[] { "qty" }));
db.PleromaMode = PleromaMode.Always;
var r = db.Execute("SELECT COUNT(*), SUM(qty) FROM sales WHERE region = 'north' OR tier = 3");
Console.WriteLine($"{r.Rows[0]["count(*)"]} {r.Rows[0]["sum(qty)"]}");
// 3 11
r = db.Execute("SELECT COUNT(DISTINCT region) FROM sales");
Console.WriteLine(r.Rows[0]["count(distinct region)"]);
// 3
db.Execute("DELETE FROM sales WHERE id = 4");
Console.WriteLine(db.Execute("SELECT COUNT(DISTINCT region) FROM sales").Rows[0]["count(distinct region)"]);
// 2
Console.WriteLine(db.PleromaVerify("sales") ?? "plane equals a rebuild from the columns");
// plane equals a rebuild from the columns
Console.WriteLine(db.PleromaSumExact("SELECT SUM(qty) FROM sales"));
// 22
PleromaMode has three values. Auto (the default) leaves whole-table counts and sums, and single-column GROUP BY,
to the engine's maintained totals. Always serves every shape the plane covers. Off bypasses the plane while it is
still kept current. PleromaVerify rebuilds the plane from the columns and compares it, and returns null when they
agree. PleromaStats reports sizes and the number of statements served and declined.
A plane created from SQL with CREATE INDEX ... USING SUMMARY, and one the planner builds, is logged and saved with
the database, and survives a restart. A plane attached with EnablePleroma is not saved: call EnablePleroma again after
Database.Open.
The Pleroma page store
PleromaPageStore keeps many exact Pleroma pages of one shape as the data itself, durably and larger than memory.
PleromaSparsePageStore does the same for sparse pages over a large vocabulary. Each page carries a Phasor check
prime, so a damaged record is detected, and records are grouped with exact-sum parity records, so a lost page or one
damaged record per group is rebuilt by Repair. Readers do not wait for a writer that is working on other pages.
using HoloDb;
using Phasor;
var shape = new HologramShape { KeyCapacity = 2, KeyCycle = 110, CompleteKeys = true };
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
using (var store = PleromaPageStore.Open(dir, shape))
{
store.AddBatch(new[] { new PageAdd(42, 5, 3), new PageAdd(42, 77, -2), new PageAdd(7, 5, 1) });
store.AppendEvent("context 42 seen"u8);
Console.WriteLine($"{store.Counts(42)[5]} {store.Counts(42)[77]}");
// 3 -2
}
using (var store = PleromaPageStore.Open(dir, shape))
{
Console.WriteLine($"{store.Counts(42)[5]} {store.Counts(7)[5]}");
// 3 1
store.ReplayEvents((seq, bytes) => Console.WriteLine($"{seq} {System.Text.Encoding.UTF8.GetString(bytes)}"));
// 2 context 42 seen
Console.WriteLine(store.Verify().Clean);
// True
}
- Writes are journaled before they are applied, and a page remembers the last batch applied to it, so a crash replays each logged add exactly once. A torn journal tail is discarded, so a batch survives whole or not at all.
- Restore points.
Checkpoint()returns a point andRestoreTo(id)returns the store to exactly that state, without a recount. A restore point is copy-on-write. - Events.
AppendEventstores an opaque record in the same sequence as the batches.ReplayEventsreturns them in order, andTrimEventsdrops an absorbed prefix. - Repair.
Verifychecks every record and parity group.Repairrebuilds a lost page or a damaged record from its group. A rebuilt page must pass its own check before it is written.
The store's shape must be CompleteKeys and admissible for a check prime. One process opens a store at a time, and a
batch is applied page by page, so a reader can see a batch on some pages and not on others.
Measured
Machine and conditions for every figure on this page. An 11th Gen Intel Core i9-11950H (8 cores, 16 threads), 31.7 GB of memory visible to the operating system, an NVMe drive, Windows 11. Each figure is one run, or the median of repeated calls in one session, on a machine that was not otherwise idle. Figures on different machines or in different sessions are not compared with each other. Each one names the rows and the statement it was measured on.
Deletes on paged tables
Warm, on a paged table of 3,000,000 rows:
DELETE on 3,000,000 rows |
Time |
|---|---|
| one row at a random key (median) | 1.0 ms |
| the first 100,000 rows | 57 ms |
the rows where g = 7 (2%) |
94 ms |
At 10,000,000 rows: deleting the first 1,000 rows takes 156 ms, because it removes the minimum of four columns and
those columns are read again. Deleting one row from the middle takes 40 ms. Deleting the 2% of rows where g = 7,
scattered through the table, takes 308 ms, which is the scan that finds them.
Learned records and routes
Repeated clustered predicate, 60,000 rows. Blocks read by a repeated WHERE clause: 59 on the first run, 8 once
the aggregate has been learned, and 1 for COUNT(*). On 10,000,000 rows, COUNT(*) with a repeated predicate fell
from 5.7 ms to 1.0 ms.
Repeated workload, 10,000,000 rows. Each round looks up 10 customers, runs two status filters within the blocks the lookup finds, and joins the customer dimension with a status filter on the fact side. The same ten customers repeat each round. Blocks of the fact table read per round:
| Round | No learned links | With learned links |
|---|---|---|
| first | 19,366 | 2,032 |
| later rounds (steady) | 6,691 | 625 |
With the structure budget reduced to 20 MB, the first round is 23,766 blocks without links and 6,425 with them, and the steady rounds are 6,691 and 625. The block counts are exact. Timings on that machine varied by about 20% between runs, so they are not given here. A first lookup of a customer no read has seen is a full scan in both arms.
ClickBench-compatible analytic SQL
The 43 ClickBench queries run on the official hits table of 99,997,497 rows and 105 columns. Results are checked
against the output of an independent engine on the same file.
- Correctness. [GATE: ClickBench correctness on this release at 1,000,000, 10,000,000 and 100,000,000 rows]
- Load and size. [GATE: ClickBench load time and disk size at 100,000,000 rows on this release]
- Timings. [GATE: ClickBench hot and cold totals, and the slowest query, at 100,000,000 rows on this release]
These are not leaderboard submissions and the relative-score formula was not run.
100,000,000 rows: writes, mixed work and a regroup
[GATE: single-row DELETE and UPDATE, autocommit, p50 and p99 at 100,000,000 rows]
[GATE: a transaction of 100 statements, p50 and p99 at 100,000,000 rows]
[GATE: mixed workload at 100,000,000 rows: group-by, point read, single write, filtered sum, top-10 customers]
[GATE: repeated scattered-predicate workload at 100,000,000 rows, first round and steady rounds]
[GATE: the one-time regroup at 100,000,000 rows]
Costs and limits
This is the one place the package states its costs. Each is a current behaviour.
- Deleting the last row that holds a column's minimum or maximum re-reads that column. A sorted key or group index that already knows the next value avoids the re-read.
- A
DELETEorUPDATEof a column that a sorted index or keyed summary reads is applied to it as a signed delta, so the structure stays current. A write the structure cannot follow drops it: a key outside a keyed summary's range, or more deleted rows than a sorted index holds as deltas.ALTER TABLE ... ADD COLUMNdrops a sorted index. Until the planner has paid for a rebuild, the scans run without it. - Learned records and routes help when a repeated predicate matches rows that sit together. A predicate whose rows are spread evenly over every block gains nothing: on a test of uniform data the repeated predicate read 59 blocks with the records and 59 without them.
- The first lookup of a literal that no earlier read has seen is a full scan, unless the dictionary or the column's range proves no row can match: a text value absent from the column, or a number outside its range, reads no block. A number inside the range is still a full scan on the first read.
- Learned records and routes are saved in
learned.idxbeside the table. A reopen keeps them only when the table has not changed since the file was written. After any later write they are learned again by use. The planner's usage ledger is memory only, and it is not written to the log. - Consolidation and regroup run as upkeep, in slices. A statement that arrives during a slice waits for that slice, not for the whole job. Scattered holes cost the page each one sits in. [GATE: the longest read wait during a consolidation of scattered holes at 10,000,000 and 100,000,000 rows]
- The holographic tree is built in slices under the shared lock. Its final merge is one pass that is not sliced, and only the publish takes the exclusive lock.
- Paged tables take one
INNER JOINper statement.ApplyChanges, the upsert form and summary tables are in-memory only. A primary key loaded out of order is held in a dictionary in memory. - Without
ORDER BYthe row order is unspecified. After aDELETE, or any write that moves a row, it can differ from the order before it. - Sums past 64 bits throw. Text
GROUP BYis ordinal and case-sensitive, and the groups of a coded group-by come back in code order unless you addORDER BY. NEARESTfrom a few thousand rows up is approximate, and its ranking is not guaranteed to order every pair of far values by distance.- The aggregate plane declines what it does not cover. A plane attached with
EnablePleromais not kept across a restart.
Dependency injection
public static IServiceCollection AddHoloDb(this IServiceCollection services, Action<HoloDbOptions>? configure = null)
AddHoloDb registers a singleton HoloDbService. It needs only IServiceCollection, from
Microsoft.Extensions.DependencyInjection.Abstractions. ServiceCollection and BuildServiceProvider come from the
full Microsoft.Extensions.DependencyInjection package, which ASP.NET Core and the generic host already include. A
console project adds it with dotnet add package Microsoft.Extensions.DependencyInjection.
using HoloDb;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddHoloDb(o => o.WalPath = null);
var provider = services.BuildServiceProvider();
var db = provider.GetRequiredService<HoloDbService>();
db.Execute("CREATE TABLE ping (id INT PRIMARY KEY)");
db.Execute("INSERT INTO ping VALUES (1)");
Console.WriteLine(db.Execute("SELECT count(*) FROM ping").Rows[0]["count(*)"]);
// 1
EvalApp pipeline integration
public static IAppBuilder WithHoloDb(this IAppBuilder builder, HoloDbService service)
WithHoloDb sets a HoloDbContext, an EvalApp global context that wraps the service, as the app's context. A step
that derives from ContextPureStep<HoloDbContext, ...> reaches the service through global.Service. An app has one
global context. When the app needs the database and other things too, put a HoloDbContext inside your own context
object and pass that to WithContext.
using EvalApp;
using HoloDb;
var service = new HoloDbService(new HoloDbOptions());
service.Execute("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)");
service.Execute("INSERT INTO users VALUES (1, 'Ada')");
var services = new Services(new HoloDbContext(service));
Eval.App("Catalog")
.WithContext(services)
.DefineDomain("Catalog")
.DefineTask<LookupQuery>("LookupUser")
.AddStep<LookupUserStep>("Lookup")
.Run(out ICompiledPipeline<LookupQuery> pipeline)
.Build();
var outcome = await pipeline.RunAsync(new LookupQuery(1));
if (outcome is PipelineResult<LookupQuery>.Success ok) Console.WriteLine(ok.Data.Name);
// Ada
public sealed record LookupQuery(long Id, string Name = "");
public sealed class Services(HoloDbContext db) : GlobalContext
{
public HoloDbContext Db { get; } = db;
}
public sealed class LookupUserStep : ContextPureStep<Services, LookupUserStep.NoDomain, LookupQuery>
{
public sealed class NoDomain : DomainContext { }
protected override ValueTask<LookupQuery> TransformAsync(LookupQuery data, Services global, NoDomain domain, CancellationToken ct)
{
var r = global.Db.Service.Database.Execute($"SELECT name FROM users WHERE id = {data.Id}");
return ValueTask.FromResult(data with { Name = (string)r.Rows[0]["name"] });
}
}
HoloDbService.ExecuteAsync and ApplyChangesAsync run the synchronous engine on a thread-pool thread, and do not go
through the pipeline. Inside a step, call the synchronous Database as above.
Usage metering
MeteredDatabase wraps a Database and reports, for each query, the work it did: the rows and bytes it read, and
whether it was served from a maintained total without reading rows.
public readonly record struct QueryUsage(long RowsScanned, long TableRows, long RowBytes, long BilledBytes, double ElapsedMs)
{
public long BytesActuallyRead { get; } // RowsScanned * RowBytes
public bool ConstantTime { get; } // true iff RowsScanned == 0
}
public sealed class MeteredDatabase
{
public MeteredDatabase(Database db, HoloPricing? pricing = null);
public (QueryResult Result, QueryUsage Usage) Run(string sql);
public double OurCostUsd(QueryUsage u);
public double ScanCostUsd(QueryUsage u, ScanPricing engine);
}
ScanPricing and HoloPricing are records that hold a price per tebibyte and a minimum bill per query. The
ScanPricing names are BigQuery, Athena and All. Database.LastRowsScanned, LastTableRows, LastRowBytes
and LastLogicalBytes are the per-thread counters Run reads.
using HoloDb;
using HoloDb.Analytics;
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)");
db.Execute("INSERT INTO t VALUES (1, 100), (2, 200)");
var metered = new MeteredDatabase(db.Database);
var (res, usage) = metered.Run("SELECT sum(v) FROM t");
Console.WriteLine(usage.ConstantTime);
// True
Console.WriteLine(metered.OurCostUsd(usage) <= metered.ScanCostUsd(usage, ScanPricing.BigQuery));
// True
Parallel fan-out (HoloPar)
HoloPar.Cpu is the parallel primitive the engine fans out through: Map(chunks, body) runs the body once per chunk.
You do not need to call it, since every query path that benefits already does. Each call site learns its own cost,
and runs inline when a fan-out would not pay. Nothing in this package is gated.
using HoloDb;
var sum = 0; var gate = new object();
HoloPar.Cpu.Map(4, i => { lock (gate) sum += i; });
Console.WriteLine(sum);
// 6
Engine building blocks
These types are public in the assembly, but none of them is needed to use the SQL surface. They are the building blocks the engine is made of.
| Type | What it is |
|---|---|
Holo (static) |
The row encoding: EncodeRow(Schema, row) binds the columns into a holographic vector, and Similarity(a, b) is the cosine similarity. NEAREST ranks with these. |
HoloLsh |
The locality-sensitive index that NEAREST builds over row encodings once a table is large enough: Add, Approx and Exact top-k. |
GrowColumn<T> |
An append-only column array with a published count, which readers can see without a lock. |
Table, Schema, Column, ColType, LiveMetrics, GroupRow |
The in-memory table primitives Database builds on. Build tables through SQL, not with new Table(schema). |
using HoloDb;
var schema = new Schema(new[] { new Column("id", ColType.Int, true), new Column("tag", ColType.Text, false) });
var v1 = Holo.EncodeRow(schema, new Dictionary<string, object?> { ["tag"] = "red" });
var v2 = Holo.EncodeRow(schema, new Dictionary<string, object?> { ["tag"] = "red" });
Console.WriteLine(Holo.Similarity(v1, v2) > 0.99);
// True
Exceptions
Every error the engine raises on purpose is a HoloException. Catch that type.
public sealed class HoloException : Exception
{
public string? Code { get; } // set for a plane-only cap, a refused plane blob, or a data page that fails its checksum
public string? Dimension { get; }
public int? Cap { get; }
public int? Needed { get; }
}
Code takes the values in HoloErrorCodes: PlaneDimensionCap, PlaneJointCap, PlaneBlobCorrupt and PlaneBlobVersion.
A damaged data page reports the code data-page-checksum, which is not one of those constants.
File layout
- In memory (
WalPath = null): nothing is written to disk. - Text log (
WalPathset, an in-memory database):WalPathis an append-only log of committed statements, andWalPath + ".snap"is the latest snapshot.Checkpoint()folds the log into a new snapshot and empties the log. - Paged table (
CreatePaged(sql, dir), or whole-database paged mode): one directory per table.- Keep these together. They are the data and its record:
data.pages(the pages),wal.log(the ring of deltas not yet written intodata.pages),table.meta9(the table record;table.meta10when the table has aNULLcolumn), and the stubstable.meta8,table.meta7andtable.meta, which make older binaries refuse the table. - Declarations, to keep with the table:
summary.decl,sorted.declandkeyed.declrecord the indexes you created withCREATE INDEX ... USING SUMMARY,CREATE INDEX(sorted) andUSING KEYED. Back them up with the table. data.crc: one CRC-32C per data page. It is rebuilt at open when it is missing or damaged.txn.spillandstmt.spill: scratch space for a transaction or a statement larger than the pool.- Derived, and safe to delete (the engine rebuilds them or earns them again):
derived.idx(the group indexes and the sorted key),summary.auto(planner-built aggregate summaries),sorted.<column>.idx(sorted index files),keyed.<name>.pksandkeyed.<name>.joins(keyed summary files and their join totals), andtree.listwith its files (the holographic tree).learned.idx(routes, predicate counts and block zones) is derived too: a file that does not match the table is discarded.
- Keep these together. They are the data and its record:
License
Proprietary: this package ships the compiled library only; see LICENSE for the full terms.
Installing and using unmodified copies for internal evaluation and non-commercial development or
testing is licensed; commercial, production, or revenue-generating use requires a separate
commercial licence from the owner. No feature on this page is gated behind a tier or a licence
key: every capability described works identically with or without one. Copyright (c) 2026
Dongyang Stephen Chen, trading as Evaluated Applications. All rights reserved.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- EvaluatedApplications.EvalApp (>= 6.0.2)
- EvaluatedApplications.Phasor (>= 3.6.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
-
net8.0
- EvaluatedApplications.EvalApp (>= 6.0.2)
- EvaluatedApplications.Phasor (>= 3.6.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on EvaluatedApplications.HoloDb:
| Package | Downloads |
|---|---|
|
EvaluatedApplications.Prose
Grammar-driven synthetic corpus generator: mines nouns, verbs and adjectives and the syntax that joins them from real text with the rules of grammar, stores them in HoloDb, and recombines them into new grammatical sentences built only from words and pairings the text used, plus reading-comprehension Q&A pairs (single-sentence, multi-sentence passage, and two-turn). Rules-first (deterministic, zero-training) part-of-speech tagging, chunking and role assignment; mining fans out across cores through EvalApp (generation does too, opt-in); an optional mini-HoloFormer scorer re-ranks generated candidates by next-lemma likelihood once trained (POS tagging stays 100% rules-based). |
|
|
EvaluatedApplications.HoloDb.Protocol
The wire contract for HoloDb clients and servers: length-prefixed TCP framing plus a columnar-preserving binary codec that encodes requests and decodes responses as typed column arrays (long[], double[], string[]) instead of JSON. Includes protocol version negotiation and three exception types for version skew, malformed frames, and server errors. Use this if you are implementing a HoloDb server or building a custom client; most applications should use EvaluatedApplications.HoloDb.Client instead. |
|
|
EvaluatedApplications.HoloGenome
Encode DNA as phasor holograms whose geometry comes from measured DNA physics. Each base is a Phasor symbol face carried on a helical phase that advances by published, sequence-dependent dinucleotide twist (Olson et al. 1998, built in), optionally with a local DNA-shape layer (minor groove width, propeller twist, roll, helix twist) read from a pentamer table you supply. Windows of sequence become fixed-width vectors you can store in HoloDb, compare, or feed to any regression. Also grades sequences against your own measurements, with no files or console so it runs in a browser: a zero-parameter helical score, a cross-validated ridge readout on the holograms, and a fitted phasor model that reports the helical period (bases per turn), each with a correlation and a block-resampled 95% interval. Research and education only: it makes no health, disease-risk or drug-response claims. Experimental (0.x). |
|
|
EvaluatedApplications.PleromaMind
A persistent conversational unit built on exact holograms, created empty from a design you choose and taught from your own text; the package ships no voice and no text. Its alphabet is Prism's 320 tokens (characters, common words and chat tags) plus subwords you can mint from your own text. It learns every exchange it hears and says, plus anything taught or ingested by reference, into exact per-conversation counts for the context lengths you pick. Memory grows with the text learned (ModelDesign.Estimate gives the cost by formula). It has a conversation memory for long matches (its trust can be calibrated on your own text and is journaled) and an optional learned sharing layer (token classes and solved trust) that learns only when you call LearnStep, with Compare scoring held-out text with and without it; it offers N sampled replies to pick from without learning anything; takes bounded and fading approve or disapprove feedback without touching memory; forgets a conversation exactly; and rebuilds its whole state bit for bit from its journal on a HoloDb page store, including after a crash. Every choice can be inspected per context length. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.4.0 | 0 | 11/12/2026 |
| 10.3.0 | 45 | 11/11/2026 |
| 10.2.0 | 59 | 11/10/2026 |
| 10.1.0 | 65 | 11/10/2026 |
| 10.0.1 | 90 | 11/10/2026 |
| 10.0.0 | 86 | 11/9/2026 |
| 9.0.0 | 111 | 11/3/2026 |
| 7.0.0 | 126 | 10/24/2026 |
| 6.0.1 | 107 | 10/23/2026 |
| 6.0.0 | 101 | 10/23/2026 |
| 5.3.1 | 110 | 10/17/2026 |
| 5.3.0 | 151 | 10/17/2026 |
| 5.2.0 | 114 | 10/16/2026 |
| 5.1.2 | 129 | 10/14/2026 |
| 5.1.1 | 114 | 10/13/2026 |
| 5.1.0 | 107 | 10/12/2026 |
| 5.0.0 | 104 | 10/11/2026 |
| 4.2.0 | 123 | 10/11/2026 |
| 4.1.0 | 105 | 10/11/2026 |
| 4.0.0 | 110 | 10/10/2026 |