Skip to main content

A sqlite3-compatible DB-API adapter for GNU recfiles

Project description

recdb

A PEP 249-style Python DB-API adapter for GNU recfiles — plain-text, human-readable, git diff-able databases.

Wikipedia: Recutils / Recfiles

Why?

Recfiles are plain text — human-readable, diffable with git diff, editable with any text editor, queryable with standard Unix tools. For small projects where textual transparency matters more than performance, they're a compelling alternative to a binary database file like SQLite.

recdb lets you use a familiar SQL subset against recfiles, with a clean one-line migration path to SQLite when you outgrow them.

Installation

pip install recdb

recdb needs at least one of the following backends to operate:

Backend Install Capability
GNU recutils apt install recutils or brew install recutils Read + write
python-recutils pip install recdb[recutils] Read + write (fallback)

GNU recutils is recommended — it is faster and more battle-tested. The python-recutils fallback is useful on platforms where installing recutils is inconvenient (e.g. Windows, CI environments). If both are installed, GNU recutils takes precedence.

Quickstart

import recdb

# ── Single-file mode — path ends in .rec ─────────────────────────────────────
conn = recdb.connect("inventory.rec")       # all tables in one file

# ── Directory mode — path has no .rec extension ───────────────────────────────
conn = recdb.connect("./data")              # items → ./data/items.rec, etc.

# ── SQLite backend (use stdlib directly — no wrapper needed) ──────────────────
import sqlite3
conn = sqlite3.connect("inventory.db")

# ── Identical API from here ───────────────────────────────────────────────────
conn.execute(
    "INSERT INTO items (name, sku, stock, price) VALUES (?, ?, ?, ?)",
    ("Widget", "WGT-001", 50, 9.99)
)
conn.commit()

cur = conn.execute("SELECT * FROM items WHERE stock > 0")
for row in cur.fetchall():
    print(row["name"], row["stock"])

conn.close()

Switching from either recfile mode to SQLite is a one-line change — replace recdb.connect(...) with sqlite3.connect("inventory.db") and the rest of your code stays the same.

Context manager

with recdb.connect("inventory.rec") as conn:
    conn.execute("UPDATE items SET stock = 45 WHERE sku = 'WGT-001'")
# commit() called automatically on clean exit

Supported SQL

Statement Example
SELECT SELECT * FROM items
Column projection SELECT name, stock FROM items
WHERE SELECT * FROM items WHERE stock < 10
ORDER BY SELECT * FROM items ORDER BY price DESC
LIMIT SELECT * FROM items LIMIT 5
INSERT INSERT INTO items (name, sku, stock) VALUES ('X', 'X-1', 5)
UPDATE UPDATE items SET stock = 50 WHERE sku = 'WGT-001'
DELETE DELETE FROM items WHERE stock = 0
CREATE TABLE CREATE TABLE items (name text, sku text, stock int, price real)
Parameter binding cursor.execute("SELECT * FROM items WHERE sku = ?", ("WGT-001",))

WHERE clause operators

= != < > <= >= LIKE AND

OR, IN, subqueries, JOIN, GROUP BY, UNION, and HAVING are not supported in the recfile backend and will raise UnsupportedSQLError. If you need any of these, it is a good signal to switch to SQLite — the migration is a one-line change.

Joins

recdb does not support JOIN. Recfiles are designed for independent, self-contained record sets — the recutils tooling has no join primitive. If your data is naturally relational (foreign keys, many-to-many), you will be better served by SQLite from the start.

For occasional cross-table lookups, the application-side join pattern works fine:

# Find all books currently checked out, then look up each borrower
checked_out = conn.execute("SELECT * FROM books WHERE checked_out_by != ''").fetchall()
for book in checked_out:
    borrower = conn.execute(
        "SELECT * FROM borrowers WHERE member_id = ?", (book["checked_out_by"],)
    ).fetchone()
    print(f"{book['title']}{borrower['name']}")

File layout

recdb supports two file layouts:

Single-file mode — one .rec file holds all tables as separate %rec: blocks:

project/
├── inventory.rec    # %rec: items block + %rec: suppliers block + ...
└── app.py
conn = recdb.connect("inventory.rec")
conn.execute("SELECT * FROM items")
conn.execute("SELECT * FROM suppliers")

Directory mode — each table gets its own .rec file:

project/
├── data/
│   ├── items.rec
│   └── suppliers.rec
└── app.py
conn = recdb.connect("./data")
conn.execute("SELECT * FROM items")      # → ./data/items.rec
conn.execute("SELECT * FROM suppliers")  # → ./data/suppliers.rec

Example .rec file content:

%rec: items
%mandatory: name sku stock price
%type: stock int
%type: price real

name: Widget A
sku: WGT-001
stock: 120
price: 9.99

name: Widget B
sku: WGT-002
stock: 5
price: 14.50

%rec: suppliers
%mandatory: name contact

name: Acme Corp
contact: acme@example.com

Rows are dicts

The recfile backend returns rows as dict. Column access by name is always safe:

row = cur.fetchone()
print(row["name"])    # works
print(row["stock"])   # works

See COMPATIBILITY.md for a full table of deviations from stdlib sqlite3.

Exceptions

All recdb errors inherit from recdb.RecDBError:

import recdb

try:
    conn.execute("SELECT * FROM items JOIN orders ON ...")
except recdb.UnsupportedSQLError:
    # JOIN not supported — switch to SQLite or use application-side join
    ...
except recdb.RecutilsNotFoundError:
    # Neither GNU recutils nor python-recutils is installed
    ...
except recdb.RecDBError:
    # Catch-all for any recdb error
    ...
Exception Raised when
RecDBError Base class for all recdb errors
SQLParseError SQL cannot be parsed (syntax error, column/value count mismatch)
UnsupportedSQLError Valid SQL that the recfile backend does not support (JOIN, OR, etc.)
RecutilsError A recutils subprocess exited with a non-zero return code
RecutilsNotFoundError Neither GNU recutils nor python-recutils is available

When to switch to SQLite

recdb is a good fit when your data is:

  • Small enough to read comfortably in a text editor
  • Stored in git alongside the code that uses it
  • Rarely queried relationally (no JOINs, or simple application-side lookups)

Switch to SQLite when you need:

  • JOINs or complex relational queries
  • Concurrent writers
  • High write throughput
  • Referential integrity enforcement

The switch is a one-line code change. That is intentional.

Requirements

  • Python 3.9+
  • sqlparse (installed automatically)
  • At least one of: GNU recutils (apt install recutils / brew install recutils) or python-recutils (pip install recdb[recutils])

Stability and 1.0 release

RecDB is a hobby project without a fixed release schedule. The 1.0 release will be considered when the project has been used successfully in other projects over time. Until then, minor versions may include breaking changes.

Known users / projects

Send a PR to add your project here — this helps track readiness for v1.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

recdb-0.2.0.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

recdb-0.2.0-py3-none-any.whl (19.2 kB view details)

Uploaded Python 3

File details

Details for the file recdb-0.2.0.tar.gz.

File metadata

  • Download URL: recdb-0.2.0.tar.gz
  • Upload date:
  • Size: 23.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for recdb-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6ab697aa00d66fe4d23fa6c78c87ca8d4bd539d8466752653247dde2a9a3003c
MD5 697dda5b7713a4c7596b242fd2b191ee
BLAKE2b-256 2f167bdf28d3010b02c5deb1239730f268a8071bc8f4296edab6dc895af941ee

See more details on using hashes here.

File details

Details for the file recdb-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: recdb-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 19.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for recdb-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1d020b4aec3c602e4073e732322e027ca60dcafac5b5c0898e6f8189f511838d
MD5 b01a44a426d0b4e4278061355c461f01
BLAKE2b-256 f9160f173c6b251840b2bbae57117bc90fa4fbf99fd93876cc830c593605f6d4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page