nusadb 0.1.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.
- nusadb-0.1.0/PKG-INFO +216 -0
- nusadb-0.1.0/README.md +198 -0
- nusadb-0.1.0/nusadb/__init__.py +65 -0
- nusadb-0.1.0/nusadb/_protocol.py +271 -0
- nusadb-0.1.0/nusadb/_scram.py +76 -0
- nusadb-0.1.0/nusadb/connection.py +887 -0
- nusadb-0.1.0/nusadb/exceptions.py +76 -0
- nusadb-0.1.0/nusadb/pool.py +82 -0
- nusadb-0.1.0/nusadb/sqlalchemy.py +213 -0
- nusadb-0.1.0/nusadb.egg-info/PKG-INFO +216 -0
- nusadb-0.1.0/nusadb.egg-info/SOURCES.txt +17 -0
- nusadb-0.1.0/nusadb.egg-info/dependency_links.txt +1 -0
- nusadb-0.1.0/nusadb.egg-info/entry_points.txt +2 -0
- nusadb-0.1.0/nusadb.egg-info/requires.txt +3 -0
- nusadb-0.1.0/nusadb.egg-info/top_level.txt +1 -0
- nusadb-0.1.0/pyproject.toml +33 -0
- nusadb-0.1.0/setup.cfg +4 -0
- nusadb-0.1.0/tests/test_driver.py +560 -0
- nusadb-0.1.0/tests/test_sqlalchemy.py +138 -0
nusadb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nusadb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python PEP-249 (DB-API 2.0) driver for NusaDB (Nusa Wire Protocol)
|
|
5
|
+
Author: NusaDB Authors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/nusadb/python
|
|
8
|
+
Project-URL: Issues, https://github.com/nusadb/python/issues
|
|
9
|
+
Keywords: nusadb,database,sql,dbapi,pep249
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Database
|
|
12
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Provides-Extra: sqlalchemy
|
|
17
|
+
Requires-Dist: SQLAlchemy>=1.4; extra == "sqlalchemy"
|
|
18
|
+
|
|
19
|
+
# nusadb — Python driver for NusaDB
|
|
20
|
+
|
|
21
|
+
A pure-Python, dependency-free [PEP-249 / DB-API 2.0](https://peps.python.org/pep-0249/)
|
|
22
|
+
driver that speaks the [Nusa Wire Protocol](../../docs/wire-protocol.md)
|
|
23
|
+
(`PROTOCOL_VERSION 1.1`) directly over a socket. No C extension; standard library only.
|
|
24
|
+
`cursor.description[i][1]` (`type_code`) carries each column's NusaDB type name (protocol 1.1).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install ./drivers/python # from the repo, or
|
|
30
|
+
pip install nusadb # once published
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import nusadb
|
|
37
|
+
|
|
38
|
+
conn = nusadb.connect(host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
|
|
39
|
+
cur = conn.cursor()
|
|
40
|
+
|
|
41
|
+
cur.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)")
|
|
42
|
+
cur.execute("INSERT INTO t VALUES ($1, $2)", [1, "alice"])
|
|
43
|
+
|
|
44
|
+
cur.execute("SELECT id, name FROM t WHERE id = $1", [1])
|
|
45
|
+
for row in cur:
|
|
46
|
+
print(row) # (1, 'alice') — INT decodes to int, TEXT to str
|
|
47
|
+
|
|
48
|
+
conn.close()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Value types
|
|
52
|
+
|
|
53
|
+
Each column's wire type tag (protocol 1.1) drives decoding, so values come back as the natural
|
|
54
|
+
Python type — full typed DB-API fidelity rather than everything-as-string:
|
|
55
|
+
|
|
56
|
+
| NusaDB type | Python type |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `BOOL` | `bool` |
|
|
59
|
+
| `INT` | `int` |
|
|
60
|
+
| `FLOAT` | `float` |
|
|
61
|
+
| `NUMERIC` | `decimal.Decimal` |
|
|
62
|
+
| `DATE` / `TIME` / `TIMESTAMP` (and `…TZ`) | `datetime.date` / `time` / `datetime` |
|
|
63
|
+
| `UUID` | `uuid.UUID` |
|
|
64
|
+
| `JSON` | parsed `dict` / `list` / scalar |
|
|
65
|
+
| `ARRAY` | `list` (elements stay `str` — the wire array tag carries no element type) |
|
|
66
|
+
| `BYTEA` | `bytes` |
|
|
67
|
+
| `TEXT` / `VARCHAR` / everything else | `str` |
|
|
68
|
+
|
|
69
|
+
A value that does not parse as its declared tag falls back to the raw text, so an unexpected wire
|
|
70
|
+
form never raises mid-fetch. `cursor.description[i][1]` (`type_code`) still carries the type name.
|
|
71
|
+
|
|
72
|
+
### Bulk insert (`executemany`)
|
|
73
|
+
|
|
74
|
+
`cursor.executemany(sql, seq_of_parameters)` runs one statement once per parameter set (DB-API 2.0);
|
|
75
|
+
`cursor.rowcount` is the total affected. The wire protocol has no batch pipeline, so this is one
|
|
76
|
+
round-trip per set.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
cur.executemany("INSERT INTO t VALUES ($1, $2)", [[1, "a"], [2, "b"], [3, "c"]])
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Bulk load / export (`COPY`)
|
|
83
|
+
|
|
84
|
+
For high-throughput load/export, `conn.copy_in` / `conn.copy_out` drive the `COPY` sub-protocol —
|
|
85
|
+
one round-trip for the whole dataset. Move bytes in the server's text format (tab-delimited fields,
|
|
86
|
+
`\N` for SQL `NULL`, one row per line); you write the `COPY` statement with any `WITH (...)` options.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import io
|
|
90
|
+
|
|
91
|
+
# Bulk load from any binary file-like (read(size) -> bytes).
|
|
92
|
+
loaded = conn.copy_in("COPY t (id, name) FROM STDIN", io.BytesIO(b"1\talice\n2\t\\N\n"))
|
|
93
|
+
|
|
94
|
+
# Bulk export into any binary file-like (write(bytes)).
|
|
95
|
+
sink = io.BytesIO()
|
|
96
|
+
exported = conn.copy_out("COPY t TO STDOUT", sink)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A `COPY` the server refuses (bad SQL, an RLS-protected table) raises; the connection stays usable.
|
|
100
|
+
|
|
101
|
+
### Parameters
|
|
102
|
+
|
|
103
|
+
Placeholders are positional **`$1`, `$2`, …** (the server's native
|
|
104
|
+
marker). `paramstyle` is reported as `"numeric"`. Pass the bound values as a
|
|
105
|
+
sequence to `execute` / `executemany`; `None` is SQL `NULL`. Values are sent in the
|
|
106
|
+
wire text format — bind with an explicit `CAST(... AS type)` when a numeric-looking
|
|
107
|
+
string must land in a non-numeric column.
|
|
108
|
+
|
|
109
|
+
### TLS
|
|
110
|
+
|
|
111
|
+
Pass an `ssl.SSLContext` to `connect(ssl=...)`. The server uses implicit TLS 1.3, so
|
|
112
|
+
the TLS session is established before any protocol frame.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import ssl, nusadb
|
|
116
|
+
ctx = ssl.create_default_context(cafile="ca.pem")
|
|
117
|
+
conn = nusadb.connect(host="db.example", port=5678, user="u", database="nusadb",
|
|
118
|
+
password="…", ssl=ctx)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Authentication
|
|
122
|
+
|
|
123
|
+
If the server runs with `--auth-user USER:PASSWORD`, pass `password=`; the driver
|
|
124
|
+
performs the SCRAM-SHA-256 handshake and verifies the server's signature (mutual auth).
|
|
125
|
+
|
|
126
|
+
### Connection pool
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from nusadb import Pool
|
|
130
|
+
|
|
131
|
+
pool = Pool(max_size=10, host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
|
|
132
|
+
with pool.connection() as conn:
|
|
133
|
+
conn.cursor().execute("SELECT 1")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Transactions
|
|
137
|
+
|
|
138
|
+
By default the connection is in **autocommit** mode (each statement is its own
|
|
139
|
+
transaction). Pass `autocommit=False` for the standard transactional model: the
|
|
140
|
+
connection opens a transaction lazily before the first statement, and
|
|
141
|
+
`commit()`/`rollback()` send `COMMIT`/`ROLLBACK`.
|
|
142
|
+
|
|
143
|
+
Inside a transaction, `savepoint(name)` marks a point you can later undo to with
|
|
144
|
+
`rollback_to(name)` (the transaction stays open) or forget with `release_savepoint(name)`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
cur.execute("INSERT INTO t VALUES (1)")
|
|
148
|
+
conn.savepoint("sp1")
|
|
149
|
+
cur.execute("INSERT INTO t VALUES (2)")
|
|
150
|
+
conn.rollback_to("sp1") # undoes (2), keeps (1); the transaction continues
|
|
151
|
+
conn.commit()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
conn = nusadb.connect(port=5678, user="nusa-root", database="nusadb", autocommit=False)
|
|
156
|
+
cur = conn.cursor()
|
|
157
|
+
cur.execute("INSERT INTO t VALUES (1)")
|
|
158
|
+
conn.rollback() # discards the insert
|
|
159
|
+
cur.execute("INSERT INTO t VALUES (2)")
|
|
160
|
+
conn.commit() # persists it
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Notifications (LISTEN/NOTIFY)
|
|
164
|
+
|
|
165
|
+
`listen(channel)` subscribes the connection; a `notify(channel, payload)` from any connection on the
|
|
166
|
+
same database is then delivered asynchronously. Collect delivered notifications with `poll(timeout)`
|
|
167
|
+
(seconds; `0` polls without blocking, `None` blocks) or drain the ones buffered during other queries
|
|
168
|
+
with `notifications()`:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
conn.listen("orders")
|
|
172
|
+
# ... elsewhere: other.notify("orders", "42")
|
|
173
|
+
note = conn.poll(5.0) # -> Notification(pid, channel, payload), or None on timeout
|
|
174
|
+
print(note.channel, note.payload)
|
|
175
|
+
conn.unlisten("orders")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## SQLAlchemy
|
|
179
|
+
|
|
180
|
+
A SQLAlchemy dialect ships in `nusadb.sqlalchemy`. Once this package is installed its
|
|
181
|
+
`sqlalchemy.dialects` entry point registers it, so `create_engine("nusadb://…")` works
|
|
182
|
+
directly:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from sqlalchemy import create_engine
|
|
186
|
+
engine = create_engine("nusadb://nusa-root@127.0.0.1:5678/nusadb")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Without installing, register it explicitly:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from sqlalchemy.dialects import registry
|
|
193
|
+
registry.register("nusadb", "nusadb.sqlalchemy", "NusaDialect")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
It supports full ORM use — declarative models, `metadata.create_all`,
|
|
197
|
+
insert/query/update/delete, pagination (`.limit()`/`.offset()`), joins, aggregates, and real
|
|
198
|
+
transactions — over the transactional connection, rewriting SQLAlchemy's `:1` markers to the
|
|
199
|
+
server's `$1`. `LIMIT`/`OFFSET` are inlined as constants (the server rejects a bound row count).
|
|
200
|
+
Reflection (`inspect(engine).get_columns(...)`) reports each column's server-side `default` and
|
|
201
|
+
infers `autoincrement` from a `nextval(...)` default, so Alembic autogenerate sees them.
|
|
202
|
+
Install the extra with `pip install nusadb[sqlalchemy]`.
|
|
203
|
+
|
|
204
|
+
## Tests
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
cargo build -p nusadb-server # the tests boot this binary
|
|
208
|
+
python -m unittest discover -s drivers/python/tests
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The tests start a real `nusadb-server` on an ephemeral port and exercise simple and
|
|
212
|
+
parameterised queries, `executemany`, errors, the pool, cancellation, and SCRAM auth.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
Apache-2.0.
|
nusadb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# nusadb — Python driver for NusaDB
|
|
2
|
+
|
|
3
|
+
A pure-Python, dependency-free [PEP-249 / DB-API 2.0](https://peps.python.org/pep-0249/)
|
|
4
|
+
driver that speaks the [Nusa Wire Protocol](../../docs/wire-protocol.md)
|
|
5
|
+
(`PROTOCOL_VERSION 1.1`) directly over a socket. No C extension; standard library only.
|
|
6
|
+
`cursor.description[i][1]` (`type_code`) carries each column's NusaDB type name (protocol 1.1).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install ./drivers/python # from the repo, or
|
|
12
|
+
pip install nusadb # once published
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import nusadb
|
|
19
|
+
|
|
20
|
+
conn = nusadb.connect(host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
|
|
21
|
+
cur = conn.cursor()
|
|
22
|
+
|
|
23
|
+
cur.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)")
|
|
24
|
+
cur.execute("INSERT INTO t VALUES ($1, $2)", [1, "alice"])
|
|
25
|
+
|
|
26
|
+
cur.execute("SELECT id, name FROM t WHERE id = $1", [1])
|
|
27
|
+
for row in cur:
|
|
28
|
+
print(row) # (1, 'alice') — INT decodes to int, TEXT to str
|
|
29
|
+
|
|
30
|
+
conn.close()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Value types
|
|
34
|
+
|
|
35
|
+
Each column's wire type tag (protocol 1.1) drives decoding, so values come back as the natural
|
|
36
|
+
Python type — full typed DB-API fidelity rather than everything-as-string:
|
|
37
|
+
|
|
38
|
+
| NusaDB type | Python type |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `BOOL` | `bool` |
|
|
41
|
+
| `INT` | `int` |
|
|
42
|
+
| `FLOAT` | `float` |
|
|
43
|
+
| `NUMERIC` | `decimal.Decimal` |
|
|
44
|
+
| `DATE` / `TIME` / `TIMESTAMP` (and `…TZ`) | `datetime.date` / `time` / `datetime` |
|
|
45
|
+
| `UUID` | `uuid.UUID` |
|
|
46
|
+
| `JSON` | parsed `dict` / `list` / scalar |
|
|
47
|
+
| `ARRAY` | `list` (elements stay `str` — the wire array tag carries no element type) |
|
|
48
|
+
| `BYTEA` | `bytes` |
|
|
49
|
+
| `TEXT` / `VARCHAR` / everything else | `str` |
|
|
50
|
+
|
|
51
|
+
A value that does not parse as its declared tag falls back to the raw text, so an unexpected wire
|
|
52
|
+
form never raises mid-fetch. `cursor.description[i][1]` (`type_code`) still carries the type name.
|
|
53
|
+
|
|
54
|
+
### Bulk insert (`executemany`)
|
|
55
|
+
|
|
56
|
+
`cursor.executemany(sql, seq_of_parameters)` runs one statement once per parameter set (DB-API 2.0);
|
|
57
|
+
`cursor.rowcount` is the total affected. The wire protocol has no batch pipeline, so this is one
|
|
58
|
+
round-trip per set.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
cur.executemany("INSERT INTO t VALUES ($1, $2)", [[1, "a"], [2, "b"], [3, "c"]])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Bulk load / export (`COPY`)
|
|
65
|
+
|
|
66
|
+
For high-throughput load/export, `conn.copy_in` / `conn.copy_out` drive the `COPY` sub-protocol —
|
|
67
|
+
one round-trip for the whole dataset. Move bytes in the server's text format (tab-delimited fields,
|
|
68
|
+
`\N` for SQL `NULL`, one row per line); you write the `COPY` statement with any `WITH (...)` options.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import io
|
|
72
|
+
|
|
73
|
+
# Bulk load from any binary file-like (read(size) -> bytes).
|
|
74
|
+
loaded = conn.copy_in("COPY t (id, name) FROM STDIN", io.BytesIO(b"1\talice\n2\t\\N\n"))
|
|
75
|
+
|
|
76
|
+
# Bulk export into any binary file-like (write(bytes)).
|
|
77
|
+
sink = io.BytesIO()
|
|
78
|
+
exported = conn.copy_out("COPY t TO STDOUT", sink)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A `COPY` the server refuses (bad SQL, an RLS-protected table) raises; the connection stays usable.
|
|
82
|
+
|
|
83
|
+
### Parameters
|
|
84
|
+
|
|
85
|
+
Placeholders are positional **`$1`, `$2`, …** (the server's native
|
|
86
|
+
marker). `paramstyle` is reported as `"numeric"`. Pass the bound values as a
|
|
87
|
+
sequence to `execute` / `executemany`; `None` is SQL `NULL`. Values are sent in the
|
|
88
|
+
wire text format — bind with an explicit `CAST(... AS type)` when a numeric-looking
|
|
89
|
+
string must land in a non-numeric column.
|
|
90
|
+
|
|
91
|
+
### TLS
|
|
92
|
+
|
|
93
|
+
Pass an `ssl.SSLContext` to `connect(ssl=...)`. The server uses implicit TLS 1.3, so
|
|
94
|
+
the TLS session is established before any protocol frame.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import ssl, nusadb
|
|
98
|
+
ctx = ssl.create_default_context(cafile="ca.pem")
|
|
99
|
+
conn = nusadb.connect(host="db.example", port=5678, user="u", database="nusadb",
|
|
100
|
+
password="…", ssl=ctx)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Authentication
|
|
104
|
+
|
|
105
|
+
If the server runs with `--auth-user USER:PASSWORD`, pass `password=`; the driver
|
|
106
|
+
performs the SCRAM-SHA-256 handshake and verifies the server's signature (mutual auth).
|
|
107
|
+
|
|
108
|
+
### Connection pool
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from nusadb import Pool
|
|
112
|
+
|
|
113
|
+
pool = Pool(max_size=10, host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
|
|
114
|
+
with pool.connection() as conn:
|
|
115
|
+
conn.cursor().execute("SELECT 1")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Transactions
|
|
119
|
+
|
|
120
|
+
By default the connection is in **autocommit** mode (each statement is its own
|
|
121
|
+
transaction). Pass `autocommit=False` for the standard transactional model: the
|
|
122
|
+
connection opens a transaction lazily before the first statement, and
|
|
123
|
+
`commit()`/`rollback()` send `COMMIT`/`ROLLBACK`.
|
|
124
|
+
|
|
125
|
+
Inside a transaction, `savepoint(name)` marks a point you can later undo to with
|
|
126
|
+
`rollback_to(name)` (the transaction stays open) or forget with `release_savepoint(name)`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
cur.execute("INSERT INTO t VALUES (1)")
|
|
130
|
+
conn.savepoint("sp1")
|
|
131
|
+
cur.execute("INSERT INTO t VALUES (2)")
|
|
132
|
+
conn.rollback_to("sp1") # undoes (2), keeps (1); the transaction continues
|
|
133
|
+
conn.commit()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
conn = nusadb.connect(port=5678, user="nusa-root", database="nusadb", autocommit=False)
|
|
138
|
+
cur = conn.cursor()
|
|
139
|
+
cur.execute("INSERT INTO t VALUES (1)")
|
|
140
|
+
conn.rollback() # discards the insert
|
|
141
|
+
cur.execute("INSERT INTO t VALUES (2)")
|
|
142
|
+
conn.commit() # persists it
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Notifications (LISTEN/NOTIFY)
|
|
146
|
+
|
|
147
|
+
`listen(channel)` subscribes the connection; a `notify(channel, payload)` from any connection on the
|
|
148
|
+
same database is then delivered asynchronously. Collect delivered notifications with `poll(timeout)`
|
|
149
|
+
(seconds; `0` polls without blocking, `None` blocks) or drain the ones buffered during other queries
|
|
150
|
+
with `notifications()`:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
conn.listen("orders")
|
|
154
|
+
# ... elsewhere: other.notify("orders", "42")
|
|
155
|
+
note = conn.poll(5.0) # -> Notification(pid, channel, payload), or None on timeout
|
|
156
|
+
print(note.channel, note.payload)
|
|
157
|
+
conn.unlisten("orders")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## SQLAlchemy
|
|
161
|
+
|
|
162
|
+
A SQLAlchemy dialect ships in `nusadb.sqlalchemy`. Once this package is installed its
|
|
163
|
+
`sqlalchemy.dialects` entry point registers it, so `create_engine("nusadb://…")` works
|
|
164
|
+
directly:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from sqlalchemy import create_engine
|
|
168
|
+
engine = create_engine("nusadb://nusa-root@127.0.0.1:5678/nusadb")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Without installing, register it explicitly:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from sqlalchemy.dialects import registry
|
|
175
|
+
registry.register("nusadb", "nusadb.sqlalchemy", "NusaDialect")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
It supports full ORM use — declarative models, `metadata.create_all`,
|
|
179
|
+
insert/query/update/delete, pagination (`.limit()`/`.offset()`), joins, aggregates, and real
|
|
180
|
+
transactions — over the transactional connection, rewriting SQLAlchemy's `:1` markers to the
|
|
181
|
+
server's `$1`. `LIMIT`/`OFFSET` are inlined as constants (the server rejects a bound row count).
|
|
182
|
+
Reflection (`inspect(engine).get_columns(...)`) reports each column's server-side `default` and
|
|
183
|
+
infers `autoincrement` from a `nextval(...)` default, so Alembic autogenerate sees them.
|
|
184
|
+
Install the extra with `pip install nusadb[sqlalchemy]`.
|
|
185
|
+
|
|
186
|
+
## Tests
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
cargo build -p nusadb-server # the tests boot this binary
|
|
190
|
+
python -m unittest discover -s drivers/python/tests
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The tests start a real `nusadb-server` on an ephemeral port and exercise simple and
|
|
194
|
+
parameterised queries, `executemany`, errors, the pool, cancellation, and SCRAM auth.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
Apache-2.0.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""nusadb — the Python driver for NusaDB (PEP-249 / DB-API 2.0).
|
|
2
|
+
|
|
3
|
+
Speaks the Nusa Wire Protocol (``PROTOCOL_VERSION 1.1``, see
|
|
4
|
+
``docs/wire-protocol.md``) directly over a socket — no C extension, stdlib only.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
import nusadb
|
|
9
|
+
|
|
10
|
+
conn = nusadb.connect(host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
|
|
11
|
+
cur = conn.cursor()
|
|
12
|
+
cur.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)")
|
|
13
|
+
cur.execute("INSERT INTO t VALUES ($1, $2)", [1, "alice"])
|
|
14
|
+
cur.execute("SELECT id, name FROM t WHERE id = $1", [1])
|
|
15
|
+
print(cur.fetchall())
|
|
16
|
+
conn.close()
|
|
17
|
+
|
|
18
|
+
Parameter markers are positional ``$1``, ``$2`` (the server's native form);
|
|
19
|
+
``paramstyle`` is reported as ``"numeric"``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from .connection import Connection, Cursor, Notification, connect
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
DatabaseError,
|
|
27
|
+
DataError,
|
|
28
|
+
Error,
|
|
29
|
+
IntegrityError,
|
|
30
|
+
InterfaceError,
|
|
31
|
+
InternalError,
|
|
32
|
+
NotSupportedError,
|
|
33
|
+
OperationalError,
|
|
34
|
+
ProgrammingError,
|
|
35
|
+
Warning,
|
|
36
|
+
)
|
|
37
|
+
from .pool import Pool
|
|
38
|
+
|
|
39
|
+
# PEP-249 module globals.
|
|
40
|
+
apilevel = "2.0"
|
|
41
|
+
threadsafety = 1 # threads may share the module but not connections
|
|
42
|
+
paramstyle = "numeric" # positional markers, written ``$1`` (server-native)
|
|
43
|
+
|
|
44
|
+
__version__ = "0.1.0"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"connect",
|
|
48
|
+
"Connection",
|
|
49
|
+
"Cursor",
|
|
50
|
+
"Notification",
|
|
51
|
+
"Pool",
|
|
52
|
+
"apilevel",
|
|
53
|
+
"threadsafety",
|
|
54
|
+
"paramstyle",
|
|
55
|
+
"Warning",
|
|
56
|
+
"Error",
|
|
57
|
+
"InterfaceError",
|
|
58
|
+
"DatabaseError",
|
|
59
|
+
"DataError",
|
|
60
|
+
"OperationalError",
|
|
61
|
+
"IntegrityError",
|
|
62
|
+
"InternalError",
|
|
63
|
+
"ProgrammingError",
|
|
64
|
+
"NotSupportedError",
|
|
65
|
+
]
|