iceql
A local RDBMS with plaintext storage (CSV + YAML). Like SQLite, a database is self-contained and queryable with SQL — but the storage stays human-readable.
SQLite database files are binary, so you cannot hand one to an LLM and have it read the contents directly. iceql stores tables as CSV and schemas as YAML, so both LLMs and humans can read the storage as-is. Writes always produce a canonical form (LF newlines, minimal quoting, one record per line), which keeps git diffs clean and meaningful.
Installation
$ uv tool install iceql # as a CLI
$ uv add iceql # as a library
Quick start
A database is just a directory.
$ iceql mydb -c "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER)"
$ iceql mydb -c "INSERT INTO users VALUES (1, 'alice', 30), (2, 'bob', NULL)"
$ iceql mydb -c "SELECT * FROM users WHERE age IS NULL"
id,name,age
2,bob,\N
The resulting files are readable as-is.
$ cat mydb/users.csv
id,name,age
1,alice,30
2,bob,\N
$ cat mydb/users.schema.yaml
version: 1
table: users
columns:
- name: id
type: integer
nullable: false
primary_key: true
- name: name
type: text
nullable: false
- name: age
type: integer
nullable: true
null_marker: \N
Running without arguments opens a REPL.
$ iceql mydb
iceql> SELECT COUNT(*) FROM users;
_col_0
------
2
iceql> .tables
users
iceql> .quit
-f table|csv|json selects the output format (defaults to table on a TTY, csv when piped).
iceql check mydb validates schema/CSV consistency (types, NOT NULL, duplicate primary keys, canonical form) and exits non-zero on errors.
This makes hand-edited CSV files verifiable in CI or a pre-commit hook.
Python API
A DB-API 2.0 style API, familiar to anyone who has used the sqlite3 module.
import iceql
conn = iceql.connect("mydb")
conn.execute("INSERT INTO users VALUES (?, ?, ?)", (3, "carol", 25))
for row in conn.execute("SELECT name FROM users WHERE age > :min", {"min": 20}):
print(row)
conn.close()
Supported SQL
- SELECT: WHERE, JOIN (INNER / LEFT), GROUP BY, aggregate functions, HAVING, ORDER BY (with NULLS FIRST / LAST), LIMIT / OFFSET, DISTINCT, IN subqueries, CTE (WITH), UNION / UNION ALL
- DML: INSERT (VALUES / SELECT), UPDATE, DELETE
- DDL: CREATE TABLE, DROP TABLE, ALTER TABLE (ADD / DROP / RENAME COLUMN, RENAME TO)
- Transactions: BEGIN / COMMIT / ROLLBACK (changes are staged in memory and flushed on COMMIT; DDL is not allowed inside a transaction)
- Placeholders:
?(qmark) and:name(named)
SQL parsing and SELECT execution are powered by sqlglot. NULL ordering follows the SQLite default (NULL sorts smallest: first in ASC, last in DESC).
Types
| Type | CSV representation |
|---|---|
| integer | decimal integer |
| real | floating point (shortest round-trip form) |
| boolean | true / false |
| text | string (quoted only when it contains , " or newlines) |
| date | YYYY-MM-DD |
| datetime | ISO-8601 |
NULL is represented as an unquoted \N (the same convention as PostgreSQL COPY).
An empty string is an empty field, so NULL and the empty string are distinguishable.
A literal string \N is escaped as \\N.
MCP server
A built-in MCP server lets LLM agents read and write the database directly.
$ uv tool install 'iceql[mcp]'
$ iceql mcp mydb --read-only # drop --read-only to enable write tools
Four tools are exposed: query (SELECT only), execute (DML / DDL), list_tables, and describe_table.
Limitations
- Window functions, DISTINCT inside aggregates (e.g.
COUNT(DISTINCT x)), and scalar subqueries in the SELECT list are not supported (they fail with a clear error) - Transactions are per-connection with no isolation between connections; a COMMIT touching multiple tables is not atomic
- Tables are fully loaded into memory at query time; the intended scope is databases small enough for an LLM to read directly (tens of thousands of rows)
- Windows is not supported (inter-process locking uses fcntl)
Development
$ uv sync --all-groups
$ uv run pytest
$ uv run ruff check src tests
$ uv run mypy
The test suite includes differential tests that run the same queries against both iceql and sqlite3 and compare the results.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file iceql-0.1.0.tar.gz.
File metadata
- Download URL: iceql-0.1.0.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e7157daeeb7220ccb8a3d02ed0430e32cd32a80e7756cf3d520c67d259b7930
|
|
| MD5 |
3a4ca5ee74c3b5628c32c86a335be742
|
|
| BLAKE2b-256 |
5af1de9b8ed3ff4bd4d42d874299ea24d48da9e629175786afcf4f89b01a145c
|
File details
Details for the file iceql-0.1.0-py3-none-any.whl.
File metadata
- Download URL: iceql-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ccaff3a7b4970736d174c2f399771c7b816cbc04295071073cd58e9260bd7eb
|
|
| MD5 |
1604809816fc3edeb7b2f0d9ec40b471
|
|
| BLAKE2b-256 |
d7fe1dc134b9244b4adba57507cb0b8f80b21972426870d2069718236b4d612b
|