sqlalchemy-d1 0.1.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,6 @@
1
+ """SQLAlchemy dialect for Cloudflare D1"""
2
+
3
+ from .dialect import D1Dialect
4
+ from sqlalchemy.dialects import registry
5
+
6
+ registry.register("d1", "sqlalchemy_d1.dialect", "D1Dialect")
@@ -0,0 +1,5 @@
1
+ from sqlalchemy.sql.compiler import SQLCompiler
2
+
3
+
4
+ class D1Compiler(SQLCompiler):
5
+ pass
@@ -0,0 +1,299 @@
1
+ # sqlalchemy_d1/dialect.py
2
+ import dbapi_d1
3
+ from sqlalchemy.engine.default import DefaultDialect
4
+ from sqlalchemy import text, types as sqltypes
5
+ from sqlalchemy.engine import reflection
6
+
7
+
8
+ class D1Dialect(DefaultDialect):
9
+ name = "d1"
10
+ driver = "dbapi-d1"
11
+ supports_alter = False
12
+ supports_sane_rowcount = True
13
+ supports_sane_multi_rowcount = True
14
+ supports_statement_cache = True
15
+ paramstyle = "qmark"
16
+
17
+ def create_connect_args(self, url):
18
+ # URL format: d1://<account_id>:<api_token>@<database_id>
19
+ account_id = url.username
20
+ api_token = url.password
21
+ database_id = url.host
22
+ return (
23
+ (),
24
+ {
25
+ "account_id": account_id,
26
+ "api_token": api_token,
27
+ "database_id": database_id,
28
+ },
29
+ )
30
+
31
+ def do_ping(self, dbapi_connection) -> bool:
32
+ """
33
+ Return if the database can be reached.
34
+ """
35
+ try:
36
+ dbapi_connection.execute(text("SELECT 1"))
37
+ except Exception as ex:
38
+ return False
39
+
40
+ return True
41
+
42
+ def do_execute(self, cursor, statement, parameters, context=None):
43
+ cursor.execute(statement, parameters)
44
+
45
+ def do_on_first_connect(self, conn, branch):
46
+ # This is called by SQLAlchemy on the first raw connection
47
+ # Reset the flag so future rollbacks raise error
48
+ conn._first_connect = False
49
+
50
+ def import_dbapi():
51
+ return dbapi_d1
52
+
53
+ def dbapi():
54
+ return dbapi_d1
55
+
56
+ @reflection.cache
57
+ def get_schema_names(self, connection, **kwargs):
58
+ # D1 is built on SQLite, which only uses one schema
59
+ return ["main"]
60
+
61
+ @reflection.cache
62
+ def get_table_names(self, connection, schema=None, **kw):
63
+ """
64
+ Return list of table names in the D1 database.
65
+ """
66
+ try:
67
+ result = connection.execute(
68
+ text("SELECT name FROM sqlite_master WHERE type='table';")
69
+ )
70
+
71
+ all_tables = [row[0] for row in result.fetchall()]
72
+ # Filter out cloudflare tables
73
+ visible_tables = [t for t in all_tables if not t.startswith("_cf")]
74
+ return visible_tables
75
+ except Exception as e:
76
+ raise RuntimeError(f"Failed to fetch table names: {e}")
77
+
78
+ @reflection.cache
79
+ def get_view_names(self, connection, schema=None, **kw):
80
+ """
81
+ Return list of view names in the D1 database.
82
+ """
83
+ try:
84
+ result = connection.execute(
85
+ text("SELECT name FROM sqlite_master WHERE type='view';")
86
+ )
87
+ all_views = [row[0] for row in result.fetchall()]
88
+ visible_views = [v for v in all_views if not v.startswith("_cf")]
89
+ return visible_views
90
+ except Exception as e:
91
+ raise RuntimeError(f"Failed to fetch view names: {e}")
92
+
93
+ @reflection.cache
94
+ def get_columns(self, connection, table_name, schema=None, **kw):
95
+ """
96
+ Return column info for a given table in D1.
97
+ """
98
+ try:
99
+ query = text(f"PRAGMA table_info({table_name});")
100
+ result = connection.execute(query).mappings()
101
+ columns = []
102
+ for row in result.fetchall():
103
+ columns.append(
104
+ {
105
+ "name": row["name"],
106
+ "type": self._resolve_type(row["type"]),
107
+ "nullable": not row["notnull"],
108
+ "default": row["dflt_value"],
109
+ "autoincrement": row["pk"] == 1,
110
+ }
111
+ )
112
+ return columns
113
+ except Exception as e:
114
+ raise RuntimeError(
115
+ f"Failed to fetch columns for table '{table_name}': {e}"
116
+ )
117
+
118
+ @reflection.cache
119
+ def get_primary_keys(self, connection, table_name, schema=None, **kw):
120
+ try:
121
+ query = text(f"PRAGMA table_info({table_name});")
122
+ result = connection.execute(query).mappings()
123
+ pks = [row["name"] for row in result.fetchall() if row["pk"] == 1]
124
+ return pks
125
+ except Exception as e:
126
+ raise RuntimeError(
127
+ f"Failed to fetch primary keys for table '{table_name}': {e}"
128
+ )
129
+
130
+ @reflection.cache
131
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
132
+ """
133
+ Return the primary key for the given table as a dict with:
134
+ - constrained_columns: list of columns in the PK
135
+ - name: name of the PK constraint (SQLite doesn't store names, so None)
136
+ """
137
+ try:
138
+ result = connection.execute(
139
+ text(f"PRAGMA table_info({table_name});")
140
+ ).mappings()
141
+ pk_columns = [
142
+ row["name"] for row in result.fetchall() if row["pk"] != 0
143
+ ]
144
+ return {"constrained_columns": pk_columns, "name": None}
145
+ except Exception as e:
146
+ raise RuntimeError(
147
+ f"Failed to fetch primary key for '{table_name}': {e}"
148
+ )
149
+
150
+ @reflection.cache
151
+ def get_foreign_keys(self, connection, table_name, schema=None, **kw):
152
+ """
153
+ Return list of foreign keys for the given table.
154
+ Each foreign key is a dict with keys: name, constrained_columns, referred_schema,
155
+ referred_table, referred_columns
156
+ """
157
+ try:
158
+ result = connection.execute(
159
+ text(f"PRAGMA foreign_key_list({table_name});")
160
+ ).mappings()
161
+ fks = []
162
+ for row in result.fetchall():
163
+ fks.append(
164
+ {
165
+ "name": row["id"], # SQLite assigns an integer id
166
+ "constrained_columns": [row["from"]],
167
+ "referred_schema": None,
168
+ "referred_table": row["table"],
169
+ "referred_columns": [row["to"]],
170
+ "options": {
171
+ "onupdate": row["on_update"],
172
+ "ondelete": row["on_delete"],
173
+ },
174
+ }
175
+ )
176
+ return fks
177
+ except Exception as e:
178
+ raise RuntimeError(
179
+ f"Failed to fetch foreign keys for '{table_name}': {e}"
180
+ )
181
+
182
+ @reflection.cache
183
+ def get_indexes(self, connection, table_name, schema=None, **kw):
184
+ """
185
+ Return list of indexes for the given table.
186
+ Each index is a dict with keys: name, column_names, unique, primary_key
187
+ """
188
+ try:
189
+ result = connection.execute(
190
+ text(
191
+ f"SELECT name, sql FROM sqlite_schema WHERE type='index' AND tbl_name='{table_name}';"
192
+ )
193
+ ).mappings()
194
+ indexes = []
195
+ for row in result.fetchall():
196
+ sql = row["sql"] or ""
197
+ indexes.append(
198
+ {
199
+ "name": row["name"],
200
+ "column_names": self._parse_index_columns(sql),
201
+ "unique": "UNIQUE" in sql.upper(),
202
+ "primary_key": False, # primary keys handled separately
203
+ }
204
+ )
205
+ return indexes
206
+ except Exception as e:
207
+ raise RuntimeError(
208
+ f"Failed to fetch indexes for '{table_name}': {e}"
209
+ )
210
+
211
+ @reflection.cache
212
+ def get_unique_constraints(
213
+ self, connection, table_name, schema=None, **kw
214
+ ):
215
+ """
216
+ Return list of unique constraints for the table.
217
+ SQLite stores unique constraints as unique indexes.
218
+ """
219
+ unique_constraints = []
220
+ indexes = self.get_indexes(connection, table_name, schema=schema)
221
+ for idx in indexes:
222
+ if idx["unique"]:
223
+ unique_constraints.append(
224
+ {
225
+ "name": idx["name"],
226
+ "column_names": idx["column_names"],
227
+ }
228
+ )
229
+ return unique_constraints
230
+
231
+ # Helper to parse index columns
232
+ def _parse_index_columns(self, sql):
233
+ """
234
+ Extract column names from CREATE INDEX SQL statement.
235
+ e.g., "CREATE UNIQUE INDEX idx_name ON mytable(col1, col2)"
236
+ """
237
+ import re
238
+
239
+ m = re.search(r"\((.*?)\)", sql)
240
+ if m:
241
+ return [c.strip().strip('"') for c in m.group(1).split(",")]
242
+ return []
243
+
244
+ @reflection.cache
245
+ def has_table(self, connection, table_name, schema=None, **kw):
246
+ """
247
+ Return True if the table exists in the database.
248
+ """
249
+ try:
250
+ result = connection.execute(
251
+ text(
252
+ "SELECT 1 FROM sqlite_master "
253
+ "WHERE type='table' AND name=:table_name;"
254
+ ),
255
+ {"table_name": table_name},
256
+ )
257
+ return result.scalar() is not None
258
+ except Exception as e:
259
+ raise RuntimeError(
260
+ f"Failed to check existence of table '{table_name}': {e}"
261
+ )
262
+
263
+ def get_check_constraints(
264
+ self, connection, table_name, schema=None, **kwargs
265
+ ):
266
+ return []
267
+
268
+ def get_table_comment(self, connection, table_name, schema=None, **kwargs):
269
+ return {"text": ""}
270
+
271
+ def get_view_definition(
272
+ self, connection, view_name, schema=None, **kwargs
273
+ ):
274
+ pass
275
+
276
+ def do_rollback(self, dbapi_connection):
277
+ pass
278
+
279
+ def _resolve_type(self, d1_type: str):
280
+ """
281
+ Map D1/SQLite type string to SQLAlchemy type.
282
+ """
283
+ if d1_type is None:
284
+ return sqltypes.NullType()
285
+ t = d1_type.upper()
286
+ if "INT" in t:
287
+ return sqltypes.Integer()
288
+ elif "CHAR" in t or "CLOB" in t or "TEXT" in t:
289
+ return sqltypes.String()
290
+ elif "BLOB" in t:
291
+ return sqltypes.LargeBinary()
292
+ elif "REAL" in t or "FLOA" in t or "DOUB" in t:
293
+ return sqltypes.Float()
294
+ elif "NUMERIC" in t or "DECIMAL" in t:
295
+ return sqltypes.Numeric()
296
+ elif "BOOL" in t:
297
+ return sqltypes.Boolean()
298
+ else:
299
+ return sqltypes.String() # fallback
@@ -0,0 +1,5 @@
1
+ from sqlalchemy.sql.type_api import TypeEngine
2
+
3
+
4
+ class D1TypeCompiler(TypeEngine):
5
+ pass
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-d1
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Chad Rossouw
6
+ Author-email: chadrossouw7247@gmail.com
7
+ Requires-Python: >=3.11,<3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Requires-Dist: dbapi-d1 (>=0.1.0)
11
+ Requires-Dist: sqlalchemy (>=1.4,<2)
12
+ Description-Content-Type: text/markdown
13
+
14
+
@@ -0,0 +1,8 @@
1
+ sqlalchemy_d1/__init__.py,sha256=GkPPj5Uy1pPfQGLG4g8GeQBMlL1y4dnVpjqktoDJacA,179
2
+ sqlalchemy_d1/compiler.py,sha256=glyHPGBCLl7i6-6PjWBrxubizF27hG-f1JdOAgnNhyc,90
3
+ sqlalchemy_d1/dialect.py,sha256=raUeLqmEc_40mY3NDkjIrMD1prvWOoZlOmTHIcG3q1M,10274
4
+ sqlalchemy_d1/type_compiler.py,sha256=PJ2JH565Y-oefYdbcU2C8r_EAGe_9RP5zfYCO60asj8,92
5
+ sqlalchemy_d1-0.1.0.dist-info/METADATA,sha256=crcIHy_fNVR4xZ137MuE0iKQMMq-Q-29rpwr0UAXejU,370
6
+ sqlalchemy_d1-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
7
+ sqlalchemy_d1-0.1.0.dist-info/entry_points.txt,sha256=TOcBWEHBuBopz72_ltXJM8l1i3X978e3uPkmIfZx45Q,58
8
+ sqlalchemy_d1-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [sqlalchemy.dialects]
2
+ d1=sqlalchemy_d1.dialect:D1Dialect
3
+