sqlodin 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ # Build outputs
2
+ bin/
3
+ build/
4
+ docs/build/
5
+ *.dSYM/
6
+
7
+ # Object files and dynamic libraries
8
+ *.o
9
+ *.so
10
+ *.dylib
11
+ *.dll
12
+
13
+ # Compiled static archive for sqlite-vec is preserved
14
+ !src/sqlite/libsqlite_vec.a
15
+
16
+ # OS files
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Python caches
21
+ __pycache__/
22
+ *.py[cod]
23
+
24
+ # Python development environments
25
+ .venv/
26
+ .pytest_cache/
27
+
28
+ # Python distribution artifacts
29
+ languages/python/dist/
30
+
31
+ # Private CLI session and unresolved write state
32
+ *.shell.db
33
+ *.shell.db-*
34
+ *.shell.db.lock
35
+
36
+ # Formal checker caches
37
+ .tlacache/
38
+ specs/states/
sqlodin-0.6.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vikrant Rathore and Ronak Rathore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sqlodin-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.5
2
+ Name: sqlodin
3
+ Version: 0.6.0
4
+ Summary: A small, explicit Python client for SQLodin's durable multi-master SQL service
5
+ Author: Vikrant Rathore, Ronak Rathore
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Provides-Extra: sqlalchemy
10
+ Requires-Dist: sqlalchemy<2.1,>=2.0.43; extra == 'sqlalchemy'
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest<10,>=8; extra == 'test'
13
+ Requires-Dist: sqlalchemy<2.1,>=2.0.43; extra == 'test'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # SQLodin for Python
17
+
18
+ A small synchronous client for SQLodin's native mTLS SQL service. Written by
19
+ Vikrant Rathore with assistance from Ronak Rathore. Python 3.11+, no runtime dependencies.
20
+ This is an initial client for the bounded SQLodin service, not a production-qualified release.
21
+
22
+ From this repository:
23
+
24
+ ```sh
25
+ uv add ./languages/python
26
+ # Development
27
+ cd languages/python
28
+ uv sync --extra test
29
+ uv run pytest
30
+ uv build
31
+ ```
32
+
33
+ Connect once and reuse the connection. Each endpoint's server name must match an
34
+ exact DNS SAN in its certificate; the address is the reachable host and port.
35
+
36
+ ```python
37
+ import sqlodin
38
+
39
+ nodes = [
40
+ sqlodin.Endpoint("10.175.52.19:7600", "node1.sqlodin.test"),
41
+ sqlodin.Endpoint("10.175.52.20:7600", "node2.sqlodin.test"),
42
+ sqlodin.Endpoint("10.175.52.21:7600", "node3.sqlodin.test"),
43
+ ]
44
+ tls = sqlodin.TLS(ca="ca.pem", cert="app.pem", key="app.key")
45
+
46
+ with sqlodin.connect(nodes, cluster="orders", tls=tls) as db:
47
+ db.execute("INSERT INTO orders(id, customer) VALUES (?, ?)", (42, "Ada"))
48
+ order = db.query("SELECT id, customer FROM orders WHERE id = ?", (42,)).one()
49
+ print(order["customer"]) # Ada
50
+ print(order[0]) # 42
51
+ ```
52
+
53
+ `execute()` returns `WriteResult(changes, applied, node, sequence)`. Successful writes
54
+ have durable quorum acceptance and local application. `query()` returns immutable
55
+ `Rows` with `columns`, iteration/indexing, `first()`, `one()`, and `scalar()`.
56
+ Rows support column names, numeric positions, `dict(row)` and `row.as_tuple()`.
57
+ Duplicate column names resolve to the first occurrence by name; positions preserve
58
+ all columns. Prefer SQL aliases for unambiguous names. SQL NULL becomes `None`,
59
+ integers retain 64-bit precision, and BLOB query values become `bytes`.
60
+
61
+ Queries default to a fresh quorum barrier. `consistency="local"` explicitly permits
62
+ stale results without a quorum. `status()` describes the contacted node's local
63
+ state; it does not prove quorum availability. Queries may run while a write is
64
+ uncertain, but their results do not resolve that write's identity.
65
+
66
+ ## Atomic write batches
67
+
68
+ ```python
69
+ with db.transaction() as tx:
70
+ tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (20, 1))
71
+ tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (20, 2))
72
+ print(tx.result.changes)
73
+ ```
74
+
75
+ The context buffers SQL, then submits one atomic transaction body on successful
76
+ exit. A Python exception discards the unsent batch. A SQL constraint rolls back the
77
+ whole batch and raises `ConstraintError`. This is a write-only buffered batch:
78
+ there is no live transaction or query inside it. Do not include `BEGIN`, `COMMIT`,
79
+ or `ROLLBACK`. Statements use plain `?` placeholders. Values remain bound parameters,
80
+ including when the batch assigns distinct parameter positions to each statement.
81
+ `execute()` does not return rows; use `query()` for reads. SQL `RETURNING` rows are
82
+ currently discarded by the engine and are not exposed by this API.
83
+
84
+ ## A timeout is not a rollback
85
+
86
+ The client retries connection failures across the supplied endpoints under one
87
+ operation deadline, preserving the same session, sequence, SQL, and parameters.
88
+ If it cannot learn the result, it raises `UnknownOutcome` and retains `db.pending`.
89
+ It refuses a new write until that request is resolved:
90
+
91
+ ```python
92
+ try:
93
+ db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (20, 1))
94
+ except sqlodin.UnknownOutcome as exc:
95
+ # Restore connectivity, then retry the exact identity.
96
+ result = db.resolve_pending()
97
+ ```
98
+
99
+ For recovery after closing the connection, persist `exc.pending.to_json()` securely
100
+ and open a new connection with `pending=sqlodin.PendingWrite.from_json(saved)`.
101
+ Then call `resolve_pending()`. Saved requests contain application SQL and values.
102
+ Only one owner may advance a session. `Expired` and `Identity_Conflict` raise
103
+ `SessionError`; they do not silently create another session or rerun a payment.
104
+ Recovery of a request lost in a Python process crash *before its pending identity
105
+ was persisted* is not automatic. Use application-level unique operation IDs and
106
+ reconciliation for that case. This client makes no general exactly-once claim.
107
+
108
+ A connection serializes calls with a lock; use distinct reusable connections for
109
+ concurrency. Do not share connections across forked processes. New sessions consume
110
+ persistent server capacity, so reuse connections rather than opening one per write.
111
+ The native client has no async interface or connection pool. The optional SQLAlchemy
112
+ adapter provides optimistic serializable transactions by default, described below.
113
+
114
+ ## Current bounds
115
+
116
+ The server supports fixed voter membership, at most 8 statements / 4096 SQL bytes /
117
+ 16 parameters per transaction, and 256 UTF-8 bytes per text parameter. Parameters
118
+ support `str`, signed 64-bit `int`, finite `float`, `Vector`, and `None`; arbitrary BLOB parameters are
119
+ not yet supported. Queries are read-only, one statement, at most 4096 rows, with a
120
+ 256 KiB internal result budget and an instruction budget. Large results fail as a
121
+ whole (`QueryError`), so use bounded application pagination. Replicated SQL follows
122
+ the engine's deterministic function policy. Host addresses are currently numeric
123
+ IPv4 endpoints. Live voter changes and production qualification remain open.
124
+
125
+ ## Vector, full-text, and hybrid search
126
+
127
+ ```python
128
+ with sqlodin.connect(nodes, cluster="orders", tls=tls) as db:
129
+ docs = db.create_search_index("documents", dimensions=3) # once
130
+ docs.put(1, title="Consensus", body="Durable Paxos replication",
131
+ vector=[0.9, 0.1, 0.0])
132
+ print(docs.full_text("Paxos").one()["title"])
133
+ print(docs.nearest([1, 0, 0], metric="l2").first())
134
+ print(docs.hybrid("durable", [1, 0, 0], limit=5, candidates=30))
135
+ # On later connections, open a handle without creating tables:
136
+ docs = db.search_index("documents", dimensions=3)
137
+ ```
138
+
139
+ `put()` and `delete()` keep ordinary content/vector storage and the FTS5 table
140
+ consistent in one durable transaction. Use these methods for all index mutations;
141
+ direct SQL can bypass that relationship. Opening a handle does not validate an
142
+ existing schema. Creation fails if either table already exists.
143
+
144
+ `full_text()` uses [FTS5 query syntax and BM25](https://www.sqlite.org/fts5.html);
145
+ lower scores rank first. `nearest()` performs an **exact distance scan**, using
146
+ sqlite-vec's `vec_distance_l2` or `vec_distance_cosine`; it is not an ANN index.
147
+ Use nonzero stored and query vectors for cosine distance. Hybrid retrieval performs
148
+ reciprocal-rank fusion, summing `1 / (rank_constant + rank)` over the two candidate
149
+ lists. Higher fused scores rank first; ties use document IDs. Both lists are
150
+ computed by one query against one fresh fenced snapshot. A candidate missing from
151
+ one list contributes only its other rank. Limits and candidate counts are 1–100.
152
+ A candidate limit bounds sorting/results, not the work needed to scan embeddings.
153
+
154
+ Vectors use immutable `sqlodin.Vector` values: 1–384 finite float32 components,
155
+ canonicalized before encoding. The whole request has a 384-component budget.
156
+ Ordinary SQL accepts these as typed parameters and returns vector BLOBs as `bytes`:
157
+
158
+ ```python
159
+ v = sqlodin.Vector([0.9, 0.1, 0.0])
160
+ row = db.query("SELECT vec_distance_l2(embedding, ?) FROM documents WHERE id=?",
161
+ (v, 1)).one()
162
+ # Decode a selected embedding with sqlodin.Vector.from_bytes(blob).
163
+ ```
164
+
165
+ Titles, bodies and FTS expressions retain the 256-byte UTF-8 parameter limit.
166
+ This is currently a bounded document/chunk API, not unrestricted document ingestion.
167
+ Searches remain subject to the read instruction/result budgets. The durable service
168
+ admits built-in FTS5 and protects its shadow tables using SQLite defensive mode;
169
+ `vec0` virtual-table creation remains unsupported. General FTS maintenance commands
170
+ and custom tokenizers are outside the tested API.
171
+
172
+ ## SQLAlchemy ORM and Core (optional)
173
+
174
+ Install the optional dependency from this checkout:
175
+
176
+ ```sh
177
+ uv pip install './languages/python[sqlalchemy]'
178
+ # For repository development:
179
+ uv sync --project languages/python --extra sqlalchemy --extra test
180
+ ```
181
+
182
+ ```python
183
+ from sqlalchemy import Column, Integer, MetaData, String, Table, select
184
+ from sqlodin.sqlalchemy import VectorType, create_engine
185
+
186
+ engine = create_engine(nodes, cluster="orders", tls=tls)
187
+ items = Table("items", MetaData(),
188
+ Column("id", Integer, primary_key=True, autoincrement=False),
189
+ Column("title", String),
190
+ Column("embedding", VectorType(3)),
191
+ )
192
+ items.create(engine, checkfirst=True)
193
+ with engine.begin() as conn:
194
+ conn.execute(items.insert(), {"id": 1, "title": "Paxos", "embedding": [1, 0, 0]})
195
+ row = conn.execute(select(items).where(items.c.id == 1)).mappings().one()
196
+ print(row["embedding"]) # Vector
197
+ engine.dispose()
198
+ ```
199
+
200
+ The registered dialect is `sqlodin://`. It uses the same native mTLS client and
201
+ bound values. SQLAlchemy `text()` queries support FTS, and `VectorType` works with
202
+ `func.vec_distance_l2`/`func.vec_distance_cosine` and typed `bindparam` values.
203
+ VectorType validates dimensions on bind/result conversion; add a database CHECK
204
+ constraint if other writers must also be constrained.
205
+
206
+ The default is **SERIALIZABLE transactions**. `Session.begin()` and `engine.begin()`
207
+ commit atomically; rollback, exceptions and connection close discard staged work.
208
+ ORM flush returns generated integer primary keys and supports reads of staged writes.
209
+ Nested savepoints support rollback and release. Provide mapped classes or declared
210
+ Table metadata: general reflection, explicit DML RETURNING and two-phase/XA commits
211
+ remain unsupported. Explicit `autocommit=True` opts into independent statement commits.
212
+
213
+ ```python
214
+ from sqlalchemy.orm import Session
215
+
216
+ with Session(engine) as session, session.begin():
217
+ parent = Parent(name="Ada")
218
+ session.add(parent)
219
+ session.flush() # parent.id is available; other sessions cannot see it yet
220
+ session.add(Child(parent_id=parent.id, label="first"))
221
+ ```
222
+
223
+ The server privately evaluates staged statements, rolls back the preview, and checks
224
+ its read revision again when the full transaction commits through Paxos. Any
225
+ intervening application write currently causes a serialization failure, including
226
+ writes to unrelated tables. Catch `sqlodin.dbapi.SerializationError` through
227
+ SQLAlchemy's `OperationalError.orig`, roll back, and retry the **whole transaction**.
228
+ Queries and flushes replay staged writes, so short transactions are preferable.
229
+ Transaction limits remain eight writes / 4096 SQL bytes / sixteen parameters / 384
230
+ vector components. Successful commit is the durable boundary, not flush.
231
+
232
+ Unknown commits raise `sqlodin.dbapi.OperationalError`; inspect `exc.orig.pending`.
233
+ Save that identity, resolve it with a native connection and the same authenticated
234
+ client SAN, and discard the affected pooled connection. Its saved read revision is
235
+ part of the retry identity. An unresolved commit blocks rollback/pool reset and new
236
+ writes. Do not start a fresh transaction as a substitute for resolving it.
237
+
238
+ See [the ORM transaction contract](../../docs/guides/orm-transactions.typ) for the execution
239
+ model, retry example, resource bounds and serializability argument.
240
+
241
+ ## Compatibility and verification
242
+
243
+ Python 0.3.0 ORM transactions require the **format-4 / SQL-policy-6** service. The
244
+ revision is persisted with application outcomes and included in commit digests.
245
+ Older stores and peers fail closed; no automatic or rolling migration is provided.
246
+ Preserve existing data until a separately validated export/import migration is available.
247
+
248
+ `tools/check_orm_transactions.py` exercises ORM begin/flush, generated keys,
249
+ relationships, rollback/close, nested savepoints, constraint handling, concurrent
250
+ conflicts, pool cleanup, uncertain commits, quorum loss and all-voter recovery.
251
+ `tools/check_python_features.py` covers vectors, FTS and SQLAlchemy search paths.
252
+ Saved local/Linux correctness reports are under `benchmarks/results/`. They are
253
+ not search throughput benchmarks or production certification.
@@ -0,0 +1,238 @@
1
+ # SQLodin for Python
2
+
3
+ A small synchronous client for SQLodin's native mTLS SQL service. Written by
4
+ Vikrant Rathore with assistance from Ronak Rathore. Python 3.11+, no runtime dependencies.
5
+ This is an initial client for the bounded SQLodin service, not a production-qualified release.
6
+
7
+ From this repository:
8
+
9
+ ```sh
10
+ uv add ./languages/python
11
+ # Development
12
+ cd languages/python
13
+ uv sync --extra test
14
+ uv run pytest
15
+ uv build
16
+ ```
17
+
18
+ Connect once and reuse the connection. Each endpoint's server name must match an
19
+ exact DNS SAN in its certificate; the address is the reachable host and port.
20
+
21
+ ```python
22
+ import sqlodin
23
+
24
+ nodes = [
25
+ sqlodin.Endpoint("10.175.52.19:7600", "node1.sqlodin.test"),
26
+ sqlodin.Endpoint("10.175.52.20:7600", "node2.sqlodin.test"),
27
+ sqlodin.Endpoint("10.175.52.21:7600", "node3.sqlodin.test"),
28
+ ]
29
+ tls = sqlodin.TLS(ca="ca.pem", cert="app.pem", key="app.key")
30
+
31
+ with sqlodin.connect(nodes, cluster="orders", tls=tls) as db:
32
+ db.execute("INSERT INTO orders(id, customer) VALUES (?, ?)", (42, "Ada"))
33
+ order = db.query("SELECT id, customer FROM orders WHERE id = ?", (42,)).one()
34
+ print(order["customer"]) # Ada
35
+ print(order[0]) # 42
36
+ ```
37
+
38
+ `execute()` returns `WriteResult(changes, applied, node, sequence)`. Successful writes
39
+ have durable quorum acceptance and local application. `query()` returns immutable
40
+ `Rows` with `columns`, iteration/indexing, `first()`, `one()`, and `scalar()`.
41
+ Rows support column names, numeric positions, `dict(row)` and `row.as_tuple()`.
42
+ Duplicate column names resolve to the first occurrence by name; positions preserve
43
+ all columns. Prefer SQL aliases for unambiguous names. SQL NULL becomes `None`,
44
+ integers retain 64-bit precision, and BLOB query values become `bytes`.
45
+
46
+ Queries default to a fresh quorum barrier. `consistency="local"` explicitly permits
47
+ stale results without a quorum. `status()` describes the contacted node's local
48
+ state; it does not prove quorum availability. Queries may run while a write is
49
+ uncertain, but their results do not resolve that write's identity.
50
+
51
+ ## Atomic write batches
52
+
53
+ ```python
54
+ with db.transaction() as tx:
55
+ tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (20, 1))
56
+ tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (20, 2))
57
+ print(tx.result.changes)
58
+ ```
59
+
60
+ The context buffers SQL, then submits one atomic transaction body on successful
61
+ exit. A Python exception discards the unsent batch. A SQL constraint rolls back the
62
+ whole batch and raises `ConstraintError`. This is a write-only buffered batch:
63
+ there is no live transaction or query inside it. Do not include `BEGIN`, `COMMIT`,
64
+ or `ROLLBACK`. Statements use plain `?` placeholders. Values remain bound parameters,
65
+ including when the batch assigns distinct parameter positions to each statement.
66
+ `execute()` does not return rows; use `query()` for reads. SQL `RETURNING` rows are
67
+ currently discarded by the engine and are not exposed by this API.
68
+
69
+ ## A timeout is not a rollback
70
+
71
+ The client retries connection failures across the supplied endpoints under one
72
+ operation deadline, preserving the same session, sequence, SQL, and parameters.
73
+ If it cannot learn the result, it raises `UnknownOutcome` and retains `db.pending`.
74
+ It refuses a new write until that request is resolved:
75
+
76
+ ```python
77
+ try:
78
+ db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (20, 1))
79
+ except sqlodin.UnknownOutcome as exc:
80
+ # Restore connectivity, then retry the exact identity.
81
+ result = db.resolve_pending()
82
+ ```
83
+
84
+ For recovery after closing the connection, persist `exc.pending.to_json()` securely
85
+ and open a new connection with `pending=sqlodin.PendingWrite.from_json(saved)`.
86
+ Then call `resolve_pending()`. Saved requests contain application SQL and values.
87
+ Only one owner may advance a session. `Expired` and `Identity_Conflict` raise
88
+ `SessionError`; they do not silently create another session or rerun a payment.
89
+ Recovery of a request lost in a Python process crash *before its pending identity
90
+ was persisted* is not automatic. Use application-level unique operation IDs and
91
+ reconciliation for that case. This client makes no general exactly-once claim.
92
+
93
+ A connection serializes calls with a lock; use distinct reusable connections for
94
+ concurrency. Do not share connections across forked processes. New sessions consume
95
+ persistent server capacity, so reuse connections rather than opening one per write.
96
+ The native client has no async interface or connection pool. The optional SQLAlchemy
97
+ adapter provides optimistic serializable transactions by default, described below.
98
+
99
+ ## Current bounds
100
+
101
+ The server supports fixed voter membership, at most 8 statements / 4096 SQL bytes /
102
+ 16 parameters per transaction, and 256 UTF-8 bytes per text parameter. Parameters
103
+ support `str`, signed 64-bit `int`, finite `float`, `Vector`, and `None`; arbitrary BLOB parameters are
104
+ not yet supported. Queries are read-only, one statement, at most 4096 rows, with a
105
+ 256 KiB internal result budget and an instruction budget. Large results fail as a
106
+ whole (`QueryError`), so use bounded application pagination. Replicated SQL follows
107
+ the engine's deterministic function policy. Host addresses are currently numeric
108
+ IPv4 endpoints. Live voter changes and production qualification remain open.
109
+
110
+ ## Vector, full-text, and hybrid search
111
+
112
+ ```python
113
+ with sqlodin.connect(nodes, cluster="orders", tls=tls) as db:
114
+ docs = db.create_search_index("documents", dimensions=3) # once
115
+ docs.put(1, title="Consensus", body="Durable Paxos replication",
116
+ vector=[0.9, 0.1, 0.0])
117
+ print(docs.full_text("Paxos").one()["title"])
118
+ print(docs.nearest([1, 0, 0], metric="l2").first())
119
+ print(docs.hybrid("durable", [1, 0, 0], limit=5, candidates=30))
120
+ # On later connections, open a handle without creating tables:
121
+ docs = db.search_index("documents", dimensions=3)
122
+ ```
123
+
124
+ `put()` and `delete()` keep ordinary content/vector storage and the FTS5 table
125
+ consistent in one durable transaction. Use these methods for all index mutations;
126
+ direct SQL can bypass that relationship. Opening a handle does not validate an
127
+ existing schema. Creation fails if either table already exists.
128
+
129
+ `full_text()` uses [FTS5 query syntax and BM25](https://www.sqlite.org/fts5.html);
130
+ lower scores rank first. `nearest()` performs an **exact distance scan**, using
131
+ sqlite-vec's `vec_distance_l2` or `vec_distance_cosine`; it is not an ANN index.
132
+ Use nonzero stored and query vectors for cosine distance. Hybrid retrieval performs
133
+ reciprocal-rank fusion, summing `1 / (rank_constant + rank)` over the two candidate
134
+ lists. Higher fused scores rank first; ties use document IDs. Both lists are
135
+ computed by one query against one fresh fenced snapshot. A candidate missing from
136
+ one list contributes only its other rank. Limits and candidate counts are 1–100.
137
+ A candidate limit bounds sorting/results, not the work needed to scan embeddings.
138
+
139
+ Vectors use immutable `sqlodin.Vector` values: 1–384 finite float32 components,
140
+ canonicalized before encoding. The whole request has a 384-component budget.
141
+ Ordinary SQL accepts these as typed parameters and returns vector BLOBs as `bytes`:
142
+
143
+ ```python
144
+ v = sqlodin.Vector([0.9, 0.1, 0.0])
145
+ row = db.query("SELECT vec_distance_l2(embedding, ?) FROM documents WHERE id=?",
146
+ (v, 1)).one()
147
+ # Decode a selected embedding with sqlodin.Vector.from_bytes(blob).
148
+ ```
149
+
150
+ Titles, bodies and FTS expressions retain the 256-byte UTF-8 parameter limit.
151
+ This is currently a bounded document/chunk API, not unrestricted document ingestion.
152
+ Searches remain subject to the read instruction/result budgets. The durable service
153
+ admits built-in FTS5 and protects its shadow tables using SQLite defensive mode;
154
+ `vec0` virtual-table creation remains unsupported. General FTS maintenance commands
155
+ and custom tokenizers are outside the tested API.
156
+
157
+ ## SQLAlchemy ORM and Core (optional)
158
+
159
+ Install the optional dependency from this checkout:
160
+
161
+ ```sh
162
+ uv pip install './languages/python[sqlalchemy]'
163
+ # For repository development:
164
+ uv sync --project languages/python --extra sqlalchemy --extra test
165
+ ```
166
+
167
+ ```python
168
+ from sqlalchemy import Column, Integer, MetaData, String, Table, select
169
+ from sqlodin.sqlalchemy import VectorType, create_engine
170
+
171
+ engine = create_engine(nodes, cluster="orders", tls=tls)
172
+ items = Table("items", MetaData(),
173
+ Column("id", Integer, primary_key=True, autoincrement=False),
174
+ Column("title", String),
175
+ Column("embedding", VectorType(3)),
176
+ )
177
+ items.create(engine, checkfirst=True)
178
+ with engine.begin() as conn:
179
+ conn.execute(items.insert(), {"id": 1, "title": "Paxos", "embedding": [1, 0, 0]})
180
+ row = conn.execute(select(items).where(items.c.id == 1)).mappings().one()
181
+ print(row["embedding"]) # Vector
182
+ engine.dispose()
183
+ ```
184
+
185
+ The registered dialect is `sqlodin://`. It uses the same native mTLS client and
186
+ bound values. SQLAlchemy `text()` queries support FTS, and `VectorType` works with
187
+ `func.vec_distance_l2`/`func.vec_distance_cosine` and typed `bindparam` values.
188
+ VectorType validates dimensions on bind/result conversion; add a database CHECK
189
+ constraint if other writers must also be constrained.
190
+
191
+ The default is **SERIALIZABLE transactions**. `Session.begin()` and `engine.begin()`
192
+ commit atomically; rollback, exceptions and connection close discard staged work.
193
+ ORM flush returns generated integer primary keys and supports reads of staged writes.
194
+ Nested savepoints support rollback and release. Provide mapped classes or declared
195
+ Table metadata: general reflection, explicit DML RETURNING and two-phase/XA commits
196
+ remain unsupported. Explicit `autocommit=True` opts into independent statement commits.
197
+
198
+ ```python
199
+ from sqlalchemy.orm import Session
200
+
201
+ with Session(engine) as session, session.begin():
202
+ parent = Parent(name="Ada")
203
+ session.add(parent)
204
+ session.flush() # parent.id is available; other sessions cannot see it yet
205
+ session.add(Child(parent_id=parent.id, label="first"))
206
+ ```
207
+
208
+ The server privately evaluates staged statements, rolls back the preview, and checks
209
+ its read revision again when the full transaction commits through Paxos. Any
210
+ intervening application write currently causes a serialization failure, including
211
+ writes to unrelated tables. Catch `sqlodin.dbapi.SerializationError` through
212
+ SQLAlchemy's `OperationalError.orig`, roll back, and retry the **whole transaction**.
213
+ Queries and flushes replay staged writes, so short transactions are preferable.
214
+ Transaction limits remain eight writes / 4096 SQL bytes / sixteen parameters / 384
215
+ vector components. Successful commit is the durable boundary, not flush.
216
+
217
+ Unknown commits raise `sqlodin.dbapi.OperationalError`; inspect `exc.orig.pending`.
218
+ Save that identity, resolve it with a native connection and the same authenticated
219
+ client SAN, and discard the affected pooled connection. Its saved read revision is
220
+ part of the retry identity. An unresolved commit blocks rollback/pool reset and new
221
+ writes. Do not start a fresh transaction as a substitute for resolving it.
222
+
223
+ See [the ORM transaction contract](../../docs/guides/orm-transactions.typ) for the execution
224
+ model, retry example, resource bounds and serializability argument.
225
+
226
+ ## Compatibility and verification
227
+
228
+ Python 0.3.0 ORM transactions require the **format-4 / SQL-policy-6** service. The
229
+ revision is persisted with application outcomes and included in commit digests.
230
+ Older stores and peers fail closed; no automatic or rolling migration is provided.
231
+ Preserve existing data until a separately validated export/import migration is available.
232
+
233
+ `tools/check_orm_transactions.py` exercises ORM begin/flush, generated keys,
234
+ relationships, rollback/close, nested savepoints, constraint handling, concurrent
235
+ conflicts, pool cleanup, uncertain commits, quorum loss and all-voter recovery.
236
+ `tools/check_python_features.py` covers vectors, FTS and SQLAlchemy search paths.
237
+ Saved local/Linux correctness reports are under `benchmarks/results/`. They are
238
+ not search throughput benchmarks or production certification.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sqlodin"
7
+ version = "0.6.0"
8
+ description = "A small, explicit Python client for SQLodin's durable multi-master SQL service"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [{name = "Vikrant Rathore"}, {name = "Ronak Rathore"}]
12
+ license = "MIT"
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ test = ["pytest>=8,<10", "SQLAlchemy>=2.0.43,<2.1"]
17
+ sqlalchemy = ["SQLAlchemy>=2.0.43,<2.1"]
18
+
19
+ [project.entry-points."sqlalchemy.dialects"]
20
+ sqlodin = "sqlodin.sqlalchemy:SQLodinDialect"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/sqlodin"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
@@ -0,0 +1,15 @@
1
+ """SQLodin: durable SQL with a small, explicit Python API."""
2
+ from .client import Connection, PendingWrite, connect
3
+ from .errors import (ConnectionError, ConstraintError, Error, PendingWriteError,
4
+ QueryError, SerializationError, SessionError, UnknownOutcome)
5
+ from .results import Row, Rows, WriteResult
6
+ from .transport import Endpoint, TLS
7
+ from .vector import Vector
8
+ from .search import SearchIndex
9
+
10
+ __all__ = [
11
+ 'Vector', 'SearchIndex', 'connect', 'Connection', 'Endpoint', 'TLS', 'PendingWrite', 'Row', 'Rows', 'WriteResult',
12
+ 'Error', 'ConnectionError', 'ConstraintError', 'PendingWriteError', 'QueryError',
13
+ 'SessionError', 'SerializationError', 'UnknownOutcome',
14
+ ]
15
+ __version__ = '0.6.0'