sqlalchemy-hawdb 0.22.0__py3-none-any.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.
@@ -0,0 +1,5 @@
1
+ """SQLAlchemy integration for HawDB."""
2
+
3
+ from sqlalchemy_hawdb.dialect import HawDBDialect
4
+
5
+ __all__ = ["HawDBDialect"]
@@ -0,0 +1,323 @@
1
+ """SQLAlchemy dialect for HawDB's embedded SQL interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+
7
+ from sqlalchemy.engine import default
8
+ from sqlalchemy.pool import NullPool, SingletonThreadPool
9
+ from sqlalchemy.sql.compiler import GenericTypeCompiler, IdentifierPreparer
10
+
11
+
12
+ class HawDBIdentifierPreparer(IdentifierPreparer):
13
+ """Quote identifiers using SQL-standard double quotes."""
14
+
15
+ reserved_words = IdentifierPreparer.reserved_words | {"match", "return", "graph"}
16
+
17
+
18
+ class HawDBTypeCompiler(GenericTypeCompiler):
19
+ """Compile SQLAlchemy types to HawDB's supported PostgreSQL type subset."""
20
+
21
+ def visit_big_integer(self, type_, **kw):
22
+ return "BIGINT"
23
+
24
+ visit_integer = visit_big_integer
25
+ visit_small_integer = visit_big_integer
26
+
27
+ def visit_boolean(self, type_, **kw):
28
+ return "BOOLEAN"
29
+
30
+ def visit_double(self, type_, **kw):
31
+ return "DOUBLE PRECISION"
32
+
33
+ visit_float = visit_double
34
+ visit_real = visit_double
35
+ visit_numeric = visit_double
36
+
37
+ def visit_string(self, type_, **kw):
38
+ return "TEXT"
39
+
40
+ visit_text = visit_string
41
+ visit_unicode = visit_string
42
+ visit_unicode_text = visit_string
43
+
44
+ def visit_large_binary(self, type_, **kw):
45
+ return "BYTEA"
46
+
47
+ def visit_uuid(self, type_, **kw):
48
+ return "UUID"
49
+
50
+ def visit_date(self, type_, **kw):
51
+ return "TEXT"
52
+
53
+ visit_time = visit_date
54
+ visit_datetime = visit_date
55
+
56
+ def visit_enum(self, type_, **kw):
57
+ return "TEXT"
58
+
59
+
60
+ class HawDBDialect(default.DefaultDialect):
61
+ name = "hawdb"
62
+ driver = "embedded"
63
+ supports_statement_cache = True
64
+ supports_native_boolean = True
65
+ supports_native_uuid = True
66
+ supports_sane_rowcount = False
67
+ supports_sane_multi_rowcount = False
68
+ supports_sequences = False
69
+ supports_identity_columns = False
70
+ supports_default_values = False
71
+ supports_empty_insert = False
72
+ supports_alter = False
73
+ supports_comments = False
74
+ postfetch_lastrowid = False
75
+ default_paramstyle = "numeric_dollar"
76
+ preparer = HawDBIdentifierPreparer
77
+ type_compiler_cls = HawDBTypeCompiler
78
+
79
+ @classmethod
80
+ def import_dbapi(cls):
81
+ try:
82
+ return importlib.import_module("hawdb.dbapi")
83
+ except ImportError as error:
84
+ raise ImportError(
85
+ "HawDB's Python binding is required; install it with "
86
+ "python -m pip install 'hawdb>=0.22.0,<0.23.0'"
87
+ ) from error
88
+
89
+ def create_connect_args(self, url):
90
+ if url.username or url.password or url.host or url.port:
91
+ raise ValueError(
92
+ "Invalid HawDB URL: provide a local database path without "
93
+ "credentials or a network host"
94
+ )
95
+ query = dict(url.query)
96
+ read_only = query.pop("read_only", "false").lower() in {"1", "true", "yes"}
97
+ if query:
98
+ options = ", ".join(sorted(query))
99
+ raise ValueError(f"unsupported HawDB URL option(s): {options}")
100
+ path = url.database
101
+ if path in (None, ":memory:"):
102
+ path = None
103
+ return (
104
+ [path],
105
+ {"read_only": read_only, "autocommit": False},
106
+ )
107
+
108
+ def do_begin(self, dbapi_connection):
109
+ # HawDB's PEP 249 connection begins on its first statement.
110
+ return None
111
+
112
+ def do_rollback(self, dbapi_connection):
113
+ dbapi_connection.rollback()
114
+
115
+ def do_commit(self, dbapi_connection):
116
+ dbapi_connection.commit()
117
+
118
+ @classmethod
119
+ def get_pool_class(cls, url):
120
+ # File handles own their path and cannot be shared across threads.
121
+ # In-memory URLs need a per-thread handle to persist across checkouts.
122
+ if url.database in (None, ":memory:"):
123
+ return SingletonThreadPool
124
+ return NullPool
125
+
126
+ def get_server_version_info(self, connection):
127
+ import hawdb
128
+
129
+ return tuple(int(part) for part in hawdb.__version__.split(".")[:3])
130
+
131
+ def get_default_schema_name(self, connection):
132
+ return "public"
133
+
134
+ def get_schema_names(self, connection, **kw):
135
+ return ["public"]
136
+
137
+ def has_table(self, connection, table_name, schema=None, **kw):
138
+ if schema not in (None, "public"):
139
+ return False
140
+ result = connection.exec_driver_sql(
141
+ "SELECT table_name FROM information_schema.tables "
142
+ "WHERE table_schema = 'public' AND table_name = $1",
143
+ (table_name,),
144
+ )
145
+ return result.first() is not None
146
+
147
+ def get_table_names(self, connection, schema=None, **kw):
148
+ if schema not in (None, "public"):
149
+ return []
150
+ result = connection.exec_driver_sql(
151
+ "SELECT table_name FROM information_schema.tables "
152
+ "WHERE table_schema = 'public' AND table_type = 'BASE TABLE' "
153
+ "ORDER BY table_name"
154
+ )
155
+ return [row[0] for row in result]
156
+
157
+ def get_view_names(self, connection, schema=None, **kw):
158
+ if schema not in (None, "public"):
159
+ return []
160
+ result = connection.exec_driver_sql(
161
+ "SELECT table_name FROM information_schema.tables "
162
+ "WHERE table_schema = 'public' AND table_type = 'MATERIALIZED VIEW' "
163
+ "ORDER BY table_name"
164
+ )
165
+ return [row[0] for row in result]
166
+
167
+ def get_columns(self, connection, table_name, schema=None, **kw):
168
+ if schema not in (None, "public"):
169
+ return []
170
+ result = connection.exec_driver_sql(
171
+ "SELECT column_name, data_type, is_nullable, column_default "
172
+ "FROM information_schema.columns "
173
+ "WHERE table_schema = 'public' AND table_name = $1 "
174
+ "ORDER BY ordinal_position",
175
+ (table_name,),
176
+ )
177
+ return [
178
+ {
179
+ "name": row[0],
180
+ "type": self._type_from_name(row[1]),
181
+ "nullable": row[2] == "YES",
182
+ "default": row[3],
183
+ "autoincrement": False,
184
+ "comment": None,
185
+ }
186
+ for row in result
187
+ ]
188
+
189
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
190
+ if schema not in (None, "public"):
191
+ return {"name": None, "constrained_columns": []}
192
+ result = connection.exec_driver_sql(
193
+ "SELECT indexname, indexdef FROM pg_catalog.pg_indexes "
194
+ "WHERE schemaname = 'public' AND tablename = $1 ORDER BY indexname",
195
+ (table_name,),
196
+ )
197
+ primary_key_name = f"{table_name}_pkey"
198
+ for row in result:
199
+ if row[0] == primary_key_name:
200
+ return {
201
+ "name": row[0],
202
+ "constrained_columns": self._index_columns(row[1]),
203
+ }
204
+ return {"name": None, "constrained_columns": []}
205
+
206
+ def get_indexes(self, connection, table_name, schema=None, **kw):
207
+ if schema not in (None, "public"):
208
+ return []
209
+ result = connection.exec_driver_sql(
210
+ "SELECT indexname, indexdef FROM pg_catalog.pg_indexes "
211
+ "WHERE schemaname = 'public' AND tablename = $1 ORDER BY indexname",
212
+ (table_name,),
213
+ )
214
+ primary_key_name = f"{table_name}_pkey"
215
+ return [
216
+ {
217
+ "name": row[0],
218
+ "column_names": self._index_columns(row[1]),
219
+ "unique": row[1].upper().startswith("CREATE UNIQUE INDEX"),
220
+ "dialect_options": {},
221
+ }
222
+ for row in result
223
+ if row[0] != primary_key_name
224
+ ]
225
+
226
+ def get_unique_constraints(self, connection, table_name, schema=None, **kw):
227
+ if schema not in (None, "public"):
228
+ return []
229
+ result = connection.exec_driver_sql(
230
+ "SELECT indexname, indexdef FROM pg_catalog.pg_indexes "
231
+ "WHERE schemaname = 'public' AND tablename = $1 ORDER BY indexname",
232
+ (table_name,),
233
+ )
234
+ primary_key_name = f"{table_name}_pkey"
235
+ return [
236
+ {
237
+ "name": row[0],
238
+ "column_names": self._index_columns(row[1]),
239
+ "duplicates_index": row[0],
240
+ }
241
+ for row in result
242
+ if row[0] != primary_key_name
243
+ and row[1].upper().startswith("CREATE UNIQUE INDEX")
244
+ ]
245
+
246
+ @staticmethod
247
+ def _index_columns(definition: str) -> list[str]:
248
+ """Parse the simple parenthesized column list emitted by pg_indexes."""
249
+ opening = closing = -1
250
+ quoted = False
251
+ depth = 0
252
+ index = 0
253
+ while index < len(definition):
254
+ char = definition[index]
255
+ if char == '"':
256
+ if (
257
+ quoted
258
+ and index + 1 < len(definition)
259
+ and definition[index + 1] == '"'
260
+ ):
261
+ index += 1
262
+ else:
263
+ quoted = not quoted
264
+ elif not quoted and char == "(":
265
+ if depth == 0:
266
+ opening = index
267
+ depth += 1
268
+ elif not quoted and char == ")" and depth:
269
+ depth -= 1
270
+ if depth == 0:
271
+ closing = index
272
+ index += 1
273
+ if opening < 0 or closing <= opening:
274
+ return []
275
+ columns = definition[opening + 1 : closing]
276
+ result = []
277
+ current = []
278
+ quoted = False
279
+ index = 0
280
+ while index < len(columns):
281
+ char = columns[index]
282
+ if char == '"':
283
+ if quoted and index + 1 < len(columns) and columns[index + 1] == '"':
284
+ current.append('"')
285
+ index += 1
286
+ else:
287
+ quoted = not quoted
288
+ elif char == "," and not quoted:
289
+ result.append("".join(current).strip())
290
+ current = []
291
+ else:
292
+ current.append(char)
293
+ index += 1
294
+ if current:
295
+ result.append("".join(current).strip())
296
+ return result
297
+
298
+ @staticmethod
299
+ def _type_from_name(name: str):
300
+ from sqlalchemy import types
301
+
302
+ normalized = name.lower()
303
+ if normalized in {"smallint", "integer", "bigint"}:
304
+ return types.Integer()
305
+ if normalized in {"real", "double precision", "numeric", "decimal"}:
306
+ return types.Float()
307
+ if normalized in {"boolean", "bool"}:
308
+ return types.Boolean()
309
+ if normalized in {
310
+ "date",
311
+ "time",
312
+ "timestamp",
313
+ "timestamp without time zone",
314
+ "timestamp with time zone",
315
+ }:
316
+ return types.Text()
317
+ if normalized in {"uuid"}:
318
+ return types.Uuid()
319
+ if normalized in {"bytea", "binary", "varbinary"}:
320
+ return types.LargeBinary()
321
+ if normalized in {"text", "character varying", "varchar", "character", "char"}:
322
+ return types.Text()
323
+ return types.NullType()
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-hawdb
3
+ Version: 0.22.0
4
+ Summary: SQLAlchemy dialect for the embedded HawDB database
5
+ License: Apache-2.0
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Topic :: Database
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: SQLAlchemy<3.0,>=2.0
16
+ Requires-Dist: hawdb<0.23.0,>=0.22.0
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest<9,>=8; extra == "test"
19
+ Requires-Dist: ruff<1,>=0.14; extra == "test"
20
+
21
+ # SQLAlchemy HawDB
22
+
23
+ `sqlalchemy-hawdb` provides a SQLAlchemy 2.x dialect for the embedded HawDB
24
+ database. It uses HawDB's own PEP 249 driver and does not add a wire protocol or
25
+ duplicate its DBAPI implementation.
26
+
27
+ ## Requirements
28
+
29
+ - Python 3.11 or newer
30
+ - HawDB Python binding 0.22.x (CI tests 0.22.0)
31
+ - SQLAlchemy 2.x
32
+
33
+ Install the dialect and its HawDB dependency from PyPI:
34
+
35
+ ```sh
36
+ python -m pip install sqlalchemy-hawdb
37
+ ```
38
+
39
+ To install this checkout instead:
40
+
41
+ ```sh
42
+ python -m pip install /path/to/sqlalchemy_hawdb
43
+ ```
44
+
45
+ ## Connect
46
+
47
+ Use an absolute database directory path with four slashes:
48
+
49
+ ```python
50
+ from sqlalchemy import Column, Integer, MetaData, String, Table, create_engine
51
+
52
+ engine = create_engine("hawdb:////var/lib/my-app/knowledge")
53
+ metadata = MetaData()
54
+ records = Table(
55
+ "records",
56
+ metadata,
57
+ Column("id", Integer, primary_key=True, autoincrement=False),
58
+ Column("body", String(500), nullable=False),
59
+ )
60
+
61
+ metadata.create_all(engine)
62
+ with engine.begin() as connection:
63
+ connection.execute(records.insert(), {"id": 1, "body": "Hello HawDB"})
64
+ ```
65
+
66
+ HawDB creates the database directory itself; its parent directory must already
67
+ exist.
68
+
69
+ Use `hawdb:///:memory:` for an in-memory database. Like SQLAlchemy's SQLite
70
+ dialect, the in-memory engine keeps one database per thread. Add
71
+ `?read_only=true` to open an existing database read-only.
72
+
73
+ HawDB connections are thread-affine and hold their database path until closed.
74
+ For file databases the dialect uses SQLAlchemy's `NullPool`: each SQLAlchemy
75
+ connection closes its HawDB handle when returned. Do not keep multiple
76
+ connections to the same database path open at once or pass a SQLAlchemy
77
+ connection between threads. In-memory connections are kept per thread and must
78
+ also be used serially within that thread.
79
+
80
+ ## Supported behavior
81
+
82
+ - SQLAlchemy Core and ORM statements using HawDB's relational SQL subset
83
+ - Positional `$1`, `$2`, ... binding through HawDB's native DBAPI driver
84
+ - Explicit SQLAlchemy transactions mapped to HawDB commit and rollback
85
+ - Table, column, primary key, unique constraint, and index reflection from
86
+ HawDB's `information_schema` and `pg_catalog` views
87
+ - The default `public` schema
88
+ - HawDB-native scalar DDL types: integer types map to `BIGINT`, floating and
89
+ numeric types to `DOUBLE PRECISION`, strings to `TEXT`, and binary values to
90
+ `BYTEA`; SQLAlchemy string lengths and numeric precision are not enforced by
91
+ HawDB
92
+
93
+ HawDB does not report affected row counts, so DBAPI `rowcount` is `-1` for
94
+ statements without a result set. Date and time types compile to `TEXT`; their
95
+ values are encoded and decoded by SQLAlchemy's standard processors.
96
+ Auto-generated identity columns, sequences, schema alteration, savepoints, and
97
+ concurrent connections to one database path are not supported by this initial
98
+ dialect.
99
+
100
+ ## Development
101
+
102
+ Install the project, the HawDB dependency, and the test tools:
103
+
104
+ ```sh
105
+ python -m pip install -e '.[test]'
106
+ python -m pytest
107
+ ```
108
+
109
+ GitHub Actions builds the source distribution and wheel, checks their metadata,
110
+ and tests the installed wheel against `hawdb==0.22.0` from PyPI on Python
111
+ 3.11-3.13 for every push and pull request. Missing HawDB wheels fail CI rather
112
+ than skipping integration tests or falling back to a source build.
113
+
114
+ ## Publishing
115
+
116
+ `.github/workflows/pypi.yaml` publishes the tested source distribution and wheel
117
+ when a GitHub Release is published. The release tag must match the version in
118
+ `pyproject.toml`, for example `v0.22.0`. A manual workflow dispatch runs the same
119
+ build and tests without uploading to PyPI.
120
+
121
+ Configure a PyPI Trusted Publisher for the `sqlalchemy-hawdb` project with:
122
+
123
+ - Owner: `nowledge-co`
124
+ - Repository: `sqlalchemy-hawdb`
125
+ - Workflow: `pypi.yaml`
126
+ - Environment: `pypi`
127
+
128
+ For the first release, configure a pending publisher in your PyPI account.
129
+ The publish job uses GitHub's `pypi` environment and OIDC; no PyPI API token
130
+ secret is required.
@@ -0,0 +1,7 @@
1
+ sqlalchemy_hawdb/__init__.py,sha256=9zJu4JP1VSSpilWq0NlddfVQLpXG7bEHHeILcbgffjM,119
2
+ sqlalchemy_hawdb/dialect.py,sha256=uYb_OYDxmyYif51vUmXP1hQVzgrzl0-Db6a07kfb_0U,10996
3
+ sqlalchemy_hawdb-0.22.0.dist-info/METADATA,sha256=WMUsSECpUEskBw7YDxsPQqLFsJp3F2imHhXhzIVVfCs,4647
4
+ sqlalchemy_hawdb-0.22.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
5
+ sqlalchemy_hawdb-0.22.0.dist-info/entry_points.txt,sha256=O6N7Zc7UOCf5nMrTDrAZBCwGI04yEvxSV4LKVUBsa6c,68
6
+ sqlalchemy_hawdb-0.22.0.dist-info/top_level.txt,sha256=DSRzcmNpZj4SJkt_sDro6bAc0WOYZTIU2JW3jprKViU,17
7
+ sqlalchemy_hawdb-0.22.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [sqlalchemy.dialects]
2
+ hawdb = sqlalchemy_hawdb.dialect:HawDBDialect
@@ -0,0 +1 @@
1
+ sqlalchemy_hawdb