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,144 @@
1
+ """Filtering mixin for query building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ from typing_extensions import Self
8
+
9
+ from oxyde.core import ir
10
+ from oxyde.queries.q import Q
11
+
12
+ if TYPE_CHECKING:
13
+ from oxyde.models.base import Model
14
+
15
+
16
+ TModel = TypeVar("TModel", bound="Model")
17
+
18
+
19
+ class FilteringMixin(Generic[TModel]):
20
+ """Mixin providing filtering capabilities."""
21
+
22
+ # These attributes are defined in the base Query class
23
+ _filter_tree: ir.FilterNode | None
24
+ model_class: type[TModel]
25
+
26
+ def _clone(self) -> Self:
27
+ """Must be implemented by the main Query class."""
28
+ raise NotImplementedError
29
+
30
+ def filter(self, *args: Any, **kwargs: Any) -> Self:
31
+ """
32
+ Filter by Q-expressions or field lookups.
33
+
34
+ Supports filtering on related model fields via FK traversal:
35
+ .filter(user__age__gte=18) # Filter by joined user.age
36
+
37
+ Args:
38
+ *args: Q expression objects for complex conditions (AND/OR/NOT)
39
+ **kwargs: Field lookups (e.g., name__icontains="foo", age__gte=18,
40
+ user__name="Alice")
41
+
42
+ Returns:
43
+ Query with filter conditions applied
44
+
45
+ Examples:
46
+ .filter(name="Alice")
47
+ .filter(age__gte=18, status="active")
48
+ .filter(Q(age__gte=18) | Q(premium=True))
49
+ .filter(Q(name="Bob"), age__lt=30)
50
+ .filter(user__age__gte=18) # FK traversal
51
+ """
52
+ clone = self._clone()
53
+ conditions: list[ir.FilterNode] = []
54
+
55
+ # Process Q-expressions from args
56
+ for arg in args:
57
+ if isinstance(arg, Q):
58
+ node = arg.to_filter_node(self.model_class, clone)
59
+ if node:
60
+ conditions.append(node)
61
+ else:
62
+ raise TypeError(
63
+ f"filter() positional args must be Q objects, got {type(arg)}"
64
+ )
65
+
66
+ # Process kwargs
67
+ if kwargs:
68
+ q_from_kwargs = Q(**kwargs)
69
+ node = q_from_kwargs.to_filter_node(self.model_class, clone)
70
+ if node:
71
+ conditions.append(node)
72
+
73
+ if not conditions:
74
+ return clone
75
+
76
+ # Merge with existing filter_tree
77
+ if clone._filter_tree:
78
+ conditions.insert(0, clone._filter_tree)
79
+
80
+ if len(conditions) == 1:
81
+ clone._filter_tree = conditions[0]
82
+ else:
83
+ clone._filter_tree = ir.filter_and(*conditions)
84
+
85
+ return clone
86
+
87
+ def exclude(self, *args: Any, **kwargs: Any) -> Self:
88
+ """
89
+ Add negated filter conditions.
90
+
91
+ Args:
92
+ *args: Q expression objects to negate
93
+ **kwargs: Field lookups to negate
94
+
95
+ Returns:
96
+ Cloned query with negated filter
97
+
98
+ Examples:
99
+ .exclude(status="banned")
100
+ .exclude(Q(age__lt=18) | Q(age__gt=65))
101
+ .exclude(user__age__lt=18) # FK traversal
102
+ """
103
+ clone = self._clone()
104
+ conditions_to_negate: list[ir.FilterNode] = []
105
+
106
+ # Process Q-expressions from args
107
+ for arg in args:
108
+ if isinstance(arg, Q):
109
+ node = arg.to_filter_node(self.model_class, clone)
110
+ if node:
111
+ conditions_to_negate.append(node)
112
+ else:
113
+ raise TypeError(
114
+ f"exclude() positional args must be Q objects, got {type(arg)}"
115
+ )
116
+
117
+ # Process kwargs
118
+ if kwargs:
119
+ q_from_kwargs = Q(**kwargs)
120
+ node = q_from_kwargs.to_filter_node(self.model_class, clone)
121
+ if node:
122
+ conditions_to_negate.append(node)
123
+
124
+ if not conditions_to_negate:
125
+ return clone
126
+
127
+ # Combine conditions with AND, then negate
128
+ if len(conditions_to_negate) == 1:
129
+ negated = ir.filter_not(conditions_to_negate[0])
130
+ else:
131
+ combined = ir.filter_and(*conditions_to_negate)
132
+ negated = ir.filter_not(combined)
133
+
134
+ # Merge negated filter into clone
135
+ if clone._filter_tree:
136
+ clone._filter_tree = ir.filter_and(clone._filter_tree, negated)
137
+ else:
138
+ clone._filter_tree = negated
139
+
140
+ return clone
141
+
142
+ def _build_filter_tree(self) -> ir.FilterNode | None:
143
+ """Return the filter tree."""
144
+ return self._filter_tree
@@ -0,0 +1,160 @@
1
+ """Joining mixin for query building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
7
+
8
+ from typing_extensions import Self
9
+
10
+ from oxyde.exceptions import FieldLookupError
11
+ from oxyde.queries.base import (
12
+ _collect_model_columns,
13
+ _primary_key_meta,
14
+ _resolve_registered_model,
15
+ )
16
+ from oxyde.queries.joins import _JoinDescriptor
17
+
18
+ if TYPE_CHECKING:
19
+ from oxyde.models.base import Model
20
+
21
+
22
+ TModel = TypeVar("TModel", bound="Model")
23
+
24
+
25
+ class JoiningMixin(Generic[TModel]):
26
+ """Mixin providing join and prefetch capabilities."""
27
+
28
+ # These attributes are defined in the base Query class
29
+ model_class: type[TModel]
30
+ _join_specs: list[_JoinDescriptor]
31
+ _prefetch_paths: list[str]
32
+
33
+ def _clone(self) -> Self:
34
+ """Must be implemented by the main Query class."""
35
+ raise NotImplementedError
36
+
37
+ def join(self, *paths: str) -> Self:
38
+ """
39
+ Add LEFT JOIN for related models.
40
+
41
+ Args:
42
+ *paths: Relation paths (e.g., "author", "author__profile")
43
+
44
+ Examples:
45
+ Post.objects.join("author")
46
+ Post.objects.join("author", "category")
47
+ """
48
+ if not paths:
49
+ raise ValueError("join() requires at least one relation path")
50
+ clone = self._clone()
51
+ for path in paths:
52
+ clone._add_join_path(path)
53
+ return clone
54
+
55
+ def prefetch(self, *paths: str) -> Self:
56
+ """
57
+ Prefetch related objects (separate queries).
58
+
59
+ Args:
60
+ *paths: Relation paths for one-to-many relations
61
+
62
+ Examples:
63
+ Author.objects.prefetch("posts")
64
+ """
65
+ if not paths:
66
+ raise ValueError("prefetch() requires at least one relation path")
67
+ clone = self._clone()
68
+ for path in paths:
69
+ if path not in clone._prefetch_paths:
70
+ clone._prefetch_paths.append(path)
71
+ return clone
72
+
73
+ def _column_for_field(self, field: str) -> str:
74
+ """Get database column name for a field."""
75
+ meta = self.model_class._db_meta.field_metadata.get(field)
76
+ if meta is not None:
77
+ return meta.db_column
78
+ return field
79
+
80
+ def _column_mappings_for_fields(self, fields: Iterable[str]) -> dict[str, str]:
81
+ """Get column mappings for fields where column differs from field name."""
82
+ mappings: dict[str, str] = {}
83
+ for field in fields:
84
+ column = self._column_for_field(field)
85
+ if column != field:
86
+ mappings[field] = column
87
+ return mappings
88
+
89
+ def _add_join_path(self, path: str) -> None:
90
+ """Add join descriptors for a relation path."""
91
+ descriptors = self._compute_join_descriptors(path)
92
+ existing = {spec.path for spec in self._join_specs}
93
+ for descriptor in descriptors:
94
+ if descriptor.path not in existing:
95
+ self._join_specs.append(descriptor)
96
+ existing.add(descriptor.path)
97
+
98
+ def _compute_join_descriptors(self, path: str) -> list[_JoinDescriptor]:
99
+ """Compute join descriptors for a relation path."""
100
+ if not path:
101
+ raise ValueError("join() path must be non-empty")
102
+ segments = path.split("__")
103
+ current_model: type[Model] = self.model_class
104
+ parent_path: str | None = None
105
+ parent_alias: str | None = None
106
+ descriptors: list[_JoinDescriptor] = []
107
+ prefix: list[str] = []
108
+
109
+ for segment in segments:
110
+ column_meta = current_model._db_meta.field_metadata.get(segment)
111
+ if column_meta is None or column_meta.foreign_key is None:
112
+ raise FieldLookupError(
113
+ f"{current_model.__name__}.{segment} is not a joinable relation"
114
+ )
115
+ target_model = _resolve_registered_model(column_meta.foreign_key.target)
116
+ prefix.append(segment)
117
+ path_key = "__".join(prefix)
118
+ alias = path_key.replace("__", "_")
119
+ pk_meta = _primary_key_meta(target_model)
120
+ descriptor = _JoinDescriptor(
121
+ path=path_key,
122
+ alias=alias,
123
+ result_prefix=path_key,
124
+ parent_path=parent_path,
125
+ parent_alias=parent_alias,
126
+ attr_name=segment,
127
+ source_column=column_meta.foreign_key.column_name,
128
+ target_column=pk_meta.db_column,
129
+ columns=_collect_model_columns(target_model),
130
+ target_model=target_model,
131
+ parent_model=current_model,
132
+ nullable=column_meta.nullable,
133
+ )
134
+ descriptors.append(descriptor)
135
+ current_model = target_model
136
+ parent_path = path_key
137
+ parent_alias = alias
138
+
139
+ return descriptors
140
+
141
+ def _join_specs_to_ir(self) -> list[dict[str, Any]]:
142
+ """Convert join specs to IR format."""
143
+ specs: list[dict[str, Any]] = []
144
+ for descriptor in self._join_specs:
145
+ specs.append(
146
+ {
147
+ "path": descriptor.path,
148
+ "alias": descriptor.alias,
149
+ "parent": descriptor.parent_alias,
150
+ "table": descriptor.target_model.get_table_name(),
151
+ "source_column": descriptor.source_column,
152
+ "target_column": descriptor.target_column,
153
+ "result_prefix": descriptor.result_prefix,
154
+ "columns": [
155
+ {"field": field, "column": column}
156
+ for field, column in descriptor.columns
157
+ ],
158
+ }
159
+ )
160
+ return specs