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.
- sqlalchemy_d1/__init__.py +6 -0
- sqlalchemy_d1/compiler.py +5 -0
- sqlalchemy_d1/dialect.py +299 -0
- sqlalchemy_d1/type_compiler.py +5 -0
- sqlalchemy_d1-0.1.0.dist-info/METADATA +14 -0
- sqlalchemy_d1-0.1.0.dist-info/RECORD +8 -0
- sqlalchemy_d1-0.1.0.dist-info/WHEEL +4 -0
- sqlalchemy_d1-0.1.0.dist-info/entry_points.txt +3 -0
sqlalchemy_d1/dialect.py
ADDED
|
@@ -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,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,,
|