stoolap-python 0.4.0__cp312-cp312-win_amd64.whl

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.
stoolap/__init__.py ADDED
@@ -0,0 +1,197 @@
1
+ # Copyright 2025 Stoolap Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Stoolap - High-performance embedded SQL database for Python.
16
+
17
+ Usage:
18
+ from stoolap import Database
19
+
20
+ db = Database.open(":memory:")
21
+ db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
22
+ db.execute("INSERT INTO users VALUES ($1, $2)", [1, "Alice"])
23
+ rows = db.query("SELECT * FROM users")
24
+ # [{"id": 1, "name": "Alice"}]
25
+
26
+ Async usage:
27
+ from stoolap import AsyncDatabase
28
+
29
+ db = await AsyncDatabase.open(":memory:")
30
+ await db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
31
+ await db.execute("INSERT INTO users VALUES ($1, $2)", [1, "Alice"])
32
+ rows = await db.query("SELECT * FROM users")
33
+ await db.close()
34
+ """
35
+
36
+ from stoolap._stoolap import (
37
+ Database,
38
+ Transaction,
39
+ PreparedStatement,
40
+ Vector,
41
+ StoolapError,
42
+ )
43
+
44
+ import asyncio
45
+
46
+
47
+ class AsyncDatabase:
48
+ """Async wrapper around Database.
49
+
50
+ All methods release the GIL and run on a thread executor,
51
+ so they won't block the asyncio event loop.
52
+ """
53
+
54
+ __slots__ = ("_db",)
55
+
56
+ def __init__(self, db: Database):
57
+ self._db = db
58
+
59
+ @classmethod
60
+ async def open(cls, path: str = ":memory:") -> "AsyncDatabase":
61
+ db = await asyncio.to_thread(Database.open, path)
62
+ return cls(db)
63
+
64
+ async def execute(self, sql: str, params=None) -> int:
65
+ return await asyncio.to_thread(self._db.execute, sql, params)
66
+
67
+ async def exec(self, sql: str) -> None:
68
+ return await asyncio.to_thread(self._db.exec, sql)
69
+
70
+ async def query(self, sql: str, params=None) -> list:
71
+ return await asyncio.to_thread(self._db.query, sql, params)
72
+
73
+ async def query_one(self, sql: str, params=None):
74
+ return await asyncio.to_thread(self._db.query_one, sql, params)
75
+
76
+ async def query_raw(self, sql: str, params=None) -> dict:
77
+ return await asyncio.to_thread(self._db.query_raw, sql, params)
78
+
79
+ async def execute_batch(self, sql: str, params_list: list) -> int:
80
+ return await asyncio.to_thread(self._db.execute_batch, sql, params_list)
81
+
82
+ def prepare(self, sql: str) -> "AsyncPreparedStatement":
83
+ stmt = self._db.prepare(sql)
84
+ return AsyncPreparedStatement(stmt)
85
+
86
+ async def begin(self) -> "AsyncTransaction":
87
+ tx = await asyncio.to_thread(self._db.begin)
88
+ return AsyncTransaction(tx)
89
+
90
+ async def close(self) -> None:
91
+ await asyncio.to_thread(self._db.close)
92
+
93
+ def __repr__(self) -> str:
94
+ return "AsyncDatabase(open)"
95
+
96
+
97
+ class AsyncTransaction:
98
+ """Async wrapper around Transaction.
99
+
100
+ Can be used as an async context manager:
101
+ async with await db.begin() as tx:
102
+ await tx.execute(...)
103
+ """
104
+
105
+ __slots__ = ("_tx",)
106
+
107
+ def __init__(self, tx: Transaction):
108
+ self._tx = tx
109
+
110
+ async def execute(self, sql: str, params=None) -> int:
111
+ return await asyncio.to_thread(self._tx.execute, sql, params)
112
+
113
+ async def query(self, sql: str, params=None) -> list:
114
+ return await asyncio.to_thread(self._tx.query, sql, params)
115
+
116
+ async def query_one(self, sql: str, params=None):
117
+ return await asyncio.to_thread(self._tx.query_one, sql, params)
118
+
119
+ async def query_raw(self, sql: str, params=None) -> dict:
120
+ return await asyncio.to_thread(self._tx.query_raw, sql, params)
121
+
122
+ async def execute_batch(self, sql: str, params_list: list) -> int:
123
+ return await asyncio.to_thread(self._tx.execute_batch, sql, params_list)
124
+
125
+ async def execute_prepared(self, stmt, params=None) -> int:
126
+ return await asyncio.to_thread(self._tx.execute_prepared, stmt._stmt if isinstance(stmt, AsyncPreparedStatement) else stmt, params)
127
+
128
+ async def query_prepared(self, stmt, params=None) -> list:
129
+ return await asyncio.to_thread(self._tx.query_prepared, stmt._stmt if isinstance(stmt, AsyncPreparedStatement) else stmt, params)
130
+
131
+ async def query_one_prepared(self, stmt, params=None):
132
+ return await asyncio.to_thread(self._tx.query_one_prepared, stmt._stmt if isinstance(stmt, AsyncPreparedStatement) else stmt, params)
133
+
134
+ async def query_raw_prepared(self, stmt, params=None) -> dict:
135
+ return await asyncio.to_thread(self._tx.query_raw_prepared, stmt._stmt if isinstance(stmt, AsyncPreparedStatement) else stmt, params)
136
+
137
+ async def commit(self) -> None:
138
+ await asyncio.to_thread(self._tx.commit)
139
+
140
+ async def rollback(self) -> None:
141
+ await asyncio.to_thread(self._tx.rollback)
142
+
143
+ async def __aenter__(self) -> "AsyncTransaction":
144
+ return self
145
+
146
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> bool:
147
+ if exc_type is not None:
148
+ await self.rollback()
149
+ else:
150
+ await self.commit()
151
+ return False
152
+
153
+ def __repr__(self) -> str:
154
+ return repr(self._tx)
155
+
156
+
157
+ class AsyncPreparedStatement:
158
+ """Async wrapper around PreparedStatement."""
159
+
160
+ __slots__ = ("_stmt",)
161
+
162
+ def __init__(self, stmt: PreparedStatement):
163
+ self._stmt = stmt
164
+
165
+ async def execute(self, params=None) -> int:
166
+ return await asyncio.to_thread(self._stmt.execute, params)
167
+
168
+ async def query(self, params=None) -> list:
169
+ return await asyncio.to_thread(self._stmt.query, params)
170
+
171
+ async def query_one(self, params=None):
172
+ return await asyncio.to_thread(self._stmt.query_one, params)
173
+
174
+ async def query_raw(self, params=None) -> dict:
175
+ return await asyncio.to_thread(self._stmt.query_raw, params)
176
+
177
+ async def execute_batch(self, params_list: list) -> int:
178
+ return await asyncio.to_thread(self._stmt.execute_batch, params_list)
179
+
180
+ @property
181
+ def sql(self) -> str:
182
+ return self._stmt.sql
183
+
184
+ def __repr__(self) -> str:
185
+ return repr(self._stmt)
186
+
187
+
188
+ __all__ = [
189
+ "Database",
190
+ "Transaction",
191
+ "PreparedStatement",
192
+ "Vector",
193
+ "AsyncDatabase",
194
+ "AsyncTransaction",
195
+ "AsyncPreparedStatement",
196
+ "StoolapError",
197
+ ]
stoolap/__init__.pyi ADDED
@@ -0,0 +1,88 @@
1
+ # Copyright 2025 Stoolap Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Any, Optional, Union, Dict, List, Sequence
16
+
17
+ Params = Optional[Union[List[Any], tuple, Dict[str, Any]]]
18
+ ParamSet = Union[List[Any], tuple]
19
+
20
+ __all__: list[str]
21
+
22
+ class StoolapError(RuntimeError): ...
23
+
24
+ class Database:
25
+ @staticmethod
26
+ def open(path: str = ":memory:") -> "Database": ...
27
+ def execute(self, sql: str, params: Params = None) -> int: ...
28
+ def exec(self, sql: str) -> None: ...
29
+ def query(self, sql: str, params: Params = None) -> List[Dict[str, Any]]: ...
30
+ def query_one(self, sql: str, params: Params = None) -> Optional[Dict[str, Any]]: ...
31
+ def query_raw(self, sql: str, params: Params = None) -> Dict[str, Any]: ...
32
+ def execute_batch(self, sql: str, params_list: Sequence[ParamSet]) -> int: ...
33
+ def prepare(self, sql: str) -> "PreparedStatement": ...
34
+ def begin(self) -> "Transaction": ...
35
+ def close(self) -> None: ...
36
+
37
+ class Transaction:
38
+ def execute(self, sql: str, params: Params = None) -> int: ...
39
+ def query(self, sql: str, params: Params = None) -> List[Dict[str, Any]]: ...
40
+ def query_one(self, sql: str, params: Params = None) -> Optional[Dict[str, Any]]: ...
41
+ def query_raw(self, sql: str, params: Params = None) -> Dict[str, Any]: ...
42
+ def execute_batch(self, sql: str, params_list: Sequence[ParamSet]) -> int: ...
43
+ def commit(self) -> None: ...
44
+ def rollback(self) -> None: ...
45
+ def __enter__(self) -> "Transaction": ...
46
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ...
47
+
48
+ class PreparedStatement:
49
+ @property
50
+ def sql(self) -> str: ...
51
+ def execute(self, params: Params = None) -> int: ...
52
+ def query(self, params: Params = None) -> List[Dict[str, Any]]: ...
53
+ def query_one(self, params: Params = None) -> Optional[Dict[str, Any]]: ...
54
+ def query_raw(self, params: Params = None) -> Dict[str, Any]: ...
55
+ def execute_batch(self, params_list: Sequence[ParamSet]) -> int: ...
56
+
57
+ class AsyncDatabase:
58
+ @classmethod
59
+ async def open(cls, path: str = ":memory:") -> "AsyncDatabase": ...
60
+ async def execute(self, sql: str, params: Params = None) -> int: ...
61
+ async def exec(self, sql: str) -> None: ...
62
+ async def query(self, sql: str, params: Params = None) -> List[Dict[str, Any]]: ...
63
+ async def query_one(self, sql: str, params: Params = None) -> Optional[Dict[str, Any]]: ...
64
+ async def query_raw(self, sql: str, params: Params = None) -> Dict[str, Any]: ...
65
+ async def execute_batch(self, sql: str, params_list: Sequence[ParamSet]) -> int: ...
66
+ def prepare(self, sql: str) -> "AsyncPreparedStatement": ...
67
+ async def begin(self) -> "AsyncTransaction": ...
68
+ async def close(self) -> None: ...
69
+
70
+ class AsyncTransaction:
71
+ async def execute(self, sql: str, params: Params = None) -> int: ...
72
+ async def query(self, sql: str, params: Params = None) -> List[Dict[str, Any]]: ...
73
+ async def query_one(self, sql: str, params: Params = None) -> Optional[Dict[str, Any]]: ...
74
+ async def query_raw(self, sql: str, params: Params = None) -> Dict[str, Any]: ...
75
+ async def execute_batch(self, sql: str, params_list: Sequence[ParamSet]) -> int: ...
76
+ async def commit(self) -> None: ...
77
+ async def rollback(self) -> None: ...
78
+ async def __aenter__(self) -> "AsyncTransaction": ...
79
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ...
80
+
81
+ class AsyncPreparedStatement:
82
+ @property
83
+ def sql(self) -> str: ...
84
+ async def execute(self, params: Params = None) -> int: ...
85
+ async def query(self, params: Params = None) -> List[Dict[str, Any]]: ...
86
+ async def query_one(self, params: Params = None) -> Optional[Dict[str, Any]]: ...
87
+ async def query_raw(self, params: Params = None) -> Dict[str, Any]: ...
88
+ async def execute_batch(self, params_list: Sequence[ParamSet]) -> int: ...
Binary file
@@ -0,0 +1,356 @@
1
+ Metadata-Version: 2.4
2
+ Name: stoolap-python
3
+ Version: 0.4.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: Apache Software License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Rust
14
+ Classifier: Topic :: Database
15
+ Requires-Dist: pytest ; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio ; extra == 'dev'
17
+ Provides-Extra: dev
18
+ License-File: LICENSE
19
+ Summary: High-performance Python driver for Stoolap embedded SQL database
20
+ Keywords: database,sql,embedded,stoolap
21
+ Home-Page: https://stoolap.io
22
+ Author: Stoolap Contributors
23
+ License: Apache-2.0
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
26
+ Project-URL: Documentation, https://stoolap.io/docs/drivers/python/
27
+ Project-URL: Homepage, https://stoolap.io
28
+ Project-URL: Repository, https://github.com/stoolap/stoolap-python
29
+
30
+ # stoolap-python
31
+
32
+ High-performance Python driver for [Stoolap](https://stoolap.io) embedded SQL database. Built with [PyO3](https://pyo3.rs) for native Rust performance with both sync and async APIs.
33
+
34
+ ## Performance
35
+
36
+ **53 out of 53 benchmark wins** against Python's built-in `sqlite3` on 10,000 rows:
37
+
38
+ | Category | Highlights |
39
+ |----------|-----------|
40
+ | **Point Queries** | SELECT by ID: 1.5x, SELECT by index: 1.4-2.0x |
41
+ | **Complex Queries** | SELECT complex: 4.8x, Scalar subquery: 19.5x |
42
+ | **Aggregations** | GROUP BY: 24.8x, COUNT DISTINCT: 207x |
43
+ | **Joins** | INNER JOIN: 1.1x, LEFT JOIN: 1.6x, Self JOIN: 1.3x |
44
+ | **Subqueries** | IN subquery: 12.7x, NOT EXISTS: 42.8x, Nested 3-level: 16.3x |
45
+ | **Window Functions** | ROW_NUMBER: 5.5x, PARTITION BY: 4.2x, ROWS frame: 2.7x |
46
+ | **Write Operations** | DELETE complex: 133x, UPDATE complex: 6.7x |
47
+
48
+ Run the benchmark yourself: `python benchmark.py`
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install stoolap-python
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ from stoolap import Database
60
+
61
+ # In-memory database
62
+ db = Database.open(":memory:")
63
+
64
+ # exec() runs one or more DDL/DML statements (no parameters)
65
+ db.exec("""
66
+ CREATE TABLE users (
67
+ id INTEGER PRIMARY KEY,
68
+ name TEXT NOT NULL,
69
+ email TEXT
70
+ );
71
+ CREATE INDEX idx_users_name ON users(name);
72
+ """)
73
+
74
+ # execute() runs a single statement with parameters, returns rows affected
75
+ db.execute(
76
+ "INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
77
+ [1, "Alice", "alice@example.com"],
78
+ )
79
+
80
+ # Named parameters (:key)
81
+ db.execute(
82
+ "INSERT INTO users (id, name, email) VALUES (:id, :name, :email)",
83
+ {"id": 2, "name": "Bob", "email": "bob@example.com"},
84
+ )
85
+
86
+ # query() returns a list of dicts
87
+ users = db.query("SELECT * FROM users ORDER BY id")
88
+ # [{"id": 1, "name": "Alice", "email": "alice@example.com"}, ...]
89
+
90
+ # query_one() returns a single dict or None
91
+ user = db.query_one("SELECT * FROM users WHERE id = $1", [1])
92
+ # {"id": 1, "name": "Alice", "email": "alice@example.com"}
93
+
94
+ # query_raw() returns columnar format (faster for large results)
95
+ raw = db.query_raw("SELECT id, name FROM users ORDER BY id")
96
+ # {"columns": ["id", "name"], "rows": [[1, "Alice"], [2, "Bob"]]}
97
+
98
+ db.close()
99
+ ```
100
+
101
+ ## Prepared Statements
102
+
103
+ Parse SQL once, execute many times with different parameters:
104
+
105
+ ```python
106
+ insert = db.prepare("INSERT INTO users (id, name) VALUES ($1, $2)")
107
+ insert.execute([1, "Alice"])
108
+ insert.execute([2, "Bob"])
109
+
110
+ # Batch execution (auto-wrapped in a transaction)
111
+ insert.execute_batch([
112
+ [3, "Charlie"],
113
+ [4, "Diana"],
114
+ ])
115
+
116
+ # Prepared queries
117
+ lookup = db.prepare("SELECT * FROM users WHERE id = $1")
118
+ user = lookup.query_one([1]) # Single row as dict or None
119
+ rows = lookup.query([1]) # All rows as list of dicts
120
+ raw = lookup.query_raw([1]) # Columnar format
121
+
122
+ # Named parameters also work with prepared statements
123
+ lookup = db.prepare("SELECT * FROM users WHERE id = :id")
124
+ user = lookup.query_one({"id": 1})
125
+ ```
126
+
127
+ ## Transactions
128
+
129
+ ```python
130
+ # Context manager (auto-commit on clean exit, auto-rollback on exception)
131
+ with db.begin() as tx:
132
+ tx.execute("INSERT INTO users (id, name) VALUES ($1, $2)", [1, "Alice"])
133
+ tx.execute("INSERT INTO users (id, name) VALUES ($1, $2)", [2, "Bob"])
134
+
135
+ # Manual control
136
+ tx = db.begin()
137
+ try:
138
+ tx.execute("INSERT INTO users (id, name) VALUES ($1, $2)", [1, "Alice"])
139
+ tx.commit()
140
+ except:
141
+ tx.rollback()
142
+ raise
143
+ ```
144
+
145
+ Transactions support `execute()`, `query()`, `query_one()`, `query_raw()`, and `execute_batch()` with both positional (`$1, $2`) and named (`:key`) parameters.
146
+
147
+ ## Batch Execution
148
+
149
+ Execute the same statement with multiple parameter sets, auto-wrapped in a transaction:
150
+
151
+ ```python
152
+ # On Database
153
+ changes = db.execute_batch(
154
+ "INSERT INTO users (id, name) VALUES ($1, $2)",
155
+ [[1, "Alice"], [2, "Bob"], [3, "Charlie"]],
156
+ )
157
+ # changes == 3
158
+
159
+ # On PreparedStatement (reuses cached plan)
160
+ stmt = db.prepare("INSERT INTO users (id, name) VALUES ($1, $2)")
161
+ changes = stmt.execute_batch([[4, "Diana"], [5, "Eve"]])
162
+ ```
163
+
164
+ ## Async API
165
+
166
+ All methods release the GIL and run on a thread executor:
167
+
168
+ ```python
169
+ from stoolap import AsyncDatabase
170
+
171
+ db = await AsyncDatabase.open(":memory:")
172
+
173
+ await db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
174
+ await db.execute("INSERT INTO users (id, name) VALUES ($1, $2)", [1, "Alice"])
175
+
176
+ rows = await db.query("SELECT * FROM users")
177
+
178
+ # Async transactions
179
+ async with await db.begin() as tx:
180
+ await tx.execute("INSERT INTO users (id, name) VALUES ($1, $2)", [2, "Bob"])
181
+
182
+ # Async prepared statements
183
+ stmt = db.prepare("SELECT * FROM users WHERE id = $1")
184
+ user = await stmt.query_one([1])
185
+
186
+ await db.close()
187
+ ```
188
+
189
+ ## Error Handling
190
+
191
+ All database errors raise `StoolapError`:
192
+
193
+ ```python
194
+ from stoolap import Database, StoolapError
195
+
196
+ db = Database.open(":memory:")
197
+ try:
198
+ db.query("SELECT * FROM nonexistent_table")
199
+ except StoolapError as e:
200
+ print(f"Database error: {e}")
201
+ ```
202
+
203
+ ## Persistence
204
+
205
+ ```python
206
+ # File-based database (data persists across restarts)
207
+ db = Database.open("file:///path/to/mydata")
208
+
209
+ # Relative paths also work
210
+ db = Database.open("./mydata")
211
+ ```
212
+
213
+ ### Configuration Options
214
+
215
+ Pass options as query parameters in the DSN:
216
+
217
+ ```python
218
+ # Max durability
219
+ db = Database.open("file:///path/to/mydata?sync_mode=full")
220
+
221
+ # Max throughput (less durable)
222
+ db = Database.open("file:///path/to/mydata?sync_mode=none&checkpoint_interval=120")
223
+ ```
224
+
225
+ | Parameter | Values | Default | Description |
226
+ |-----------|--------|---------|-------------|
227
+ | `sync_mode` | `none`, `normal`, `full` | `normal` | Durability level (`full` = fsync every write, `normal` = fsync every 1s) |
228
+ | `checkpoint_interval` | seconds | `60` | Seconds between checkpoint cycles (seal + compact + WAL truncate) |
229
+ | `compact_threshold` | count | `4` | Sub-target volumes per table before merging |
230
+ | `target_volume_rows` | count | `1048576` | Target rows per cold volume (controls compaction split boundary) |
231
+ | `checkpoint_on_close` | `on`, `off` | `on` | Seal all hot rows on clean shutdown for fast startup |
232
+ | `keep_snapshots` | count | `3` | Number of backup snapshots to retain |
233
+ | `compression` | `on`, `off` | `on` | Enable both WAL + volume compression (LZ4) |
234
+ | `wal_compression` | `on`, `off` | `on` | WAL compression only |
235
+ | `volume_compression` | `on`, `off` | `on` | Cold volume file compression only |
236
+ | `compression_threshold` | bytes | `64` | Minimum data size before compression |
237
+ | `wal_buffer_size` | bytes | `65536` | WAL write buffer size |
238
+ | `wal_flush_trigger` | bytes | `32768` | WAL size before flush |
239
+ | `wal_max_size` | bytes | `67108864` | WAL size before rotation (64 MB) |
240
+ | `commit_batch_size` | count | `100` | Commits batched before syncing (normal mode) |
241
+ | `sync_interval_ms` | milliseconds | `1000` | Minimum ms between syncs (normal mode) |
242
+
243
+ ## Type Mapping
244
+
245
+ | Python | Stoolap | Notes |
246
+ |--------|---------|-------|
247
+ | `int` | `INTEGER` | 64-bit signed |
248
+ | `float` | `FLOAT` | 64-bit double |
249
+ | `str` | `TEXT` | UTF-8 |
250
+ | `bool` | `BOOLEAN` | |
251
+ | `None` | `NULL` | |
252
+ | `datetime.datetime` | `TIMESTAMP` | Converted to/from UTC |
253
+ | `dict` / `list` | `JSON` | Serialized via `json.dumps` |
254
+ | `Vector` | `VECTOR(N)` | `list[float]` on output |
255
+
256
+ ## Vector Similarity Search
257
+
258
+ Store embeddings and perform k-NN similarity search using HNSW indexes:
259
+
260
+ ```python
261
+ from stoolap import Database, Vector
262
+
263
+ db = Database.open(":memory:")
264
+
265
+ # Create a table with a VECTOR column
266
+ db.exec("""
267
+ CREATE TABLE documents (
268
+ id INTEGER PRIMARY KEY,
269
+ title TEXT,
270
+ embedding VECTOR(3)
271
+ );
272
+ CREATE INDEX idx_emb ON documents(embedding) USING HNSW WITH (metric = 'cosine');
273
+ """)
274
+
275
+ # Insert vectors using the Vector wrapper
276
+ db.execute(
277
+ "INSERT INTO documents VALUES ($1, $2, $3)",
278
+ [1, "Hello world", Vector([0.1, 0.2, 0.3])],
279
+ )
280
+ db.execute(
281
+ "INSERT INTO documents VALUES ($1, $2, $3)",
282
+ [2, "Goodbye world", Vector([0.9, 0.1, 0.0])],
283
+ )
284
+
285
+ # k-NN search: find 5 nearest neighbors
286
+ results = db.query(
287
+ "SELECT id, title, VEC_DISTANCE_COSINE(embedding, '[0.1, 0.2, 0.3]') AS dist "
288
+ "FROM documents ORDER BY dist LIMIT 5"
289
+ )
290
+
291
+ # Read vectors back as list[float]
292
+ row = db.query_one("SELECT embedding FROM documents WHERE id = 1")
293
+ emb = row["embedding"] # [0.1, 0.2, 0.3]
294
+ ```
295
+
296
+ ### Distance Functions
297
+
298
+ | Function | Description |
299
+ |----------|-------------|
300
+ | `VEC_DISTANCE_L2(a, b)` | Euclidean distance |
301
+ | `VEC_DISTANCE_COSINE(a, b)` | Cosine distance (1 - similarity) |
302
+ | `VEC_DISTANCE_IP(a, b)` | Negative inner product |
303
+
304
+ ### Vector Utilities
305
+
306
+ | Function | Description |
307
+ |----------|-------------|
308
+ | `VEC_DIMS(v)` | Number of dimensions |
309
+ | `VEC_NORM(v)` | L2 norm (magnitude) |
310
+ | `VEC_TO_TEXT(v)` | Convert to string `[1.0, 2.0, 3.0]` |
311
+
312
+ ### HNSW Index Options
313
+
314
+ ```sql
315
+ CREATE INDEX idx ON table(column) USING HNSW WITH (metric = 'cosine');
316
+ ```
317
+
318
+ Supported metrics: `l2` (default), `cosine`, `ip` (inner product).
319
+
320
+ ## Features
321
+
322
+ Stoolap is a full-featured embedded SQL database:
323
+
324
+ - **MVCC Transactions** with snapshot isolation
325
+ - **Cost-based query optimizer** with adaptive execution
326
+ - **Parallel query execution** (filter, join, sort, distinct)
327
+ - **JOINs**: INNER, LEFT, RIGHT, FULL OUTER, CROSS, NATURAL
328
+ - **Subqueries**: scalar, EXISTS, IN, NOT IN, ANY/ALL, correlated
329
+ - **Window functions**: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE
330
+ - **CTEs**: WITH and WITH RECURSIVE
331
+ - **Aggregations**: GROUP BY, HAVING, ROLLUP, CUBE, GROUPING SETS
332
+ - **Vector similarity search** with HNSW indexes (L2, cosine, inner product)
333
+ - **Indexes**: B-tree, Hash, Bitmap (auto-selected), HNSW, multi-column composite
334
+ - **110+ built-in functions**: string, math, date/time, JSON, vector, aggregate
335
+ - **Immutable volume-based storage** with columnar format, zone maps, bloom filters, and LZ4 compression
336
+ - **WAL + checkpoint cycles** for crash recovery (seal + compact + WAL truncate)
337
+ - **Aggregation pushdown** to cold volume statistics (COUNT, SUM, MIN, MAX)
338
+ - **Semantic query caching** with predicate subsumption
339
+
340
+ ## Building from Source
341
+
342
+ Requires [Rust](https://rustup.rs) (stable) and Python >= 3.9.
343
+
344
+ ```bash
345
+ git clone https://github.com/stoolap/stoolap-python.git
346
+ cd stoolap-python
347
+ python -m venv .venv && source .venv/bin/activate
348
+ pip install maturin pytest pytest-asyncio
349
+ maturin develop --release
350
+ pytest
351
+ ```
352
+
353
+ ## License
354
+
355
+ Apache-2.0
356
+
@@ -0,0 +1,8 @@
1
+ stoolap/__init__.py,sha256=TwTBj8idGN8YDdes7aDh23DhD5lw5tvKz5Vf1t5E0Cg,6643
2
+ stoolap/__init__.pyi,sha256=VIPUxHz83uIqgFSXj1VSgAK51_SRBJhrnUG5ZTUQVy4,4484
3
+ stoolap/_stoolap.cp312-win_amd64.pyd,sha256=RNFE2QqsC_gbLdgasG2V4gwvgo_yknMRFkXF9ZFrJJs,11305984
4
+ stoolap_python-0.4.0.dist-info/METADATA,sha256=JJIFNyDO2hwgBxsVB4CkX8Thr9f-L8O6NNkBZrDttuA,11779
5
+ stoolap_python-0.4.0.dist-info/WHEEL,sha256=IerCNAQpy9eepfUjoenIR-EbFm_XEgG9BeNfw_zQQO0,97
6
+ stoolap_python-0.4.0.dist-info/licenses/LICENSE,sha256=Cb7GYpXhyRRRuc77Ba8E-ORZCasHVawr_w_wtbK316Q,11551
7
+ stoolap_python-0.4.0.dist-info/sboms/stoolap-python.cyclonedx.json,sha256=OP2vfnV_jVubKn19HmoqAoHWQbFshx6iNAfpQeeqi2M,116689
8
+ stoolap_python-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.12.6)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-win_amd64