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 +197 -0
- stoolap/__init__.pyi +88 -0
- stoolap/_stoolap.cp312-win_amd64.pyd +0 -0
- stoolap_python-0.4.0.dist-info/METADATA +356 -0
- stoolap_python-0.4.0.dist-info/RECORD +8 -0
- stoolap_python-0.4.0.dist-info/WHEEL +4 -0
- stoolap_python-0.4.0.dist-info/licenses/LICENSE +201 -0
- stoolap_python-0.4.0.dist-info/sboms/stoolap-python.cyclonedx.json +3758 -0
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,,
|