sqlobjects 1.9.0__tar.gz → 1.10.0__tar.gz

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 (81) hide show
  1. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/CHANGELOG.md +16 -0
  2. {sqlobjects-1.9.0/sqlobjects.egg-info → sqlobjects-1.10.0}/PKG-INFO +12 -5
  3. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/README.md +11 -4
  4. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/01-database-session-guide.md +61 -6
  5. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/02-model-definition-guide.md +35 -2
  6. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/03-query-operations-guide.md +3 -3
  7. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/04-crud-operations-guide.md +32 -4
  8. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/05-relationships-guide.md +92 -2
  9. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/07-performance-guide.md +7 -6
  10. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/README.md +2 -2
  11. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/pyproject.toml +2 -1
  12. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/__init__.py +1 -1
  13. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/cte.py +1 -1
  14. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/mixins.py +8 -0
  15. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/builder.py +3 -3
  16. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queryset.py +1 -1
  17. {sqlobjects-1.9.0 → sqlobjects-1.10.0/sqlobjects.egg-info}/PKG-INFO +12 -5
  18. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/LICENSE +0 -0
  19. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/06-validation-signals-guide.md +0 -0
  20. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/setup.cfg +0 -0
  21. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/_install_rules.py +0 -0
  22. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/cascade.py +0 -0
  23. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/contrib/__init__.py +0 -0
  24. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/contrib/asgi.py +0 -0
  25. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/contrib/fastapi.py +0 -0
  26. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/database/__init__.py +0 -0
  27. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/database/config.py +0 -0
  28. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/database/manager.py +0 -0
  29. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/exceptions.py +0 -0
  30. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/__init__.py +0 -0
  31. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/aggregate.py +0 -0
  32. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/base.py +0 -0
  33. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/explain.py +0 -0
  34. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/function.py +0 -0
  35. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/mixins.py +0 -0
  36. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/scalar.py +0 -0
  37. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/subquery.py +0 -0
  38. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/terminal.py +0 -0
  39. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/window.py +0 -0
  40. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/__init__.py +0 -0
  41. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/core.py +0 -0
  42. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/functions.py +0 -0
  43. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/proxies.py +0 -0
  44. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/__init__.py +0 -0
  45. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/descriptors.py +0 -0
  46. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/managers.py +0 -0
  47. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/prefetch.py +0 -0
  48. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/utils.py +0 -0
  50. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/shortcuts.py +0 -0
  51. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/__init__.py +0 -0
  52. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/base.py +0 -0
  53. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/comparators.py +0 -0
  54. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/registry.py +0 -0
  55. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/utils.py +0 -0
  56. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/internal/__init__.py +0 -0
  57. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/internal/operations.py +0 -0
  58. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/internal/results.py +0 -0
  59. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/metadata.py +0 -0
  60. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/model.py +0 -0
  61. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/__init__.py +0 -0
  62. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/bulk.py +0 -0
  63. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/core.py +0 -0
  64. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/upsert.py +0 -0
  65. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/__init__.py +0 -0
  66. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/dialect.py +0 -0
  67. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/executor.py +0 -0
  68. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/session.py +0 -0
  69. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/signals.py +0 -0
  70. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/sql_logging.py +0 -0
  71. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/__init__.py +0 -0
  72. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/inspect.py +0 -0
  73. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/naming.py +0 -0
  74. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/pattern.py +0 -0
  75. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/validators.py +0 -0
  76. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/SOURCES.txt +0 -0
  77. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
  78. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/entry_points.txt +0 -0
  79. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/requires.txt +0 -0
  80. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/top_level.txt +0 -0
  81. {sqlobjects-1.9.0 → sqlobjects-1.10.0}/tests/test_config.py +0 -0
@@ -1,3 +1,19 @@
1
+ ## 1.10.0 (2026-07-24)
2
+
3
+ ### Feat
4
+
5
+ - **model**: add public get_dirty_fields() accessor on BaseMixin
6
+
7
+ ### Fix
8
+
9
+ - **build**: sync __version__ with pyproject and auto-update via cz version_files
10
+
11
+ ## 1.9.1 (2026-04-09)
12
+
13
+ ### Fix
14
+
15
+ - **query**: order_by uses replace semantics instead of append
16
+
1
17
  ## 1.9.0 (2026-03-27)
2
18
 
3
19
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.9.0
3
+ Version: 1.10.0
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -48,6 +48,8 @@ loading. SQLObjects combines the familiar Django ORM API with the performance an
48
48
  - **📊 High performance** - Built on SQLAlchemy Core for optimal performance
49
49
  - **🔄 Smart operations** - Automatic CREATE/UPDATE detection and bulk operations
50
50
  - **🎣 Lifecycle hooks** - Comprehensive signal system for database operations
51
+ - **🌊 Cascade operations** - Database- and ORM-level cascade deletes/updates with automatic detection
52
+ - **📝 SQL logging** - Zero-config `sqlobjects.sql` logger with user-code caller rewriting
51
53
  - **🗄️ Multi-database support** - Seamless multi-database configuration and routing
52
54
 
53
55
  ## 🚀 Quick Start
@@ -89,7 +91,7 @@ active_users = await User.objects.filter(
89
91
  ).order_by("-age").limit(10).all()
90
92
 
91
93
  # Complex queries with Q objects
92
- from sqlobjects.queries import Q
94
+ from sqlobjects import Q
93
95
 
94
96
  users = await User.objects.filter(
95
97
  Q(User.age >= 18) & (Q(User.username.like("%admin%")) | Q(User.is_active == True))
@@ -380,9 +382,14 @@ uv run pytest
380
382
 
381
383
  See our [TODO.md](TODO.md) for planned features:
382
384
 
383
- - **v2.0**: Database health checks, window functions, advanced bulk operations
384
- - **v2.1**: Advanced field optimization, query performance tools
385
- - **v2.2+**: CTE support, advanced SQL functions
385
+ - **Database management**: connection health checks, dynamic default-database switching, connection-pool monitoring
386
+ - **Advanced SQL**: additional aggregate functions (array / JSON aggregation)
387
+ - **Query optimization**: index suggestions and slow-query analysis tools
388
+ - **Migrations**: model-diff migration generation and batched data-migration tooling
389
+
390
+ Already implemented: window functions, CTEs (including recursive), `EXPLAIN`
391
+ query analysis, cascade operations, SQL logging, and high-performance bulk
392
+ operations.
386
393
 
387
394
  ## 📄 License
388
395
 
@@ -19,6 +19,8 @@ loading. SQLObjects combines the familiar Django ORM API with the performance an
19
19
  - **📊 High performance** - Built on SQLAlchemy Core for optimal performance
20
20
  - **🔄 Smart operations** - Automatic CREATE/UPDATE detection and bulk operations
21
21
  - **🎣 Lifecycle hooks** - Comprehensive signal system for database operations
22
+ - **🌊 Cascade operations** - Database- and ORM-level cascade deletes/updates with automatic detection
23
+ - **📝 SQL logging** - Zero-config `sqlobjects.sql` logger with user-code caller rewriting
22
24
  - **🗄️ Multi-database support** - Seamless multi-database configuration and routing
23
25
 
24
26
  ## 🚀 Quick Start
@@ -60,7 +62,7 @@ active_users = await User.objects.filter(
60
62
  ).order_by("-age").limit(10).all()
61
63
 
62
64
  # Complex queries with Q objects
63
- from sqlobjects.queries import Q
65
+ from sqlobjects import Q
64
66
 
65
67
  users = await User.objects.filter(
66
68
  Q(User.age >= 18) & (Q(User.username.like("%admin%")) | Q(User.is_active == True))
@@ -351,9 +353,14 @@ uv run pytest
351
353
 
352
354
  See our [TODO.md](TODO.md) for planned features:
353
355
 
354
- - **v2.0**: Database health checks, window functions, advanced bulk operations
355
- - **v2.1**: Advanced field optimization, query performance tools
356
- - **v2.2+**: CTE support, advanced SQL functions
356
+ - **Database management**: connection health checks, dynamic default-database switching, connection-pool monitoring
357
+ - **Advanced SQL**: additional aggregate functions (array / JSON aggregation)
358
+ - **Query optimization**: index suggestions and slow-query analysis tools
359
+ - **Migrations**: model-diff migration generation and batched data-migration tooling
360
+
361
+ Already implemented: window functions, CTEs (including recursive), `EXPLAIN`
362
+ query analysis, cascade operations, SQL logging, and high-performance bulk
363
+ operations.
357
364
 
358
365
  ## 📄 License
359
366
 
@@ -104,7 +104,7 @@ async with ctx_session() as session:
104
104
 
105
105
  # Good: Connection pool configuration
106
106
  await init_db(
107
- "postgresql://localhost/db",
107
+ "postgresql+asyncpg://localhost/db",
108
108
  pool_size=20,
109
109
  max_overflow=30,
110
110
  pool_timeout=30,
@@ -141,7 +141,7 @@ await Post.objects.create(title="Post", author_id=user.id)
141
141
  ```python
142
142
  # Production configuration
143
143
  await init_db(
144
- "postgresql://localhost/db",
144
+ "postgresql+asyncpg://localhost/db",
145
145
  pool_size=20, # Base connections
146
146
  max_overflow=30, # Burst capacity
147
147
  pool_timeout=30, # Wait time for connection
@@ -249,7 +249,7 @@ except OperationalError:
249
249
  ## Complete Example
250
250
 
251
251
  ```python
252
- from sqlobjects.database import init_dbs, create_tables, close_all_dbs
252
+ from sqlobjects.database import init_dbs, create_tables, close_dbs
253
253
  from sqlobjects.session import ctx_session, ctx_sessions
254
254
  from sqlobjects.model import ObjectModel
255
255
  from sqlobjects.fields import Column, StringColumn
@@ -264,8 +264,8 @@ class Log(ObjectModel):
264
264
  async def main():
265
265
  # Setup
266
266
  await init_dbs({
267
- "main": {"url": "postgresql://localhost/main", "pool_size": 20},
268
- "analytics": {"url": "sqlite:///analytics.db"}
267
+ "main": {"url": "postgresql+asyncpg://localhost/main", "pool_size": 20},
268
+ "analytics": {"url": "sqlite+aiosqlite:///analytics.db"}
269
269
  }, default="main")
270
270
 
271
271
  await create_tables(ObjectModel, "main")
@@ -286,7 +286,7 @@ async def main():
286
286
  )
287
287
 
288
288
  # Cleanup
289
- await close_all_dbs()
289
+ await close_dbs()
290
290
 
291
291
  # Run
292
292
  import asyncio
@@ -330,3 +330,58 @@ async def get_user(user_id: int, session: AsyncSession = Depends(get_db_session)
330
330
  user = await User.objects.using(session).get(User.id == user_id)
331
331
  return {"username": user.username}
332
332
  ```
333
+
334
+ ## SQL Logging
335
+
336
+ SQLObjects ships zero-config SQL logging through the standard library `logging`
337
+ module. Every executed statement is logged at `DEBUG` level to the logger named
338
+ **`sqlobjects.sql`**. This logger is an `ObjectLogger` (installed automatically
339
+ on import) that rewrites each record's caller fields — `pathname`, `filename`,
340
+ `funcName`, `lineno` — to the first user-code frame, skipping internal
341
+ `sqlobjects.*` and `sqlalchemy.*` frames. Your log output therefore points at
342
+ the line in your application that issued the query, not at ORM internals.
343
+
344
+ ### Enabling
345
+
346
+ ```python
347
+ import logging
348
+
349
+ # Show SQL (and everything else) at DEBUG
350
+ logging.basicConfig(level=logging.DEBUG)
351
+ logging.getLogger("sqlobjects.sql").setLevel(logging.DEBUG)
352
+
353
+ # Now queries emit DEBUG log records
354
+ users = await User.objects.filter(User.is_active == True).all()
355
+ ```
356
+
357
+ To capture SQL only, without lowering the level of the rest of your app, attach
358
+ a dedicated handler to the `sqlobjects.sql` logger:
359
+
360
+ ```python
361
+ import logging
362
+
363
+ sql_logger = logging.getLogger("sqlobjects.sql")
364
+ sql_logger.setLevel(logging.DEBUG)
365
+
366
+ handler = logging.StreamHandler()
367
+ handler.setFormatter(logging.Formatter("%(pathname)s:%(lineno)d - %(message)s"))
368
+ sql_logger.addHandler(handler)
369
+ ```
370
+
371
+ Statement compilation only happens when the logger is actually enabled for
372
+ `DEBUG`, so leaving logging off has no runtime cost. Each record carries the
373
+ compiled SQL as the message plus an `extra` dict with `sql`, `params`, and
374
+ `duration_ms`.
375
+
376
+ ### Public API
377
+
378
+ ```python
379
+ from sqlobjects import ObjectLogger, get_caller_frame
380
+ ```
381
+
382
+ - **`ObjectLogger`** — the `logging.Logger` subclass used for `sqlobjects.sql`.
383
+ It accepts an `extra_skip_packages` list to skip additional module prefixes
384
+ (e.g. your own middleware) when resolving the user-code frame.
385
+ - **`get_caller_frame(extra_skip_packages=None, max_frames=1)`** — returns the
386
+ first user-code frame as a `"path:lineno in func"` string (or a list when
387
+ `max_frames > 1`), applying the same skip rules. Useful for custom logging.
@@ -69,7 +69,8 @@ class User(ObjectModel):
69
69
 
70
70
  ```python
71
71
  from sqlobjects.model import ObjectModel
72
- from sqlobjects.fields import Column, StringColumn, index, constraint
72
+ from sqlobjects.fields import Column, StringColumn, IntegerColumn
73
+ from sqlobjects.metadata import index, constraint
73
74
 
74
75
  class User(ObjectModel):
75
76
  username: Column[str] = StringColumn(length=50)
@@ -88,6 +89,38 @@ class User(ObjectModel):
88
89
  ]
89
90
  ```
90
91
 
92
+ ### Named & Composite Foreign Keys
93
+
94
+ For composite foreign keys, custom constraint names, or explicit referential
95
+ actions defined at the table level, use the `foreignkey()` constraint builder
96
+ in `Config.constraints`. For simple single-column foreign keys, prefer the
97
+ `foreign_key()` field descriptor (see the Relationships guide) instead.
98
+
99
+ ```python
100
+ from sqlobjects.model import ObjectModel
101
+ from sqlobjects.fields import Column, StringColumn, IntegerColumn
102
+ from sqlobjects.metadata import foreignkey
103
+
104
+ class OrderItem(ObjectModel):
105
+ order_id: Column[int] = IntegerColumn()
106
+ product_id: Column[int] = IntegerColumn()
107
+ quantity: Column[int] = IntegerColumn()
108
+
109
+ class Config:
110
+ constraints = [
111
+ # Named single-column foreign key with explicit referential action
112
+ foreignkey("order_id", "Order.id", name="fk_items_order", ondelete="CASCADE"),
113
+ # Composite foreign key
114
+ foreignkey(["product_id", "order_id"], ["Product.id", "Order.id"]),
115
+ ]
116
+ ```
117
+
118
+ `foreignkey(fields, references, *, name=None, ondelete=None, onupdate=None,
119
+ deferrable=False, initially="IMMEDIATE")` accepts a single field/reference or a
120
+ list for composite keys. References may be given as class names (e.g.
121
+ `"Order.id"`) or table names (e.g. `"orders.id"`); class names are resolved
122
+ automatically.
123
+
91
124
  ### Field Parameters
92
125
 
93
126
  ```python
@@ -333,8 +366,8 @@ from sqlobjects.model import ObjectModel
333
366
  from sqlobjects.fields import (
334
367
  Column, column, StringColumn, IntegerColumn,
335
368
  BooleanColumn, DateTimeColumn, JsonColumn,
336
- index, constraint
337
369
  )
370
+ from sqlobjects.metadata import index, constraint
338
371
  from datetime import datetime
339
372
 
340
373
  class User(ObjectModel):
@@ -41,7 +41,7 @@ users = await User.objects.filter(User.username.like("admin%")).all()
41
41
  ### Complex Filtering with Q Objects
42
42
 
43
43
  ```python
44
- from sqlobjects.queries import Q
44
+ from sqlobjects import Q
45
45
 
46
46
  # OR conditions
47
47
  users = await User.objects.filter(
@@ -242,7 +242,7 @@ result = await User.objects.with_cte(adults).filter(adults.c.age < 30).all()
242
242
  base = Employee.objects.filter(Employee.manager_id.is_(None)).cte("hierarchy", recursive=True)
243
243
  recursive_part = Employee.objects.join(base, Employee.manager_id == base.c.id)
244
244
  hierarchy = base.union_all(recursive_part)
245
- all_employees = await Employee.objects.with_cte(hierarchy).select_from(hierarchy).all()
245
+ all_employees = await Employee.objects.with_cte(hierarchy).all()
246
246
  ```
247
247
 
248
248
  ### Manual Joins
@@ -406,7 +406,7 @@ users = await User.objects.filter(
406
406
  ```python
407
407
  from sqlobjects.model import ObjectModel
408
408
  from sqlobjects.fields import Column, StringColumn, IntegerColumn, BooleanColumn
409
- from sqlobjects.queries import Q
409
+ from sqlobjects import Q
410
410
  from sqlobjects.expressions import func
411
411
  from datetime import datetime, timedelta
412
412
 
@@ -69,7 +69,7 @@ mappings = [
69
69
  await User.objects.bulk_update(mappings, match_fields=["id"], batch_size=1000)
70
70
 
71
71
  # Update all matching records
72
- await User.objects.filter(User.department == "sales").update_all(is_active=False)
72
+ await User.objects.filter(User.department == "sales").update(is_active=False)
73
73
  ```
74
74
 
75
75
  ### Delete Operations
@@ -84,9 +84,37 @@ user_ids = [1, 2, 3, 4, 5]
84
84
  await User.objects.bulk_delete(user_ids, id_field="id", batch_size=1000)
85
85
 
86
86
  # Delete all matching records
87
- await User.objects.filter(User.is_active == False).delete_all()
87
+ await User.objects.filter(User.is_active == False).delete()
88
88
  ```
89
89
 
90
+ ### Cascade on Delete
91
+
92
+ Both delete entry points detect and apply cascade behavior automatically:
93
+
94
+ - **`instance.delete(cascade=None)`** — with the default `None`, the instance
95
+ auto-detects whether any relationship declares an on-delete cascade and
96
+ handles related objects accordingly. Pass `cascade=True` to force cascade
97
+ handling or `cascade=False` to skip it and issue a direct delete.
98
+ - **`QuerySet.delete(cascade="auto")`** — accepts a strategy string:
99
+ - `"auto"` (default): choose automatically based on the model's relationships
100
+ and delete signals
101
+ - `"full"`: complete cascade deletion with full ORM functionality (fires signals)
102
+ - `"fast"`: fast cascade deletion with minimal ORM processing
103
+ - `"none"`: direct SQL deletion without any ORM cascade processing
104
+
105
+ ```python
106
+ # Instance delete with explicit cascade control
107
+ user = await User.objects.get(User.id == 1)
108
+ await user.delete(cascade=True) # force cascade
109
+ await user.delete(cascade=False) # skip cascade, direct delete
110
+
111
+ # QuerySet delete with an explicit strategy
112
+ await User.objects.filter(User.is_active == False).delete(cascade="fast")
113
+ ```
114
+
115
+ See the Relationships guide for how cascade behavior is configured on
116
+ relationships and foreign keys.
117
+
90
118
  ### Bulk Operations
91
119
 
92
120
  ```python
@@ -192,8 +220,8 @@ user.is_active = False
192
220
  await user.save() # Only updates email and is_active fields
193
221
 
194
222
  # Check dirty fields
195
- if user._has_changes():
196
- changed_fields = user._get_changed_fields()
223
+ changed_fields = user.get_dirty_fields()
224
+ if changed_fields:
197
225
  print(f"Modified fields: {changed_fields}")
198
226
  ```
199
227
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  ```python
16
16
  from sqlobjects.model import ObjectModel
17
- from sqlobjects.fields import Column, StringColumn, foreign_key, relationship, Related
17
+ from sqlobjects.fields import Column, StringColumn, column, foreign_key, relationship, Related
18
18
 
19
19
  # One-to-Many (Foreign Key)
20
20
  class Post(ObjectModel):
@@ -227,6 +227,96 @@ class Category(ObjectModel):
227
227
  category = await Category.objects.select_related("parent").prefetch_related("children").get(Category.id == 1)
228
228
  ```
229
229
 
230
+ ## Cascade Operations
231
+
232
+ SQLObjects offers cascade behavior at two independent layers. Choose the one
233
+ that matches how you want related rows handled when a parent is updated or
234
+ deleted.
235
+
236
+ ### Database-Level (`ondelete` / `onupdate`)
237
+
238
+ Declared on the `foreign_key()` field descriptor, these translate directly into
239
+ the SQL `ON DELETE` / `ON UPDATE` referential actions enforced by the database
240
+ engine. Use the `OnDelete` enum (or the equivalent string) for clarity.
241
+
242
+ ```python
243
+ from sqlobjects.model import ObjectModel
244
+ from sqlobjects.fields import Column, StringColumn, foreign_key, relationship, Related
245
+ from sqlobjects import OnDelete
246
+
247
+ class Post(ObjectModel):
248
+ title: Column[str] = StringColumn(length=200)
249
+ # DB deletes child rows automatically when the parent User is deleted
250
+ author_id: Column[int] = foreign_key("User.id", ondelete=OnDelete.CASCADE, nullable=False)
251
+ author: Related["User"] = relationship("User", back_populates="posts")
252
+ ```
253
+
254
+ Available `OnDelete` values: `CASCADE`, `SET_NULL`, `RESTRICT`, `NO_ACTION`.
255
+ `onupdate` accepts the same set. Strings such as `ondelete="CASCADE"` are also
256
+ accepted.
257
+
258
+ ### ORM-Level (`relationship(cascade=...)`)
259
+
260
+ Declared on `relationship()`, these are applied by the ORM when it saves or
261
+ deletes an instance — independent of what the database constraint does. Use the
262
+ `CascadePresets` constants for common combinations.
263
+
264
+ ```python
265
+ from sqlobjects import CascadePresets
266
+
267
+ class User(ObjectModel):
268
+ username: Column[str] = StringColumn(length=50)
269
+ # Saving a User cascades to its posts; deleting a User deletes them,
270
+ # and posts removed from the collection are deleted as orphans
271
+ posts: Related[list["Post"]] = relationship(
272
+ "Post",
273
+ back_populates="author",
274
+ cascade=CascadePresets.ALL_DELETE_ORPHAN,
275
+ )
276
+ ```
277
+
278
+ `CascadePresets` combinations:
279
+
280
+ - `NONE` — no cascade
281
+ - `SAVE_UPDATE` — cascade save/update only
282
+ - `DELETE` — cascade delete only
283
+ - `SAVE_DELETE` — `"save-update, delete"`
284
+ - `ALL` — `"save-update, merge, refresh-expire"`
285
+ - `ALL_DELETE_ORPHAN` — `ALL` plus orphan deletion
286
+
287
+ The `cascade` argument also accepts a raw string (e.g. `cascade="save-update, delete"`),
288
+ a `CascadeOption`, or a set of `CascadeOption` values.
289
+
290
+ ### Automatic Detection on `delete()`
291
+
292
+ `Model.delete()` inspects the model's relationships and applies cascade handling
293
+ automatically:
294
+
295
+ ```python
296
+ user = await User.objects.get(User.id == 1)
297
+ await user.delete() # auto-detects on-delete cascade relations
298
+ await user.delete(cascade=True) # force ORM cascade handling
299
+ await user.delete(cascade=False) # skip cascade, direct delete
300
+ ```
301
+
302
+ For bulk deletes, `QuerySet.delete(cascade="auto")` chooses a strategy
303
+ (`"full"`, `"fast"`, or `"none"`) based on the model's relationships and delete
304
+ signals. See the CRUD guide for details.
305
+
306
+ ### ✅ Do
307
+
308
+ - **Use `ondelete`** when you want the database to enforce referential integrity
309
+ - **Use `relationship(cascade=...)`** when related objects must go through the
310
+ ORM (e.g. to fire delete signals or handle orphans)
311
+ - **Combine both layers** when appropriate — they are independent
312
+
313
+ ### ❌ Don't
314
+
315
+ - **Don't assume ORM cascade removes rows the DB constraint handles** (or vice
316
+ versa) — they are configured separately
317
+ - **Don't rely on `ondelete=CASCADE`** to fire `before_delete` / `after_delete`
318
+ signals; database-level cascades bypass the ORM
319
+
230
320
  ## Performance Tips
231
321
 
232
322
  ### N+1 Query Prevention
@@ -348,7 +438,7 @@ class Post(ObjectModel):
348
438
 
349
439
  ```python
350
440
  from sqlobjects.model import ObjectModel
351
- from sqlobjects.fields import Column, StringColumn, foreign_key, relationship, Related
441
+ from sqlobjects.fields import Column, StringColumn, column, foreign_key, relationship, Related
352
442
  from datetime import datetime, timedelta
353
443
 
354
444
  class User(ObjectModel):
@@ -93,7 +93,7 @@ class User(ObjectModel):
93
93
  ```python
94
94
  # Production configuration
95
95
  await init_db(
96
- "postgresql://localhost/db",
96
+ "postgresql+asyncpg://localhost/db",
97
97
  pool_size=20, # Base connections
98
98
  max_overflow=30, # Burst capacity
99
99
  pool_timeout=30, # Wait time for connection
@@ -243,7 +243,7 @@ await User.objects.bulk_create(data, batch_size=2000)
243
243
 
244
244
  # Connection pool configuration
245
245
  await init_db(
246
- "postgresql://localhost/db",
246
+ "postgresql+asyncpg://localhost/db",
247
247
  pool_size=20,
248
248
  max_overflow=40,
249
249
  pool_pre_ping=True
@@ -258,7 +258,7 @@ await User.objects.bulk_create(data, batch_size=1000)
258
258
 
259
259
  # Connection pool configuration
260
260
  await init_db(
261
- "mysql://localhost/db",
261
+ "mysql+aiomysql://localhost/db",
262
262
  pool_size=15,
263
263
  max_overflow=25,
264
264
  pool_recycle=3600 # Important for MySQL
@@ -273,7 +273,7 @@ await User.objects.bulk_create(data, batch_size=100)
273
273
 
274
274
  # Single connection for SQLite
275
275
  await init_db(
276
- "sqlite:///app.db",
276
+ "sqlite+aiosqlite:///app.db",
277
277
  pool_size=1, # SQLite doesn't support concurrent writes
278
278
  max_overflow=0
279
279
  )
@@ -431,7 +431,8 @@ users = await User.objects.prefetch_related("posts").all()
431
431
 
432
432
  ```python
433
433
  from sqlobjects.model import ObjectModel
434
- from sqlobjects.fields import Column, StringColumn, foreign_key, relationship, Related
434
+ from sqlobjects.fields import Column, StringColumn, column, foreign_key, relationship, Related
435
+ from sqlobjects.metadata import index
435
436
  from sqlobjects.session import ctx_session
436
437
  from sqlobjects.database import init_db
437
438
  import time
@@ -456,7 +457,7 @@ class Post(ObjectModel):
456
457
  async def main():
457
458
  # Configure connection pool
458
459
  await init_db(
459
- "postgresql://localhost/db",
460
+ "postgresql+asyncpg://localhost/db",
460
461
  pool_size=20,
461
462
  max_overflow=30,
462
463
  pool_recycle=3600,
@@ -4,11 +4,11 @@ Best practices and usage patterns for SQLObjects, optimized for AI coding assist
4
4
 
5
5
  ## Quick Navigation
6
6
 
7
- - **[01. Database & Session Guide](01-database-session-guide.md)** - Connection management and transactions
7
+ - **[01. Database & Session Guide](01-database-session-guide.md)** - Connection management, transactions, and SQL logging
8
8
  - **[02. Model Definition Guide](02-model-definition-guide.md)** - Model creation and field configuration
9
9
  - **[03. Query Operations Guide](03-query-operations-guide.md)** - Filtering, sorting, and data retrieval
10
10
  - **[04. CRUD Operations Guide](04-crud-operations-guide.md)** - Create, read, update, delete operations
11
- - **[05. Relationships Guide](05-relationships-guide.md)** - Model relationships and loading strategies
11
+ - **[05. Relationships Guide](05-relationships-guide.md)** - Model relationships, loading strategies, and cascade operations
12
12
  - **[06. Validation & Signals Guide](06-validation-signals-guide.md)** - Data validation and lifecycle hooks
13
13
  - **[07. Performance Guide](07-performance-guide.md)** - Optimization techniques and best practices
14
14
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlobjects"
3
- version = "1.9.0"
3
+ version = "1.10.0"
4
4
  description = "Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -89,6 +89,7 @@ name = "cz_conventional_commits"
89
89
  tag_format = "$version"
90
90
  version_scheme = "pep440"
91
91
  version_provider = "uv"
92
+ version_files = ["sqlobjects/__init__.py:__version__"]
92
93
  update_changelog_on_bump = true
93
94
 
94
95
  [tool.pyright]
@@ -19,7 +19,7 @@ from .queryset import Q, QuerySet
19
19
  from .sql_logging import ObjectLogger, get_caller_frame
20
20
 
21
21
 
22
- __version__ = "0.3.0"
22
+ __version__ = "1.10.0"
23
23
 
24
24
  __all__ = [
25
25
  # Core classes
@@ -33,7 +33,7 @@ class CTEExpression:
33
33
  >>> base = Employee.objects.filter(Employee.manager_id.is_(None)).cte("hierarchy", recursive=True)
34
34
  >>> recursive = Employee.objects.join(base, Employee.manager_id == base.c.id)
35
35
  >>> hierarchy = base.union_all(recursive)
36
- >>> all_employees = await Employee.objects.with_cte(hierarchy).select_from(hierarchy).all()
36
+ >>> all_employees = await Employee.objects.with_cte(hierarchy).all()
37
37
  """
38
38
 
39
39
  def __init__(self, queryset: QuerySet, name: str, recursive: bool = False):
@@ -136,6 +136,14 @@ class BaseMixin:
136
136
  if not hasattr(self, "_state_manager"):
137
137
  self._state_manager = _StateManager()
138
138
 
139
+ def get_dirty_fields(self) -> set[str]:
140
+ """Get fields modified since the last save or database load.
141
+
142
+ Returns:
143
+ Set of field names with unsaved changes
144
+ """
145
+ return self._state_manager.get_dirty_fields().copy()
146
+
139
147
  @classmethod
140
148
  def get_table(cls) -> Table:
141
149
  """Get SQLAlchemy Core Table definition.
@@ -70,16 +70,16 @@ class QueryBuilder:
70
70
  return new_builder
71
71
 
72
72
  def add_ordering(self, *fields):
73
- """Add ORDER BY fields to the query.
73
+ """Set ORDER BY fields for the query, replacing any existing ordering.
74
74
 
75
75
  Args:
76
76
  *fields: Field names or SQLAlchemy ordering expressions
77
77
 
78
78
  Returns:
79
- New QueryBuilder instance with added ordering
79
+ New QueryBuilder instance with replaced ordering
80
80
  """
81
81
  new_builder = self.copy()
82
- new_builder.ordering.extend(fields)
82
+ new_builder.ordering = list(fields)
83
83
  return new_builder
84
84
 
85
85
  def add_limit(self, count: int):
@@ -762,7 +762,7 @@ class QuerySet(Generic[T]):
762
762
  hierarchy = base.union_all(recursive_part)
763
763
  all_employees = await Employee.objects.with_cte(
764
764
  hierarchy
765
- ).select_from(hierarchy).all()
765
+ ).all()
766
766
  """
767
767
  return CTEExpression(self, name, recursive)
768
768
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.9.0
3
+ Version: 1.10.0
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -48,6 +48,8 @@ loading. SQLObjects combines the familiar Django ORM API with the performance an
48
48
  - **📊 High performance** - Built on SQLAlchemy Core for optimal performance
49
49
  - **🔄 Smart operations** - Automatic CREATE/UPDATE detection and bulk operations
50
50
  - **🎣 Lifecycle hooks** - Comprehensive signal system for database operations
51
+ - **🌊 Cascade operations** - Database- and ORM-level cascade deletes/updates with automatic detection
52
+ - **📝 SQL logging** - Zero-config `sqlobjects.sql` logger with user-code caller rewriting
51
53
  - **🗄️ Multi-database support** - Seamless multi-database configuration and routing
52
54
 
53
55
  ## 🚀 Quick Start
@@ -89,7 +91,7 @@ active_users = await User.objects.filter(
89
91
  ).order_by("-age").limit(10).all()
90
92
 
91
93
  # Complex queries with Q objects
92
- from sqlobjects.queries import Q
94
+ from sqlobjects import Q
93
95
 
94
96
  users = await User.objects.filter(
95
97
  Q(User.age >= 18) & (Q(User.username.like("%admin%")) | Q(User.is_active == True))
@@ -380,9 +382,14 @@ uv run pytest
380
382
 
381
383
  See our [TODO.md](TODO.md) for planned features:
382
384
 
383
- - **v2.0**: Database health checks, window functions, advanced bulk operations
384
- - **v2.1**: Advanced field optimization, query performance tools
385
- - **v2.2+**: CTE support, advanced SQL functions
385
+ - **Database management**: connection health checks, dynamic default-database switching, connection-pool monitoring
386
+ - **Advanced SQL**: additional aggregate functions (array / JSON aggregation)
387
+ - **Query optimization**: index suggestions and slow-query analysis tools
388
+ - **Migrations**: model-diff migration generation and batched data-migration tooling
389
+
390
+ Already implemented: window functions, CTEs (including recursive), `EXPLAIN`
391
+ query analysis, cascade operations, SQL logging, and high-performance bulk
392
+ operations.
386
393
 
387
394
  ## 📄 License
388
395
 
File without changes
File without changes