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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Shared utilities for the migration system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from oxyde._msgpack import msgpack
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_dialect(dialect: str) -> None:
|
|
15
|
+
"""Reject unsupported migration and SQL dialects."""
|
|
16
|
+
if dialect != "postgres":
|
|
17
|
+
raise ValueError("Only the postgres dialect is supported (PostgreSQL 18+).")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_dialect(url: str) -> str:
|
|
21
|
+
"""Validate a PostgreSQL URL and return the PostgreSQL dialect."""
|
|
22
|
+
from oxyde.db.pool import _validate_url_scheme
|
|
23
|
+
|
|
24
|
+
_validate_url_scheme(url)
|
|
25
|
+
return "postgres"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_migration_module(filepath: Path) -> Any | None:
|
|
29
|
+
"""Load a migration module from file.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
filepath: Path to migration .py file
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Loaded module, or None if loading failed
|
|
36
|
+
"""
|
|
37
|
+
spec = importlib.util.spec_from_file_location(filepath.stem, filepath)
|
|
38
|
+
if spec is None or spec.loader is None:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
module = importlib.util.module_from_spec(spec)
|
|
42
|
+
sys.modules[filepath.stem] = module
|
|
43
|
+
spec.loader.exec_module(module)
|
|
44
|
+
return module
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_query_result(result_bytes: bytes) -> list[dict[str, Any]]:
|
|
48
|
+
"""Parse MessagePack query result into list of dicts.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
result_bytes: Raw MessagePack bytes from query
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of row dicts with column names as keys
|
|
55
|
+
"""
|
|
56
|
+
if not result_bytes:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
result = msgpack.unpackb(result_bytes, raw=False)
|
|
60
|
+
|
|
61
|
+
# Format: [columns, rows] where columns is list of names, rows is list of lists
|
|
62
|
+
if isinstance(result, list) and len(result) == 2:
|
|
63
|
+
columns, rows = result
|
|
64
|
+
if isinstance(columns, list) and isinstance(rows, list):
|
|
65
|
+
return [dict(zip(columns, row)) for row in rows]
|
|
66
|
+
|
|
67
|
+
# Fallback: already list of dicts
|
|
68
|
+
if isinstance(result, list) and all(isinstance(r, dict) for r in result):
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── Legacy field-format normalization ────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def normalize_field_dict(field: dict) -> dict:
|
|
78
|
+
"""Ensure a FieldDef dict carries ``column_type`` (ColumnTypeSpec).
|
|
79
|
+
|
|
80
|
+
Legacy migration files store ``python_type`` names; Rust requires
|
|
81
|
+
``column_type``. Derivation uses the same single mapping module as live
|
|
82
|
+
models. Support for the legacy form is deprecated and will become an
|
|
83
|
+
error in 1.0 (run ``oxyde migrations squash`` to convert).
|
|
84
|
+
"""
|
|
85
|
+
if "column_type" in field:
|
|
86
|
+
return field
|
|
87
|
+
|
|
88
|
+
from oxyde.core.column_types import compute_column_type, spec_from_legacy_name
|
|
89
|
+
|
|
90
|
+
db_type = field.get("db_type")
|
|
91
|
+
if not field.get("python_type") and not db_type:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Migration field {field.get('name', '?')!r} defines neither "
|
|
94
|
+
"'column_type' nor legacy 'python_type'/'db_type' — the column "
|
|
95
|
+
"type would silently degrade to TEXT. Use the current format "
|
|
96
|
+
"(column_type={'kind': ...}) or regenerate the migration with "
|
|
97
|
+
"'oxyde makemigrations'."
|
|
98
|
+
)
|
|
99
|
+
if db_type:
|
|
100
|
+
# An explicit db_type owns the column kind, same rule as live models
|
|
101
|
+
# (compute_column_type): a type name the mapping does not know is
|
|
102
|
+
# `unknown` — never a fallback to python_type, or legacy history and
|
|
103
|
+
# makemigrations would disagree on the same column.
|
|
104
|
+
spec = compute_column_type(None, db_type) or {"kind": "unknown"}
|
|
105
|
+
else:
|
|
106
|
+
spec = spec_from_legacy_name(
|
|
107
|
+
str(field.get("python_type", "")),
|
|
108
|
+
max_length=field.get("max_length"),
|
|
109
|
+
max_digits=field.get("max_digits"),
|
|
110
|
+
decimal_places=field.get("decimal_places"),
|
|
111
|
+
)
|
|
112
|
+
normalized = dict(field)
|
|
113
|
+
normalized["column_type"] = spec
|
|
114
|
+
return normalized
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def op_uses_legacy_fields(op: dict) -> bool:
|
|
118
|
+
"""True if the operation carries any field dict in the legacy form."""
|
|
119
|
+
return any("column_type" not in f for f in _iter_field_dicts(op))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normalize_op_fields(op: dict) -> dict:
|
|
123
|
+
"""Normalize every FieldDef dict inside a migration operation."""
|
|
124
|
+
op = dict(op)
|
|
125
|
+
table = op.get("table")
|
|
126
|
+
if isinstance(table, dict) and "fields" in table:
|
|
127
|
+
table = dict(table)
|
|
128
|
+
table["fields"] = [normalize_field_dict(f) for f in table["fields"]]
|
|
129
|
+
op["table"] = table
|
|
130
|
+
for key in ("field", "field_def", "old_field", "new_field"):
|
|
131
|
+
if isinstance(op.get(key), dict):
|
|
132
|
+
op[key] = normalize_field_dict(op[key])
|
|
133
|
+
if isinstance(op.get("table_fields"), list):
|
|
134
|
+
op["table_fields"] = [normalize_field_dict(f) for f in op["table_fields"]]
|
|
135
|
+
return op
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _iter_field_dicts(op: dict) -> Iterator[dict]:
|
|
139
|
+
table = op.get("table")
|
|
140
|
+
if isinstance(table, dict):
|
|
141
|
+
yield from table.get("fields", [])
|
|
142
|
+
for key in ("field", "field_def", "old_field", "new_field"):
|
|
143
|
+
if isinstance(op.get(key), dict):
|
|
144
|
+
yield op[key]
|
|
145
|
+
yield from op.get("table_fields") or []
|
oxyde/models/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""ORM model definitions and field configuration.
|
|
2
|
+
|
|
3
|
+
This module provides the core model system built on Pydantic v2:
|
|
4
|
+
|
|
5
|
+
Classes:
|
|
6
|
+
Model: Base class for all database models. Provides:
|
|
7
|
+
- Automatic table name generation from class name
|
|
8
|
+
- QueryManager via Model.objects for CRUD operations
|
|
9
|
+
- save(), delete(), refresh() instance methods
|
|
10
|
+
- Lifecycle hooks: pre_save, post_save, pre_delete, post_delete
|
|
11
|
+
|
|
12
|
+
Field: Enhanced field configuration function (wraps Pydantic Field):
|
|
13
|
+
- db_pk: Mark as primary key (auto-increment if int)
|
|
14
|
+
- db_index: Create single-column index
|
|
15
|
+
- db_unique: Add UNIQUE constraint
|
|
16
|
+
- db_default: Database-side DEFAULT value
|
|
17
|
+
- db_column: Override column name
|
|
18
|
+
- db_fk: Foreign key reference ("table.column")
|
|
19
|
+
|
|
20
|
+
Index: Composite index decorator for Meta.indexes.
|
|
21
|
+
Check: CHECK constraint decorator for Meta.constraints.
|
|
22
|
+
|
|
23
|
+
Model Registry:
|
|
24
|
+
register_table(): Register model in global registry.
|
|
25
|
+
unregister_table(): Remove model from registry.
|
|
26
|
+
registered_tables(): Get dict of all registered models.
|
|
27
|
+
iter_tables(): Iterate over registered models.
|
|
28
|
+
clear_registry(): Remove all registered models.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
from oxyde import Model, Field, Index
|
|
32
|
+
|
|
33
|
+
@Index("name", "email", unique=True)
|
|
34
|
+
class User(Model):
|
|
35
|
+
id: int | None = Field(default=None, db_pk=True)
|
|
36
|
+
name: str = Field(db_index=True)
|
|
37
|
+
email: str = Field(db_unique=True)
|
|
38
|
+
|
|
39
|
+
class Meta:
|
|
40
|
+
is_table = True
|
|
41
|
+
table_name = "users"
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import warnings
|
|
45
|
+
|
|
46
|
+
from .base import Model
|
|
47
|
+
from .decorators import ( # Keep Index for Meta.indexes, Check for Meta.constraints
|
|
48
|
+
Check,
|
|
49
|
+
Index,
|
|
50
|
+
)
|
|
51
|
+
from .field import Field, OxydeFieldInfo
|
|
52
|
+
from .registry import (
|
|
53
|
+
clear_registry,
|
|
54
|
+
iter_tables,
|
|
55
|
+
register_table,
|
|
56
|
+
registered_tables,
|
|
57
|
+
unregister_table,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def __getattr__(name: str) -> type:
|
|
62
|
+
if name == "OxydeModel":
|
|
63
|
+
warnings.warn(
|
|
64
|
+
"OxydeModel is deprecated, use Model instead",
|
|
65
|
+
DeprecationWarning,
|
|
66
|
+
stacklevel=2,
|
|
67
|
+
)
|
|
68
|
+
return Model
|
|
69
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"Model",
|
|
74
|
+
"Field",
|
|
75
|
+
"OxydeFieldInfo",
|
|
76
|
+
"Index",
|
|
77
|
+
"Check",
|
|
78
|
+
"register_table",
|
|
79
|
+
"unregister_table",
|
|
80
|
+
"registered_tables",
|
|
81
|
+
"iter_tables",
|
|
82
|
+
"clear_registry",
|
|
83
|
+
]
|