Black Rainbow LabsInillucent
Documentation for 0.1.7

Build with
Inillucent

Install it with your agent

Paste this into Claude Code, Codex, Cursor or any agent that can fetch a page and run a command.

Download and install Inillucent on this machine. It is an embedded SQL database with vector and keyword search, in one file, with an MCP server.

1. Fetch https://inillucent.com/install.md and follow the steps for my operating system.
2. Confirm it worked by running `inillucent --version`.
3. Register the MCP server with this agent using the MCP section of that page.
4. Then tell me the version you installed, where the four programs are, and the command that creates my first database.
01

What Inillucent is

Inillucent is an embedded database written in Rust. It stores relational data, full-text indexes, vector indexes, and hybrid retrieval indexes in the same file and runs inside your application.

Embedded means there isn't a database server to install or keep alive, your program opens a file, sends SQL to a library in the same process, and receives typed rows back. We can reach the same engine from a terminal through inillucent-shell.

The SQL dialect follows SQLite 3.53.4 closely. If you have used SQLite, most statements will look familiar. If SQL is new to you, start with the next four chapters and type the examples in order.

  • Relational tables use B+trees, snapshot isolation, and a redo write-ahead log.
  • VECTOR(N) columns and inillucent_hnsw indexes put nearest-neighbor search in ordinary SQL.
  • inillucent_search combines lexical and vector retrieval, adds calibrated confidence, and can decline to answer.
  • Unsupported features fail with a named error instead of returning an approximate answer.

The first query

SELECT asks for a value, and this query doesn't need a table, so it's a safe way to confirm the shell and engine are working.

SQL / input
SELECT 'Inillucent is ready' AS status;
Resultobserved
|       status        |
|---------------------|
| Inillucent is ready |
02

Build and open a database

Install the four programs, then give inillucent-shell a file name. The file is created when it doesn't exist and reopened when it does.

One line installs Inillucent, and the two commands are the first example below rather than part of this sentence, because a command set in a paragraph is a command a reader has to retype. Each downloads the archive for the machine, checks its SHA-256 against the published sums, and puts the four programs on PATH without administrator rights. There is no macOS archive yet, so a Mac builds from a checkout; https://inillucent.com/install.md carries the steps for all three.

An .rdb suffix is a convention, not a requirement. The path can be relative or absolute. Keep the file on a local filesystem. One process owns it by default, because PRAGMA locking_mode is exclusive; set it to normal and several processes can share one file, over the same lock protocol SQLite uses.

Install it, one line

The first line is Windows and the second is Linux; run the one for this machine. Both check the archive against the published SHA-256 before unpacking it and write nothing outside the home directory. They are a code block with a Copy button rather than a sentence, because a pipe into a shell is exactly the kind of line that gets mistyped.

Shell / run one
irm https://inillucent.com/downloads/install.ps1 | iex
curl -fsSL https://inillucent.com/downloads/install.sh | sh
Thenobserved
$ inillucent --version
inillucent 0.1.2

Build and identify the shell

From a checkout, a release build enables the same fat link-time optimization and single code-generation unit the performance gates use. The version output names both the SQL compatibility target and the first-party engine underneath it. Remove -version to enter the prompt, then type .help for shell commands.

Shell / input
cargo build --release -p inillucent-cli
target/release/inillucent-shell library.rdb -version
Terminalobserved
SQLite 3.53.4
inillucent (a first-party engine implementing the SQLite ABI)
03

Use the shell

The shell is shaped like sqlite3. It reads SQL from the prompt, a command argument, or standard input, and it can render the same rows as a table, CSV, JSON, Markdown, etc.

Dot commands configure the shell and don't go to the SQL parser, while SQL statements do. We use .headers on with .mode table while learning, because column names make a result easier to check.

The -bail option stops a script at its first error. That is useful in automation, where continuing after a failed CREATE TABLE can make every later message misleading.

  • .help lists the shell commands available in this build.
  • .tables lists the tables and views in the open database.
  • .schema prints the stored CREATE statement for each object.
  • .mode changes how every following result is rendered.
  • .parameter manages bound values that can be reused safely.
  • .quit closes the current session and its database file.

Run one query without entering the prompt

Flags may be combined, so this returns a Markdown table with a heading and exits.

SQL / input
inillucent-shell -markdown -header library.rdb "SELECT 2 + 2 AS answer;"
Terminalobserved
| answer |
|--------|
|      4 |
04

Values, types, and NULL

A SQL value is NULL, an integer, a real number, text, or a blob. A column type describes how values should be converted and, in a STRICT table, what the column may hold.

NULL means missing or unknown, and it isn't the same value as 0 or an empty string. We use IS NULL and IS NOT NULL to test it, because NULL = NULL is unknown rather than true.

Ordinary tables use SQLite affinity. A value such as the text 42 is converted when it enters an INTEGER column. STRICT adds type enforcement after affinity has had that chance to convert it.

  • INTEGER stores signed whole numbers without a fractional part.
  • REAL stores floating-point numbers with a fractional part.
  • TEXT stores text as a sequence of UTF-8 bytes.
  • BLOB stores arbitrary bytes without trying to interpret them.
  • VECTOR(N) stores exactly N little-endian 32-bit floating-point numbers.
  • ANY in a STRICT table deliberately accepts every SQL value type.

See the storage class of each value

typeof() reports the class held by the value after column affinity was applied.

SQL / input
CREATE TABLE measurements (whole INTEGER, decimal REAL, note TEXT, missing TEXT);
INSERT INTO measurements VALUES ('42', 3, 7, NULL);
SELECT typeof(whole), typeof(decimal), typeof(note), missing IS NULL AS is_missing
FROM measurements;
Resultobserved
| typeof(whole) | typeof(decimal) | typeof(note) | is_missing |
|---------------|-----------------|--------------|------------|
| integer       | real            | text         |          1 |
05

Tables and constraints

A table gives rows a durable shape. Constraints reject rows that would break rules such as required values, unique names, valid ranges, or references to another table.

INTEGER PRIMARY KEY is the table key and receives a number automatically when it is omitted. AUTOINCREMENT prevents a deleted key from being reused. WITHOUT ROWID stores a declared primary key directly, which is useful when the natural key is not an integer.

Foreign keys are checked when PRAGMA foreign_keys is ON. They support immediate and deferred checks plus CASCADE, SET NULL, SET DEFAULT, RESTRICT, and NO ACTION for updates and deletes.

  • NOT NULL requires every stored row to have a value.
  • UNIQUE prevents two rows from repeating the same column combination.
  • CHECK requires its expression to evaluate to true or NULL.
  • DEFAULT supplies a value when an INSERT omits the column.
  • STRICT rejects values that cannot become the declared column type.
  • Generated columns, composite keys, and named constraints follow SQLite syntax.

Create two related tables

Each book points to an author. The default and check constraints are applied to every new row. The listing also shows measurements, the table chapter 4 made, because these chapters build up one database in order.

SQL / input
PRAGMA foreign_keys = ON;
CREATE TABLE authors (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);
CREATE TABLE books (
  id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  author_id INTEGER NOT NULL REFERENCES authors(id),
  year INTEGER NOT NULL,
  rating REAL DEFAULT 0 CHECK (rating BETWEEN 0 AND 5)
) STRICT;
SELECT name, type FROM sqlite_schema WHERE type = 'table' ORDER BY name;
Resultobserved
|     name     | type  |
|--------------|-------|
| authors      | table |
| books        | table |
| measurements | table |
06

Insert, update, and delete rows

INSERT adds rows, UPDATE changes selected rows, and DELETE removes selected rows. RETURNING lets each statement give the affected values back immediately.

A WHERE clause decides which rows UPDATE or DELETE touches. When the condition is new, we'll run the same WHERE with SELECT first, because leaving WHERE out means every row.

ON CONFLICT handles a uniqueness collision. DO NOTHING skips the incoming row, while DO UPDATE changes the existing row. INSERT OR IGNORE and INSERT OR REPLACE provide SQLite-compatible conflict policies.

Write rows and return the change

The authors go in first, because every book names one and the foreign key is on. The last statement updates one row and returns the stored value after the update.

SQL / input
INSERT INTO authors (id, name) VALUES
  (1, 'Octavia Butler'),
  (2, 'Ursula Le Guin'),
  (3, 'Ted Chiang');

INSERT INTO books (id, title, author_id, year, rating)
VALUES (1, 'Kindred', 1, 1979, 4.8),
       (2, 'Parable of the Sower', 1, 1993, 4.7),
       (3, 'The Left Hand of Darkness', 2, 1969, 4.6),
       (4, 'Stories of Your Life', 3, 2002, 4.9);

UPDATE books SET rating = 4.9 WHERE id = 1
RETURNING id, title, rating;
Resultobserved
| id |  title  | rating |
|----|---------|--------|
|  1 | Kindred |    4.9 |

Upsert a row

excluded refers to the row INSERT tried to add. This pattern creates an author or updates the existing name with the same id.

SQL / input
INSERT INTO authors (id, name) VALUES (1, 'Octavia E. Butler')
ON CONFLICT(id) DO UPDATE SET name = excluded.name
RETURNING id, name;
Resultobserved
| id |       name        |
|----|-------------------|
|  1 | Octavia E. Butler |
07

Select, filter, and sort rows

SELECT describes the rows and columns you want. FROM chooses a source, WHERE filters it, ORDER BY sorts it, and LIMIT controls how many rows are returned.

SQL is declarative, so we describe the answer instead of the steps used to find it, and the planner chooses a table scan, index lookup, join strategy, temporary sort, etc. We don't have to encode that physical plan in the query.

LIKE matches text patterns, where % means any sequence and _ means one character. GLOB uses shell-style * and ?. COLLATE NOCASE provides ASCII case-insensitive ordering and comparison, while RTRIM ignores trailing spaces.

  • DISTINCT removes duplicate result rows.
  • LIMIT count OFFSET skip pages through a result.
  • CASE chooses a value from conditions.
  • CAST converts a value deliberately.
  • IN and BETWEEN express common ranges and sets.
  • IS and IS NOT handle NULL safely.

Find recent books

The filter is evaluated before sorting, and only the named columns appear in the result.

SQL / input
SELECT title, year
FROM books
WHERE year >= 1990
ORDER BY year;
Resultobserved
|        title         | year |
|----------------------|------|
| Parable of the Sower | 1993 |
| Stories of Your Life | 2002 |
08

Joins, subqueries, and CTEs

Joins connect related rows. Subqueries place one query inside another, and common table expressions give a query a temporary name for the duration of one statement.

INNER JOIN returns matching pairs. LEFT, RIGHT, and FULL OUTER JOIN preserve unmatched rows from one or both sides. CROSS JOIN produces every possible pair and normally needs a small input.

A subquery may appear in WHERE, FROM, or as a value. It may be correlated, which means it refers to the current row of its outer query. WITH RECURSIVE can walk a hierarchy or generate a sequence one row at a time.

Join books to authors

The ON expression states how one row in books relates to one row in authors.

SQL / input
SELECT b.title, a.name AS author
FROM books AS b
JOIN authors AS a ON a.id = b.author_id
ORDER BY b.year;
Resultobserved
|           title           |      author       |
|---------------------------|-------------------|
| The Left Hand of Darkness | Ursula Le Guin    |
| Kindred                   | Octavia E. Butler |
| Parable of the Sower      | Octavia E. Butler |
| Stories of Your Life      | Ted Chiang        |

Use a CTE and a subquery

recent names the filtered set, while the IN subquery chooses the author ids that contain Butler.

SQL / input
WITH recent AS (
  SELECT * FROM books WHERE year >= 1990
)
SELECT title FROM recent
WHERE author_id IN (
  SELECT id FROM authors WHERE name LIKE '%Butler%'
);
Resultobserved
|        title         |
|----------------------|
| Parable of the Sower |
09

Groups and compound queries

Aggregate functions summarize rows, compound operators combine complete query results, and window functions compute a value across a set of rows related to the current one.

GROUP BY makes one result row per group. HAVING filters those groups after aggregates are calculated. COUNT, SUM, AVG, MIN, MAX, group_concat, json_group_array, and json_group_object are available.

UNION removes duplicates, UNION ALL keeps them, INTERSECT keeps common rows, and EXCEPT keeps rows found only on the left.

Window functions run. OVER (...) with PARTITION BY, the ROWS, RANGE and GROUPS frame clauses, every EXCLUDE bound, and all eleven window-only functions answer the way SQLite 3.53.4 answers: row_number, rank, dense_rank, percent_rank, cume_dist, ntile, lag, lead, first_value, last_value and nth_value.

Summarize each author

COUNT and AVG receive the rows in each author_id group. AVG is rounded, because the mean of two ratings is a binary floating point number and prints as one.

SQL / input
SELECT author_id, COUNT(*) AS books,
       ROUND(AVG(rating), 2) AS avg_rating
FROM books
GROUP BY author_id
ORDER BY author_id;
Resultobserved
| author_id | books | avg_rating |
|-----------|-------|------------|
|         1 |     2 |        4.8 |
|         2 |     1 |        4.6 |
|         3 |     1 |        4.9 |
10

Transactions and savepoints

A transaction makes several statements one unit. COMMIT publishes all of them, while ROLLBACK restores the database to the state before BEGIN.

Savepoints create smaller rollback boundaries inside a transaction. ROLLBACK TO removes changes after a savepoint and keeps the transaction open, while RELEASE removes the savepoint name.

Readers use snapshots and don't see partial writes, while one writer may commit at a time. busy_timeout controls how long another writer waits, and synchronous OFF, NORMAL, or FULL chooses the durability tradeoff.

  • Deferred foreign keys are checked when the transaction commits.
  • Search shadow tables participate in the same commit and rollback.
  • ATTACH can commit changes across two files with a super-journal.
  • Recovery runs on open and replays complete WAL records idempotently.

Keep one row and discard the draft

Bloodchild survives the commit. The notes row is written after draft, so ROLLBACK TO removes it and COMMIT publishes only the first.

SQL / input
BEGIN;
INSERT INTO books VALUES (5, 'Bloodchild', 1, 1984, 4.7);
SAVEPOINT draft;
INSERT INTO books VALUES (6, 'Unpublished notes', 1, 2026, 0);
ROLLBACK TO draft;
RELEASE draft;
COMMIT;
SELECT id, title FROM books WHERE id IN (5, 6);
Resultobserved
| id |   title    |
|----|------------|
|  5 | Bloodchild |
11

Indexes, views, triggers, and schema changes

Indexes make selected lookups cheaper, views save queries under a name, and triggers run SQL when rows change. ALTER, DROP, ANALYZE, and REINDEX maintain the schema over time.

A regular CREATE INDEX builds a B+tree from one or more columns. EXPLAIN QUERY PLAN shows whether a query scans a table, searches an index, uses a vector index, or needs a temporary sort.

CREATE TRIGGER supports BEFORE, AFTER, and INSTEAD OF, FOR EACH ROW, WHEN, OLD and NEW values, and RAISE. ALTER TABLE can rename a table, rename a column, add a column, or drop a column.

  • CREATE VIEW stores a reusable SELECT.
  • ANALYZE writes sqlite_stat1 for the planner.
  • REINDEX rebuilds indexes after a collation change.
  • DROP TABLE, VIEW, INDEX, and TRIGGER remove schema objects.
  • Partial indexes, expression indexes, and indexes on WITHOUT ROWID tables all run.

Build an index and inspect the plan

EXPLAIN QUERY PLAN prints the operator tree in SQLite's own idiom. The line names the access path: a SEARCH through books_by_author, the index the statement above just created.

SQL / input
CREATE INDEX books_by_author ON books(author_id);
EXPLAIN QUERY PLAN
SELECT * FROM books WHERE author_id = 1;
Resultobserved
QUERY PLAN
`--SEARCH books USING INDEX books_by_author (author_id=?)
12

Built-in and application functions

Inillucent recognizes 190 built-in function names across text, numbers, dates, JSON, aggregation, vectors, and formatting. That is 172 of the 177 the pinned SQLite library answers, plus 18 vector functions of its own. The five absent are fts3_tokenizer, fts5, fts5_get_locale, fts5_insttoken and fts5_locale. Applications may register their own scalar functions, aggregate functions, and collations.

Common scalar functions include abs, coalesce, hex, instr, length, lower, upper, printf, random, replace, round, substr, trim, typeof, unicode, and zeroblob. The math set includes trig, logarithm, power, floor, ceil, sqrt, etc.

Date and time functions include date, time, datetime, julianday, unixepoch, strftime, and timediff. Modifiers such as start of month and +1 day follow the SQLite model.

  • Core values and text: changes, char, coalesce, concat, concat_ws, format, glob, hex, ifnull, iif, instr, last_insert_rowid, length, like, likelihood, likely, lower, ltrim, nullif, octet_length, printf, quote, random, randomblob, replace, rtrim, sqlite_source_id, sqlite_version, substr, substring, total_changes, trim, typeof, unhex, unicode, unlikely, upper, and zeroblob.
  • Math: abs, acos, acosh, asin, asinh, atan, atan2, atanh, ceil, ceiling, cos, cosh, degrees, exp, floor, ln, log, log10, log2, mod, pi, pow, power, radians, round, sign, sin, sinh, sqrt, tan, tanh, and trunc.
  • Date and time: date, datetime, julianday, strftime, time, timediff, and unixepoch.
  • Aggregates: avg, count, group_concat, json_group_array, json_group_object, jsonb_group_array, jsonb_group_object, max, min, string_agg, sum, and total.
  • Windows: cume_dist, dense_rank, first_value, lag, last_value, lead, nth_value, ntile, percent_rank, rank, and row_number. Each needs an OVER clause, and each answers the way SQLite 3.53.4 answers.
  • Vectors: vector_distance_cos, vector_distance_l2, and vector_dot. The complete JSON and JSONB families are listed in the next chapter.

Transform values in a result

Functions may be nested, aliased, filtered, and ordered like any other expression.

SQL / input
SELECT upper('inillucent') AS name,
       printf('%05.2f', 3.14159) AS padded,
       date('2026-09-07', '+1 day') AS tomorrow,
       round(sqrt(81), 0) AS root;
Resultobserved
|    name    | padded |  tomorrow  | root |
|------------|--------|------------|------|
| INILLUCENT | 03.14  | 2026-09-08 |  9.0 |
13

JSON and JSONB

JSON functions create, validate, read, and update structured documents. JSONB keeps the parsed binary form when several operations would otherwise parse the same text repeatedly.

A JSON path starts with $. Object labels follow a dot, and array positions use brackets. A missing path returns NULL. A malformed path or malformed document returns an error instead of an invented value.

The scalar set includes json, jsonb, json_array, json_object, json_extract, json_set, json_insert, json_replace, json_patch, json_remove, json_type, json_valid, json_pretty, json_quote, their JSONB forms, group aggregates, etc. json_each and json_tree are registered internally but aren't yet reachable from SQL.

Create and read a document

json_object creates valid JSON, and json_extract returns a SQL text value at the requested path.

SQL / input
SELECT json_extract(
  json_object('format', 'paperback', 'pages', 264),
  '$.format'
) AS format,
json_array_length(json_array('sql', 'search', 'json')) AS topics;
Resultobserved
|  format   | topics |
|-----------|--------|
| paperback |      3 |
14

Full-text search with FTS5

FTS5 indexes words instead of comparing every row. MATCH queries the index, while bm25() supplies a relevance score for ordering matching documents.

Create an FTS5 virtual table with the text columns you want to search. It can store its own content or follow an external content table. Prefix, phrase, Boolean, and column-restricted queries use SQLite FTS5 syntax.

The index lives in ordinary database trees and follows the surrounding transaction, so an insert that rolls back isn't searchable later. Rebuild and delete-all maintenance commands follow the FTS5 control-column form.

Search a small library

MATCH searches all indexed columns. rowid remains available for joining the hit back to an ordinary table.

SQL / input
CREATE VIRTUAL TABLE library_search USING fts5(title, body);
INSERT INTO library_search(rowid, title, body) VALUES
  (1, 'Kindred', 'A time travel novel about family and history'),
  (2, 'Parable', 'A future shaped by climate and community');
SELECT rowid, title FROM library_search
WHERE library_search MATCH 'family';
Resultobserved
| rowid |  title  |
|-------|---------|
|     1 | Kindred |
15

Spatial search with R-Tree

An R-Tree finds rectangles that overlap a point or region without scanning every shape. rtree stores floating-point coordinates and rtree_i32 stores integers.

The first column is an id. Every dimension then has a minimum and maximum column, so a two-dimensional table has id, min_x, max_x, min_y, and max_y. A table may have one to five dimensions.

The query below asks which stored rectangle overlaps the square from 8 through 12 on both axes. The minimum must be below the query maximum, and the maximum must be above the query minimum.

Find an overlapping rectangle

Only the first stored rectangle overlaps the requested area.

SQL / input
CREATE VIRTUAL TABLE places USING rtree(
  id, min_x, max_x, min_y, max_y
);
INSERT INTO places VALUES
  (1, 0, 10, 0, 10),
  (2, 20, 30, 20, 30);
SELECT id FROM places
WHERE min_x <= 12 AND max_x >= 8
  AND min_y <= 12 AND max_y >= 8;
Resultobserved
| id |
|----|
|  1 |
16

Vector columns and HNSW indexes

A VECTOR(N) column stores an embedding with a fixed number of dimensions. Distance functions can scan it exactly, and an inillucent_hnsw index gives the planner an approximate nearest-neighbor path. Inillucent can also produce the embeddings, in the same process, with no embedding server.

Embeddings arrive as a BLOB of little-endian f32 values. Bind that byte array as a parameter in application code. The hexadecimal literals below make the bytes visible for a two-dimensional teaching example.

vector_distance_cos returns 0 for the same direction and 1 for orthogonal unit vectors. vector_distance_l2 returns Euclidean distance, and vector_dot returns a dot product. ORDER BY distance with LIMIT k is the shape the planner recognizes for HNSW. An index minimizes cosine unless it was declared WITH (metric = 'l2'), and a query whose distance function does not match the metric its index was built under plans as an exhaustive scan rather than a probe.

Most applications supply their own vectors. Inillucent can also produce them: inillucent setup-embeddings all downloads ONNX Runtime and nomic-embed-text-v1.5 on Windows, macOS or Linux, checks every byte against a digest pinned in the build, and leaves the SQL function embed(TEXT) answering with nothing to export by hand. It is about 620 MB once, and a build without the feature refuses by name rather than returning a vector of zeroes.

Loading the model costs 650 to 800 milliseconds and one embedding costs 12 to 36 milliseconds, so when the weights are in memory is a decision rather than a detail. The resident profile keeps them for the life of the process, on-demand drops them after every call, and the default idle profile keeps them through a burst of questions and lets them go a few minutes later. A process that answers one question and exits wants on-demand; an ingestion run wants resident.

  • The column refuses the wrong byte width.
  • CREATE INDEX backfills existing rows and follows later writes.
  • A query may use a WHERE predicate with nearest-neighbor ordering.
  • The index uses the same retrieval store as inillucent_search rather than a second HNSW implementation.
  • embed(TEXT) returns the 3,072 bytes a VECTOR(768) column holds, computed in this process, and is called once for the statement rather than once per row.
  • Write a computed vector wherever an expression goes: a VALUES row, an UPDATE ... SET, a RETURNING clause or an INSERT ... SELECT.
  • One command installs the runtime and the weights on all three platforms, verified by digest.

Measure three directions

east is the query vector. northeast is closer than north, and the observed distances make that ordering explicit.

SQL / input
CREATE TABLE passages (
  id INTEGER PRIMARY KEY, body TEXT, embedding VECTOR(2)
);
INSERT INTO passages VALUES
  (1, 'east',      x'0000803f00000000'),
  (2, 'north',     x'000000000000803f'),
  (3, 'northeast', x'f304353ff304353f');
SELECT id, body, ROUND(vector_distance_cos(
  embedding, x'0000803f00000000'
), 4) AS distance
FROM passages ORDER BY distance LIMIT 3;
Resultobserved
| id |   body    | distance |
|----|-----------|----------|
|  1 | east      |      0.0 |
|  3 | northeast |   0.2929 |
|  2 | north     |      1.0 |

Put HNSW on the column

The plan names the vector index and the requested k, which confirms that we aren't looking at an exhaustive table scan. The second line is the sort that orders the rows the probe returned, which is over k rows rather than over the table.

SQL / input
CREATE INDEX passage_vectors
ON passages USING inillucent_hnsw (embedding);
EXPLAIN QUERY PLAN
SELECT id FROM passages
ORDER BY vector_distance_cos(
  embedding, x'0000803f00000000'
) LIMIT 2;
Resultobserved
QUERY PLAN
|--SEARCH passages USING VECTOR INDEX passage_vectors (k=2)
`--USE TEMP B-TREE FOR ORDER BY

Embed text inside the database

One command installs the model, and embed(TEXT) then answers with nothing exported by hand. The length is 768 dimensions at four bytes each. The function needs a binary built with the embedding feature: the published 0.1.2 archives carry it, and the 0.1.1 archives answer no such function: embed.

SQL / input
-- once per machine, about 620 MB
-- $ inillucent setup-embeddings all

CREATE TABLE note (id INTEGER PRIMARY KEY, body TEXT, v VECTOR(768));
INSERT INTO note (body, v) VALUES (?1, embed(?1));
SELECT length(embed('flight details for next week'));
Resultobserved
| length(embed('flight details for next week')) |
|-----------------------------------------------|
|                                          3072 |
18

Inspection and PRAGMA

PRAGMA statements inspect the schema and storage or configure one connection. They are the quickest way to answer what columns, indexes, files, page counts, and durability settings a database has.

Schema inspection includes table_info, table_xinfo, table_list, index_list, index_xinfo, and database_list. Storage inspection includes page_size, page_count, and freelist_count.

Operational checks include integrity_check, quick_check, foreign_key_check, and wal_checkpoint. Connection settings include cache_size, synchronous, busy_timeout, and foreign_keys.

Inspect a table

cid is the zero-based column position, dflt_value is the stored default expression, and pk identifies primary-key order.

SQL / input
PRAGMA table_info(books);
Resultobserved
| cid |   name    |  type   | notnull | dflt_value | pk |
|-----|-----------|---------|---------|------------|----|
|   0 | id        | INTEGER |       0 |            |  1 |
|   1 | title     | TEXT    |       1 |            |  0 |
|   2 | author_id | INTEGER |       1 |            |  0 |
|   3 | year      | INTEGER |       1 |            |  0 |
|   4 | rating    | REAL    |       0 | 0          |  0 |

Check the whole file

A one-row ok result means the structural checks passed.

SQL / input
PRAGMA integrity_check;
Resultobserved
| integrity_check |
|-----------------|
| ok              |
19

Attached and temporary databases

ATTACH opens another file under a schema name, and TEMP creates connection-local tables, indexes, views, and triggers. A query can join across main, temp, and attached schemas.

Prefix an object with its schema, such as archive.books. DETACH closes the attached schema after statements using it have finished. database_list shows the names and paths currently available.

A commit that writes two durable files uses a super-journal so recovery can decide the transaction consistently. Temporary objects use their own connection-local database and are gone after the connection closes.

Join an attached archive

The schema prefix tells the engine which file owns each table.

SQL / input
ATTACH DATABASE 'archive.rdb' AS archive;
CREATE TABLE archive.books (id INTEGER PRIMARY KEY, title TEXT);
INSERT INTO archive.books VALUES (9, 'A Wizard of Earthsea');
SELECT title, 'archive' AS location FROM archive.books;
DETACH DATABASE archive;
Resultobserved
|        title         | location |
|----------------------|----------|
| A Wizard of Earthsea | archive  |
20

Use Inillucent from an application

Every language reaches the engine through one stable C ABI. There are client libraries for eight of them, and the next chapter has the install line and a worked example for each.

Values stay typed as Null, Integer, Real, Text or Blob. A result is materialized before it is handed back, so it carries an exact total, and a flag saying whether the row limit left any rows behind.

Unsupported is its own status and it names the construct the engine has not implemented, so an application can tell that apart from a mistyped statement. The capability table answers the same question before a statement is composed. Today 23 of 24 capabilities are supported, and cancellation is the declared exception.

  • A Database owns one file and creates connections.
  • Bound parameters are copied before the call returns.
  • Read only mode is decided by the binder, which classifies each statement.
  • A transaction is held open, so a write can be checked before it commits.
  • Every C handle has a free function, and returned pointers stay valid until it is freed.
  • The reference Python binding uses ctypes and the standard library.

Open, write, and query from Rust

Placeholders keep values separate from SQL text. This prevents quoting bugs and lets the driver preserve types.

SQL / input
use inillucent_driver::{Database, Value};

let database = Database::open("library.rdb")?;
let connection = database.connect();
connection.execute(
    "INSERT INTO authors VALUES (?1, ?2)",
    &[Value::Integer(1), Value::Text("Octavia Butler".into())],
)?;
let rows = connection.query(
    "SELECT id, name FROM authors", &[], 200
)?;
println!("{} row", rows.total);
Terminalobserved
1 row
21

Client libraries

Client libraries for TypeScript, JavaScript, Python, Rust, Go, Java, C# and PHP. Install one, open a file, and read and write rows from your own code.

All eight call the same C ABI and give the same API, so a program written against one reads the same in the next. Values stay typed, NULL is never the empty string, and a result carries an exact total beside the rows a limit handed back.

The tabs below show the same program in each language: create a person table, insert two rows, read them back by column name, and update one. Every output is what that program really printed.

Each library is graded by the same conformance suite this engine’s own Rust driver runs, so a client passes when it agrees with the engine. The libraries live in the inillucent-clients repository, and each has a README with the full reference.

Install
npm install inillucent-client
Requires
Node 18 or later
Insert
import { connect } from 'inillucent-client';

const db = connect('app.rdb');

db.execute(`
  CREATE TABLE person (
    id         INTEGER PRIMARY KEY,
    first_name TEXT NOT NULL,
    last_name  TEXT NOT NULL,
    email      TEXT,
    age        INTEGER,
    height_m   REAL
  )
`);

const insert =
  'INSERT INTO person (first_name, last_name, email, age, height_m) VALUES (?1, ?2, ?3, ?4, ?5)';
db.execute(insert, ['Ada', 'Lovelace', '[email protected]', 36, 1.65]);
db.execute(insert, ['Grace', 'Hopper', null, 85, 1.57]);
Read
for (const person of db.query(
  'SELECT id, first_name, last_name, email, age, height_m FROM person ORDER BY id',
)) {
  console.log(person.id, person.first_name, person.last_name, person.email, person.age, person.height_m);
}

// One value.
db.scalar('SELECT COUNT(*) FROM person');                                 // 2
db.scalar('SELECT email FROM person WHERE last_name = ?1', ['Lovelace']); // '[email protected]'
Outputobserved
1 Ada Lovelace [email protected] 36 1.65
2 Grace Hopper null 85 1.57
Update
const changed = db.execute('UPDATE person SET email = ?1 WHERE last_name = ?2', [
  '[email protected]',
  'Hopper',
]);
console.log('updated:', changed.affected);
console.log('email now:', db.scalar('SELECT email FROM person WHERE last_name = ?1', ['Hopper']));
Outputobserved
updated: 1
email now: [email protected]

The API is the same in all eight. Each library’s own README has the full reference, and the programs above are in the inillucent-clients repository, where one command runs the same conformance suite against every one of them.

22

Migrate existing data

inillucent migrate copies a SQLite file, a running PostgreSQL or MySQL server, or a legacy retrieval index into a new .rdb, verifies every table by row count and by digest, and publishes the destination only after every check passes.

Migration is copy-based. The source is never written to, which makes going back a path choice rather than a reconstruction. The report and manifest record what was copied and how it was checked.

A path is a SQLite file. A postgres:// or mysql:// URL is a running server, read over its own wire protocol inside one repeatable-read snapshot, so every table and the schema itself are as of one instant. Values with no equivalent here are carried as the server's own text rendering rather than rounded into a float.

Inillucent doesn't use SQLite page format, so it can't open an existing .sqlite file directly. We import it instead, recreating the supported SQLite views, triggers, indexes, constraints, rows, etc. in the Inillucent file.

Import a SQLite file

A path is read as a SQLite file, and --kind names the source when it is not one. The destination is refused if it already exists. The separate inillucent-migrate program takes a legacy retrieval index directory instead.

SQL / input
inillucent migrate library.sqlite --destination library.rdb
Terminalobserved
imported library.sqlite into library.rdb

Import from a running PostgreSQL server

A postgres:// or mysql:// URL is read over that server's own wire protocol, inside one repeatable-read snapshot, so every table is as of one instant. Each table is then checked twice over - a count taken separately from the scan, and an order-independent digest - and every check is printed whether it passed or not. A failed check publishes nothing and says where the staging file is. transport reads plaintext here because the server is on a loopback address; a migration to any other host uses verified TLS and refuses rather than falling back.

SQL / input
inillucent migrate "postgres://[email protected]:5433/doc_library" \n  --destination library-from-postgres.rdb
Terminalobserved
postgres://[email protected]:5433/doc_library -> library-from-postgres.rdb
PostgreSQL 17.2, 2 tables, 4 rows
transport: plaintext
  pass structure.integrity every tree walks in key order on a fresh open
  pass source.count.authors 2 rows, counted separately from the scan
  pass count.authors 2 rows
  pass digest.authors 524844e270589a8f07bedd02e7c89369a1cd5ed0d6bfe4e06ed12cbe62af22a8
  pass source.count.books 2 rows, counted separately from the scan
  pass count.books 2 rows
  pass digest.books be5909cc041c098b53154531a69a79dca67986baaab8b293db54e11993ad92c9
published: library-from-postgres.rdb
23

Storage, recovery, and concurrency

The relational engine uses 32 KiB pages by default, a 128 MiB buffer pool, a segmented redo WAL, fuzzy checkpoints, snapshots for readers, and an undo buffer for explicit rollback.

Meta pages are double-written and a rollback journal keeps a pre-image of both, so a crash while a checkpoint is recording itself is undone along with the pages that checkpoint moved. Each WAL record has a crc32c checksum, as does each rollback-journal page image, and recovery runs on every open and stops at the first record whose bytes are not the bytes that were written. Checkpoints can retire old segments after their pages are durable. Values too large for a leaf move into blob extents.

One writer at a time, and readers use a version log so a writer doesn't expose half of a transaction. Several processes can share one file under PRAGMA locking_mode = normal, over the same lock protocol SQLite uses; the default is exclusive, because releasing the file between statements means reading the meta record again before every one. Threads inside a single process are not supported. Group commit lets writers waiting at the same boundary share the durable flush.

  • Page sizes from 8 KiB through 64 KiB are allowed.
  • The default buffer pool has 4,096 frames.
  • synchronous OFF, NORMAL, and FULL are implemented.
  • busy_timeout controls writer waiting.
  • WAL and rollback-journal modes are understood by the transaction layer, and a checkpoint is undoable in both: the log is logical, so a page a checkpoint half wrote is content the log has stopped describing, and a rollback journal holds its previous image until the checkpoint is durable.
  • Deterministic crash campaigns cover torn writes, I/O errors, disk full, recovery, and checkpointing, one cut point of a commit at a time, in DELETE, TRUNCATE and PERSIST mode.

Read storage state

These values come from the open database instead of from configuration documentation. synchronous reports 2, which is FULL. PRAGMA page_count answers in the same way and counts the pages this particular file holds.

SQL / input
PRAGMA page_size;
PRAGMA journal_mode;
PRAGMA synchronous;
Resultobserved
| page_size |
|-----------|
|     32768 |
| journal_mode |
|--------------|
| delete       |
| synchronous |
|-------------|
|           2 |
24

Compatibility and current limits

Inillucent targets SQLite behavior and tests every statement against a pinned SQLite 3.53.4 oracle. The differential probe is 416 cases, run through both shells over a fresh database each and compared byte by byte: 403 produce SQLite's exact bytes, nothing is refused, and 7 answer differently.

Of the 13 that are not byte-equal, six are vector-search features SQLite has no equivalent for, so there is nothing to be equal to. Three print numbers about SQLite's own C structures, three are a page size and a locking mode this engine chose and measured, and one is two pinned SQLite artifacts disagreeing with each other. There are no known wrong answers.

Partial indexes, indexes on expressions, and indexes on WITHOUT ROWID tables all run, as do json_each, json_tree, generate_series and the table-valued pragma forms. VACUUM and VACUUM INTO both rebuild the file. Several processes can share one file under PRAGMA locking_mode = normal, though the default is exclusive because releasing the file between statements costs real speed.

What is outside the surface: SQLite's file format, which is imported rather than opened; threads inside one process; a second writer; and online cancellation. The relational engine is 339% faster than SQLite at 100,000 rows on the measured Windows gate, and six of its thirty workloads are slower, the largest being compiling SELECT 1 on every call.

  • RIGHT and FULL OUTER JOIN run.
  • Recursive CTEs and correlated subqueries run.
  • STRICT tables, generated columns, CHECK, UNIQUE, and foreign keys run.
  • BEFORE, AFTER, and INSTEAD OF triggers run.
  • All four ALTER TABLE forms run.
  • Partial indexes, expression indexes, and indexes on WITHOUT ROWID tables run.
  • FTS5, R-Tree, JSON, vector columns, HNSW, and inillucent_search run.
  • ATTACH, DETACH, temporary objects, savepoints, and cross-file commit run.
  • 172 of the 177 function names the pinned SQLite library answers, 67 of 67 pragmas, and 63 of 65 shell dot commands answer.
  • The shell and driver return Unsupported separately from syntax, constraint, and I/O errors.

Ask before composing a feature

The driver capability table is checked against the running engine in both directions, so a stale yes and a stale no both fail its probe. cancel is partial because cancellation is observed between units of work rather than at every instruction, and readonly_open is partial because read-only is enforced by the driver above the engine rather than by the file handle.

SQL / input
let table = inillucent_driver::CAPABILITIES;
let yes = table.iter().filter(|e| e.support.name() == "yes").count();
let partial: Vec<&str> = table.iter()
    .filter(|e| e.support.name() == "partial")
    .map(|e| e.name)
    .collect();

println!("{} capabilities reported", table.len());
println!("  supported: {yes}");
println!("  partial: {}", partial.join(", "));
Terminalobserved
24 capabilities reported
  supported: 22
  partial: cancel, readonly_open