Skip to main content

inillucent

inillucent is an embedded SQL database. It speaks SQLite's dialect on its own storage, and it has keyword search and vector search built in. One .rdb file holds the tables and the search indexes.

This package gives Python two ways to use it:

  • The driver: Database, Connection and Rows. It calls the inillucent C library inside your Python process through ctypes. Under CPython the library builds each result's rows as Python objects, the way Python's own sqlite3 module does. Use the driver in an application.
  • The command line helpers: run() and query(). Each call starts the inillucent program and reads the JSON it prints. Use them in scripts, for commands the driver has no method for.

Install

pip install inillucent

The package needs Python 3.9 or later. The wheel for your platform holds the C library and the four programs, so the install needs no compiler, no Rust toolchain and no network access after the download.

Platform Wheel tag
Windows, x64 win_amd64
macOS 13 or later, Apple silicon and Intel macosx_13_0_universal2
Linux, x64 manylinux_2_28_x86_64
Linux, arm64 manylinux_2_28_aarch64

The driver loads the C library from the wheel's _lib folder. Set INILLUCENT_DRIVER_LIB to the path of a different library file to load that one. The helpers run the programs in the wheel's _bin folder. A source install has neither folder. It needs a library you built with cargo build -p inillucent-driver-capi, named by INILLUCENT_DRIVER_LIB.

A first program

from inillucent import Database

with Database("app.rdb") as database:
    connection = database.connect()
    connection.execute("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT)")
    connection.execute("INSERT INTO people VALUES (?1, ?2)", [1, "Ada"])
    rows = connection.execute("SELECT id, name FROM people", limit=200)
    print(rows.rows, rows.total)   # [[1, 'Ada']] 1

Database("app.rdb") creates the file when it does not exist. Leaving the with block closes every connection, writes everything to the file and closes it.

?1, ?2 and so on are bound to the values in the list, in order. Bind values this way instead of pasting them into the SQL text.

limit caps the rows returned. rows.total still counts every row the statement produced, and rows.more is True when rows were left out. With no limit, execute() returns every row.

Transactions

with connection.transaction() as transaction:
    transaction.execute("INSERT INTO people VALUES (2, 'Grace')")
    transaction.execute("UPDATE people SET name = 'Ada Lovelace' WHERE id = 1")

Leaving the block normally commits. Leaving it with an exception rolls back. A statement that fails inside Transaction.execute() rolls back the whole transaction before it raises.

Statements you run many times

with connection.prepare("INSERT INTO people VALUES (?1, ?2)") as statement:
    for key, name in [(3, "Grace"), (4, "Edsger")]:
        statement.execute([key, name])

Values

Python value Bound as Read back as
None NULL None
bool INTEGER 0 or 1 int
int INTEGER int
float REAL float
str TEXT str
bytes, bytearray, memoryview BLOB bytes

Any other type raises TypeError.

Threads

The engine is single threaded, and the interpreter lock is let go while a statement runs. So that two threads are never inside one database together, every call on a Database, and on the connections, statements and transactions opened from it, takes one lock that the Database owns. A second thread waits until the first call returns and then runs. Connection.cancel() is the one call that does not take the lock, because it exists to be made from another thread while a statement runs. The lock covers one call. A Transaction that stays open between calls does not stop another thread's statements on the same connection from running inside it, so give each thread its own connection, or keep a transaction on one thread.

Encrypted databases

run and query, the command line helpers, take a key argument.

from inillucent import query, run

key = "x'" + "5a" * 32 + "'"
run("exec", db="app.rdb", key=key, sql="CREATE TABLE note (body TEXT)")
rows = query("SELECT body FROM note", db="app.rdb", key=key)

The key travels to the program in the INILLUCENT_KEY environment variable. It is never put on the command line, because a command line is visible in the process list. When you pass no key, the environment the program inherits is left alone.

A key written x' followed by 64 hexadecimal digits and ' is a raw 32 byte key. Any other text is a passphrase, which is stretched with 600,000 rounds of PBKDF2 on every open, so a raw key opens faster.

A database created with a key is encrypted. Opening it without the key, or with a wrong key, fails with status corrupt. Opening a plaintext database with a key fails the same way.

Without the key, run returns a result whose status is corrupt, and query raises an Error with that status.

Errors

from inillucent import DriverError, Unsupported

try:
    connection.execute("SELECT * FROM absent")
except Unsupported as why:
    print("not built yet:", why.feature)
except DriverError as why:
    print(why.status_name, why.message)   # not_found no such table: absent

Every refusal raises DriverError, also exported as Error. DriverError has these attributes:

Attribute What it holds
status the status as a number
status_name the status as text, such as not_found
message the error text
feature the missing feature, when the status is unsupported
detail more detail, when the engine gives it
offset the byte offset in the SQL where the error is, or None

The thirteen status names are unsupported, syntax, not_found, constraint, readonly, busy, interrupted, corrupt, io, full, too_big, invalid_state and internal.

Unsupported is the subclass raised for status unsupported. It means the engine has not built that feature. The SQL is not wrong, and a different spelling fails the same way. Catch Unsupported before DriverError so the two cases stay apart. Call capabilities() before you write an unusual statement.

From the command line

pip install puts four programs on PATH:

Program What it is
inillucent the command line: 35 commands, each with --output json
inillucent-shell an interactive shell that works like sqlite3, with 63 of its 65 dot commands
inillucent-mcp an MCP server: 30 of the same commands served to an AI agent
inillucent-migrate builds a database from a legacy retrieval index. inillucent migrate copies a SQLite file or a PostgreSQL or MySQL database
inillucent create notes.rdb
inillucent --db notes.rdb exec "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)"
inillucent --db notes.rdb query "SELECT * FROM notes"
inillucent --db notes.rdb describe notes
inillucent-shell notes.rdb
inillucent help

The inillucent program exits with code 0 on success, 1 on a failure, 2 when the command line cannot be read, and 3 when the engine has not built a feature.

The command line helpers in Python

from inillucent import run, query

print(run("describe", db="notes.rdb", table="notes")["ddl"])
print(query("SELECT * FROM notes WHERE id > ?1", db="notes.rdb", params=[3]))
Helper What it does
run(command, db, **arguments) runs one inillucent command with --output json and returns the result as a dict. table="notes" becomes --table notes. A refusal is returned with ok set to False. It raises RuntimeError only when the program could not be run. The program's output is read as UTF-8 on every platform
query(sql, db, params, limit) runs one query and returns its rows as a list of dict keyed by column name. Raises DriverError or Unsupported on a refusal
binary(program) returns the path of one of the four programs in the wheel

Each helper call starts a process, so use the driver for anything that runs often. query() returns at most 200 rows, the default of the query command. Pass limit=0 for every row.

params reaches the program on standard input, so the number of values and the size of a value have no limit from the command line. bytes, bytearray and memoryview are bound as a BLOB. A NaN is bound as NULL, as the driver binds it. Infinity raises ValueError, because the command line has no spelling for it, and a list, a dict or any other type raises TypeError. The statement is an argument, so a statement longer than about 32,000 characters on Windows raises ValueError; open a Database and call execute for that.

For an AI agent

MCP, the Model Context Protocol, is how an AI agent calls tools. Add inillucent-mcp to an MCP client's configuration:

{
  "mcpServers": {
    "inillucent": {
      "command": "inillucent-mcp",
      "args": ["--db", "app.rdb"]
    }
  }
}

inillucent-mcp serves 30 of the command line's commands as MCP tools. The tools are generated from the same command table as the command line. --readonly refuses every statement that changes data. --root DIR refuses every path outside DIR.

The API

These are the driver's classes and functions. cargo test -p inillucent-compat --test tooling documentation:: reads this table and fails if a name in the first column is not declared in the driver source.

what one line
Database(path, create=True, read_only=False, diagnostics=False) opens a database file. path is a str, bytes or any os.PathLike, and a path holding a NUL character raises ValueError. create=True makes the file when it is missing. A missing file opened with read_only=True raises DriverError with the status NOT_FOUND, whatever create says. A file that is not a database, has a newer format or needs a key is refused here; a damaged log is reported by the first statement, which is when the log is read.
Database.connect() returns a new Connection. Each connection is its own session, and temp. tables and ATTACH belong to one session. Each connection has a transaction of its own. A BEGIN on one is not joined by another, the other reads only what is committed, a second writer waits for the busy timeout and then raises DriverError with the status BUSY, and a write through one connection is kept when another rolls back, as in Python's sqlite3. A connection other than the first runs on an engine of its own, opened over the file on its first statement, so keep a connection instead of opening one for each statement. Connection.close() releases that engine.
Database.path a property: the path of the database file.
Database.checkpoint() copies everything written so far into the database file.
Database.integrity_check() checks every table and index, and raises on the first problem.
Database.backup_to(path) copies the database to path, then opens and checks the copy.
Database.close() closes every connection, writes everything to the file and closes it. Leaving a with block calls it. A Statement or Transaction that is still open makes the close raise DriverError with the status INVALID_STATE. The database stays open and a later close() works once the statement or transaction is closed.
Connection.execute(sql, params, limit) runs one statement and returns Rows. Text that holds a second statement raises DriverError with the status INVALID_STATE and runs nothing, as Python's sqlite3 refuses it; execute_batch runs several. params is a list or tuple bound by position to ?1, ?2, and so on. A dict raises TypeError, because named parameters are not supported, and so does a str or bytes. limit is a count from 0 to 18446744073709551615, a negative number means every row, and a larger number raises ValueError. A statement with parameters is prepared once and kept, up to 128 of them, the way Python's sqlite3 keeps its statements. A float parameter that is NaN is stored as NULL, as SQLite stores it.
Connection.execute_many(sql, rows) runs one statement once for each list of parameters, taken from any iterable, a generator included, in one call to the library, and returns the rows changed in all. It opens no transaction; wrap it in one to write the rows together. Under CPython the library reads a list or tuple of rows of None, int, bool, float, str and bytes straight from the Python objects; any other batch crosses as JSON.
Connection.execute_batch(sql) runs several statements separated by semicolons, for their effect.
Connection.prepare(sql) compiles a statement and returns a Statement you can run many times.
Connection.transaction() begins a transaction and returns a Transaction.
Connection.last_insert_rowid a property: the rowid the last INSERT assigned.
Connection.total_changes a property: how many rows every statement on this connection has changed.
Connection.in_transaction a property: whether a transaction is open.
Connection.schema_cookie a property: a number that changes whenever the schema changes.
Connection.cancel() asks the running statement to stop, from another thread. It stops at the next check, so the stop is not instant.
Connection.close() closes the connection.
Statement.execute(params, limit) binds the values, runs the statement and returns Rows.
Statement.execute_many(rows) runs the statement once for each list of parameters and returns the rows changed in all.
Statement.close() frees the compiled statement.
Transaction.execute(sql) runs one statement inside the transaction and returns the number of rows it changed.
Transaction.commit() commits the transaction.
Transaction.rollback() rolls the transaction back.
Rows one result: columns, column_types, rows, total, more, affected, elapsed_us and tag. Supports len(), iteration and indexing by row number.
capabilities() returns every capability the engine declares, as a list of dict with name, supported and note.
supports(name) returns 1 for yes, 0 for no, -1 for partial, and -2 for a name this build does not know. Treat -2 as no.
abi_version() returns the version of the C library's interface, such as 1.0.0.
DriverError the exception for every refusal. Also exported as Error.
Unsupported the subclass of DriverError for a feature the engine has not built.

inillucent.__version__ is the package version.

More

MIT licence. Source: https://github.com/Black-Rainbow-Labs/Inillucent

Metadata

Release files for inillucent 2.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for inillucent 2.8.0
File
inillucent-2.8.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
inillucent-2.8.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
inillucent-2.8.0-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
inillucent-2.8.0-py3-none-macosx_13_0_universal2.whl Python 3 none macOS 13.0+ universal2 (ARM64, x86-64) Details

Total release size: 134.5 MB

Release files / inillucent-2.8.0-py3-none-win_amd64.whl

Download URL inillucent-2.8.0-py3-none-win_amd64.whl
Size 26.1 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
22a24fba5d581d49262a8babc1d11fca2cb0adfe7329097f483fbf3acb9f29a3
BLAKE2b-256 checksum
How to use checksums
efb9e6286bd9bc25916622ed3606928ac3599b72dd7e298cdbb5f0c99caab8ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / inillucent-2.8.0-py3-none-manylinux_2_28_x86_64.whl

Download URL inillucent-2.8.0-py3-none-manylinux_2_28_x86_64.whl
Size 27.9 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
1f8263b89047309eedb41e681efbd04d598132fd183b4b06409c5849e0dd4be7
BLAKE2b-256 checksum
How to use checksums
9fd190fc0a99e6613d5b4053ff20e6477319cb5d1c4b5545ee40d20d0608c80c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / inillucent-2.8.0-py3-none-manylinux_2_28_aarch64.whl

Download URL inillucent-2.8.0-py3-none-manylinux_2_28_aarch64.whl
Size 25.7 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
40dfbd912bc1f908f54e2564bb7bfe4cbe376c539835932788633b0215a479a2
BLAKE2b-256 checksum
How to use checksums
3e80094208beb23d888f74b2655e71df917b69cb16be37f60bfc57f209601703
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / inillucent-2.8.0-py3-none-macosx_13_0_universal2.whl

Download URL inillucent-2.8.0-py3-none-macosx_13_0_universal2.whl
Size 54.8 MB
Tags Python 3 macOS 13.0+ universal2 (ARM64, x86-64)
SHA-256 checksum
How to use checksums
4b6f8fbdb046fd6674caa689e6cfbc8dbdf24bb42ae6985d8f8305907d65511d
BLAKE2b-256 checksum
How to use checksums
64a2d6c6757d2522d384fb5739b13b98d0b845fd17d3d403839ed094a3e23ebd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release history Release notifications | RSS feed

This release

2.8.0 This release

4 release files

2.7.0

4 release files

2.6.0

4 release files

2.5.5

4 release files

2.5.4

4 release files

2.5.3

4 release files

2.5.2

4 release files

2.5.1

4 release files

2.5.0

4 release files

2.4.2

4 release files

2.4.1

4 release files

2.4.0

4 release files

2.3.4

4 release files

2.3.3

4 release files

2.3.2

4 release files

2.3.1

4 release files

2.3.0

4 release files

2.2.0

4 release files

2.1.5

4 release files

2.1.4

4 release files

2.1.3

4 release files

2.1.2

4 release files

2.1.1

4 release files

2.1.0

4 release files

2.0.9

4 release files

2.0.8

4 release files

2.0.7

4 release files

2.0.6

4 release files

2.0.5

4 release files

2.0.4

4 release files

2.0.3

4 release files

2.0.2

4 release files

2.0.1

4 release files

1.0.33

4 release files

1.0.32

4 release files

1.0.31

4 release files

1.0.30

4 release files

1.0.29

4 release files

0.1.9

4 release files

0.1.8

4 release files

0.1.7

4 release files

0.1.6

4 release files

0.1.5

1 release file

0.1.3

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page