EvaluatedApplications.HoloDb
5.3.0
See the version list below for details.
dotnet add package EvaluatedApplications.HoloDb --version 5.3.0
NuGet\Install-Package EvaluatedApplications.HoloDb -Version 5.3.0
<PackageReference Include="EvaluatedApplications.HoloDb" Version="5.3.0" />
<PackageVersion Include="EvaluatedApplications.HoloDb" Version="5.3.0" />
<PackageReference Include="EvaluatedApplications.HoloDb" />
paket add EvaluatedApplications.HoloDb --version 5.3.0
#r "nuget: EvaluatedApplications.HoloDb, 5.3.0"
#:package EvaluatedApplications.HoloDb@5.3.0
#addin nuget:?package=EvaluatedApplications.HoloDb&version=5.3.0
#tool nuget:?package=EvaluatedApplications.HoloDb&version=5.3.0
HoloDb
An embeddable holographic SQL database for .NET. It speaks a real SQL dialect, is transactional (ACID with a write-ahead log), and stores data in a columnar engine with compiled predicates, holo-native constant-time aggregates, and hash joins — plus an optional durable larger-than-memory tier (paged buffer-pool + write-ahead log, no memory-mapped files). It runs inside an EvalApp harness, so parallel scans execute as compiled, resource-gated pipelines.
This file is the complete public surface: every public type, method and SQL feature this package ships, each with a one-line purpose and a working example.
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.
Table of contents
- Quick start
- Changes in 5.1.2
- Changes in 5.1.1
- Changes in 5.1.0
- Core concepts
- Opening a database
- SQL reference
- BulkLoad — fast typed-array ingest
- Atomic change batches and upsert
- Summary tables: the plane without the rows
- QueryResult — reading results
- Durable, larger-than-memory (paged) tables
- Dependency injection
- EvalApp pipeline integration
- Usage metering and the billing model
- Parallel fan-out primitive (HoloPar)
- Advanced / low-level engine types
- Invariants and gotchas
- 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
Changes in 5.1.2
A statement is refused when any part of it would be ignored, instead of running as something else. LEFT, RIGHT,
FULL, CROSS and NATURAL joins, JOIN ... USING, comma joins, OFFSET, UNION, INTERSECT, EXCEPT, a second
statement after ;, ROLLBACK TO and trailing text after a statement each throw a HoloException that names what is not
supported. A table alias throws and asks for the full table name. NEAREST combined with a join, GROUP BY, HAVING,
ORDER BY, DISTINCT or an aggregate, HAVING without GROUP BY, DISTINCT with an aggregate, LIMIT 0 on an
aggregate-only query and a non-integer LIMIT are refused the same way. Statements that ran correctly before are
unchanged.
Changes in 5.1.1
A table with a Pleroma plane answers an equi-join COUNT or SUM faster. A join whose two tables have not been written since the same join last ran is served from a result memo kept with the plane: the first call, and the first call after any write to either table, costs the same as before, and the memo never changes a result. Dimension GROUP BY after a join on an INT key encodes through the column's zone map.
Changes in 5.1.0
New SQL (see the DDL table): CREATE INDEX [name] ON t (cols) [INCLUDE (cols)] [USING SORTED|SUMMARY] [WITH (...)], DROP INDEX, SHOW INDEXES and EXPLAIN.
New diagnostic hook: Database.RouteTrace is a thread-static List<string>?. Set it to a list and the
engine appends the access paths and the route each statement on that thread takes; set it back to null
to stop. EXPLAIN reports the same decision without running the query.
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)");
db.Execute("INSERT INTO s VALUES (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;
// access:block-wise mask scan
// route:predicate count (block-wise byte-mask, no position list)
foreach (var r in db.Execute("EXPLAIN SELECT count(*) FROM s WHERE n > 1").Rows)
Console.WriteLine($"{r["kind"]}: {r["route"]}");
// access: block-wise mask scan
// route: predicate count (block-wise byte-mask, no position list)
Behaviour that differs from 5.0.0:
- Older binaries refuse new files. A write-ahead log or snapshot that contains
INCLUDE,USING,WITH,DROP INDEX,SHOW INDEXESorEXPLAINtext cannot be opened by an older binary (its parser throws). The snapshot is written in the new v4 form only when a table has an index name or a SQL-created plane; every other database stays readable by a 5.0.0 binary. RunCheckpoint()on the new binary before an older one opens the same file. - Rollback.
BEGINno longer copies the database; a rollback replays a journal. After a rolled-backDELETEthe table holds the same rows, but their order may differ from before the transaction. - Text
GROUP BYis ordinal (case-sensitive).'Ada'and'ada'are two groups, where 5.0.0 merged them. The coded group-by paths (single-table and after a join) emit groups in ascending code order; addORDER BYwhen order matters. - Plane routing. In
Automode aGROUP BYwith aWHEREgoes to the engine below 140,000 rows (a fixed engine constant, set from an early measurement and not re-derived on the current build);PleromaMode.Alwaysstill uses the plane. UPDATEvalidates everySETvalue before changing any row, so a bad value leaves the table untouched.
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE s (id INT PRIMARY KEY, tag TEXT)");
db.Execute("INSERT INTO s VALUES (1, 'Ada')");
db.Execute("INSERT INTO s VALUES (2, 'ada')");
db.Execute("INSERT INTO s VALUES (3, 'Ada')");
foreach (var r in db.Execute("SELECT tag, count(*) FROM s GROUP BY tag ORDER BY tag").Rows)
Console.WriteLine($"{r["tag"]} {r["count(*)"]}");
// Ada 2
// ada 1
Measured against 5.0.0 on an idle 16-core machine (release gate EXP-PB13, two runs of each build, rows
identical to SQLite): no benchmark cell is slower; single-row autocommit UPDATE/DELETE at 1M rows
went from about 69 ms/67 ms to about 12 us/9 us, BEGIN from 66 ms to 200 ns, and the 10M-row invoicing
queries are faster on every join and group-by shape.
Core concepts
- Columns are authoritative; holograms are derived. Every row lives in plain typed columnar
storage (
long[]/double[]/string[]), scanned via compiled predicates. A holographic vector (VSA/HRR bind+bundle, used forNEARESTsimilarity search) is computed FROM the columns, never the other way round — losing a hologram loses nothing durable. Exact answers (sums, counts) come from exact integer accumulators, not from holograms. - ACID. Atomicity + Isolation: one global lock serialises mutations, and a pre-statement
undo journal (each changed row is journaled as it changes,
BEGINcopies nothing) is replayed on error orROLLBACK; DDL inside a transaction uses a snapshot. Durability: committed statements are write-ahead-logged andfsync'd. Consistency: schema/type,NOT NULL(the key, and every column not declaredNULL), andPRIMARY KEYuniqueness are enforced on write. - Two storage shapes, one SQL surface. In-RAM tables (
CREATE TABLEby default) and paged, durable, larger-than-memory tables (CreatePaged/OpenPaged, or whole-databaseHoloDbOptions.Paged) are driven through the sameExecute(sql)/BulkLoadcalls — see Durable tables for where their capabilities differ. - Constant-time exact aggregates. Whole-table and single-column-grouped
COUNT/SUM/MIN/MAXonINTcolumns read a maintained accumulator instead of scanning — flat cost as the table grows.REALaggregates and groupedREAL SUMare a fast SIMD scan, not maintained (see gotchas). - Not a toy dialect, but not all of SQL either. Real
WHERE/GROUP BY/HAVING/ORDER BY/LIMIT/DISTINCT/a singleINNER JOIN/subqueryIN/transactions, but no multi-way joins, noUPSERTkeyword (theON CONFLICTform is there), onePRIMARY KEYper table. A column holds SQLNULLwhen it is declaredNULL(NULL); every other column isNOT NULL. See the SQL reference for the exact grammar.
Opening a database
public sealed class HoloDbOptions
{
public string? LicenseKey { get; set; } // accepted, currently informational — nothing is gated by tier
public string? WalPath { get; set; } // null = pure in-memory, no durability
public long CheckpointThresholdBytes { get; set; } // default 8 MB; 0 disables automatic checkpoints
public bool Paged { get; set; } // opt in to whole-database paged-default mode (below)
}
var db = new HoloDbService(new HoloDbOptions { WalPath = "app.wal" });
HoloDbServiceis the recommended entry point: async-friendly, licensed (informational only), wraps the engine below.Execute(sql)— run a statement synchronously, return aQueryResult.ExecuteAsync(sql, ct = default)— the same, offloaded to the thread pool.BulkLoad(table, cols, data, count)— see BulkLoad.Checkpoint()— fold the write-ahead log into a fresh snapshot now (no-op in-memory).CreatePaged/OpenPaged/PagedBulkLoad/ClosePaged— see Durable tables.Close()— release every open paged table's file handles; call before re-opening the sameWalPathin-process, or on shutdown..Database— the underlyingDatabasefor direct synchronous access (below)..LicenseMode—Unlicensed/Licensed; informational, gates nothing today.
Database(HoloDbnamespace) is the engineHoloDbServicewraps; use it directly if you don't need the async wrapper or licensing shim.Database.Open(walPath = null, checkpointThresholdBytes = 8 MB, paged = false)— static factory.walPath = nullis pure in-memory. Recovery replays the write-ahead log over the last snapshot, so recovery time is bounded by live data, not by write history.Execute(sql),BulkLoad(...),Checkpoint(),Close()— same contracts asHoloDbService.WalPath— the configured log path, ornullfor in-memory (diagnostics).IsPagedDefault—trueiff this instance actually ended up in whole-database paged mode.TableColumns(table)— the table'sColumnlist in schema order, e.g. for building typedBulkLoadarrays without hardcoding types.LastRowsScanned,LastTableRows,LastRowBytes,LastLogicalBytes— static, per-thread usage counters for the most recently executed query on the calling thread; see metering.
var path = "app.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();
SQL reference
DDL: CREATE TABLE, ALTER TABLE, CREATE INDEX
| Statement | Purpose |
|---|---|
CREATE TABLE [IF NOT EXISTS] t (col TYPE [NULL \| NOT NULL] [PRIMARY KEY], ...) |
Define a table. Exactly one PRIMARY KEY, type INT or TEXT (a GUID is just a TEXT value — there is no dedicated GUID type), never NULL. A column is NOT NULL unless it says NULL: see NULL. IF NOT EXISTS makes a redundant create a no-op instead of throwing. |
ALTER TABLE t ADD [COLUMN] col TYPE [NULL] [DEFAULT <literal> \| NULL] |
Add one column to an existing table. ADD-only — no DROP, rename, or type change. A NOT NULL column needs a literal DEFAULT; a column declared NULL may leave DEFAULT out (existing rows are NULL). The default backfills rows that existed at ALTER time only. |
CREATE INDEX [name] ON t(col) |
A single-column index, INT or TEXT only (never REAL) and NOT NULL columns only (a column declared NULL is refused; the summary index below accepts one). The optional name is kept (unique per table); an unnamed index is called ix_<table>_<col>. USING SORTED is the same thing spelled out. |
CREATE INDEX [name] ON t(catCol, orderCol) |
A composite index: per-catCol-value buckets of primary keys pre-sorted by orderCol (orderCol must be INT). Speeds up WHERE catCol = 'v' ORDER BY orderCol LIMIT k. |
CREATE INDEX [name] ON t(dims...) [INCLUDE (measures...)] USING SUMMARY [WITH (max_dim_bits = n, max_joint_bits = n)] |
Build the Pleroma aggregate plane with SQL: key columns are its dimensions (INT/TEXT), INCLUDE columns its measures (INT). Caps default to 10 and 12 bits. One summary index per table; in-RAM tables only (a paged table refuses). A dimension past its cap does not fail the statement: the result row lists it and queries that need it go to the engine. USING NEAREST is parsed and refused (the similarity index is built automatically). |
DROP INDEX name [ON t] |
Remove an index by its given or generated name (ON t is only needed if two tables share the name). |
SHOW INDEXES [ON t] |
One row per index: table, name, kind (sorted/text/composite/summary), columns, include, rows, bytes, served, declined, over_cap. |
EXPLAIN [PLAN [FOR]] SELECT ... |
Report how a SELECT would be answered, without running it or changing anything. One row per step: step, kind, route, detail, est_rows. access rows name the row-access path (primary-key lookup, index seek, zone-map block skip with the column, block-wise mask scan), decline rows give the reason a faster route was not taken (for example a plane that does not hold the column, a dimension over its cap, a join with more than 4,095 keys), and the last route row is the one that serves the query (maintained accumulator, fused filter and aggregate, coded GROUP BY, columnar join, Pleroma plane, row-decode fallback). A subquery in IN (SELECT ...) is shown unresolved. |
Column types accept SQL-ish synonyms: INT/INTEGER, TEXT/VARCHAR/STRING,
REAL/FLOAT/DOUBLE/DECIMAL/NUMERIC — all three map onto exactly three physical storage
kinds (long, string, double).
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE users (id INT PRIMARY KEY, age INT, name TEXT)");
db.Execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, age INT, name TEXT)"); // no-op
db.Execute("INSERT INTO users VALUES (1, 30, 'Ada')");
db.Execute("ALTER TABLE users ADD COLUMN vip INT DEFAULT 0");
db.Execute("UPDATE users SET vip = 1 WHERE id = 1");
Console.WriteLine(db.Execute("SELECT vip FROM users WHERE id = 1").GetLong(0, 0));
// 1
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE events (id INT PRIMARY KEY, category TEXT, ts INT)");
db.Execute("CREATE INDEX ON events(category)");
db.Execute("CREATE INDEX ON events(category, ts)"); // composite
db.Execute("INSERT INTO events VALUES (1, 'login', 100), (2, 'login', 200), (3, 'logout', 50)");
Console.WriteLine(db.Execute("SELECT count(*) FROM events WHERE category = 'login'").Rows[0]["count(*)"]);
// 2
DML: INSERT, UPDATE, DELETE
| Statement | Purpose |
|---|---|
INSERT INTO t [(col, ...)] VALUES (v, ...)[, (v, ...)]... |
Insert one or more rows (batch form supported). An omitted column (a partial column list) is NULL when it is declared NULL, else its type's zero value (0/0.0/""). NULL is a value here (VALUES (1, NULL)); into a NOT NULL column it throws. Throws on a duplicate primary key. |
INSERT INTO t [(col, ...)] VALUES (...)[, ...] ON CONFLICT (pk) DO UPDATE SET col = EXCLUDED.col[, ...] |
Upsert. A row whose primary key exists changes only the SET columns (every other column keeps its value); a new key is inserted. EXCLUDED.col must name the same column, and every SET column must be in the INSERT column list. DO NOTHING, expressions, WHERE, INSERT OR REPLACE and REPLACE INTO throw. In-RAM tables only. See Atomic change batches. |
UPDATE t SET col = <literal>[, ...] [WHERE ...] |
Update matching rows. The right-hand side of SET must be a literal (NULL for a column declared NULL) — SET v = v + 1 is not supported (throws). Updating the primary key column itself throws. |
DELETE FROM t [WHERE ...] |
Delete matching rows. |
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 SQL method (Database.Execute, HoloDbService.Execute/ExecuteAsync) has a parameterized
overload: ? placeholders, substituted in order by the trailing arguments as correctly quoted/
escaped SQL literals (ADO.NET-style positional binding) — added 2026-10, customer finding F-05.
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
A string argument has its embedded ' doubled automatically — the same rule raw SQL text
already requires ('O''Brien' when you write the literal yourself: a single quote inside a string
literal is escaped by doubling it, per Sql.cs's lexer). Parameters don't replace that rule, they
apply it for you. ? only has placeholder meaning OUTSIDE a string literal — WHERE name = 'what?' is untouched. Supported argument types: string, bool, any integer type (long/int/
short/.../ulong), any floating-point type (double/float/decimal). A null (or DBNull)
argument is the SQL literal NULL (a value in INSERT/UPDATE SET; in a comparison it throws, write IS NULL). An argument-count mismatch or an unsupported CLR type throws HoloException (e.g. DateTime — there is no date
column type; encode a date as an INT day/epoch number instead). Database.Bind(sql, args) is
also exposed directly if you want the literal SQL text without executing it (e.g. to log it). A bare
Bind(sql, null) is one NULL argument. A name in the SELECT list that is not a column of the table
throws HoloException: no such column 'name'.
var db2 = new HoloDbService(new HoloDbOptions());
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 when it is declared NULL: CREATE TABLE orders (id INT PRIMARY KEY, region TEXT NULL, amount INT NULL, qty INT NOT NULL).
Every other column is NOT NULL (NOT NULL is accepted and means the default), and a primary key is never NULL. A NOT NULL column that is left out
of an INSERT still gets its type zero (0/0.0/""); a NULL column that is left out is NULL. NULL is a value in INSERT, UPDATE .. SET,
ON CONFLICT .. SET, ALTER .. DEFAULT, Database.Bind(.., null), RowChange.Upsert (a C# null) and Database.BulkLoad(.., nulls); it is refused
(with a message that says to write IS NULL) in = != < > <= >=, IN, BETWEEN, LIKE, NEAREST (..) and a join ON, because a comparison with NULL is never true.
- Three-valued logic.
WHEREandHAVINGkeep a row only when the predicate is true. A comparison,LIKE,INorBETWEENon aNULLis unknown;false AND unknownis false,true OR unknownis true,NOT unknownis unknown.x NOT IN (SELECT ..)is never true when the subquery returns aNULL.IS NULLandIS NOT NULLare always true or false. - Aggregates skip
NULL.COUNT(*)counts rows;COUNT(col),COUNT(DISTINCT col),SUM,AVG,MINandMAXcount and combine the non-NULLvalues.SUM,AVG,MINandMAXof a group whose values are allNULLareNULL. Whole-tableCOUNT,SUM/AVG/MIN/MAXof anINTcolumn and a one-columnGROUP BYwithCOUNT,SUMandAVGstay maintained answers withNULLs present (no row is read). On an empty selectionSUMis0andAVGis0.0, as for aNOT NULLcolumn. GROUP BYandDISTINCTmake one group (one value) of all theNULLs, apart from0and''.ORDER BYputsNULLfirst ascending and last descending. AnINNER JOINnever matches aNULLkey, not even anotherNULL.NEAREST: aNULLcell adds nothing to the row's similarity.- Results. A result that holds a
NULLis row-form: read it withRowsorGetValue, where the cell isDBNull.Value;QueryResult.IsNull(row, col)says so.GetLong,GetDoubleandGetTextserve a non-NULLcell of the matching type and throw aHoloExceptionon aNULL. - Where it stops.
CREATE INDEX(sorted, text, composite) takesNOT NULLcolumns only. A Pleroma plane stores the type zero for aNULLand declines (the engine answers) any statement that touches a plane column holding one, so it never answers wrongly. A plane-only (summary) table has no rows to fall back on and refuses aNULL(map it to a label, or add an indicator measure). Paged tables do not takeNULLcolumns. - Speed. A column that has never received a
NULLhas no null bitmap, and a statement that touches no column holding aNULLand has noIS [NOT] NULLtakes the routes it takes for aNOT NULLcolumn. The maintained answers listed above stay flat withNULLs present. Any other statement on a column that holdsNULLs filters with a null-aware compiled predicate and then builds boxed rows (aSELECT *or a filtered aggregate, for example), slower than the typed columnar result aNOT NULLcolumn returns. A table that declares aNULLcolumn is not read lock-free, and the live snapshot does not serve a statement that references a declaredNULLcolumn, even before it holds aNULL: declare a columnNULLonly where you need it. - Older binaries. A log with
NULLin it, or a snapshot of a table with aNULLcolumn (format 6), is refused by an older HoloDb binary with a clear error until a newer one has opened it and calledCheckpoint(); a database with noNULLcolumn writes the same files as before.
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 (the NULL region is neither equal nor different)
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
// A change feed carries NULLs as C# nulls; BulkLoad takes a mask per column (true = NULL) and a left-out nullable column is NULL.
db.ApplyChanges("orders", new[] { RowChange.Upsert(new Dictionary<string, object?> { ["id"] = 4L, ["region"] = null, ["amount"] = 5L, ["qty"] = 1L }) });
db.BulkLoad("orders", new[] { "id", "amount", "qty" }, new Array[] { new long[] { 5, 6 }, new long[] { 0, 7 }, new long[] { 1, 1 } }, 2,
new bool[]?[] { null, new[] { true, false }, null });
Console.WriteLine(db.Execute("SELECT COUNT(*) FROM orders WHERE amount IS NULL").Rows[0]["count(*)"]);
// 2 (id 3 and id 5)
SELECT: projection, DISTINCT, aggregates
SELECT [DISTINCT] col [, col ...] | agg(col) | *
FROM t
[INNER JOIN t2 ON ...]
[NEAREST (col = val, ...)]
[WHERE ...]
[GROUP BY col [, col ...]]
[HAVING ...]
[ORDER BY col [ASC|DESC] [, ...]]
[LIMIT n]
Aggregate functions: COUNT, SUM, MIN, MAX, AVG (COUNT(*) and COUNT(col) both
supported). Result columns for aggregates are named in lower-case agg(col) form, e.g.
count(*), sum(amount) — not the SQL text as typed, never aliased. AVG over an INT column
returns a double, never integer-truncated.
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
JOIN
One INNER JOIN ... ON a = b per query (JOIN/INNER JOIN are equivalent). Qualify a projected or
join-condition column with table.column when both sides share a name.
Anything else is refused with a HoloException naming it, never answered as if it were an inner join: LEFT/RIGHT/FULL [OUTER] JOIN, CROSS JOIN, NATURAL JOIN, JOIN ... USING (...), and comma joins (FROM a, b). Table aliases
(FROM orders o, FROM orders AS o) are refused too: write the table name (orders.id). The same refusal covers any text left
over after a complete statement (OFFSET, UNION, ROLLBACK TO ..., a second statement after ;), so a clause the dialect does
not have can never be silently dropped.
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
WHERE operators
AND / OR / NOT / parentheses, comparisons (= != < > <= >=, <> is accepted as a synonym
for !=), LIKE (% = any run of characters, _ = any one character, case-insensitive),
IN (v, ...) / IN (SELECT ...) (a scalar subquery), BETWEEN lo AND hi, each with an optional
leading NOT.
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)");
db.Execute("SELECT name FROM p WHERE name LIKE 'Widget%'"); // Widget, Widgetpro
db.Execute("SELECT name FROM p WHERE id IN (1,2)"); // Widget, Gadget
db.Execute("SELECT name FROM p WHERE price BETWEEN 50 AND 100"); // Gadget
db.Execute("SELECT name FROM p WHERE id IN (SELECT id FROM p WHERE price > 50)"); // Gadget, Widgetpro
GROUP BY / HAVING, ORDER BY, LIMIT
GROUP BY accepts one or more columns; HAVING may reference an aggregate (count(*),
sum(col), …) or a grouped column. ORDER BY accepts multiple keys with independent ASC/DESC.
LIMIT n caps the row count (used alone or with NEAREST's top-k). See the aggregates example
above — the same clauses apply there.
These combinations are refused with a HoloException because the engine would otherwise ignore one of the clauses: HAVING
without GROUP BY (filter rows with WHERE), DISTINCT together with an aggregate (use GROUP BY, or COUNT(DISTINCT col)),
and LIMIT 0 on an aggregate-only query. NEAREST accepts only a projection, WHERE and LIMIT: with JOIN, GROUP BY,
HAVING, ORDER BY (the rows already come back ranked; select score to read the ranking), DISTINCT or an aggregate it is refused.
NEAREST — holographic similarity search
SELECT ... FROM t NEAREST (col = val [, col = val ...]) [WHERE ...] [LIMIT k]
Ranks rows by cosine similarity of their holographic encoding to a partial key (any subset of
columns, any mix of INT/TEXT/REAL) and returns the top k (LIMIT; omitted = all rows,
ranked). No separate vector index to build or keep in sync — it is derived from the same columns
the table already holds, incrementally updated on INSERT (O(1) per new row). Below ~8,000 rows
ranking is exact (brute-force cosine over every row); at/above that it switches to an approximate,
sub-linear LSH candidate search — both return the same shape of result, just traded for speed at
scale. An UPDATE/DELETE/ALTER TABLE on the table forces the next NEAREST to rebuild its
index (bucket removal isn't tracked incrementally).
Similarity is graded, not exact-match-only (fixed 2026-10, customer finding F-04): an INT/
REAL column scores a close value higher than a far one (e.g. querying price = 1000, 900 and
1100 rank right behind the exact match, well ahead of 1800), and a TEXT column scores a
near-identical string higher than an unrelated one via character-trigram overlap (e.g. querying
name = 'ACME LTD', 'ACME LTD.' ranks well ahead of 'BOLT SUPPLIES'). This grading is a
well-behaved LOCAL/mid-range decay, not a global guarantee that EVERY pair orders strictly by
distance — two values several orders of magnitude apart can occasionally tie or swap rank with each
other (both score low either way); it reliably separates "close" from "far", not every far value
from every farther one. Add score to the projection to see the ranking metric itself (cosine
similarity, descending, 1.0 for an exact match):
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
score is a pseudo-column, like an aggregate's agg(col) name — it only appears when you ask
for it by name; SELECT * keeps returning just the table's own columns (no shape change for
existing callers). It is not a real schema column, so it can't appear in WHERE/GROUP BY/etc.
Transactions: BEGIN / COMMIT / ROLLBACK
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
A multi-statement transaction is a series of separate Execute calls on the same Database
instance — the engine's internal lock releases between them, so if multiple callers share one
Database concurrently, holding a transaction open across statements from different callers is
the caller's own responsibility (serialise/gate it yourself). BulkLoad cannot run inside an
explicit transaction (throws).
BulkLoad — fast typed-array ingest
int BulkLoad(string table, IReadOnlyList<string> cols, IReadOnlyList<Array> data, int count)
Appends count rows from typed column arrays (long[] for INT, double[] for REAL,
string[] for TEXT) — one array per name in cols — bypassing the SQL parser entirely. Atomic
(validates every key up front); throws on a duplicate primary key, same as INSERT. A column
omitted from cols is filled with its type's zero value, exactly like a partial INSERT.
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
Atomic change batches and upsert
A live replica turns an ordered change feed from another system into one call per batch. ApplyChanges(table, changes)
on Database (and ApplyChanges / ApplyChangesAsync on HoloDbService) applies upserts and deletes atomically:
one write lock, so a reader sees the table before the batch or after it, never part of it, on every read path including
the lock-free live metrics. The batch is one WAL commit group (one fsync, all or nothing on replay). A table with a
Pleroma plane takes the whole batch in one pass.
RowChange.Upsert(row)carries a full row (every column, case-insensitive names; INT takes any integral type, REAL a float or integral, TEXT a string; no NaN; a C#nullis SQLNULLand is accepted only for a column declaredNULL). It inserts a new key or replaces the whole row of an existing one. A missing column throws: there is no default-on-omit, so an omitted column would silently become0,""orNULL. For a partial update use the SQL form, where theSETlist says which columns change.RowChange.Delete(key)deletes by primary key; a key that is not there is a counted no-op.- Changes count as if applied in order (the last change of a key wins); only the net effect runs.
ChangeResulthasInserted,Updated,Deleted,MissingDeletes(andApplied). - Everything is validated before anything changes. A failure leaves the table, its accumulators and its plane as they
were (up to physical row order after a rolled-back delete, as for
ROLLBACK). - Allowed inside
BEGIN..COMMIT(the batch then commits or rolls back with it). A paged table throws (HoloExceptionnaming the table). - The log holds the net effect as plain
DELETE,UPDATEandINSERTlines, so an older binary can replay it: the upsert keywords never reach the log. Text keeps every character it was given, line breaks and backslashes included: a statement with a line break is written to the log escaped (S \e ...) and read back exactly. An older HoloDb binary refuses a log that holds such a line, with a clear error, until a newer binary has opened it and calledCheckpoint(); a log with no line break in any text is unchanged.
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
The SQL form logs the same net-effect lines. It reports Affected = rows inserted plus rows updated. It is the only way to
change some columns of a row and keep the rest.
Several tables in one step
A source transaction that touches a fact row and its dimension row, or a batch plus the position it came from, must not be
seen half applied. ApplyChanges(IReadOnlyList<TableChanges>) (on Database and HoloDbService, with
ApplyChangesAsync) applies the changes of several tables as one atomic step: one write lock, one undo journal, one WAL
commit group, and each table's plane and live snapshot maintained once. A reader (a single statement such as a JOIN, or any
later statement) sees every table before the batch or every table after it.
- Every table, key and value is validated before the first change; a failure leaves all tables and planes exactly as they were. Unknown table, a table named twice (put its changes in one entry) and a paged table are refused by name.
- Tables apply and log in list order; each table's changes behave as in the single-table call. You get one
ChangeResultper entry, in list order. Allowed insideBEGIN..COMMIT(the whole batch commits or rolls back with it). - The lock is released between statements, so a cross-table invariant can only be read in ONE statement (a join); two separate
Executecalls can still straddle a batch.
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
Comparing key sets: KeyDigest
KeyDigest(table, modulus) returns, for an INT primary key, one KeyBucket(Count, Sum) per bucket (Sum is an exact
Int128; bucket b holds the keys with floor(key mod modulus) = b, so negative keys land in [0, modulus) too) in one
pass over the key column: a fingerprint of modulus numbers however many rows the table has. A source computes the same
numbers with a GROUP BY on the same bucket (for a power-of-two modulus, pk & (m - 1)), so two systems exchange
modulus small rows instead of every key and list keys only for the buckets that differ. Equal buckets do not prove equal
keys (two keys that left and two that arrived with the same total cancel), so keep an occasional full comparison as the
backstop. A TEXT key or a paged table is refused.
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: the plane without the rows
A summary table keeps no rows. It stores only the Pleroma plane (the exact summaries behind COUNT, SUM, GROUP BY and
WHERE), the value dictionaries and the position of each feed, so its memory does not depend on how many rows went through
it. The plane-servable reads still answer; everything else is refused by name, because there is no row to scan.
CREATE SUMMARY TABLE sales (region TEXT DIMENSION, channel TEXT DIMENSION, units INT MEASURE, revenue INT MEASURE)
[WITH (max_dim_bits = 10, max_joint_bits = 12)]
or Database.CreatePlaneOnly(table, dimensions, measures, maxDimBits, maxJointBits). 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 (exact, Int128 inside). The
columns are fixed when the table is created: a new dimension means a new table filled again from the source.
- Writing.
ApplyPlaneChanges(table, source, position, changes)(also onHoloDbService, with an async form) takesPlaneChange.Add(row),PlaneChange.Remove(row)andPlaneChange.Replace(oldRow, newRow). A row is an array in the table's declared column order or a name-to-value dictionary with every column present. ARemovecarries the row's values: the table cannot look them up. The whole batch is one atomic plane pass and one log commit group. - Exactly once. There is no key to deduplicate on, so a replayed batch would count twice. Every batch carries the
positionitssourcehas reached; a batch at or below the position already applied for that source is a no-op and comes back withSkipped = true. The position is a number (an offset, a sequence number, a version) or opaque bytes compared lexicographically as unsigned numbers (a SQL Server LSN is 10 big-endian bytes; use one fixed width per source). It is saved in the same log statement and snapshot as the counts.PlanePosition(table, source)(numbers) andPlanePositionBytes(table, source)read it back (null before the first batch). - No negative counts. The changes are walked in order before anything is applied. If a
Removewould take the count of a dimension-value combination below zero (the row was never added, or was already removed) the whole batch is refused: no count, dictionary entry or position changes. ARemovewith the right dimensions and a wrong measure value cannot be detected here; compare the totals with the source (PlaneTotals(table)returns the row count and the exact sum of every measure). - Caps. A dimension holds at most
2^max_dim_bitsdistinct values (at most 4,096) for the life of the table (a value never leaves its dictionary), and all dimensions together at mostmax_joint_bitsflag bits (at most 16). A batch that would pass a cap is refused whole, with the cap named; theHoloExceptioncarriesCode(HoloErrorCodes.PlaneDimensionCaporPlaneJointCap),Dimension,CapandNeeded, so a caller can tell a cap from any other failure without reading the message. Each distinct value costs about 250 bytes (the dictionary entry plus the extra plane cells its bit width forces). - Export and import.
ExportPlane(table)returns the whole state of a plane-only table as one blob (columns and caps, every source's position, the value dictionaries and the exact cells), behind a magic, a format version and a SHA-256 checksum. Its size follows the plane's bit widths, not the number of rows counted.ImportPlane(table, blob, replace: false)makes a plane-only table from a blob (under any name;replace: trueoverwrites a table of the same columns and caps in one step). The blob is checked before anything changes: a damaged, truncated or altered blob is refused withHoloErrorCodes.PlaneBlobCorrupt, one from a newer format withPlaneBlobVersion, and nothing is imported. On a database with a log the import is made durable by an immediate checkpoint. The copy is a live table: it keeps its positions, so a replayed batch is still skipped. - Reading.
COUNT,SUM,AVGof the measures;MIN,MAXandCOUNT(DISTINCT ...)of a dimension;WHEREover dimensions (=,!=,<,IN,BETWEEN,LIKE,AND/OR/NOT);GROUP BY,HAVINGoverCOUNT/SUM,ORDER BY,LIMIT. Not served, and refused with a message that names the reason:SELECT *or any row list, aWHEREon a measure,MIN/MAXof a measure or of an empty selection, joins,DISTINCT,NEAREST, and every row statement (INSERT,UPDATE,DELETE,ALTER,CREATE INDEX,ApplyChanges,BulkLoad).EXPLAINandRouteTracereport the plane route;PleromaModedoes not apply (the plane is the only route). - Durable. With a write-ahead log the table is saved in the snapshot (format v5, written only when a summary table
exists) as its dictionaries, positions and the exact cells of the plane, and every batch is a log statement. A reopen
restores the same counts and positions; re-sending the whole feed after a crash lands on the same state, whatever the
crash point. An older binary refuses a database that holds a summary table.
PleromaVerify(table)checks the plane against itself (totals = every dimension = the joint, no negative count), since no row exists to rebuild it from. Not allowed insideBEGIN; in-RAM databases only.
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)");
// A feed delivers rows with the position it has reached; the table keeps no rows, only the summary.
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 }),
};
var r = db.ApplyPlaneChanges("sales", "orders-feed", 100, batch);
Console.WriteLine($"{r.Added} added, skipped: {r.Skipped}");
// 3 added, skipped: False
// The same batch again (a replayed feed) is a no-op.
Console.WriteLine($"replay skipped: {db.ApplyPlaneChanges("sales", "orders-feed", 100, batch).Skipped}");
// replay skipped: True
// A row changed at the source: send the old image and the new one.
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
// A remove must name a row that was added; otherwise the whole batch is refused and nothing changes.
try { db.ApplyPlaneChanges("sales", "orders-feed", 102, new[] { PlaneChange.Remove(new object?[] { "east", "web", 1L, 1L }) }); }
catch (HoloException e) { Console.WriteLine(e.Message.Split(':')[0]); }
// plane-only table 'sales'
// There are no rows to list.
try { db.Execute("SELECT * FROM sales"); }
catch (HoloException e) { Console.WriteLine(e.Message.Split(". ")[0]); }
// plane-only table 'sales' cannot answer this statement: SELECT * lists rows
Measured (verb planeonly scale, each point in its own process): with 5 columns and 3 dimensions of 4, 3 and 7 bits, the table
holds 1.82 MB at 100,000 rows and at 10,000,000 rows (log-log slope 0.00 in rows); the same data kept as rows takes 441 MB
at 2,000,000 rows (a narrow copy with only these columns: 204 MB). A filtered or two-column read costs 0.2 to 0.4 ms at any size, where the
engine's own scan of the same two statements takes 7.6 and 16 ms at 2,000,000 rows. A snapshot is 0.9 MB and reopens in about 2 ms whatever the row
count; the log before a checkpoint costs about 35 bytes per row. Applying costs about 0.6 to 0.9 us per row once a batch holds a few
hundred rows (2,048 rows as single-row calls cost 30 to 40 times as much); a batch that contains removes pays one extra read of
the count plane (about 1.1 ms at 14 joint bits).
QueryResult — reading results
Shape reference (abbreviated — bodies omitted, not literal source):
public sealed class QueryResult
{
public int Affected { get; } // rows affected (mutations) / returned (SELECT)
public List<Dictionary<string,object>> Rows { get; } // lazily materialised row-dict view; always safe
public bool IsColumnar { get; } // true for a plain, non-aggregated projection scan
public int RowCount { get; }
public IReadOnlyList<string> Columns { get; }
public bool IsIntColumn(int col); public bool IsRealColumn(int col);
public long GetLong(int row, int col); public double GetDouble(int row, int col); public string GetText(int row, int col);
public object GetValue(int row, int col); // works for both columnar and row-dict results
public static QueryResult FromRows(List<Dictionary<string,object>> rows, string[] columns); // rebuild from wire data (HoloDb.Protocol)
}
.Rows (a case-insensitive Dictionary<string,object> per row) always works, for any query
shape, and is the simplest way to read a result. IsColumnar is true only for a plain
projection scan — SELECT col[, col...] FROM t [WHERE ...] [ORDER BY ...] [LIMIT ...] with no
aggregate, GROUP BY, JOIN, DISTINCT, or NEAREST — in which case GetLong/GetDouble/
GetText read the underlying typed arrays with zero boxing, the fast path for large result sets.
For every other query shape (aggregates, GROUP BY, JOIN, DISTINCT, NEAREST), use .Rows or
GetValue. GetLong/GetDouble/GetText also work on a row-form result when the cell holds that
type (GetLong on count(*)); a NULL cell, a cell of another type or an out-of-range index throws a
HoloException naming the column (test IsNull first).
var db = new HoloDbService(new HoloDbOptions());
db.Execute("CREATE TABLE t (id INT PRIMARY KEY, name TEXT)");
db.Execute("INSERT INTO t VALUES (1, 'Ada'), (2, 'Bo')");
var r = db.Execute("SELECT id, name FROM t WHERE id >= 1 ORDER BY id"); // plain projection -> columnar
Console.WriteLine(r.IsColumnar);
// True
for (var i = 0; i < r.RowCount; i++) Console.WriteLine($"{r.GetLong(i, 0)}:{r.GetText(i, 1)}");
// 1:Ada
// 2:Bo
Durable, larger-than-memory (paged) tables
For data that outgrows RAM, create a paged table: stored on a buffer-pool + write-ahead-log
substrate (plain FileStream-based, not memory-mapped), so it is durable and its data can
exceed memory — cold pages demand-page back from disk. Whole-table aggregates and single-column
GROUP BY count/sum stay O(1) regardless of table size. INSERT/UPDATE/DELETE/SELECT work
through the normal Execute(sql) path once the table exists; transactions, primary-key uniqueness
(with O(1) point lookup), a single INNER JOIN (COUNT/SUM only — no GROUP BY over a join,
and no multi-way join), GROUP BY/HAVING, ORDER BY, LIMIT, DISTINCT, and NEAREST are all
supported on a paged table. The primary-key index is kept in RAM (about 35 bytes/row, measured 34 to 39) — for a table far
larger than RAM, that index, not the buffer pool, is the limiting resource.
CreatePaged(createTableSql, dir, bufferFrames = 8192)— create a table backed by the paged store indir(RAM budget =bufferFrames× 8 KB). Returns the table name.OpenPaged(dir, ifNotExists = false)— re-open a paged table previously created indir, recovering committed state from its log.ifNotExists: if a table of that name is already registered on thisDatabase, return its name instead of throwing.PagedBulkLoad(table, rows, commitEvery = 4096)— streamobject?[]rows (schema column order) into a paged table, committing everycommitEveryrows. Returns the row count.ClosePaged(table)— checkpoint and close one paged table's store.
var db = new HoloDbService(new HoloDbOptions());
var dir = Path.Combine(Path.GetTempPath(), "orders-paged");
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); // recovers from the log
Console.WriteLine(db.Execute($"SELECT count(*) FROM {reopened}").Rows[0]["count(*)"]);
// 2
db.ClosePaged(reopened);
Whole-database paged-default mode (HoloDbOptions.Paged = true, or Database.Open(walPath, ckpt, paged: true)) lets a plain CREATE TABLE/BulkLoad target the paged substrate automatically
— no CreatePaged/PagedBulkLoad call needed. It only takes effect for a brand-new database
(no existing WalPath/WalPath + ".snap" on disk); opening an existing legacy database with
Paged = true silently falls back to the in-RAM engine — check Database.IsPagedDefault to see
which mode you actually got.
var dir = Path.Combine(Path.GetTempPath(), "pageddefault-demo");
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();
Pleroma aggregate plane and COUNT(DISTINCT)
An optional, derived summary of a table's small-domain columns, held in Phasor Pleroma stores. The columns stay the source of truth; the plane is rebuilt from them at any time and never answers approximately. Pick dimensions (INT or TEXT columns, used by WHERE, GROUP BY, COUNT(DISTINCT), MIN/MAX and join keys) and measures (INT columns whose SUM is served; exact past Int64 internally).
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)");
// attach the plane: dimensions answer WHERE / GROUP BY / COUNT DISTINCT, measures answer SUM
db.EnablePleroma("sales", new PleromaPlaneOptions(new[] { "region", "tier" }, new[] { "qty" }));
db.PleromaMode = PleromaMode.Always; // serve every shape the plane covers (the default, Auto, leaves O(1) accumulator shapes to the engine)
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"); // writes keep the plane current
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
The same plane from plain SQL (so a remote client can ask for it too):
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)");
db.Execute("CREATE INDEX by_region ON sales (region, tier) INCLUDE (qty) USING SUMMARY");
var show = db.Execute("SHOW INDEXES ON sales");
Console.WriteLine($"{show.GetValue(0, 1)} {show.GetValue(0, 2)} {show.GetValue(0, 3)}"); // by_region summary region,tier
db.PleromaMode = PleromaMode.Always;
Console.WriteLine(db.Execute("SELECT SUM(qty) FROM sales WHERE region = 'north'").Rows[0]["sum(qty)"]); // 6
db.Execute("DROP INDEX by_region");
- Served by the plane: COUNT and SUM (with any AND/OR/NOT/LIKE/IN/BETWEEN over dimensions), GROUP BY on one or several dimensions with HAVING on count/sum,
COUNT(DISTINCT dim), MIN/MAX of a dimension, and a single equi-joinCOUNT/SUMbetween two tables that both have planes. - Declined (the engine answers): AVG, columns that are not plane dimensions or measures, a dimension past
MaxDimBitsdistinct values, multi-dimension queries when the joint width exceedsMaxJointBits, joins with a WHERE or more than 4095 distinct keys. A declined query is never wrong, only unaccelerated. PleromaMode:Auto(default) leaves whole-table and single-column GROUP BY to the engine's own O(1) accumulators;Alwaysserves every covered shape;Offbypasses the plane while it is still maintained.- SQL form:
CREATE INDEX sm ON sales (region, tier) INCLUDE (qty) USING SUMMARYbuilds the same plane, works throughHoloDb.Serverlike any statement, is replayed from the write-ahead log onOpen, survivesCheckpoint()(the snapshot stores the definition and rebuilds the plane at load), and is undone byROLLBACK.DROP INDEX smremoves it;SHOW INDEXESreports its size, served/declined counts and over-cap dimensions. - Lifecycle: in-RAM tables only. A plane attached with
EnablePleromais not persisted: call it again afterDatabase.Open(a SQL-created one is persisted).ROLLBACKrestores it with the table.PleromaStatsreports sizes and served/declined counts;PleromaVerifycompares it with a fresh rebuild. - Sums past Int64: the plane raises an error instead of wrapping;
PleromaSumExactreturns the exactInt128. COUNT(DISTINCT col)is a general SQL addition: it must be the only kind of select item (severalCOUNT(DISTINCT ...)items are allowed), with no GROUP BY or JOIN, and is not available on paged tables.- Memory: the plane's cells are about 0.92 MB for a 14-bit joint whatever the row count (plus the same again in pending deltas before a flush), and attaching a plane added 1.9 to 4.6 MB of heap in the benchmark (measured at 100,000 to 1,000,000 rows).
Pleroma page store (the Pleroma as the data)
PleromaPageStore keeps a family of Phasor Pleroma pages of one shape (for example dense CompleteKeys pages, one per context) durably and larger than memory, with no columns behind them. Each page is saved through RawCells and RawCheckCells into HoloDb's paged buffer pool and write-ahead log; hot pages are live Pleroma objects in a byte-budgeted cache, cold pages load on demand.
using HoloDb;
using Phasor;
var shape = new HologramShape { KeyCapacity = 2, KeyCycle = 110, CompleteKeys = true }; // every page: 110 exact key counts
var dir = Path.Combine(Path.GetTempPath(), "plrstore-demo");
if (Directory.Exists(dir)) Directory.Delete(dir, true);
using (var store = PleromaPageStore.Open(dir, shape))
{
// adds are journaled before they are applied; page 42 and page 7 are created on first use
store.AddBatch(new[] { new PageAdd(42, 5, 3), new PageAdd(42, 77, -2), new PageAdd(7, 5, 1) });
store.AppendEvent("context 42 seen"u8); // an opaque owner event, ordered with the batches
Console.WriteLine($"{store.Counts(42)[5]} {store.Counts(42)[77]}"); // 3 -2
}
using (var store = PleromaPageStore.Open(dir, shape)) // reopened: the pages come back exactly
{
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 (every record's check and parity group)
Console.WriteLine(store.Stats().Pages); // 2
}
Directory.Delete(dir, true);
- API:
Open(path, shape, options),AddBatch(IReadOnlyList<PageAdd>)(returns the batch's sequence number),Get(pageKey)(a copy; null if absent),Read(pageKey, fn)(zero-copy),Counts(pageKey),Contains,PageKeys,KeysInRange(lo, hi)(children or prefix scan when the owner packs a parent into the high bits of the key),AppendEvent/ReplayEvents/TrimEvents,Checkpoint,Checkpoints,RestoreTo,Verify,Repair,Stats,Dispose. Page keys are non-negative longs. - Concurrency: readers never take a store-wide lock. A read holds a shared lock on its page's stripe (1,024 stripes), so it waits only for a write to a page on the same stripe, or for
Verify/Repair/RestoreTo. One writer at a time. A batch is applied page by page: a reader sees every page whole, but may see a batch applied to some pages and not yet to others. A stress test compares concurrent readers with a single-threaded oracle. - Writes: every
AddBatchis appended to the journal (fsynced unlessSyncEveryBatchis false) before it is applied. Each page remembers the sequence number of the last batch applied to it, so crash recovery replays a logged add exactly once, even over pages the cache had already flushed. A torn journal tail is discarded, so a batch survives whole or not at all. - Restore points:
Checkpoint()returns aPleromaCheckpoint(id and journal sequence number).RestoreTo(id)returns the store to exactly that state, with no recount: pages written since go back, pages created since vanish, later points are dropped. It is copy-on-write: before a record is first overwritten after a restore point, its old bytes are appended to that point's undo file, so a restore point costs one before-image per page changed since, not a copy of the data file.MaxCheckpoints(default 8) bounds how many are kept. Events stay in the event journal (read them withReplayEvents(afterSeq)). - Event journal:
AppendEventstores an opaque record in the same sequence space as the batches;ReplayEventsreturns them in order;TrimEvents(upToSeq)drops the absorbed prefix and sequence numbers never repeat. - Redundancy: the store's shape carries a Phasor
CheckPrime: every page keeps a mirror of its cells in a second prime, both copies are saved, andPleroma.VerifyCheckproves a record intact (Verifyruns it on every page;VerifyOnLoadruns it on every load, about 100 microseconds for a 110-key page). Records are grouped (ParityGroup, default 8) with onePleromaParityexact-sum record per group (one over the main cells, one over the check cells), and the members of a group sit on different 8 KB pager pages, so any one lost pager page, or any one damaged record per group, is rebuilt byRepair. A rebuilt page must pass its own check before it is written; a member and its group's parity both damaged are reported as unrecoverable, never rebuilt wrong. - Cache:
CacheBytes(default 64 MB) bounds the live pages (clock eviction);Statsreports pages, resident pages and bytes, hits, misses and hit rate, page flushes, the last sequence number and the restore points kept. - Limits: one shape per store (reopening with another shape is refused); the shape must be
CompleteKeysand admissible for a check prime; shapes with a continuous component, a relation face or a non-unitTimeScalecannot be saved; one process opens a store at a time.
Sparse pages for a large vocabulary
PleromaSparsePageStore stores Phasor PleromaSparsePage pages (a histogram over the whole 2^61 key cycle whose size follows the keys present) with the same journal, exactly-once replay, striped concurrent reads and cache. Pages have different sizes, so records come in capacity classes: class c holds pages of capacity up to 2^c in a record of one fixed size, laid out as header, capacity, cells, support count and RawKeys (several records to an 8 KB pager page for small classes, a chain of pages for large ones). When a page outgrows its class the next flush writes it into the larger class and appends a new directory entry (the directory is last-entry-wins). AddBatch(IReadOnlyList<SparseAdd>), Get, Read, Counts, KeysInRange, Verify (header hash, then support against cells), Checkpoint, Stats (pages per class, class changes). Parity groups, restore points and the event journal are in the dense store only.
Dependency injection
public static IServiceCollection AddHoloDb(this IServiceCollection services, Action<HoloDbOptions>? configure = null)
Registers a singleton HoloDbService. Needs only IServiceCollection
(Microsoft.Extensions.DependencyInjection.Abstractions) — add the full
Microsoft.Extensions.DependencyInjection package (or use ASP.NET Core / Generic Host, which
already provides it) to get BuildServiceProvider().
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)
Attaches a HoloDbContext (an EvalApp GlobalContext wrapping .Service) to a pipeline app, so a
context-aware step (ContextPureStep<HoloDbContext, TDomain, T>) can reach global.Service inside
TransformAsync without threading a connection through every record. An app has one global context
(WithContext sets it; a second call replaces the first), and WithHoloDb is the shorthand that makes
that context a HoloDbContext. When the app needs the database and something else (another service, a
second database, other pipelines), the context is your own object that holds a HoloDbContext for each
database, in one app, as below; steps reach it as global.Db.Service.
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')");
// One app, one context object of your own: it holds the database (and anything else a step needs).
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
// Top-level statements must precede type declarations in a Program.cs:
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
(Task.Run) and are not gated by the pipeline; inside a step, call the synchronous Database as above.
Usage metering and the billing model
HoloDb.Analytics wraps a Database to report, per query, the WORK actually done versus what a
per-scan warehouse (BigQuery, Athena) would bill for the same query on the same metric (bytes
processed) — the spine of the metered analytics API this engine also runs behind.
Shape reference (abbreviated — bodies omitted, not literal source):
public readonly record struct QueryUsage(long RowsScanned, long TableRows, long RowBytes, long BilledBytes, double ElapsedMs)
{
public long BytesActuallyRead { get; } // RowsScanned * RowBytes — 0 for a maintained-accumulator aggregate
public bool ConstantTime { get; } // true iff RowsScanned == 0
public double MarginHeadroom { get; } // BilledBytes / BytesActuallyRead
}
public sealed record ScanPricing(string Name, double UsdPerTiB, long MinBytesPerQuery) { /* .BigQuery, .Athena, .All */ }
public sealed record HoloPricing(double UsdPerTiB = 0.625, long MinBytesPerQuery = 10L * 1024 * 1024) { public double CostUsd(QueryUsage u); public double DiscountVs(ScanPricing warehouse); }
public sealed class MeteredDatabase
{
public MeteredDatabase(Database db, HoloPricing? pricing = null);
public Database Inner { get; }
public (QueryResult Result, QueryUsage Usage) Run(string sql);
public double OurCostUsd(QueryUsage u);
public double ScanCostUsd(QueryUsage u, ScanPricing engine);
}
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 (served from the maintained accumulator, no scan)
Console.WriteLine(metered.OurCostUsd(usage) <= metered.ScanCostUsd(usage, ScanPricing.BigQuery));
// True
Database.LastRowsScanned/LastTableRows/LastRowBytes/LastLogicalBytes (static, per-thread)
are the lower-level counters MeteredDatabase.Run reads; use them directly if you call
Database.Execute yourself and just want the numbers without the pricing wrapper.
Parallel fan-out primitive (HoloPar)
public interface IParallelMap { void Map(int chunks, Action<int> body /* + compiler-filled caller info */); }
public static class HoloPar { public static IParallelMap Cpu { get; } public static bool Licensed { get; } }
The single parallel primitive the columnar query engine fans out through internally (never a raw
Parallel.For/new Thread) — exposed publicly, but you don't need to call it: every query path
that benefits from it already does. It runs on EvalApp's adaptive dispatch: each call site learns its own cost and runs inline when a
fan-out would not pay. Licensed is always true and is not a license/tier check — nothing
here is license-gated.
var sum = 0; var gate = new object();
HoloPar.Cpu.Map(4, i => { lock (gate) sum += i; });
Console.WriteLine(sum);
// 6
Advanced / low-level engine types
These are public types (the assembly ships them unmarked internal), but none of them is returned
by, or needed to call, Database/HoloDbService/SQL — they are the engine's own building blocks.
Documented here for completeness; prefer the SQL surface above for anything application-level.
| Type | What it is |
|---|---|
Holo (static) |
The row-hologram codec: EncodeRow(Schema, row) binds each column into a block-partitioned holographic vector; Similarity(a, b) is cosine similarity. This is what NEAREST runs on. NumberFace(v)/TextFace(s) are the graded per-value encodings behind it (geometric-period number ladder; character-trigram text bundle) — see the NEAREST section above. |
HoloLsh |
A locality-sensitive-hash index over row holograms (SimHash over random hyperplanes): Add, Approx (sub-linear top-k), Exact (brute-force top-k). What NEAREST builds over a table once it crosses ~8,000 rows. |
GrowColumn<T> |
An append-only, copy-on-grow T[] with a published count — the lock-free-readable column primitive under every in-RAM table. |
Table / Schema / Column / ColType / LiveMetrics / GroupRow |
The in-RAM table/schema primitives Database builds on (Schema requires exactly one PRIMARY KEY, INT or TEXT). Public by assembly visibility, not a supported construction path — build tables through Database/SQL, not new Table(schema). |
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
Invariants and gotchas
- Upsert. A plain
INSERTon an existing primary key throws. UseINSERT .. ON CONFLICT (pk) DO UPDATE SET col = EXCLUDED.colorApplyChanges(see Atomic change batches). - One global reader-writer lock.
SELECTtakes the shared read lock (many run concurrently); every mutation/DDL/transaction statement takes the exclusive write lock (serialised). A lock-free "live metrics" fast path serves pure-accumulator reads (whole-tableCOUNT/SUM, a single-columnGROUP BY) off a snapshot published after each write, so a streaming ingest never stalls a dashboard read. A statement that references a column declaredNULLis not served by it (NULL). - A multi-statement transaction is a series of separate
Executecalls — the lock releases between them, so cross-statement isolation between different callers sharing oneDatabaseis the caller's own responsibility. NEARESTsimilarity is graded but not a global total order (2026-10 fix, F-04): close values reliably rank ahead of far ones at local/mid range; it is not guaranteed that EVERY pair of far values orders strictly by distance from each other (both score low either way). See the NEAREST section above.- No
ASaliasing (columns or tables). It does not exist anywhere in the grammar today; a table alias in aFROM/JOINthrows. Projected columns always keep their raw/agg(col)name (readQueryResultby position if you want a different label). A column not declaredNULLhas a type-zero default (0/0.0/""), never a null state; see NULL for the columns that do. - Indexes are
INTorTEXTonly, neverREAL. REALcolumns never get a maintained accumulator (INTsums are exact Int128 running totals). Whole-tableREALSUM/MIN/MAX/AVGis a SIMD scan (still sub-millisecond at typical sizes); groupedREALSUMscans too — the one place this engine doesn't maintain a running accumulator.- Aggregate result columns are lower-case
agg(col), e.g.count(*),sum(x)— never the SQL text as typed, never an alias. - Paged tables have a narrower JOIN ceiling than in-RAM tables: one
INNER JOIN,COUNT/SUMonly, noGROUP BYover a join. This is exactly whyHoloDbOptions.Paged/Database.Open(..., paged: true)is an opt-in, not the library default. BulkLoadcannot run inside an explicit transaction (throws).- A legacy (non-paged) database's write-ahead log is literally SQL text, replayed by
re-parsing it on the next
Open. A build that understands a newer keyword (e.g.ALTER) writes that text into the log; an older binary's parser then throws on it and refuses to open the database until a newer binary checkpoints it first (Checkpoint()writes a fresh schema-only snapshot + empty log). Mixed binary versions sharing one on-disk legacy database must checkpoint on the newer binary before an older one opens that file. Paged tables have no such risk — their log is a generic, statement-agnostic physical redo log of raw pages. DebugType=none, no XML doc file, no PDB ship in the package — no IntelliSense doc text or symbol debugging from the NuGet package itself; this file is the documentation.HoloDbOptions.LicenseKey/Database's license plumbing is accepted but has no effect on which capabilities are available — nothing in this package is gated behind a tier or a license key.
Exceptions
Every error this engine raises deliberately (bad SQL, a schema violation, a duplicate key, an
unsupported shape) is a HoloException : Exception with a plain message — catch that one type:
public sealed class HoloException : Exception { public HoloException(string message); public string? Code { get; } public string? Dimension { get; } public int? Cap { get; } public int? Needed { get; } } // Code is set only for a plane-only cap refusal (HoloErrorCodes.PlaneDimensionCap / PlaneJointCap) or a refused plane blob (PlaneBlobCorrupt / PlaneBlobVersion)
File layout
- In-memory (
WalPath = null): nothing touches disk; state does not survive the process. - Legacy durable (
WalPathset,Pagednot set / existing legacy database):WalPathis an append-only text log of committed SQL;WalPath + ".snap"is the latest binary snapshot of all table data.Checkpoint()folds the log into a fresh snapshot and truncates the log. - Paged / durable larger-than-memory (
CreatePaged(sql, dir, ...), or whole-databasePaged = true): one self-contained paged store per table — a buffer pool + a physical page-level write-ahead log — under its own directory (dir, or one subdirectory per table underWalPath + ".tables"in whole-database paged mode), auto-discovered onOpen. No memory-mapped files anywhere in either storage shape.
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.
HoloDbOptions.LicenseKey is accepted but has no effect on which capabilities are available.
| 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 (>= 4.4.1)
- EvaluatedApplications.Phasor (>= 2.19.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
-
net8.0
- EvaluatedApplications.EvalApp (>= 4.4.1)
- EvaluatedApplications.Phasor (>= 2.19.1)
- 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 |