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.
Files changed (66) hide show
  1. oxyde/__init__.py +151 -0
  2. oxyde/_msgpack.py +3 -0
  3. oxyde/cli/__init__.py +7 -0
  4. oxyde/cli/app.py +107 -0
  5. oxyde/cli/codegen.py +66 -0
  6. oxyde/cli/config.py +84 -0
  7. oxyde/cli/migrations.py +536 -0
  8. oxyde/codegen/__init__.py +15 -0
  9. oxyde/codegen/stub_generator.py +1229 -0
  10. oxyde/core/__init__.py +90 -0
  11. oxyde/core/column_types.py +367 -0
  12. oxyde/core/ir.py +380 -0
  13. oxyde/core/types.py +69 -0
  14. oxyde/core/wrapper.py +242 -0
  15. oxyde/db/__init__.py +230 -0
  16. oxyde/db/pool.py +305 -0
  17. oxyde/db/registry.py +113 -0
  18. oxyde/db/schema.py +51 -0
  19. oxyde/db/transaction.py +392 -0
  20. oxyde/exceptions.py +89 -0
  21. oxyde/migrations/__init__.py +49 -0
  22. oxyde/migrations/config.py +187 -0
  23. oxyde/migrations/context.py +598 -0
  24. oxyde/migrations/executor.py +456 -0
  25. oxyde/migrations/extract.py +316 -0
  26. oxyde/migrations/generator.py +534 -0
  27. oxyde/migrations/replay.py +367 -0
  28. oxyde/migrations/squash.py +109 -0
  29. oxyde/migrations/tracker.py +162 -0
  30. oxyde/migrations/utils.py +145 -0
  31. oxyde/models/__init__.py +83 -0
  32. oxyde/models/base.py +871 -0
  33. oxyde/models/decorators.py +111 -0
  34. oxyde/models/field.py +204 -0
  35. oxyde/models/lookups.py +667 -0
  36. oxyde/models/metadata.py +174 -0
  37. oxyde/models/registry.py +170 -0
  38. oxyde/models/serializers.py +144 -0
  39. oxyde/models/utils.py +116 -0
  40. oxyde/py.typed +0 -0
  41. oxyde/queries/__init__.py +79 -0
  42. oxyde/queries/aggregates.py +145 -0
  43. oxyde/queries/base.py +249 -0
  44. oxyde/queries/conditions.py +92 -0
  45. oxyde/queries/expressions.py +171 -0
  46. oxyde/queries/insert.py +124 -0
  47. oxyde/queries/joins.py +66 -0
  48. oxyde/queries/manager.py +409 -0
  49. oxyde/queries/mixins/__init__.py +68 -0
  50. oxyde/queries/mixins/aggregation.py +335 -0
  51. oxyde/queries/mixins/debug.py +154 -0
  52. oxyde/queries/mixins/execution.py +701 -0
  53. oxyde/queries/mixins/filtering.py +144 -0
  54. oxyde/queries/mixins/joining.py +160 -0
  55. oxyde/queries/mixins/mutation.py +508 -0
  56. oxyde/queries/mixins/pagination.py +145 -0
  57. oxyde/queries/q.py +250 -0
  58. oxyde/queries/raw.py +114 -0
  59. oxyde/queries/select.py +345 -0
  60. oxyde/queries/typed.py +213 -0
  61. oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
  62. oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
  63. oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
  64. oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
  65. oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. 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"]