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,111 @@
|
|
|
1
|
+
"""Table-level constraint definitions: Index and Check.
|
|
2
|
+
|
|
3
|
+
This module defines dataclasses for table-level constraints that cannot be
|
|
4
|
+
expressed at the field level. Use these in the model's Meta class.
|
|
5
|
+
|
|
6
|
+
When to use:
|
|
7
|
+
Field-level (use Field()):
|
|
8
|
+
- Single-column indexes: Field(db_index=True)
|
|
9
|
+
- Single-column unique: Field(db_unique=True)
|
|
10
|
+
- Primary key: Field(db_pk=True)
|
|
11
|
+
|
|
12
|
+
Table-level (use this module):
|
|
13
|
+
- Composite indexes: Index(("col1", "col2"))
|
|
14
|
+
- Partial indexes: Index(("email",), where="deleted_at IS NULL")
|
|
15
|
+
- Custom index methods: Index(("data",), method="gin")
|
|
16
|
+
- CHECK constraints: Check("price > 0")
|
|
17
|
+
|
|
18
|
+
Classes:
|
|
19
|
+
Index: Composite/partial index definition.
|
|
20
|
+
fields: Column names as tuple
|
|
21
|
+
method: btree | hash | gin | gist (default: btree)
|
|
22
|
+
name: Custom index name (auto-generated if None)
|
|
23
|
+
unique: Create UNIQUE index
|
|
24
|
+
where: Partial index condition (SQL fragment)
|
|
25
|
+
|
|
26
|
+
Check: CHECK constraint definition.
|
|
27
|
+
expression: SQL boolean expression
|
|
28
|
+
name: Constraint name (auto-generated if None)
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
class Order(Model):
|
|
32
|
+
user_id: int
|
|
33
|
+
created_at: datetime
|
|
34
|
+
total: Decimal
|
|
35
|
+
status: str
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
is_table = True
|
|
39
|
+
indexes = [
|
|
40
|
+
Index(("user_id", "created_at")),
|
|
41
|
+
Index(("status",), where="status != 'archived'"),
|
|
42
|
+
]
|
|
43
|
+
constraints = [
|
|
44
|
+
Check("total >= 0", name="positive_total"),
|
|
45
|
+
]
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from typing import Literal
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Index:
|
|
54
|
+
"""Index definition for Meta.indexes (table-level).
|
|
55
|
+
|
|
56
|
+
Used for composite indexes and partial indexes.
|
|
57
|
+
For single-field indexes, use Field(db_index=True) instead.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
>>> class User(Model):
|
|
61
|
+
... class Meta:
|
|
62
|
+
... indexes = [
|
|
63
|
+
... Index(("city", "created_at"), method="btree"),
|
|
64
|
+
... Index(("email",), unique=True, where="deleted_at IS NULL"),
|
|
65
|
+
... ]
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
fields: tuple[str, ...]
|
|
69
|
+
method: Literal["btree", "hash", "gin", "gist"] | None = None
|
|
70
|
+
name: str | None = None
|
|
71
|
+
unique: bool = False
|
|
72
|
+
where: str | None = None # Partial index condition
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
fields: tuple[str, ...] | list[str],
|
|
77
|
+
method: Literal["btree", "hash", "gin", "gist"] | None = None,
|
|
78
|
+
name: str | None = None,
|
|
79
|
+
unique: bool = False,
|
|
80
|
+
where: str | None = None,
|
|
81
|
+
):
|
|
82
|
+
if not fields:
|
|
83
|
+
raise ValueError("Index requires at least one field")
|
|
84
|
+
self.fields = tuple(fields) if isinstance(fields, list) else fields
|
|
85
|
+
self.method = method
|
|
86
|
+
self.name = name
|
|
87
|
+
self.unique = unique
|
|
88
|
+
if where is not None:
|
|
89
|
+
where = where.strip() or None
|
|
90
|
+
self.where = where
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Check:
|
|
95
|
+
"""Check constraint definition for Meta.constraints (table-level).
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> class Event(Model):
|
|
99
|
+
... start_date: datetime
|
|
100
|
+
... end_date: datetime
|
|
101
|
+
... class Meta:
|
|
102
|
+
... constraints = [
|
|
103
|
+
... Check("start_date < end_date", name="valid_dates"),
|
|
104
|
+
... ]
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
expression: str
|
|
108
|
+
name: str | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = ["Index", "Check"]
|
oxyde/models/field.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Field configuration for Model with database metadata.
|
|
2
|
+
|
|
3
|
+
This module extends Pydantic's Field() with database-specific attributes.
|
|
4
|
+
OxydeFieldInfo stores both Pydantic validation rules and DB schema metadata.
|
|
5
|
+
|
|
6
|
+
Design:
|
|
7
|
+
- OxydeFieldInfo inherits from pydantic.fields.FieldInfo
|
|
8
|
+
- DB attributes stored as slots (not json_schema_extra) for type safety
|
|
9
|
+
- Field() function is a factory returning OxydeFieldInfo
|
|
10
|
+
|
|
11
|
+
Attribute Categories:
|
|
12
|
+
Primary Key:
|
|
13
|
+
db_pk: Mark as PRIMARY KEY (auto-increment if int, None default)
|
|
14
|
+
|
|
15
|
+
Indexing:
|
|
16
|
+
db_index: Create single-column index
|
|
17
|
+
db_index_name: Custom index name (default: ix_{table}_{column})
|
|
18
|
+
db_index_method: Index type - btree, hash, gin, gist (PostgreSQL)
|
|
19
|
+
db_unique: UNIQUE constraint
|
|
20
|
+
|
|
21
|
+
Column Mapping:
|
|
22
|
+
db_column: Override column name (default: field name)
|
|
23
|
+
db_type: Override SQL type (e.g., "BIGSERIAL", "JSONB")
|
|
24
|
+
db_default: SQL DEFAULT expression (e.g., "NOW()", "gen_random_uuid()")
|
|
25
|
+
db_comment: SQL COMMENT ON COLUMN
|
|
26
|
+
|
|
27
|
+
Foreign Keys:
|
|
28
|
+
db_fk: Target field for FK (default: auto-detect PK of related model)
|
|
29
|
+
db_on_delete: CASCADE | SET NULL | RESTRICT | NO ACTION
|
|
30
|
+
db_on_update: CASCADE | SET NULL | RESTRICT | NO ACTION
|
|
31
|
+
db_nullable: Override NULL/NOT NULL in DB. If None: infer from type hint
|
|
32
|
+
(X | None = nullable)
|
|
33
|
+
|
|
34
|
+
Relations (virtual, not stored in DB):
|
|
35
|
+
db_reverse_fk: Field name on related model for reverse lookup
|
|
36
|
+
db_m2m: Many-to-many relation flag
|
|
37
|
+
db_through: M2M junction table name
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
from oxyde import Model, Field
|
|
41
|
+
|
|
42
|
+
class User(Model):
|
|
43
|
+
id: int | None = Field(default=None, db_pk=True)
|
|
44
|
+
email: str = Field(db_unique=True, db_index=True)
|
|
45
|
+
name: str = Field(max_length=100) # Pydantic validation
|
|
46
|
+
bio: str | None = Field(default=None, db_type="TEXT")
|
|
47
|
+
created_at: datetime = Field(db_default="NOW()")
|
|
48
|
+
|
|
49
|
+
class Meta:
|
|
50
|
+
is_table = True
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from typing import Any, Literal
|
|
56
|
+
|
|
57
|
+
from pydantic.fields import FieldInfo, PydanticUndefined
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OxydeFieldInfo(FieldInfo): # type: ignore[misc]
|
|
61
|
+
"""Extended FieldInfo with type-safe database metadata.
|
|
62
|
+
|
|
63
|
+
This class extends Pydantic's FieldInfo to add database-specific metadata
|
|
64
|
+
while maintaining full compatibility with Pydantic validation.
|
|
65
|
+
|
|
66
|
+
All db_* parameters are stored as type-safe attributes (not in json_schema_extra),
|
|
67
|
+
providing IDE autocomplete and type checking.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = FieldInfo.__slots__ + (
|
|
71
|
+
"db_pk",
|
|
72
|
+
"db_index",
|
|
73
|
+
"db_index_name",
|
|
74
|
+
"db_index_method",
|
|
75
|
+
"db_unique",
|
|
76
|
+
"db_column",
|
|
77
|
+
"db_type",
|
|
78
|
+
"db_default",
|
|
79
|
+
"db_comment",
|
|
80
|
+
"db_fk",
|
|
81
|
+
"db_on_delete",
|
|
82
|
+
"db_on_update",
|
|
83
|
+
"db_nullable",
|
|
84
|
+
"db_reverse_fk",
|
|
85
|
+
"db_m2m",
|
|
86
|
+
"db_through",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
default: Any = PydanticUndefined,
|
|
92
|
+
*,
|
|
93
|
+
# === Database metadata ===
|
|
94
|
+
db_pk: bool = False,
|
|
95
|
+
db_index: bool = False,
|
|
96
|
+
db_index_name: str | None = None,
|
|
97
|
+
db_index_method: Literal["btree", "hash", "gin", "gist"] | None = None,
|
|
98
|
+
db_unique: bool = False,
|
|
99
|
+
db_column: str | None = None,
|
|
100
|
+
db_type: str | None = None,
|
|
101
|
+
db_default: str | None = None,
|
|
102
|
+
db_comment: str | None = None,
|
|
103
|
+
db_fk: str | None = None,
|
|
104
|
+
db_on_delete: Literal[
|
|
105
|
+
"CASCADE", "SET NULL", "RESTRICT", "NO ACTION"
|
|
106
|
+
] = "RESTRICT",
|
|
107
|
+
db_on_update: Literal[
|
|
108
|
+
"CASCADE", "SET NULL", "RESTRICT", "NO ACTION"
|
|
109
|
+
] = "CASCADE",
|
|
110
|
+
db_nullable: bool | None = None,
|
|
111
|
+
db_reverse_fk: str | None = None,
|
|
112
|
+
db_m2m: bool = False,
|
|
113
|
+
db_through: str | None = None,
|
|
114
|
+
**kwargs: Any, # All Pydantic parameters: ge, le, alias, description, etc.
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Initialize OxydeFieldInfo with Pydantic and database metadata.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
default: Default value for the field
|
|
120
|
+
db_pk: Mark this field as primary key
|
|
121
|
+
db_index: Create an index on this field
|
|
122
|
+
db_index_name: Custom index name
|
|
123
|
+
db_index_method: Index method (btree, hash, gin, gist)
|
|
124
|
+
db_unique: Add unique constraint
|
|
125
|
+
db_column: Database column name (overrides field name)
|
|
126
|
+
db_type: SQL type override (e.g., "BIGSERIAL", "TEXT", "JSONB")
|
|
127
|
+
db_default: SQL DEFAULT expression (e.g., "NOW()", "0")
|
|
128
|
+
db_comment: SQL COMMENT for the column
|
|
129
|
+
db_fk: Foreign key target. For model type hint: field name (default: PK).
|
|
130
|
+
For primitive type hint: "table.column" format.
|
|
131
|
+
db_on_delete: Foreign key ON DELETE action
|
|
132
|
+
db_on_update: Foreign key ON UPDATE action
|
|
133
|
+
db_nullable: Override NULL/NOT NULL in DB. None = infer from type hint.
|
|
134
|
+
db_reverse_fk: Reverse FK field name for loading relations
|
|
135
|
+
db_m2m: Mark this field as many-to-many relation
|
|
136
|
+
db_through: M2M through table name
|
|
137
|
+
**kwargs: All other Pydantic FieldInfo parameters
|
|
138
|
+
"""
|
|
139
|
+
# Initialize Pydantic FieldInfo (validators, alias, constraints)
|
|
140
|
+
super().__init__(default=default, **kwargs)
|
|
141
|
+
|
|
142
|
+
# Store DB metadata as type-safe attributes
|
|
143
|
+
self.db_pk = db_pk
|
|
144
|
+
self.db_index = db_index
|
|
145
|
+
self.db_index_name = db_index_name
|
|
146
|
+
self.db_index_method = db_index_method
|
|
147
|
+
self.db_unique = db_unique
|
|
148
|
+
self.db_column = db_column
|
|
149
|
+
self.db_type = db_type
|
|
150
|
+
self.db_default = db_default
|
|
151
|
+
self.db_comment = db_comment
|
|
152
|
+
self.db_fk = db_fk
|
|
153
|
+
self.db_on_delete = db_on_delete
|
|
154
|
+
self.db_on_update = db_on_update
|
|
155
|
+
self.db_nullable = db_nullable
|
|
156
|
+
self.db_reverse_fk = db_reverse_fk
|
|
157
|
+
self.db_m2m = db_m2m
|
|
158
|
+
self.db_through = db_through
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def is_virtual(self) -> bool:
|
|
162
|
+
"""True for relation fields not stored in DB (reverse FK, M2M)."""
|
|
163
|
+
return bool(self.db_reverse_fk or self.db_m2m)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def Field(
|
|
167
|
+
default: Any = PydanticUndefined,
|
|
168
|
+
**kwargs: Any,
|
|
169
|
+
) -> Any:
|
|
170
|
+
"""Create Oxyde field with Pydantic and database metadata.
|
|
171
|
+
|
|
172
|
+
Combines Pydantic validation (ge, le, alias, etc.) with database metadata
|
|
173
|
+
(db_pk, db_index, etc.) in a single type-safe API.
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
Basic fields:
|
|
177
|
+
>>> id: int | None = Field(default=None, db_pk=True)
|
|
178
|
+
>>> email: str = Field(db_unique=True, db_index=True)
|
|
179
|
+
>>> age: int = Field(ge=18, le=150) # Pydantic validators
|
|
180
|
+
|
|
181
|
+
With column mapping:
|
|
182
|
+
>>> created_at: datetime = Field(
|
|
183
|
+
... alias="createdAt", # JSON API
|
|
184
|
+
... db_column="created_timestamp", # Database
|
|
185
|
+
... db_default="NOW()"
|
|
186
|
+
... )
|
|
187
|
+
|
|
188
|
+
Foreign keys (type hint must be Model, db_fk specifies target):
|
|
189
|
+
>>> author: Author | None = Field(default=None, db_on_delete="CASCADE")
|
|
190
|
+
>>> author: Author | None = Field(db_fk="uuid", db_on_delete="CASCADE")
|
|
191
|
+
|
|
192
|
+
Reverse relations:
|
|
193
|
+
>>> posts: list[Post] = Field(db_reverse_fk="author")
|
|
194
|
+
|
|
195
|
+
Many-to-many:
|
|
196
|
+
>>> tags: list[Tag] = Field(db_m2m=True, db_through="PostTag")
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
OxydeFieldInfo instance with both Pydantic and database metadata
|
|
200
|
+
"""
|
|
201
|
+
return OxydeFieldInfo(default=default, **kwargs)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
__all__ = ["OxydeFieldInfo", "Field"]
|