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,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 []
@@ -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
+ ]