Skip to main content

SQLodin for Python

A small synchronous client for SQLodin's native mTLS SQL service. Written by Vikrant Rathore with assistance from Ronak Rathore. Python 3.11+, no runtime dependencies. This is an initial client for the bounded SQLodin service, not a production-qualified release.

From this repository:

uv add ./languages/python
# Development
cd languages/python
uv sync --extra test
uv run pytest
uv build

Connect once and reuse the connection. Each endpoint's server name must match an exact DNS SAN in its certificate; the address is the reachable host and port.

import sqlodin

nodes = [
    sqlodin.Endpoint("10.175.52.19:7600", "node1.sqlodin.test"),
    sqlodin.Endpoint("10.175.52.20:7600", "node2.sqlodin.test"),
    sqlodin.Endpoint("10.175.52.21:7600", "node3.sqlodin.test"),
]
tls = sqlodin.TLS(ca="ca.pem", cert="app.pem", key="app.key")

with sqlodin.connect(nodes, cluster="orders", tls=tls) as db:
    db.execute("INSERT INTO orders(id, customer) VALUES (?, ?)", (42, "Ada"))
    order = db.query("SELECT id, customer FROM orders WHERE id = ?", (42,)).one()
    print(order["customer"])  # Ada
    print(order[0])           # 42

execute() returns WriteResult(changes, applied, node, sequence). Successful writes have durable quorum acceptance and local application. query() returns immutable Rows with columns, iteration/indexing, first(), one(), and scalar(). Rows support column names, numeric positions, dict(row) and row.as_tuple(). Duplicate column names resolve to the first occurrence by name; positions preserve all columns. Prefer SQL aliases for unambiguous names. SQL NULL becomes None, integers retain 64-bit precision, and BLOB query values become bytes.

Queries default to a fresh quorum barrier. consistency="local" explicitly permits stale results without a quorum. status() describes the contacted node's local state; it does not prove quorum availability. Queries may run while a write is uncertain, but their results do not resolve that write's identity.

Atomic write batches

with db.transaction() as tx:
    tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (20, 1))
    tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (20, 2))
print(tx.result.changes)

The context buffers SQL, then submits one atomic transaction body on successful exit. A Python exception discards the unsent batch. A SQL constraint rolls back the whole batch and raises ConstraintError. This is a write-only buffered batch: there is no live transaction or query inside it. Do not include BEGIN, COMMIT, or ROLLBACK. Statements use plain ? placeholders. Values remain bound parameters, including when the batch assigns distinct parameter positions to each statement. execute() does not return rows; use query() for reads. SQL RETURNING rows are currently discarded by the engine and are not exposed by this API.

A timeout is not a rollback

The client retries connection failures across the supplied endpoints under one operation deadline, preserving the same session, sequence, SQL, and parameters. If it cannot learn the result, it raises UnknownOutcome and retains db.pending. It refuses a new write until that request is resolved:

try:
    db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (20, 1))
except sqlodin.UnknownOutcome as exc:
    # Restore connectivity, then retry the exact identity.
    result = db.resolve_pending()

For recovery after closing the connection, persist exc.pending.to_json() securely and open a new connection with pending=sqlodin.PendingWrite.from_json(saved). Then call resolve_pending(). Saved requests contain application SQL and values. Only one owner may advance a session. Expired and Identity_Conflict raise SessionError; they do not silently create another session or rerun a payment. Recovery of a request lost in a Python process crash before its pending identity was persisted is not automatic. Use application-level unique operation IDs and reconciliation for that case. This client makes no general exactly-once claim.

A connection serializes calls with a lock; use distinct reusable connections for concurrency. Do not share connections across forked processes. New sessions consume persistent server capacity, so reuse connections rather than opening one per write. The native client has no async interface or connection pool. The optional SQLAlchemy adapter provides optimistic serializable transactions by default, described below.

Current bounds

The server supports fixed voter membership, at most 8 statements / 4096 SQL bytes / 16 parameters per transaction, and 256 UTF-8 bytes per text parameter. Parameters support str, signed 64-bit int, finite float, Vector, and None; arbitrary BLOB parameters are not yet supported. Queries are read-only, one statement, at most 4096 rows, with a 256 KiB internal result budget and an instruction budget. Large results fail as a whole (QueryError), so use bounded application pagination. Replicated SQL follows the engine's deterministic function policy. Host addresses are currently numeric IPv4 endpoints. Live voter changes and production qualification remain open.

with sqlodin.connect(nodes, cluster="orders", tls=tls) as db:
    docs = db.create_search_index("documents", dimensions=3)  # once
    docs.put(1, title="Consensus", body="Durable Paxos replication",
             vector=[0.9, 0.1, 0.0])
    print(docs.full_text("Paxos").one()["title"])
    print(docs.nearest([1, 0, 0], metric="l2").first())
    print(docs.hybrid("durable", [1, 0, 0], limit=5, candidates=30))
    # On later connections, open a handle without creating tables:
    docs = db.search_index("documents", dimensions=3)

put() and delete() keep ordinary content/vector storage and the FTS5 table consistent in one durable transaction. Use these methods for all index mutations; direct SQL can bypass that relationship. Opening a handle does not validate an existing schema. Creation fails if either table already exists.

full_text() uses FTS5 query syntax and BM25; lower scores rank first. nearest() performs an exact distance scan, using sqlite-vec's vec_distance_l2 or vec_distance_cosine; it is not an ANN index. Use nonzero stored and query vectors for cosine distance. Hybrid retrieval performs reciprocal-rank fusion, summing 1 / (rank_constant + rank) over the two candidate lists. Higher fused scores rank first; ties use document IDs. Both lists are computed by one query against one fresh fenced snapshot. A candidate missing from one list contributes only its other rank. Limits and candidate counts are 1–100. A candidate limit bounds sorting/results, not the work needed to scan embeddings.

Vectors use immutable sqlodin.Vector values: 1–384 finite float32 components, canonicalized before encoding. The whole request has a 384-component budget. Ordinary SQL accepts these as typed parameters and returns vector BLOBs as bytes:

v = sqlodin.Vector([0.9, 0.1, 0.0])
row = db.query("SELECT vec_distance_l2(embedding, ?) FROM documents WHERE id=?",
               (v, 1)).one()
# Decode a selected embedding with sqlodin.Vector.from_bytes(blob).

Titles, bodies and FTS expressions retain the 256-byte UTF-8 parameter limit. This is currently a bounded document/chunk API, not unrestricted document ingestion. Searches remain subject to the read instruction/result budgets. The durable service admits built-in FTS5 and protects its shadow tables using SQLite defensive mode; vec0 virtual-table creation remains unsupported. General FTS maintenance commands and custom tokenizers are outside the tested API.

SQLAlchemy ORM and Core (optional)

Install the optional dependency from this checkout:

uv pip install './languages/python[sqlalchemy]'
# For repository development:
uv sync --project languages/python --extra sqlalchemy --extra test
from sqlalchemy import Column, Integer, MetaData, String, Table, select
from sqlodin.sqlalchemy import VectorType, create_engine

engine = create_engine(nodes, cluster="orders", tls=tls)
items = Table("items", MetaData(),
    Column("id", Integer, primary_key=True, autoincrement=False),
    Column("title", String),
    Column("embedding", VectorType(3)),
)
items.create(engine, checkfirst=True)
with engine.begin() as conn:
    conn.execute(items.insert(), {"id": 1, "title": "Paxos", "embedding": [1, 0, 0]})
    row = conn.execute(select(items).where(items.c.id == 1)).mappings().one()
    print(row["embedding"])  # Vector
engine.dispose()

The registered dialect is sqlodin://. It uses the same native mTLS client and bound values. SQLAlchemy text() queries support FTS, and VectorType works with func.vec_distance_l2/func.vec_distance_cosine and typed bindparam values. VectorType validates dimensions on bind/result conversion; add a database CHECK constraint if other writers must also be constrained.

The default is SERIALIZABLE transactions. Session.begin() and engine.begin() commit atomically; rollback, exceptions and connection close discard staged work. ORM flush returns generated integer primary keys and supports reads of staged writes. Nested savepoints support rollback and release. Provide mapped classes or declared Table metadata: general reflection, explicit DML RETURNING and two-phase/XA commits remain unsupported. Explicit autocommit=True opts into independent statement commits.

from sqlalchemy.orm import Session

with Session(engine) as session, session.begin():
    parent = Parent(name="Ada")
    session.add(parent)
    session.flush()  # parent.id is available; other sessions cannot see it yet
    session.add(Child(parent_id=parent.id, label="first"))

The server privately evaluates staged statements, rolls back the preview, and checks its read revision again when the full transaction commits through Paxos. Any intervening application write currently causes a serialization failure, including writes to unrelated tables. Catch sqlodin.dbapi.SerializationError through SQLAlchemy's OperationalError.orig, roll back, and retry the whole transaction. Queries and flushes replay staged writes, so short transactions are preferable. Transaction limits remain eight writes / 4096 SQL bytes / sixteen parameters / 384 vector components. Successful commit is the durable boundary, not flush.

Unknown commits raise sqlodin.dbapi.OperationalError; inspect exc.orig.pending. Save that identity, resolve it with a native connection and the same authenticated client SAN, and discard the affected pooled connection. Its saved read revision is part of the retry identity. An unresolved commit blocks rollback/pool reset and new writes. Do not start a fresh transaction as a substitute for resolving it.

See the ORM transaction contract for the execution model, retry example, resource bounds and serializability argument.

Compatibility and verification

Python 0.3.0 ORM transactions require the format-4 / SQL-policy-6 service. The revision is persisted with application outcomes and included in commit digests. Older stores and peers fail closed; no automatic or rolling migration is provided. Preserve existing data until a separately validated export/import migration is available.

tools/check_orm_transactions.py exercises ORM begin/flush, generated keys, relationships, rollback/close, nested savepoints, constraint handling, concurrent conflicts, pool cleanup, uncertain commits, quorum loss and all-voter recovery. tools/check_python_features.py covers vectors, FTS and SQLAlchemy search paths. Saved local/Linux correctness reports are under benchmarks/results/. They are not search throughput benchmarks or production certification.

Release files for sqlodin 0.6.0

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

Source distribution (sdist)

Source distribution for sqlodin 0.6.0
File Size Uploaded
sqlodin-0.6.0.tar.gz 36.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sqlodin 0.6.0
File Interpreter ABI Platform
sqlodin-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 62.7 kB

Release files / sqlodin-0.6.0.tar.gz

Download URL sqlodin-0.6.0.tar.gz
Size 36.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8374c1984c9cc6ef22435ac03b21dea683dd5085ebb0d4bd55e83bc6b279efd3
BLAKE2b-256 checksum
How to use checksums
bb49f905856ae0bc5733db7eb039350cdfc8a4b4743643c54910873feb04ef18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / sqlodin-0.6.0-py3-none-any.whl

Download URL sqlodin-0.6.0-py3-none-any.whl
Size 26.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
78397c8c5c24968d45707da75f67ed8c542d9f10d02a628e7f2d35787260e0ac
BLAKE2b-256 checksum
How to use checksums
88f129e350deaa72e59e7752246f773aa35753fe0ca0c0712fde8f59a8ccc3bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 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