sqliter-py 0.12.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 +13 -0
- sqliter/model/model.py +42 -3
- 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 +573 -51
- sqliter/sqliter.py +141 -47
- 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.12.0.dist-info → sqliter_py-0.16.0.dist-info}/METADATA +23 -7
- sqliter_py-0.16.0.dist-info/RECORD +47 -0
- {sqliter_py-0.12.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.12.0.dist-info/RECORD +0 -15
sqliter/tui/demos/orm.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""ORM Features demos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from sqliter import SqliterDB
|
|
9
|
+
from sqliter.orm import BaseDBModel, ForeignKey
|
|
10
|
+
from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_lazy_loading() -> str:
|
|
14
|
+
"""Load related objects on-demand using foreign keys.
|
|
15
|
+
|
|
16
|
+
Accessing a ForeignKey field triggers a database query to fetch the
|
|
17
|
+
related object only when you need it.
|
|
18
|
+
"""
|
|
19
|
+
output = io.StringIO()
|
|
20
|
+
|
|
21
|
+
class Author(BaseDBModel):
|
|
22
|
+
name: str
|
|
23
|
+
|
|
24
|
+
class Book(BaseDBModel):
|
|
25
|
+
title: str
|
|
26
|
+
author: ForeignKey[Author] = ForeignKey(Author)
|
|
27
|
+
|
|
28
|
+
db = SqliterDB(memory=True)
|
|
29
|
+
db.create_table(Author)
|
|
30
|
+
db.create_table(Book)
|
|
31
|
+
|
|
32
|
+
author = db.insert(Author(name="J.K. Rowling"))
|
|
33
|
+
book1 = db.insert(Book(title="Harry Potter 1", author=author))
|
|
34
|
+
book2 = db.insert(Book(title="Harry Potter 2", author=author))
|
|
35
|
+
|
|
36
|
+
output.write(f"Author: {author.name}\n")
|
|
37
|
+
output.write(f"Author ID: {author.pk}\n")
|
|
38
|
+
|
|
39
|
+
# Access related author through foreign key - triggers lazy load
|
|
40
|
+
output.write("\nAccessing book.author triggers lazy load:\n")
|
|
41
|
+
output.write(f" '{book1.title}' was written by {book1.author.name}\n")
|
|
42
|
+
|
|
43
|
+
output.write(f"\n'{book2.title}' was written by {book2.author.name}\n")
|
|
44
|
+
output.write("Related objects loaded on-demand from database\n")
|
|
45
|
+
|
|
46
|
+
db.close()
|
|
47
|
+
return output.getvalue()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_orm_style_access() -> str:
|
|
51
|
+
"""Insert records with foreign key relationships.
|
|
52
|
+
|
|
53
|
+
BaseDBModel provides attribute-style access to fields, with automatic
|
|
54
|
+
primary key generation via the pk field. Foreign keys store related
|
|
55
|
+
object primary keys.
|
|
56
|
+
"""
|
|
57
|
+
output = io.StringIO()
|
|
58
|
+
|
|
59
|
+
class Author(BaseDBModel):
|
|
60
|
+
name: str
|
|
61
|
+
|
|
62
|
+
class Book(BaseDBModel):
|
|
63
|
+
title: str
|
|
64
|
+
author: ForeignKey[Author] = ForeignKey(Author)
|
|
65
|
+
|
|
66
|
+
db = SqliterDB(memory=True)
|
|
67
|
+
db.create_table(Author)
|
|
68
|
+
db.create_table(Book)
|
|
69
|
+
|
|
70
|
+
author = db.insert(Author(name="Jane Austen"))
|
|
71
|
+
book = db.insert(Book(title="Pride and Prejudice", author=author))
|
|
72
|
+
|
|
73
|
+
output.write("Created book:\n")
|
|
74
|
+
output.write(f" title: {book.title}\n")
|
|
75
|
+
output.write(f" author: {book.author.name}\n")
|
|
76
|
+
output.write(
|
|
77
|
+
"\nForeign key stores the primary key internally,\n"
|
|
78
|
+
"but access returns the object\n"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
db.close()
|
|
82
|
+
return output.getvalue()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _run_nullable_foreign_key() -> str:
|
|
86
|
+
"""Declare nullable FKs using Optional[T] in the type annotation.
|
|
87
|
+
|
|
88
|
+
SQLiter auto-detects nullability from the annotation so you don't
|
|
89
|
+
need to pass null=True explicitly.
|
|
90
|
+
|
|
91
|
+
Note: this demo already uses ForeignKey[Optional[Author]], but
|
|
92
|
+
annotation-based nullability is most reliable when models are defined at
|
|
93
|
+
module level (especially if you use type aliases). We include null=True
|
|
94
|
+
here for compatibility.
|
|
95
|
+
"""
|
|
96
|
+
output = io.StringIO()
|
|
97
|
+
|
|
98
|
+
class Author(BaseDBModel):
|
|
99
|
+
name: str
|
|
100
|
+
|
|
101
|
+
class Book(BaseDBModel):
|
|
102
|
+
title: str
|
|
103
|
+
author: ForeignKey[Optional[Author]] = ForeignKey(
|
|
104
|
+
Author, on_delete="SET NULL", null=True
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
db = SqliterDB(memory=True)
|
|
108
|
+
db.create_table(Author)
|
|
109
|
+
db.create_table(Book)
|
|
110
|
+
|
|
111
|
+
author = db.insert(Author(name="Jane Austen"))
|
|
112
|
+
book_with = db.insert(Book(title="Pride and Prejudice", author=author))
|
|
113
|
+
book_without = db.insert(Book(title="Anonymous Work", author=None))
|
|
114
|
+
|
|
115
|
+
book1 = db.get(Book, book_with.pk)
|
|
116
|
+
book2 = db.get(Book, book_without.pk)
|
|
117
|
+
|
|
118
|
+
if book1 is not None:
|
|
119
|
+
author_name = book1.author.name if book1.author else "None"
|
|
120
|
+
output.write(f"'{book1.title}' author: {author_name}\n")
|
|
121
|
+
if book2 is not None:
|
|
122
|
+
output.write(f"'{book2.title}' author: {book2.author}\n")
|
|
123
|
+
|
|
124
|
+
output.write("\nOptional[Author] auto-sets null=True on the FK column\n")
|
|
125
|
+
|
|
126
|
+
db.close()
|
|
127
|
+
return output.getvalue()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _run_relationship_navigation() -> str:
|
|
131
|
+
"""Navigate from one object to another using foreign keys.
|
|
132
|
+
|
|
133
|
+
ForeignKey fields let you traverse relationships by accessing
|
|
134
|
+
related objects as attributes.
|
|
135
|
+
"""
|
|
136
|
+
output = io.StringIO()
|
|
137
|
+
|
|
138
|
+
class Team(BaseDBModel):
|
|
139
|
+
name: str
|
|
140
|
+
|
|
141
|
+
class Player(BaseDBModel):
|
|
142
|
+
name: str
|
|
143
|
+
team: ForeignKey[Team] = ForeignKey(Team)
|
|
144
|
+
|
|
145
|
+
db = SqliterDB(memory=True)
|
|
146
|
+
db.create_table(Team)
|
|
147
|
+
db.create_table(Player)
|
|
148
|
+
|
|
149
|
+
team = db.insert(Team(name="Lakers"))
|
|
150
|
+
player1 = db.insert(Player(name="LeBron", team=team))
|
|
151
|
+
player2 = db.insert(Player(name="Davis", team=team))
|
|
152
|
+
|
|
153
|
+
output.write(f"Team: {team.name}\n")
|
|
154
|
+
|
|
155
|
+
# Navigate from player to team via FK
|
|
156
|
+
output.write(f"\n{player1.name} plays for: {player1.team.name}\n")
|
|
157
|
+
output.write(f"{player2.name} plays for: {player2.team.name}\n")
|
|
158
|
+
output.write("Foreign keys enable relationship navigation\n")
|
|
159
|
+
|
|
160
|
+
db.close()
|
|
161
|
+
return output.getvalue()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _run_reverse_relationships() -> str:
|
|
165
|
+
"""Access related objects in reverse using related_name.
|
|
166
|
+
|
|
167
|
+
When you define a ForeignKey, SQLiter automatically creates a reverse
|
|
168
|
+
relationship to access all objects that reference a given object.
|
|
169
|
+
"""
|
|
170
|
+
output = io.StringIO()
|
|
171
|
+
|
|
172
|
+
class Author(BaseDBModel):
|
|
173
|
+
name: str
|
|
174
|
+
|
|
175
|
+
class Book(BaseDBModel):
|
|
176
|
+
title: str
|
|
177
|
+
author: ForeignKey[Author] = ForeignKey(Author, related_name="books")
|
|
178
|
+
|
|
179
|
+
db = SqliterDB(memory=True)
|
|
180
|
+
db.create_table(Author)
|
|
181
|
+
db.create_table(Book)
|
|
182
|
+
|
|
183
|
+
author = db.insert(Author(name="Jane Austen"))
|
|
184
|
+
db.insert(Book(title="Pride and Prejudice", author=author))
|
|
185
|
+
db.insert(Book(title="Emma", author=author))
|
|
186
|
+
db.insert(Book(title="Sense and Sensibility", author=author))
|
|
187
|
+
|
|
188
|
+
output.write(f"Author: {author.name}\n")
|
|
189
|
+
|
|
190
|
+
# Access reverse relationship - get all books by this author
|
|
191
|
+
# Note: 'books' attribute added dynamically by ForeignKey descriptor
|
|
192
|
+
output.write("\nAccessing author.books (reverse relationship):\n")
|
|
193
|
+
reverse_attr = "books" # Dynamic attribute added by FK descriptor
|
|
194
|
+
books_query = getattr(author, reverse_attr)
|
|
195
|
+
books = books_query.fetch_all()
|
|
196
|
+
for book in books:
|
|
197
|
+
output.write(f" - {book.title}\n")
|
|
198
|
+
|
|
199
|
+
output.write(f"\nTotal books: {len(books)}\n")
|
|
200
|
+
output.write("Reverse relationships auto-generated from FKs\n")
|
|
201
|
+
|
|
202
|
+
db.close()
|
|
203
|
+
return output.getvalue()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _run_select_related_basic() -> str:
|
|
207
|
+
"""Demonstrate eager loading with select_related().
|
|
208
|
+
|
|
209
|
+
Shows how select_related() fetches related objects in a single JOIN query
|
|
210
|
+
instead of lazy loading (which causes N+1 queries).
|
|
211
|
+
"""
|
|
212
|
+
output = io.StringIO()
|
|
213
|
+
|
|
214
|
+
class Author(BaseDBModel):
|
|
215
|
+
name: str
|
|
216
|
+
|
|
217
|
+
class Book(BaseDBModel):
|
|
218
|
+
title: str
|
|
219
|
+
author: ForeignKey[Author] = ForeignKey(Author)
|
|
220
|
+
|
|
221
|
+
db = SqliterDB(memory=True)
|
|
222
|
+
db.create_table(Author)
|
|
223
|
+
db.create_table(Book)
|
|
224
|
+
|
|
225
|
+
# Insert test data
|
|
226
|
+
author1 = db.insert(Author(name="Jane Austen"))
|
|
227
|
+
author2 = db.insert(Author(name="Charles Dickens"))
|
|
228
|
+
|
|
229
|
+
db.insert(Book(title="Pride and Prejudice", author=author1))
|
|
230
|
+
db.insert(Book(title="Emma", author=author1))
|
|
231
|
+
db.insert(Book(title="Oliver Twist", author=author2))
|
|
232
|
+
|
|
233
|
+
# Eager load - single JOIN query
|
|
234
|
+
output.write("Fetching books with eager loading:\n")
|
|
235
|
+
books = db.select(Book).select_related("author").fetch_all()
|
|
236
|
+
|
|
237
|
+
for book in books:
|
|
238
|
+
output.write(f" '{book.title}' by {book.author.name}\n")
|
|
239
|
+
|
|
240
|
+
output.write("\nAll authors loaded in single query (no N+1 problem)\n")
|
|
241
|
+
|
|
242
|
+
db.close()
|
|
243
|
+
return output.getvalue()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _run_select_related_nested() -> str:
|
|
247
|
+
"""Demonstrate nested relationship eager loading.
|
|
248
|
+
|
|
249
|
+
Shows how to load nested relationships using double underscore syntax:
|
|
250
|
+
select_related("book__author") loads both Book and Author in one query.
|
|
251
|
+
"""
|
|
252
|
+
output = io.StringIO()
|
|
253
|
+
|
|
254
|
+
class Author(BaseDBModel):
|
|
255
|
+
name: str
|
|
256
|
+
|
|
257
|
+
class Book(BaseDBModel):
|
|
258
|
+
title: str
|
|
259
|
+
author: ForeignKey[Author] = ForeignKey(Author)
|
|
260
|
+
|
|
261
|
+
class Comment(BaseDBModel):
|
|
262
|
+
text: str
|
|
263
|
+
book: ForeignKey[Book] = ForeignKey(Book)
|
|
264
|
+
|
|
265
|
+
db = SqliterDB(memory=True)
|
|
266
|
+
db.create_table(Author)
|
|
267
|
+
db.create_table(Book)
|
|
268
|
+
db.create_table(Comment)
|
|
269
|
+
|
|
270
|
+
# Insert nested test data
|
|
271
|
+
author = db.insert(Author(name="Jane Austen"))
|
|
272
|
+
book = db.insert(Book(title="Pride and Prejudice", author=author))
|
|
273
|
+
db.insert(Comment(text="Amazing book!", book=book))
|
|
274
|
+
|
|
275
|
+
# Load nested relationship - single query joins Comment -> Book -> Author
|
|
276
|
+
output.write("Loading nested relationships:\n")
|
|
277
|
+
comment = db.select(Comment).select_related("book__author").fetch_one()
|
|
278
|
+
|
|
279
|
+
if comment is not None:
|
|
280
|
+
output.write(f"Comment: {comment.text}\n")
|
|
281
|
+
output.write(f"Book: {comment.book.title}\n")
|
|
282
|
+
# Access author through book's foreign key relationship
|
|
283
|
+
# Both book and author were loaded in a single JOIN query
|
|
284
|
+
output.write(f"Author: {comment.book.author.name}\n")
|
|
285
|
+
|
|
286
|
+
output.write("\nNested relationships loaded in single query\n")
|
|
287
|
+
|
|
288
|
+
db.close()
|
|
289
|
+
return output.getvalue()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _run_relationship_filter_traversal() -> str:
|
|
293
|
+
"""Demonstrate relationship filter traversal.
|
|
294
|
+
|
|
295
|
+
Shows how to filter by fields on related models using double underscore
|
|
296
|
+
syntax: filter(author__name="Jane Austen")
|
|
297
|
+
"""
|
|
298
|
+
output = io.StringIO()
|
|
299
|
+
|
|
300
|
+
class Author(BaseDBModel):
|
|
301
|
+
name: str
|
|
302
|
+
|
|
303
|
+
class Book(BaseDBModel):
|
|
304
|
+
title: str
|
|
305
|
+
author: ForeignKey[Author] = ForeignKey(Author)
|
|
306
|
+
|
|
307
|
+
db = SqliterDB(memory=True)
|
|
308
|
+
db.create_table(Author)
|
|
309
|
+
db.create_table(Book)
|
|
310
|
+
|
|
311
|
+
# Insert test data
|
|
312
|
+
author1 = db.insert(Author(name="Jane Austen"))
|
|
313
|
+
author2 = db.insert(Author(name="Charles Dickens"))
|
|
314
|
+
|
|
315
|
+
db.insert(Book(title="Pride and Prejudice", author=author1))
|
|
316
|
+
db.insert(Book(title="Emma", author=author1))
|
|
317
|
+
db.insert(Book(title="Oliver Twist", author=author2))
|
|
318
|
+
db.insert(Book(title="Great Expectations", author=author2))
|
|
319
|
+
|
|
320
|
+
# Filter by related field
|
|
321
|
+
output.write("Filtering by author name:\n")
|
|
322
|
+
books = db.select(Book).filter(author__name="Jane Austen").fetch_all()
|
|
323
|
+
|
|
324
|
+
for book in books:
|
|
325
|
+
output.write(f" {book.title}\n")
|
|
326
|
+
|
|
327
|
+
output.write(f"\nFound {len(books)} book(s) by Jane Austen\n")
|
|
328
|
+
output.write("(Automatic JOIN added behind the scenes)\n")
|
|
329
|
+
|
|
330
|
+
db.close()
|
|
331
|
+
return output.getvalue()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _run_select_related_combined() -> str:
|
|
335
|
+
"""Demonstrate combining select_related() with relationship filters.
|
|
336
|
+
|
|
337
|
+
Shows how to use select_related() with filter() for optimal performance:
|
|
338
|
+
load related objects AND filter by them in a single query.
|
|
339
|
+
"""
|
|
340
|
+
output = io.StringIO()
|
|
341
|
+
|
|
342
|
+
class Author(BaseDBModel):
|
|
343
|
+
name: str
|
|
344
|
+
|
|
345
|
+
class Book(BaseDBModel):
|
|
346
|
+
title: str
|
|
347
|
+
year: int
|
|
348
|
+
author: ForeignKey[Author] = ForeignKey(Author)
|
|
349
|
+
|
|
350
|
+
db = SqliterDB(memory=True)
|
|
351
|
+
db.create_table(Author)
|
|
352
|
+
db.create_table(Book)
|
|
353
|
+
|
|
354
|
+
# Insert test data
|
|
355
|
+
author1 = db.insert(Author(name="Jane Austen"))
|
|
356
|
+
author2 = db.insert(Author(name="Charles Dickens"))
|
|
357
|
+
|
|
358
|
+
db.insert(Book(title="Pride and Prejudice", year=1813, author=author1))
|
|
359
|
+
db.insert(Book(title="Emma", year=1815, author=author1))
|
|
360
|
+
db.insert(Book(title="Oliver Twist", year=1838, author=author2))
|
|
361
|
+
|
|
362
|
+
# Combine filter + eager load
|
|
363
|
+
output.write("Filter and eager load in single query:\n")
|
|
364
|
+
books = (
|
|
365
|
+
db.select(Book)
|
|
366
|
+
.select_related("author")
|
|
367
|
+
.filter(author__name__startswith="Jane")
|
|
368
|
+
.fetch_all()
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
for book in books:
|
|
372
|
+
output.write(f" {book.title} ({book.year}) by {book.author.name}\n")
|
|
373
|
+
|
|
374
|
+
output.write(f"\n{len(books)} result(s) with authors preloaded\n")
|
|
375
|
+
|
|
376
|
+
db.close()
|
|
377
|
+
return output.getvalue()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_category() -> DemoCategory:
|
|
381
|
+
"""Get the ORM Features demo category."""
|
|
382
|
+
return DemoCategory(
|
|
383
|
+
id="orm",
|
|
384
|
+
title="ORM Features",
|
|
385
|
+
icon="",
|
|
386
|
+
demos=[
|
|
387
|
+
Demo(
|
|
388
|
+
id="orm_lazy",
|
|
389
|
+
title="Lazy Loading",
|
|
390
|
+
description="Load related data on demand",
|
|
391
|
+
category="orm",
|
|
392
|
+
code=extract_demo_code(_run_lazy_loading),
|
|
393
|
+
execute=_run_lazy_loading,
|
|
394
|
+
),
|
|
395
|
+
Demo(
|
|
396
|
+
id="orm_fk_insert",
|
|
397
|
+
title="Inserting with Foreign Keys",
|
|
398
|
+
description="Create records linked to other records",
|
|
399
|
+
category="orm",
|
|
400
|
+
code=extract_demo_code(_run_orm_style_access),
|
|
401
|
+
execute=_run_orm_style_access,
|
|
402
|
+
),
|
|
403
|
+
Demo(
|
|
404
|
+
id="orm_nullable_fk",
|
|
405
|
+
title="Nullable Foreign Keys",
|
|
406
|
+
description="Auto-detect nullable FKs from annotations",
|
|
407
|
+
category="orm",
|
|
408
|
+
code=extract_demo_code(_run_nullable_foreign_key),
|
|
409
|
+
execute=_run_nullable_foreign_key,
|
|
410
|
+
),
|
|
411
|
+
Demo(
|
|
412
|
+
id="orm_relationships",
|
|
413
|
+
title="Relationship Navigation",
|
|
414
|
+
description="Navigate using foreign keys",
|
|
415
|
+
category="orm",
|
|
416
|
+
code=extract_demo_code(_run_relationship_navigation),
|
|
417
|
+
execute=_run_relationship_navigation,
|
|
418
|
+
),
|
|
419
|
+
Demo(
|
|
420
|
+
id="orm_reverse",
|
|
421
|
+
title="Reverse Relationships",
|
|
422
|
+
description="Access related objects via related_name",
|
|
423
|
+
category="orm",
|
|
424
|
+
code=extract_demo_code(_run_reverse_relationships),
|
|
425
|
+
execute=_run_reverse_relationships,
|
|
426
|
+
),
|
|
427
|
+
Demo(
|
|
428
|
+
id="orm_select_related",
|
|
429
|
+
title="Eager Loading with select_related()",
|
|
430
|
+
description="Fetch related objects in a single JOIN query",
|
|
431
|
+
category="orm",
|
|
432
|
+
code=extract_demo_code(_run_select_related_basic),
|
|
433
|
+
execute=_run_select_related_basic,
|
|
434
|
+
),
|
|
435
|
+
Demo(
|
|
436
|
+
id="orm_select_related_nested",
|
|
437
|
+
title="Nested Relationship Loading",
|
|
438
|
+
description="Load nested relationships with double underscore",
|
|
439
|
+
category="orm",
|
|
440
|
+
code=extract_demo_code(_run_select_related_nested),
|
|
441
|
+
execute=_run_select_related_nested,
|
|
442
|
+
),
|
|
443
|
+
Demo(
|
|
444
|
+
id="orm_filter_traversal",
|
|
445
|
+
title="Relationship Filter Traversal",
|
|
446
|
+
description="Filter by related object fields",
|
|
447
|
+
category="orm",
|
|
448
|
+
code=extract_demo_code(_run_relationship_filter_traversal),
|
|
449
|
+
execute=_run_relationship_filter_traversal,
|
|
450
|
+
),
|
|
451
|
+
Demo(
|
|
452
|
+
id="orm_select_related_combined",
|
|
453
|
+
title="Combining select_related with Filters",
|
|
454
|
+
description="Eager load and filter by relationships",
|
|
455
|
+
category="orm",
|
|
456
|
+
code=extract_demo_code(_run_select_related_combined),
|
|
457
|
+
execute=_run_select_related_combined,
|
|
458
|
+
),
|
|
459
|
+
],
|
|
460
|
+
)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Query Results & Aggregation demos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
from sqliter import SqliterDB
|
|
8
|
+
from sqliter.model import BaseDBModel
|
|
9
|
+
from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run_fetch_all() -> str:
|
|
13
|
+
"""Fetch all records matching a query.
|
|
14
|
+
|
|
15
|
+
Use fetch_all() to get a list of all matching records.
|
|
16
|
+
"""
|
|
17
|
+
output = io.StringIO()
|
|
18
|
+
|
|
19
|
+
class User(BaseDBModel):
|
|
20
|
+
name: str
|
|
21
|
+
age: int
|
|
22
|
+
|
|
23
|
+
db = SqliterDB(memory=True)
|
|
24
|
+
db.create_table(User)
|
|
25
|
+
|
|
26
|
+
for i in range(5):
|
|
27
|
+
db.insert(User(name=f"User {i}", age=20 + i))
|
|
28
|
+
|
|
29
|
+
results = db.select(User).fetch_all()
|
|
30
|
+
output.write(f"Total users: {len(results)}\n")
|
|
31
|
+
for user in results:
|
|
32
|
+
output.write(f" - {user.name}, age {user.age}\n")
|
|
33
|
+
|
|
34
|
+
db.close()
|
|
35
|
+
return output.getvalue()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_fetch_one() -> str:
|
|
39
|
+
"""Fetch a single record or return None if not found.
|
|
40
|
+
|
|
41
|
+
Use fetch_one() to get one matching record, returning None if
|
|
42
|
+
no records match the query.
|
|
43
|
+
"""
|
|
44
|
+
output = io.StringIO()
|
|
45
|
+
|
|
46
|
+
class Task(BaseDBModel):
|
|
47
|
+
title: str
|
|
48
|
+
priority: int
|
|
49
|
+
|
|
50
|
+
db = SqliterDB(memory=True)
|
|
51
|
+
db.create_table(Task)
|
|
52
|
+
|
|
53
|
+
db.insert(Task(title="High priority", priority=1))
|
|
54
|
+
db.insert(Task(title="Medium priority", priority=2))
|
|
55
|
+
db.insert(Task(title="Low priority", priority=3))
|
|
56
|
+
|
|
57
|
+
task = db.select(Task).filter(priority__eq=1).fetch_one()
|
|
58
|
+
if task is not None:
|
|
59
|
+
output.write(f"Single result: {task.title}\n")
|
|
60
|
+
|
|
61
|
+
# Also test no results case
|
|
62
|
+
no_task = db.select(Task).filter(priority__eq=999).fetch_one()
|
|
63
|
+
if no_task is None:
|
|
64
|
+
output.write("No task found with priority 999\n")
|
|
65
|
+
|
|
66
|
+
db.close()
|
|
67
|
+
return output.getvalue()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run_fetch_first_last() -> str:
|
|
71
|
+
"""Fetch the first or last record from results.
|
|
72
|
+
|
|
73
|
+
Use fetch_first() or fetch_last() to get a single record
|
|
74
|
+
from the beginning or end of the result set.
|
|
75
|
+
"""
|
|
76
|
+
output = io.StringIO()
|
|
77
|
+
|
|
78
|
+
class Item(BaseDBModel):
|
|
79
|
+
name: str
|
|
80
|
+
|
|
81
|
+
db = SqliterDB(memory=True)
|
|
82
|
+
db.create_table(Item)
|
|
83
|
+
|
|
84
|
+
for name in ["Alpha", "Beta", "Gamma", "Delta"]:
|
|
85
|
+
db.insert(Item(name=name))
|
|
86
|
+
|
|
87
|
+
first = db.select(Item).fetch_first()
|
|
88
|
+
if first is not None:
|
|
89
|
+
output.write(f"First: {first.name}\n")
|
|
90
|
+
|
|
91
|
+
last = db.select(Item).fetch_last()
|
|
92
|
+
if last is not None:
|
|
93
|
+
output.write(f"Last: {last.name}\n")
|
|
94
|
+
|
|
95
|
+
db.close()
|
|
96
|
+
return output.getvalue()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_count() -> str:
|
|
100
|
+
"""Count the number of matching records.
|
|
101
|
+
|
|
102
|
+
Use count() to efficiently count records without fetching them.
|
|
103
|
+
"""
|
|
104
|
+
output = io.StringIO()
|
|
105
|
+
|
|
106
|
+
class Product(BaseDBModel):
|
|
107
|
+
name: str
|
|
108
|
+
category: str
|
|
109
|
+
|
|
110
|
+
db = SqliterDB(memory=True)
|
|
111
|
+
db.create_table(Product)
|
|
112
|
+
|
|
113
|
+
db.insert(Product(name="Laptop", category="electronics"))
|
|
114
|
+
db.insert(Product(name="Phone", category="electronics"))
|
|
115
|
+
db.insert(Product(name="Desk", category="furniture"))
|
|
116
|
+
|
|
117
|
+
total = db.select(Product).count()
|
|
118
|
+
output.write(f"Total products: {total}\n")
|
|
119
|
+
|
|
120
|
+
electronics = db.select(Product).filter(category__eq="electronics").count()
|
|
121
|
+
output.write(f"Electronics: {electronics}\n")
|
|
122
|
+
|
|
123
|
+
db.close()
|
|
124
|
+
return output.getvalue()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run_exists() -> str:
|
|
128
|
+
"""Check if any records match the query.
|
|
129
|
+
|
|
130
|
+
Use exists() to efficiently check for matching records without
|
|
131
|
+
fetching them - returns True/False.
|
|
132
|
+
"""
|
|
133
|
+
output = io.StringIO()
|
|
134
|
+
|
|
135
|
+
class User(BaseDBModel):
|
|
136
|
+
username: str
|
|
137
|
+
|
|
138
|
+
db = SqliterDB(memory=True)
|
|
139
|
+
db.create_table(User)
|
|
140
|
+
|
|
141
|
+
db.insert(User(username="alice"))
|
|
142
|
+
db.insert(User(username="bob"))
|
|
143
|
+
|
|
144
|
+
exists = db.select(User).filter(username__eq="alice").exists()
|
|
145
|
+
output.write(f"User 'alice' exists: {exists}\n")
|
|
146
|
+
|
|
147
|
+
not_exists = db.select(User).filter(username__eq="charlie").exists()
|
|
148
|
+
output.write(f"User 'charlie' exists: {not_exists}\n")
|
|
149
|
+
|
|
150
|
+
db.close()
|
|
151
|
+
return output.getvalue()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _run_aggregates() -> str:
|
|
155
|
+
"""Calculate aggregates using Python after fetching data.
|
|
156
|
+
|
|
157
|
+
SQLiter doesn't support SQL-level aggregates (GROUP BY, HAVING).
|
|
158
|
+
Use Python's sum(), len(), etc. after fetching results.
|
|
159
|
+
"""
|
|
160
|
+
output = io.StringIO()
|
|
161
|
+
|
|
162
|
+
class Sale(BaseDBModel):
|
|
163
|
+
amount: float
|
|
164
|
+
|
|
165
|
+
db = SqliterDB(memory=True)
|
|
166
|
+
db.create_table(Sale)
|
|
167
|
+
|
|
168
|
+
for amount in [10.0, 20.0, 30.0, 40.0, 50.0]:
|
|
169
|
+
db.insert(Sale(amount=amount))
|
|
170
|
+
|
|
171
|
+
# Note: SQLiter doesn't support SQL-level aggregates (GROUP BY, HAVING)
|
|
172
|
+
# Use Python for calculations after fetching data
|
|
173
|
+
results = db.select(Sale).fetch_all()
|
|
174
|
+
total = sum(s.amount for s in results)
|
|
175
|
+
average = total / len(results)
|
|
176
|
+
output.write(f"Total sales: ${total:.2f}\n")
|
|
177
|
+
output.write(f"Average sale: ${average:.2f}\n")
|
|
178
|
+
output.write(f"Count: {len(results)}\n")
|
|
179
|
+
output.write("\n(Aggregates calculated in Python, not SQL)\n")
|
|
180
|
+
|
|
181
|
+
db.close()
|
|
182
|
+
return output.getvalue()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_category() -> DemoCategory:
|
|
186
|
+
"""Get the Query Results demo category."""
|
|
187
|
+
return DemoCategory(
|
|
188
|
+
id="results",
|
|
189
|
+
title="Query Results",
|
|
190
|
+
icon="",
|
|
191
|
+
demos=[
|
|
192
|
+
Demo(
|
|
193
|
+
id="result_fetch_all",
|
|
194
|
+
title="Fetch All",
|
|
195
|
+
description="Get all matching records",
|
|
196
|
+
category="results",
|
|
197
|
+
code=extract_demo_code(_run_fetch_all),
|
|
198
|
+
execute=_run_fetch_all,
|
|
199
|
+
),
|
|
200
|
+
Demo(
|
|
201
|
+
id="result_fetch_one",
|
|
202
|
+
title="Fetch One",
|
|
203
|
+
description="Get single record or None",
|
|
204
|
+
category="results",
|
|
205
|
+
code=extract_demo_code(_run_fetch_one),
|
|
206
|
+
execute=_run_fetch_one,
|
|
207
|
+
),
|
|
208
|
+
Demo(
|
|
209
|
+
id="result_first_last",
|
|
210
|
+
title="Fetch First/Last",
|
|
211
|
+
description="Get first or last record",
|
|
212
|
+
category="results",
|
|
213
|
+
code=extract_demo_code(_run_fetch_first_last),
|
|
214
|
+
execute=_run_fetch_first_last,
|
|
215
|
+
),
|
|
216
|
+
Demo(
|
|
217
|
+
id="result_count",
|
|
218
|
+
title="Count",
|
|
219
|
+
description="Count matching records",
|
|
220
|
+
category="results",
|
|
221
|
+
code=extract_demo_code(_run_count),
|
|
222
|
+
execute=_run_count,
|
|
223
|
+
),
|
|
224
|
+
Demo(
|
|
225
|
+
id="result_exists",
|
|
226
|
+
title="Exists",
|
|
227
|
+
description="Check if any records match",
|
|
228
|
+
category="results",
|
|
229
|
+
code=extract_demo_code(_run_exists),
|
|
230
|
+
execute=_run_exists,
|
|
231
|
+
),
|
|
232
|
+
Demo(
|
|
233
|
+
id="result_aggregates",
|
|
234
|
+
title="Aggregates",
|
|
235
|
+
description="Calculate sum, average, etc.",
|
|
236
|
+
category="results",
|
|
237
|
+
code=extract_demo_code(_run_aggregates),
|
|
238
|
+
execute=_run_aggregates,
|
|
239
|
+
),
|
|
240
|
+
],
|
|
241
|
+
)
|