sqliter-py 0.9.0__py3-none-any.whl → 0.16.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.
- sqliter/constants.py +4 -3
- sqliter/exceptions.py +43 -0
- sqliter/model/__init__.py +38 -3
- sqliter/model/foreign_key.py +153 -0
- sqliter/model/model.py +42 -3
- sqliter/model/unique.py +20 -11
- sqliter/orm/__init__.py +16 -0
- sqliter/orm/fields.py +412 -0
- sqliter/orm/foreign_key.py +8 -0
- sqliter/orm/model.py +243 -0
- sqliter/orm/query.py +221 -0
- sqliter/orm/registry.py +169 -0
- sqliter/query/query.py +720 -69
- sqliter/sqliter.py +533 -76
- sqliter/tui/__init__.py +62 -0
- sqliter/tui/__main__.py +6 -0
- sqliter/tui/app.py +179 -0
- sqliter/tui/demos/__init__.py +96 -0
- sqliter/tui/demos/base.py +114 -0
- sqliter/tui/demos/caching.py +283 -0
- sqliter/tui/demos/connection.py +150 -0
- sqliter/tui/demos/constraints.py +211 -0
- sqliter/tui/demos/crud.py +154 -0
- sqliter/tui/demos/errors.py +231 -0
- sqliter/tui/demos/field_selection.py +150 -0
- sqliter/tui/demos/filters.py +389 -0
- sqliter/tui/demos/models.py +248 -0
- sqliter/tui/demos/ordering.py +156 -0
- sqliter/tui/demos/orm.py +460 -0
- sqliter/tui/demos/results.py +241 -0
- sqliter/tui/demos/string_filters.py +210 -0
- sqliter/tui/demos/timestamps.py +126 -0
- sqliter/tui/demos/transactions.py +177 -0
- sqliter/tui/runner.py +116 -0
- sqliter/tui/styles/app.tcss +130 -0
- sqliter/tui/widgets/__init__.py +7 -0
- sqliter/tui/widgets/code_display.py +81 -0
- sqliter/tui/widgets/demo_list.py +65 -0
- sqliter/tui/widgets/output_display.py +92 -0
- {sqliter_py-0.9.0.dist-info → sqliter_py-0.16.0.dist-info}/METADATA +27 -11
- sqliter_py-0.16.0.dist-info/RECORD +47 -0
- {sqliter_py-0.9.0.dist-info → sqliter_py-0.16.0.dist-info}/WHEEL +2 -2
- sqliter_py-0.16.0.dist-info/entry_points.txt +3 -0
- sqliter_py-0.9.0.dist-info/RECORD +0 -14
sqliter/orm/query.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Query builders for reverse relationships."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
Any,
|
|
8
|
+
Optional,
|
|
9
|
+
Protocol,
|
|
10
|
+
overload,
|
|
11
|
+
runtime_checkable,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
15
|
+
from sqliter.model.model import BaseDBModel
|
|
16
|
+
from sqliter.sqliter import SqliterDB
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class HasPKAndContext(Protocol):
|
|
21
|
+
"""Protocol for model instances with pk and db_context."""
|
|
22
|
+
|
|
23
|
+
pk: Optional[int]
|
|
24
|
+
db_context: Optional[SqliterDB]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReverseQuery:
|
|
28
|
+
"""Query builder for reverse relationships.
|
|
29
|
+
|
|
30
|
+
Delegates to QueryBuilder for actual SQL execution.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
instance: HasPKAndContext,
|
|
36
|
+
to_model: type[BaseDBModel],
|
|
37
|
+
fk_field: str,
|
|
38
|
+
db_context: Optional[SqliterDB],
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize reverse query.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
instance: The model instance (e.g., Author)
|
|
44
|
+
to_model: The related model class (e.g., Book)
|
|
45
|
+
fk_field: The FK field name (e.g., "author")
|
|
46
|
+
db_context: Database connection for queries
|
|
47
|
+
"""
|
|
48
|
+
self.instance = instance
|
|
49
|
+
self.to_model = to_model
|
|
50
|
+
self.fk_field = fk_field
|
|
51
|
+
self._db = db_context
|
|
52
|
+
self._filters: dict[str, Any] = {}
|
|
53
|
+
self._limit: Optional[int] = None
|
|
54
|
+
self._offset: Optional[int] = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def fk_value(self) -> Optional[int]:
|
|
58
|
+
"""Get the FK ID value from the instance."""
|
|
59
|
+
return self.instance.pk
|
|
60
|
+
|
|
61
|
+
def filter(self, **kwargs: Any) -> ReverseQuery: # noqa: ANN401
|
|
62
|
+
"""Store filters for later execution.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
**kwargs: Filter criteria
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Self for chaining
|
|
69
|
+
"""
|
|
70
|
+
self._filters.update(kwargs)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def limit(self, count: int) -> ReverseQuery:
|
|
74
|
+
"""Set limit on query results.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
count: Maximum number of results
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Self for chaining
|
|
81
|
+
"""
|
|
82
|
+
self._limit = count
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def offset(self, count: int) -> ReverseQuery:
|
|
86
|
+
"""Set offset on query results.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
count: Number of results to skip
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Self for chaining
|
|
93
|
+
"""
|
|
94
|
+
self._offset = count
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def fetch_all(self) -> list[BaseDBModel]:
|
|
98
|
+
"""Execute query using stored db_context.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of related model instances
|
|
102
|
+
"""
|
|
103
|
+
fk_id = self.fk_value
|
|
104
|
+
if fk_id is None or self._db is None:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
# Build query with FK filter and additional filters
|
|
108
|
+
query = self._db.select(self.to_model).filter(
|
|
109
|
+
**{f"{self.fk_field}_id": fk_id}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Apply additional filters
|
|
113
|
+
if self._filters:
|
|
114
|
+
query = query.filter(**self._filters)
|
|
115
|
+
|
|
116
|
+
# Apply limit and offset
|
|
117
|
+
if self._limit is not None:
|
|
118
|
+
query = query.limit(self._limit)
|
|
119
|
+
if self._offset is not None:
|
|
120
|
+
query = query.offset(self._offset)
|
|
121
|
+
|
|
122
|
+
return query.fetch_all()
|
|
123
|
+
|
|
124
|
+
def fetch_one(self) -> Optional[BaseDBModel]:
|
|
125
|
+
"""Execute query and return single result.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Related model instance or None
|
|
129
|
+
"""
|
|
130
|
+
results = self.limit(1).fetch_all()
|
|
131
|
+
return results[0] if results else None
|
|
132
|
+
|
|
133
|
+
def count(self) -> int:
|
|
134
|
+
"""Count related objects.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Number of related objects
|
|
138
|
+
"""
|
|
139
|
+
fk_id = self.fk_value
|
|
140
|
+
if fk_id is None or self._db is None:
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
# Build query with FK filter and additional filters
|
|
144
|
+
query = self._db.select(self.to_model).filter(
|
|
145
|
+
**{f"{self.fk_field}_id": fk_id}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Apply additional filters
|
|
149
|
+
if self._filters:
|
|
150
|
+
query = query.filter(**self._filters)
|
|
151
|
+
|
|
152
|
+
return query.count()
|
|
153
|
+
|
|
154
|
+
def exists(self) -> bool:
|
|
155
|
+
"""Check if any related objects exist.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if at least one related object exists
|
|
159
|
+
"""
|
|
160
|
+
return self.count() > 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ReverseRelationship:
|
|
164
|
+
"""Descriptor that returns ReverseQuery when accessed.
|
|
165
|
+
|
|
166
|
+
Added automatically to models during class creation by ForeignKeyDescriptor.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self, from_model: type[BaseDBModel], fk_field: str, related_name: str
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Initialize reverse relationship descriptor.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
from_model: The model with the FK field (e.g., Book)
|
|
176
|
+
fk_field: The FK field name (e.g., "author")
|
|
177
|
+
related_name: The name of this reverse relationship (e.g., "books")
|
|
178
|
+
"""
|
|
179
|
+
self.from_model = from_model
|
|
180
|
+
self.fk_field = fk_field
|
|
181
|
+
self.related_name = related_name
|
|
182
|
+
|
|
183
|
+
@overload
|
|
184
|
+
def __get__(
|
|
185
|
+
self, instance: None, owner: type[object]
|
|
186
|
+
) -> ReverseRelationship: ...
|
|
187
|
+
|
|
188
|
+
@overload
|
|
189
|
+
def __get__(
|
|
190
|
+
self, instance: HasPKAndContext, owner: type[object]
|
|
191
|
+
) -> ReverseQuery: ...
|
|
192
|
+
|
|
193
|
+
def __get__(
|
|
194
|
+
self, instance: Optional[HasPKAndContext], owner: type[object]
|
|
195
|
+
) -> ReverseRelationship | ReverseQuery:
|
|
196
|
+
"""Return ReverseQuery when accessed on instance.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
instance: Model instance (e.g., Author)
|
|
200
|
+
owner: Model class
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
ReverseQuery for fetching related objects
|
|
204
|
+
"""
|
|
205
|
+
if instance is None:
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
return ReverseQuery(
|
|
209
|
+
instance=instance,
|
|
210
|
+
to_model=self.from_model,
|
|
211
|
+
fk_field=self.fk_field,
|
|
212
|
+
db_context=instance.db_context,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def __set__(self, instance: object, value: object) -> None:
|
|
216
|
+
"""Prevent setting reverse relationships."""
|
|
217
|
+
msg = (
|
|
218
|
+
f"Cannot set reverse relationship '{self.related_name}'. "
|
|
219
|
+
f"Use the ForeignKey field on {self.from_model.__name__} instead."
|
|
220
|
+
)
|
|
221
|
+
raise AttributeError(msg)
|
sqliter/orm/registry.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Model registry for ORM functionality.
|
|
2
|
+
|
|
3
|
+
Central registry for:
|
|
4
|
+
- Model classes by table name
|
|
5
|
+
- Foreign key relationships
|
|
6
|
+
- Pending reverse relationships
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, ClassVar, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelRegistry:
|
|
15
|
+
"""Registry for ORM models and FK relationships.
|
|
16
|
+
|
|
17
|
+
Uses automatic setup via descriptor __set_name__ hook - no manual setup
|
|
18
|
+
required.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_models: ClassVar[dict[str, type]] = {}
|
|
22
|
+
_foreign_keys: ClassVar[dict[str, list[dict[str, Any]]]] = {}
|
|
23
|
+
_pending_reverses: ClassVar[dict[str, list[dict[str, Any]]]] = {}
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def register_model(cls, model_class: type[Any]) -> None:
|
|
27
|
+
"""Register a model class in the global registry.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
model_class: The model class to register
|
|
31
|
+
"""
|
|
32
|
+
table_name = model_class.get_table_name()
|
|
33
|
+
cls._models[table_name] = model_class
|
|
34
|
+
|
|
35
|
+
# Process any pending reverse relationships for this model
|
|
36
|
+
if table_name in cls._pending_reverses:
|
|
37
|
+
for pending in cls._pending_reverses[table_name]:
|
|
38
|
+
cls._add_reverse_relationship_now(**pending)
|
|
39
|
+
del cls._pending_reverses[table_name]
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def register_foreign_key(
|
|
43
|
+
cls,
|
|
44
|
+
from_model: type[Any],
|
|
45
|
+
to_model: type[Any],
|
|
46
|
+
fk_field: str,
|
|
47
|
+
on_delete: str,
|
|
48
|
+
related_name: Optional[str] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Register a FK relationship.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
from_model: The model with the FK field
|
|
54
|
+
to_model: The model being referenced
|
|
55
|
+
fk_field: Name of the FK field
|
|
56
|
+
on_delete: Action when related object is deleted
|
|
57
|
+
related_name: Name for reverse relationship
|
|
58
|
+
"""
|
|
59
|
+
from_table = from_model.get_table_name()
|
|
60
|
+
|
|
61
|
+
if from_table not in cls._foreign_keys:
|
|
62
|
+
cls._foreign_keys[from_table] = []
|
|
63
|
+
|
|
64
|
+
cls._foreign_keys[from_table].append(
|
|
65
|
+
{
|
|
66
|
+
"to_model": to_model,
|
|
67
|
+
"fk_field": fk_field,
|
|
68
|
+
"on_delete": on_delete,
|
|
69
|
+
"related_name": related_name,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def get_model(cls, table_name: str) -> Optional[type[Any]]:
|
|
75
|
+
"""Get model by table name.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
table_name: The table name to look up
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The model class or None if not found
|
|
82
|
+
"""
|
|
83
|
+
return cls._models.get(table_name)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def get_foreign_keys(cls, table_name: str) -> list[dict[str, Any]]:
|
|
87
|
+
"""Get FK relationships for a model.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
table_name: The table name to look up
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of FK relationship dictionaries
|
|
94
|
+
"""
|
|
95
|
+
return cls._foreign_keys.get(table_name, [])
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def add_reverse_relationship(
|
|
99
|
+
cls,
|
|
100
|
+
from_model: type[Any],
|
|
101
|
+
to_model: type[Any],
|
|
102
|
+
fk_field: str,
|
|
103
|
+
related_name: str,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Automatically add reverse relationship descriptor during class.
|
|
106
|
+
|
|
107
|
+
Called by ForeignKeyDescriptor.__set_name__ during class creation.
|
|
108
|
+
If to_model doesn't exist yet, stores as pending and adds when
|
|
109
|
+
to_model is registered.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
from_model: The model with the FK field (e.g., Book)
|
|
113
|
+
to_model: The model being referenced (e.g., Author)
|
|
114
|
+
fk_field: Name of the FK field (e.g., "author")
|
|
115
|
+
related_name: Name for reverse relationship (e.g., "books")
|
|
116
|
+
"""
|
|
117
|
+
to_table = to_model.get_table_name()
|
|
118
|
+
|
|
119
|
+
# Check if to_model has been registered yet
|
|
120
|
+
if to_table in cls._models:
|
|
121
|
+
# Model exists, add reverse relationship now
|
|
122
|
+
cls._add_reverse_relationship_now(
|
|
123
|
+
from_model, to_model, fk_field, related_name
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
# Model doesn't exist yet, store as pending
|
|
127
|
+
if to_table not in cls._pending_reverses:
|
|
128
|
+
cls._pending_reverses[to_table] = []
|
|
129
|
+
cls._pending_reverses[to_table].append(
|
|
130
|
+
{
|
|
131
|
+
"from_model": from_model,
|
|
132
|
+
"to_model": to_model,
|
|
133
|
+
"fk_field": fk_field,
|
|
134
|
+
"related_name": related_name,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def _add_reverse_relationship_now(
|
|
140
|
+
cls,
|
|
141
|
+
from_model: type[Any],
|
|
142
|
+
to_model: type[Any],
|
|
143
|
+
fk_field: str,
|
|
144
|
+
related_name: str,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Add reverse relationship descriptor to model.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
from_model: The model with the FK field (e.g., Book)
|
|
150
|
+
to_model: The model being referenced (e.g., Author)
|
|
151
|
+
fk_field: Name of the FK field (e.g., "author")
|
|
152
|
+
related_name: Name for reverse relationship (e.g., "books")
|
|
153
|
+
"""
|
|
154
|
+
from sqliter.orm.query import ReverseRelationship # noqa: PLC0415
|
|
155
|
+
|
|
156
|
+
# Guard against overwriting existing attributes
|
|
157
|
+
if hasattr(to_model, related_name):
|
|
158
|
+
msg = (
|
|
159
|
+
f"Reverse relationship '{related_name}' already exists on "
|
|
160
|
+
f"{to_model.__name__}"
|
|
161
|
+
)
|
|
162
|
+
raise AttributeError(msg)
|
|
163
|
+
|
|
164
|
+
# Add reverse relationship descriptor to to_model
|
|
165
|
+
setattr(
|
|
166
|
+
to_model,
|
|
167
|
+
related_name,
|
|
168
|
+
ReverseRelationship(from_model, fk_field, related_name),
|
|
169
|
+
)
|