oxyde-postgres 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.
- oxyde/__init__.py +151 -0
- oxyde/_msgpack.py +3 -0
- oxyde/cli/__init__.py +7 -0
- oxyde/cli/app.py +107 -0
- oxyde/cli/codegen.py +66 -0
- oxyde/cli/config.py +84 -0
- oxyde/cli/migrations.py +536 -0
- oxyde/codegen/__init__.py +15 -0
- oxyde/codegen/stub_generator.py +1229 -0
- oxyde/core/__init__.py +90 -0
- oxyde/core/column_types.py +367 -0
- oxyde/core/ir.py +380 -0
- oxyde/core/types.py +69 -0
- oxyde/core/wrapper.py +242 -0
- oxyde/db/__init__.py +230 -0
- oxyde/db/pool.py +305 -0
- oxyde/db/registry.py +113 -0
- oxyde/db/schema.py +51 -0
- oxyde/db/transaction.py +392 -0
- oxyde/exceptions.py +89 -0
- oxyde/migrations/__init__.py +49 -0
- oxyde/migrations/config.py +187 -0
- oxyde/migrations/context.py +598 -0
- oxyde/migrations/executor.py +456 -0
- oxyde/migrations/extract.py +316 -0
- oxyde/migrations/generator.py +534 -0
- oxyde/migrations/replay.py +367 -0
- oxyde/migrations/squash.py +109 -0
- oxyde/migrations/tracker.py +162 -0
- oxyde/migrations/utils.py +145 -0
- oxyde/models/__init__.py +83 -0
- oxyde/models/base.py +871 -0
- oxyde/models/decorators.py +111 -0
- oxyde/models/field.py +204 -0
- oxyde/models/lookups.py +667 -0
- oxyde/models/metadata.py +174 -0
- oxyde/models/registry.py +170 -0
- oxyde/models/serializers.py +144 -0
- oxyde/models/utils.py +116 -0
- oxyde/py.typed +0 -0
- oxyde/queries/__init__.py +79 -0
- oxyde/queries/aggregates.py +145 -0
- oxyde/queries/base.py +249 -0
- oxyde/queries/conditions.py +92 -0
- oxyde/queries/expressions.py +171 -0
- oxyde/queries/insert.py +124 -0
- oxyde/queries/joins.py +66 -0
- oxyde/queries/manager.py +409 -0
- oxyde/queries/mixins/__init__.py +68 -0
- oxyde/queries/mixins/aggregation.py +335 -0
- oxyde/queries/mixins/debug.py +154 -0
- oxyde/queries/mixins/execution.py +701 -0
- oxyde/queries/mixins/filtering.py +144 -0
- oxyde/queries/mixins/joining.py +160 -0
- oxyde/queries/mixins/mutation.py +508 -0
- oxyde/queries/mixins/pagination.py +145 -0
- oxyde/queries/q.py +250 -0
- oxyde/queries/raw.py +114 -0
- oxyde/queries/select.py +345 -0
- oxyde/queries/typed.py +213 -0
- oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
- oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
- oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
- oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
- oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
- oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
oxyde/core/ir.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Intermediate Representation (IR) builders for Rust query execution.
|
|
2
|
+
|
|
3
|
+
This module provides functions to construct query IR dicts that are
|
|
4
|
+
serialized to MessagePack and sent to the Rust core for SQL generation.
|
|
5
|
+
|
|
6
|
+
IR Protocol:
|
|
7
|
+
Every IR payload has:
|
|
8
|
+
proto: Version number (currently 1)
|
|
9
|
+
op: Operation type ("select", "insert", "update", "delete", "raw")
|
|
10
|
+
table: Target table name
|
|
11
|
+
|
|
12
|
+
Filter Tree:
|
|
13
|
+
WHERE clauses are represented as a tree of FilterNode dicts:
|
|
14
|
+
|
|
15
|
+
Condition (leaf):
|
|
16
|
+
{"type": "condition", "field": "age", "operator": ">=", "value": 18}
|
|
17
|
+
|
|
18
|
+
AND (branch):
|
|
19
|
+
{"type": "and", "conditions": [node1, node2, ...]}
|
|
20
|
+
|
|
21
|
+
OR (branch):
|
|
22
|
+
{"type": "or", "conditions": [node1, node2]}
|
|
23
|
+
|
|
24
|
+
NOT (branch):
|
|
25
|
+
{"type": "not", "condition": node}
|
|
26
|
+
|
|
27
|
+
Functions:
|
|
28
|
+
filter_condition(field, operator, value) → FilterNode
|
|
29
|
+
filter_and(*nodes) → FilterNode
|
|
30
|
+
filter_or(*nodes) → FilterNode
|
|
31
|
+
filter_not(node) → FilterNode
|
|
32
|
+
|
|
33
|
+
build_select_ir(...) → dict:
|
|
34
|
+
SELECT with columns, filter_tree, order_by, limit, joins, aggregates, etc.
|
|
35
|
+
|
|
36
|
+
build_insert_ir(...) → dict:
|
|
37
|
+
INSERT with values or bulk_values, pk_column for RETURNING.
|
|
38
|
+
|
|
39
|
+
build_update_ir(...) → dict:
|
|
40
|
+
UPDATE with values and filter_tree, or bulk_update for CASE WHEN.
|
|
41
|
+
|
|
42
|
+
build_delete_ir(...) → dict:
|
|
43
|
+
DELETE with filter_tree.
|
|
44
|
+
|
|
45
|
+
build_raw_sql_ir(sql, params) → dict:
|
|
46
|
+
Raw SQL escape hatch.
|
|
47
|
+
|
|
48
|
+
Example IR (SELECT):
|
|
49
|
+
{
|
|
50
|
+
"proto": 1,
|
|
51
|
+
"op": "select",
|
|
52
|
+
"table": "users",
|
|
53
|
+
"cols": ["id", "name", "email"],
|
|
54
|
+
"column_types": {"id": {"kind": "big_integer"}, "name": {"kind": "string"}},
|
|
55
|
+
"filter_tree": {"type": "condition", "field": "active", "operator": "=", "value": true},
|
|
56
|
+
"order_by": [("created_at", "desc")],
|
|
57
|
+
"limit": 10
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Rust Processing:
|
|
61
|
+
IR bytes → oxyde-codec::QueryIR → oxyde-sql::build_sql() → SQL string
|
|
62
|
+
SQL → oxyde-driver::execute_query() → rows → MessagePack bytes → Python
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
from collections.abc import MutableMapping, Sequence
|
|
68
|
+
from typing import Any
|
|
69
|
+
|
|
70
|
+
IR_PROTO_VERSION = 1
|
|
71
|
+
|
|
72
|
+
FilterNode = MutableMapping[str, Any]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def filter_condition(
|
|
76
|
+
field: str,
|
|
77
|
+
operator: str,
|
|
78
|
+
value: Any,
|
|
79
|
+
*,
|
|
80
|
+
column: str | None = None,
|
|
81
|
+
escape: str | None = None,
|
|
82
|
+
) -> FilterNode:
|
|
83
|
+
"""Create a simple condition node."""
|
|
84
|
+
payload: FilterNode = {
|
|
85
|
+
"type": "condition",
|
|
86
|
+
"field": field,
|
|
87
|
+
"operator": operator,
|
|
88
|
+
"value": value,
|
|
89
|
+
}
|
|
90
|
+
if column is not None and column != field:
|
|
91
|
+
payload["column"] = column
|
|
92
|
+
if escape is not None:
|
|
93
|
+
payload["escape"] = escape
|
|
94
|
+
return payload
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def filter_and(*nodes: FilterNode) -> FilterNode:
|
|
98
|
+
"""Combine nodes with logical AND."""
|
|
99
|
+
normalized = [_ensure_filter_node(node) for node in nodes if node]
|
|
100
|
+
if not normalized:
|
|
101
|
+
raise ValueError("filter_and() requires at least one condition")
|
|
102
|
+
if len(normalized) == 1:
|
|
103
|
+
return normalized[0]
|
|
104
|
+
return {"type": "and", "conditions": normalized}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def filter_or(*nodes: FilterNode) -> FilterNode:
|
|
108
|
+
"""Combine nodes with logical OR."""
|
|
109
|
+
normalized = [_ensure_filter_node(node) for node in nodes if node]
|
|
110
|
+
if len(normalized) < 2:
|
|
111
|
+
raise ValueError("filter_or() requires at least two conditions")
|
|
112
|
+
return {"type": "or", "conditions": normalized}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def filter_not(node: FilterNode) -> FilterNode:
|
|
116
|
+
"""Negate a filter node."""
|
|
117
|
+
return {"type": "not", "condition": _ensure_filter_node(node)}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _ensure_filter_node(node: FilterNode) -> FilterNode:
|
|
121
|
+
if not isinstance(node, MutableMapping) or "type" not in node:
|
|
122
|
+
raise TypeError("Filter node must be a mapping with a 'type' key")
|
|
123
|
+
return node
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_select_ir(
|
|
127
|
+
*,
|
|
128
|
+
table: str,
|
|
129
|
+
columns: Sequence[str] | None = None,
|
|
130
|
+
column_types: dict[str, dict] | None = None,
|
|
131
|
+
column_db_types: dict[str, str] | None = None,
|
|
132
|
+
model: str | None = None,
|
|
133
|
+
column_mappings: dict[str, str] | None = None,
|
|
134
|
+
filter_tree: FilterNode | None = None,
|
|
135
|
+
distinct: bool | None = None,
|
|
136
|
+
limit: int | None = None,
|
|
137
|
+
offset: int | None = None,
|
|
138
|
+
order_by: Sequence[tuple[str, str]] | None = None,
|
|
139
|
+
joins: Sequence[dict[str, Any]] | None = None,
|
|
140
|
+
group_by: Sequence[str] | None = None,
|
|
141
|
+
having: FilterNode | None = None,
|
|
142
|
+
aggregates: Sequence[dict[str, Any]] | None = None,
|
|
143
|
+
returning: bool | None = None,
|
|
144
|
+
exists: bool | None = None,
|
|
145
|
+
count: bool | None = None,
|
|
146
|
+
lock: str | None = None,
|
|
147
|
+
pk_column: str | None = None,
|
|
148
|
+
union_query: dict[str, Any] | None = None,
|
|
149
|
+
union_all: bool | None = None,
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
"""Build a SELECT query IR payload.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
column_types: Optional mapping of column name to ColumnTypeSpec dict
|
|
155
|
+
({"kind": ...}). Used by Rust for typed binding and decoding
|
|
156
|
+
column_db_types: Optional mapping of column name (keyed like
|
|
157
|
+
column_types) to its db_type override. PostgreSQL filters cast
|
|
158
|
+
array values and array-lookup operands to an array override.
|
|
159
|
+
pk_column: Primary key column name, used for deduplication in JOIN queries.
|
|
160
|
+
"""
|
|
161
|
+
payload: dict[str, Any] = {
|
|
162
|
+
"proto": IR_PROTO_VERSION,
|
|
163
|
+
"op": "select",
|
|
164
|
+
"table": table,
|
|
165
|
+
}
|
|
166
|
+
# cols is optional when count=True or exists=True
|
|
167
|
+
if columns:
|
|
168
|
+
payload["cols"] = list(columns)
|
|
169
|
+
if column_types:
|
|
170
|
+
payload["column_types"] = dict(column_types)
|
|
171
|
+
if column_db_types:
|
|
172
|
+
payload["column_db_types"] = dict(column_db_types)
|
|
173
|
+
if filter_tree:
|
|
174
|
+
payload["filter_tree"] = filter_tree
|
|
175
|
+
if model:
|
|
176
|
+
payload["model"] = model
|
|
177
|
+
if column_mappings:
|
|
178
|
+
payload["column_mappings"] = dict(column_mappings)
|
|
179
|
+
if distinct is not None:
|
|
180
|
+
payload["distinct"] = bool(distinct)
|
|
181
|
+
if limit is not None:
|
|
182
|
+
payload["limit"] = int(limit)
|
|
183
|
+
if offset is not None:
|
|
184
|
+
payload["offset"] = int(offset)
|
|
185
|
+
if order_by:
|
|
186
|
+
payload["order_by"] = [(str(col), direction) for col, direction in order_by]
|
|
187
|
+
if joins:
|
|
188
|
+
payload["joins"] = list(joins)
|
|
189
|
+
if group_by:
|
|
190
|
+
payload["group_by"] = list(group_by)
|
|
191
|
+
if having:
|
|
192
|
+
payload["having"] = having
|
|
193
|
+
if aggregates:
|
|
194
|
+
payload["aggregates"] = [dict(agg) for agg in aggregates]
|
|
195
|
+
if returning is not None:
|
|
196
|
+
payload["returning"] = bool(returning)
|
|
197
|
+
if exists is not None:
|
|
198
|
+
payload["exists"] = bool(exists)
|
|
199
|
+
if count is not None:
|
|
200
|
+
payload["count"] = bool(count)
|
|
201
|
+
if lock is not None:
|
|
202
|
+
payload["lock"] = lock
|
|
203
|
+
if pk_column is not None:
|
|
204
|
+
payload["pk_column"] = pk_column
|
|
205
|
+
if union_query is not None:
|
|
206
|
+
payload["union_query"] = union_query
|
|
207
|
+
if union_all is not None:
|
|
208
|
+
payload["union_all"] = union_all
|
|
209
|
+
return payload
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_insert_ir(
|
|
213
|
+
*,
|
|
214
|
+
table: str,
|
|
215
|
+
values: dict[str, Any] | None = None,
|
|
216
|
+
bulk_values: Sequence[dict[str, Any]] | None = None,
|
|
217
|
+
column_types: dict[str, dict] | None = None,
|
|
218
|
+
model: str | None = None,
|
|
219
|
+
returning: bool | None = None,
|
|
220
|
+
pk_column: str | None = None,
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
"""Build an INSERT query IR payload.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
table: Table name
|
|
226
|
+
values: Single row values dict
|
|
227
|
+
bulk_values: Multiple rows for bulk insert
|
|
228
|
+
column_types: ColumnTypeSpec dicts for typed parameter binding.
|
|
229
|
+
model: Model name for validation
|
|
230
|
+
returning: Whether to return inserted rows
|
|
231
|
+
pk_column: Primary key column name (defaults to "id" if not specified)
|
|
232
|
+
"""
|
|
233
|
+
if not values and not bulk_values:
|
|
234
|
+
raise ValueError("INSERT requires either 'values' or 'bulk_values'")
|
|
235
|
+
payload: dict[str, Any] = {
|
|
236
|
+
"proto": IR_PROTO_VERSION,
|
|
237
|
+
"op": "insert",
|
|
238
|
+
"table": table,
|
|
239
|
+
}
|
|
240
|
+
if values:
|
|
241
|
+
payload["values"] = dict(values)
|
|
242
|
+
if bulk_values:
|
|
243
|
+
payload["bulk_values"] = [dict(row) for row in bulk_values]
|
|
244
|
+
if column_types:
|
|
245
|
+
payload["column_types"] = dict(column_types)
|
|
246
|
+
if model:
|
|
247
|
+
payload["model"] = model
|
|
248
|
+
if returning is not None:
|
|
249
|
+
payload["returning"] = bool(returning)
|
|
250
|
+
if pk_column is not None:
|
|
251
|
+
payload["pk_column"] = pk_column
|
|
252
|
+
return payload
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def build_update_ir(
|
|
256
|
+
*,
|
|
257
|
+
table: str,
|
|
258
|
+
values: dict[str, Any] | None = None,
|
|
259
|
+
filter_tree: FilterNode | None = None,
|
|
260
|
+
column_types: dict[str, dict] | None = None,
|
|
261
|
+
column_db_types: dict[str, str] | None = None,
|
|
262
|
+
model: str | None = None,
|
|
263
|
+
returning: bool | None = None,
|
|
264
|
+
bulk_update: Sequence[dict[str, Any]] | None = None,
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
"""Build an UPDATE query IR payload.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
table: Table name
|
|
270
|
+
values: Values to update
|
|
271
|
+
filter_tree: Filter conditions
|
|
272
|
+
column_types: ColumnTypeSpec dicts for typed parameter binding.
|
|
273
|
+
column_db_types: Column db_type overrides for array casts and JSON lookups.
|
|
274
|
+
model: Model name
|
|
275
|
+
returning: Whether to return updated rows
|
|
276
|
+
bulk_update: Bulk update rows
|
|
277
|
+
"""
|
|
278
|
+
if not values and not bulk_update:
|
|
279
|
+
raise ValueError("UPDATE requires either 'values' or 'bulk_update'")
|
|
280
|
+
if values and bulk_update:
|
|
281
|
+
raise ValueError("Provide either 'values' or 'bulk_update', not both")
|
|
282
|
+
payload: dict[str, Any] = {
|
|
283
|
+
"proto": IR_PROTO_VERSION,
|
|
284
|
+
"op": "update",
|
|
285
|
+
"table": table,
|
|
286
|
+
}
|
|
287
|
+
if values:
|
|
288
|
+
payload["values"] = dict(values)
|
|
289
|
+
if bulk_update:
|
|
290
|
+
normalized_rows: list[dict[str, Any]] = []
|
|
291
|
+
for entry in bulk_update:
|
|
292
|
+
filters_payload = dict(entry.get("filters") or {})
|
|
293
|
+
values_payload = dict(entry.get("values") or {})
|
|
294
|
+
if not filters_payload or not values_payload:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
"Each bulk_update entry requires 'filters' and 'values'"
|
|
297
|
+
)
|
|
298
|
+
normalized_rows.append(
|
|
299
|
+
{
|
|
300
|
+
"filters": filters_payload,
|
|
301
|
+
"values": values_payload,
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
# Wrap in object with "rows" field to match Rust BulkUpdate struct
|
|
305
|
+
payload["bulk_update"] = {"rows": normalized_rows}
|
|
306
|
+
if column_types:
|
|
307
|
+
payload["column_types"] = dict(column_types)
|
|
308
|
+
if column_db_types:
|
|
309
|
+
payload["column_db_types"] = dict(column_db_types)
|
|
310
|
+
if filter_tree:
|
|
311
|
+
payload["filter_tree"] = filter_tree
|
|
312
|
+
if model:
|
|
313
|
+
payload["model"] = model
|
|
314
|
+
if returning is not None:
|
|
315
|
+
payload["returning"] = bool(returning)
|
|
316
|
+
return payload
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def build_delete_ir(
|
|
320
|
+
*,
|
|
321
|
+
table: str,
|
|
322
|
+
filter_tree: FilterNode | None = None,
|
|
323
|
+
column_types: dict[str, dict] | None = None,
|
|
324
|
+
column_db_types: dict[str, str] | None = None,
|
|
325
|
+
model: str | None = None,
|
|
326
|
+
returning: bool | None = None,
|
|
327
|
+
) -> dict[str, Any]:
|
|
328
|
+
"""Build a DELETE query IR payload."""
|
|
329
|
+
payload: dict[str, Any] = {
|
|
330
|
+
"proto": IR_PROTO_VERSION,
|
|
331
|
+
"op": "delete",
|
|
332
|
+
"table": table,
|
|
333
|
+
}
|
|
334
|
+
if filter_tree:
|
|
335
|
+
payload["filter_tree"] = filter_tree
|
|
336
|
+
if column_types:
|
|
337
|
+
payload["column_types"] = dict(column_types)
|
|
338
|
+
if column_db_types:
|
|
339
|
+
payload["column_db_types"] = dict(column_db_types)
|
|
340
|
+
if model:
|
|
341
|
+
payload["model"] = model
|
|
342
|
+
if returning is not None:
|
|
343
|
+
payload["returning"] = bool(returning)
|
|
344
|
+
return payload
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def build_raw_sql_ir(*, sql: str, params: list[Any] | None = None) -> dict[str, Any]:
|
|
348
|
+
"""Build a raw SQL query IR payload.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
sql: Raw SQL statement
|
|
352
|
+
params: Optional parameters for the SQL statement
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
IR payload dict
|
|
356
|
+
"""
|
|
357
|
+
payload: dict[str, Any] = {
|
|
358
|
+
"proto": IR_PROTO_VERSION,
|
|
359
|
+
"op": "raw",
|
|
360
|
+
"table": "", # Empty table for raw SQL (required by QueryIR)
|
|
361
|
+
"sql": sql,
|
|
362
|
+
}
|
|
363
|
+
if params:
|
|
364
|
+
payload["params"] = list(params)
|
|
365
|
+
return payload
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
__all__ = [
|
|
369
|
+
"IR_PROTO_VERSION",
|
|
370
|
+
"FilterNode",
|
|
371
|
+
"filter_condition",
|
|
372
|
+
"filter_and",
|
|
373
|
+
"filter_or",
|
|
374
|
+
"filter_not",
|
|
375
|
+
"build_select_ir",
|
|
376
|
+
"build_raw_sql_ir",
|
|
377
|
+
"build_insert_ir",
|
|
378
|
+
"build_update_ir",
|
|
379
|
+
"build_delete_ir",
|
|
380
|
+
]
|
oxyde/core/types.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Unified type registry for the Oxyde ORM.
|
|
2
|
+
|
|
3
|
+
TYPE_REGISTRY maps Python types to their ORM-level descriptors:
|
|
4
|
+
- category: lookup category for query filters
|
|
5
|
+
- serialize: value → msgpack-safe value
|
|
6
|
+
|
|
7
|
+
Column type classification lives in `core/column_types.py`
|
|
8
|
+
(`compute_column_type`); this registry covers the remaining roles.
|
|
9
|
+
Uses exact type() lookup — no issubclass ordering issues (bool vs int).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import date, datetime, time, timedelta
|
|
17
|
+
from decimal import Decimal
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any
|
|
20
|
+
from uuid import UUID
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class TypeDescriptor:
|
|
25
|
+
"""Describes how a Python type maps to ORM operations."""
|
|
26
|
+
|
|
27
|
+
category: str # "string", "numeric", "datetime", "time", "uuid", "bool", "generic"
|
|
28
|
+
serialize: Callable[[Any], Any] # value → msgpack-safe value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TYPE_REGISTRY: dict[type, TypeDescriptor] = {
|
|
32
|
+
bool: TypeDescriptor("bool", lambda v: v),
|
|
33
|
+
int: TypeDescriptor("numeric", lambda v: v),
|
|
34
|
+
float: TypeDescriptor("numeric", lambda v: v),
|
|
35
|
+
str: TypeDescriptor("string", lambda v: v),
|
|
36
|
+
bytes: TypeDescriptor("generic", lambda v: v),
|
|
37
|
+
bytearray: TypeDescriptor("generic", bytes),
|
|
38
|
+
datetime: TypeDescriptor("datetime", lambda v: v.isoformat()),
|
|
39
|
+
date: TypeDescriptor("datetime", lambda v: v.isoformat()),
|
|
40
|
+
# Own category: comparisons apply, but a time-of-day has no calendar
|
|
41
|
+
# part, so date-part lookups (year/month/day) must not be offered.
|
|
42
|
+
time: TypeDescriptor("time", lambda v: v.isoformat()),
|
|
43
|
+
timedelta: TypeDescriptor("generic", lambda v: int(v.total_seconds() * 1_000_000)),
|
|
44
|
+
UUID: TypeDescriptor("uuid", str),
|
|
45
|
+
Decimal: TypeDescriptor("numeric", str),
|
|
46
|
+
dict: TypeDescriptor("generic", lambda v: v),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def serialize_value(value: Any) -> Any:
|
|
51
|
+
"""Serialize a value for msgpack/IR using TYPE_REGISTRY.
|
|
52
|
+
|
|
53
|
+
Handles lists, tuples (as lists) and dicts recursively. Returns value
|
|
54
|
+
unchanged if type is not in the registry (int, str, float, bool, None
|
|
55
|
+
pass through as-is since msgpack handles them natively).
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(value, (list, tuple)):
|
|
58
|
+
return [serialize_value(v) for v in value]
|
|
59
|
+
if isinstance(value, dict):
|
|
60
|
+
return {k: serialize_value(v) for k, v in value.items()}
|
|
61
|
+
if isinstance(value, Enum):
|
|
62
|
+
return value.value
|
|
63
|
+
desc = TYPE_REGISTRY.get(type(value))
|
|
64
|
+
if desc is not None:
|
|
65
|
+
return desc.serialize(value)
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["TypeDescriptor", "TYPE_REGISTRY", "serialize_value"]
|
oxyde/core/wrapper.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Dynamic import of _oxyde_core Rust extension with ABI version checking.
|
|
2
|
+
|
|
3
|
+
This module handles the import of the Rust PyO3 extension module (_oxyde_core)
|
|
4
|
+
and provides stub functions if the module is not available or incompatible.
|
|
5
|
+
|
|
6
|
+
ABI Version:
|
|
7
|
+
EXPECTED_ABI_VERSION must match __abi_version__ in the Rust module.
|
|
8
|
+
If mismatch, ImportError is raised prompting rebuild.
|
|
9
|
+
|
|
10
|
+
Import Strategy:
|
|
11
|
+
1. Try to import _oxyde_core
|
|
12
|
+
2. Check ABI version
|
|
13
|
+
3. Export available functions with hasattr() checks for backwards compat
|
|
14
|
+
4. If import fails, create stub functions that raise RuntimeError
|
|
15
|
+
|
|
16
|
+
Why Stub Functions?
|
|
17
|
+
Allows Python code to import oxyde even without the Rust module built.
|
|
18
|
+
Useful for:
|
|
19
|
+
- IDE autocomplete without full build
|
|
20
|
+
- Documentation generation
|
|
21
|
+
- Partial testing (model definitions, IR building)
|
|
22
|
+
|
|
23
|
+
Functions Exported:
|
|
24
|
+
Pool management:
|
|
25
|
+
init_pool, init_pool_overwrite, close_pool, close_all_pools
|
|
26
|
+
|
|
27
|
+
Query execution:
|
|
28
|
+
execute, execute_in_transaction
|
|
29
|
+
|
|
30
|
+
Transactions:
|
|
31
|
+
begin_transaction, commit_transaction, rollback_transaction
|
|
32
|
+
create_savepoint, rollback_to_savepoint, release_savepoint
|
|
33
|
+
|
|
34
|
+
Debugging:
|
|
35
|
+
render_sql, render_sql_debug, explain_query
|
|
36
|
+
|
|
37
|
+
Migrations:
|
|
38
|
+
migration_compute_diff, migration_to_sql
|
|
39
|
+
|
|
40
|
+
Validation:
|
|
41
|
+
register_validator (no-op, not implemented in Rust)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import sys
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
# Expected ABI version - must match __abi_version__ in Rust module
|
|
51
|
+
EXPECTED_ABI_VERSION = 1
|
|
52
|
+
|
|
53
|
+
# Function registry: (rust_name, export_name, is_async, is_noop_fallback)
|
|
54
|
+
# - rust_name: name in _oxyde_core module
|
|
55
|
+
# - export_name: name to export (usually same, but explain -> explain_query)
|
|
56
|
+
# - is_async: True for async functions
|
|
57
|
+
# - is_noop_fallback: True if stub should be no-op instead of raising
|
|
58
|
+
_FUNCTION_REGISTRY: list[tuple[str, str, bool, bool]] = [
|
|
59
|
+
# Pool management
|
|
60
|
+
("init_pool", "init_pool", True, False),
|
|
61
|
+
("init_pool_overwrite", "init_pool_overwrite", True, False),
|
|
62
|
+
("close_pool", "close_pool", True, False),
|
|
63
|
+
("close_all_pools", "close_all_pools", True, False),
|
|
64
|
+
# Query execution
|
|
65
|
+
("execute", "execute", True, False),
|
|
66
|
+
("execute_in_transaction", "execute_in_transaction", True, False),
|
|
67
|
+
# Transactions
|
|
68
|
+
("begin_transaction", "begin_transaction", True, False),
|
|
69
|
+
("commit_transaction", "commit_transaction", True, False),
|
|
70
|
+
("rollback_transaction", "rollback_transaction", True, False),
|
|
71
|
+
("create_savepoint", "create_savepoint", True, False),
|
|
72
|
+
("rollback_to_savepoint", "rollback_to_savepoint", True, False),
|
|
73
|
+
("release_savepoint", "release_savepoint", True, False),
|
|
74
|
+
# Pool introspection
|
|
75
|
+
("pool_backend", "pool_backend", True, False),
|
|
76
|
+
# Debug/introspection
|
|
77
|
+
("render_sql", "render_sql", True, False),
|
|
78
|
+
("render_sql_debug", "render_sql_debug", False, False),
|
|
79
|
+
("explain", "explain_query", True, False), # Renamed for API consistency
|
|
80
|
+
# Migrations
|
|
81
|
+
("migration_compute_diff", "migration_compute_diff", False, False),
|
|
82
|
+
("migration_to_sql", "migration_to_sql", False, False),
|
|
83
|
+
# Validation (no-op - not implemented in Rust, kept for compatibility)
|
|
84
|
+
("register_validator", "register_validator", False, True),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _make_stub(name: str, is_async: bool, is_noop: bool) -> Callable[..., Any]:
|
|
89
|
+
"""Create a stub function that either raises RuntimeError or is a no-op."""
|
|
90
|
+
if is_noop:
|
|
91
|
+
|
|
92
|
+
def noop_stub(*args: Any, **kwargs: Any) -> None:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
noop_stub.__name__ = name
|
|
96
|
+
noop_stub.__doc__ = f"No-op stub for {name} (Rust module not available)"
|
|
97
|
+
return noop_stub
|
|
98
|
+
|
|
99
|
+
error_msg = (
|
|
100
|
+
f"Rust core module not available or missing '{name}'. "
|
|
101
|
+
"Please rebuild with: maturin develop --release"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if is_async:
|
|
105
|
+
|
|
106
|
+
async def async_stub(*args: Any, **kwargs: Any) -> Any:
|
|
107
|
+
raise RuntimeError(error_msg)
|
|
108
|
+
|
|
109
|
+
async_stub.__name__ = name
|
|
110
|
+
async_stub.__doc__ = f"Stub for {name} - raises RuntimeError"
|
|
111
|
+
return async_stub
|
|
112
|
+
else:
|
|
113
|
+
|
|
114
|
+
def sync_stub(*args: Any, **kwargs: Any) -> Any:
|
|
115
|
+
raise RuntimeError(error_msg)
|
|
116
|
+
|
|
117
|
+
sync_stub.__name__ = name
|
|
118
|
+
sync_stub.__doc__ = f"Stub for {name} - raises RuntimeError"
|
|
119
|
+
return sync_stub
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _load_functions() -> dict[str, Callable[..., Any]]:
|
|
123
|
+
"""Load functions from Rust module or create stubs."""
|
|
124
|
+
exports: dict[str, Callable[..., Any]] = {}
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
import _oxyde_core # type: ignore[import-untyped, import-not-found, unused-ignore]
|
|
128
|
+
|
|
129
|
+
# ABI version check
|
|
130
|
+
abi_version = getattr(_oxyde_core, "__abi_version__", None)
|
|
131
|
+
if abi_version is None:
|
|
132
|
+
raise ImportError("Rust core module does not expose __abi_version__")
|
|
133
|
+
if abi_version != EXPECTED_ABI_VERSION:
|
|
134
|
+
raise ImportError(
|
|
135
|
+
f"ABI version mismatch: expected {EXPECTED_ABI_VERSION}, "
|
|
136
|
+
f"got {abi_version}. Please reinstall oxyde-postgres-core."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Load each function from registry
|
|
140
|
+
for rust_name, export_name, is_async, is_noop in _FUNCTION_REGISTRY:
|
|
141
|
+
if hasattr(_oxyde_core, rust_name):
|
|
142
|
+
exports[export_name] = getattr(_oxyde_core, rust_name)
|
|
143
|
+
else:
|
|
144
|
+
# Function missing in Rust module (older version?)
|
|
145
|
+
exports[export_name] = _make_stub(export_name, is_async, is_noop)
|
|
146
|
+
|
|
147
|
+
except ImportError as e:
|
|
148
|
+
print(f"Warning: Failed to import Rust core module: {e}", file=sys.stderr)
|
|
149
|
+
print("Oxyde ORM will not be fully functional.", file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
# Create all stubs
|
|
152
|
+
for _, export_name, is_async, is_noop in _FUNCTION_REGISTRY:
|
|
153
|
+
exports[export_name] = _make_stub(export_name, is_async, is_noop)
|
|
154
|
+
|
|
155
|
+
return exports
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Load functions once at module import
|
|
159
|
+
_exports = _load_functions()
|
|
160
|
+
|
|
161
|
+
# Export individual functions for IDE support and type checking
|
|
162
|
+
execute = _exports["execute"]
|
|
163
|
+
execute_in_transaction = _exports["execute_in_transaction"]
|
|
164
|
+
init_pool = _exports["init_pool"]
|
|
165
|
+
init_pool_overwrite = _exports["init_pool_overwrite"]
|
|
166
|
+
close_pool = _exports["close_pool"]
|
|
167
|
+
close_all_pools = _exports["close_all_pools"]
|
|
168
|
+
begin_transaction = _exports["begin_transaction"]
|
|
169
|
+
commit_transaction = _exports["commit_transaction"]
|
|
170
|
+
rollback_transaction = _exports["rollback_transaction"]
|
|
171
|
+
create_savepoint = _exports["create_savepoint"]
|
|
172
|
+
rollback_to_savepoint = _exports["rollback_to_savepoint"]
|
|
173
|
+
release_savepoint = _exports["release_savepoint"]
|
|
174
|
+
register_validator = _exports["register_validator"]
|
|
175
|
+
pool_backend = _exports["pool_backend"]
|
|
176
|
+
render_sql = _exports["render_sql"]
|
|
177
|
+
render_sql_debug = _exports["render_sql_debug"]
|
|
178
|
+
explain_query = _exports["explain_query"]
|
|
179
|
+
migration_compute_diff = _exports["migration_compute_diff"]
|
|
180
|
+
migration_to_sql = _exports["migration_to_sql"]
|
|
181
|
+
|
|
182
|
+
# ── Core exception classes ────────────────────────────────────────────────
|
|
183
|
+
# Raised by the Rust core; constraint classification happens in the driver
|
|
184
|
+
# (sqlx ErrorKind), never by matching error message text. Stubs keep
|
|
185
|
+
# isinstance() checks working when the extension is unavailable.
|
|
186
|
+
try:
|
|
187
|
+
from _oxyde_core import ( # type: ignore[import-untyped, import-not-found, unused-ignore]
|
|
188
|
+
CheckViolationError,
|
|
189
|
+
DatabaseError,
|
|
190
|
+
ForeignKeyViolationError,
|
|
191
|
+
IntegrityError,
|
|
192
|
+
NotNullViolationError,
|
|
193
|
+
UniqueViolationError,
|
|
194
|
+
)
|
|
195
|
+
except ImportError: # pragma: no cover - extension missing
|
|
196
|
+
|
|
197
|
+
class DatabaseError(RuntimeError): # type: ignore[no-redef]
|
|
198
|
+
"""Stub: Rust core module not available."""
|
|
199
|
+
|
|
200
|
+
class IntegrityError(DatabaseError): # type: ignore[no-redef]
|
|
201
|
+
"""Stub: Rust core module not available."""
|
|
202
|
+
|
|
203
|
+
class UniqueViolationError(IntegrityError): # type: ignore[no-redef]
|
|
204
|
+
"""Stub: Rust core module not available."""
|
|
205
|
+
|
|
206
|
+
class ForeignKeyViolationError(IntegrityError): # type: ignore[no-redef]
|
|
207
|
+
"""Stub: Rust core module not available."""
|
|
208
|
+
|
|
209
|
+
class NotNullViolationError(IntegrityError): # type: ignore[no-redef]
|
|
210
|
+
"""Stub: Rust core module not available."""
|
|
211
|
+
|
|
212
|
+
class CheckViolationError(IntegrityError): # type: ignore[no-redef]
|
|
213
|
+
"""Stub: Rust core module not available."""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = [
|
|
217
|
+
"execute",
|
|
218
|
+
"execute_in_transaction",
|
|
219
|
+
"init_pool",
|
|
220
|
+
"init_pool_overwrite",
|
|
221
|
+
"close_pool",
|
|
222
|
+
"close_all_pools",
|
|
223
|
+
"begin_transaction",
|
|
224
|
+
"commit_transaction",
|
|
225
|
+
"rollback_transaction",
|
|
226
|
+
"create_savepoint",
|
|
227
|
+
"rollback_to_savepoint",
|
|
228
|
+
"release_savepoint",
|
|
229
|
+
"register_validator",
|
|
230
|
+
"pool_backend",
|
|
231
|
+
"render_sql",
|
|
232
|
+
"render_sql_debug",
|
|
233
|
+
"explain_query",
|
|
234
|
+
"migration_compute_diff",
|
|
235
|
+
"migration_to_sql",
|
|
236
|
+
"DatabaseError",
|
|
237
|
+
"IntegrityError",
|
|
238
|
+
"UniqueViolationError",
|
|
239
|
+
"ForeignKeyViolationError",
|
|
240
|
+
"NotNullViolationError",
|
|
241
|
+
"CheckViolationError",
|
|
242
|
+
]
|