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.
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/CHANGELOG.md +16 -0
- {sqlobjects-1.9.0/sqlobjects.egg-info → sqlobjects-1.10.0}/PKG-INFO +12 -5
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/README.md +11 -4
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/01-database-session-guide.md +61 -6
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/02-model-definition-guide.md +35 -2
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/03-query-operations-guide.md +3 -3
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/04-crud-operations-guide.md +32 -4
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/05-relationships-guide.md +92 -2
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/07-performance-guide.md +7 -6
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/README.md +2 -2
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/pyproject.toml +2 -1
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/__init__.py +1 -1
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/cte.py +1 -1
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/mixins.py +8 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/builder.py +3 -3
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queryset.py +1 -1
- {sqlobjects-1.9.0 → sqlobjects-1.10.0/sqlobjects.egg-info}/PKG-INFO +12 -5
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/LICENSE +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/setup.cfg +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/cascade.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/contrib/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/contrib/asgi.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/contrib/fastapi.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/database/manager.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/exceptions.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/function.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/mixins.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/core.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/proxies.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/descriptors.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/prefetch.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/relations/utils.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/comparators.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/types/registry.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/metadata.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/model.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/core.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/dialect.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/queries/executor.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/session.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/sql_logging.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/SOURCES.txt +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.9.0 → sqlobjects-1.10.0}/sqlobjects.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
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
|
-
- **
|
|
384
|
-
- **
|
|
385
|
-
- **
|
|
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
|
|
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
|
-
- **
|
|
355
|
-
- **
|
|
356
|
-
- **
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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).
|
|
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
|
|
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").
|
|
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).
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
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
|
|
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.
|
|
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]
|
|
@@ -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).
|
|
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
|
-
"""
|
|
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
|
|
79
|
+
New QueryBuilder instance with replaced ordering
|
|
80
80
|
"""
|
|
81
81
|
new_builder = self.copy()
|
|
82
|
-
new_builder.ordering
|
|
82
|
+
new_builder.ordering = list(fields)
|
|
83
83
|
return new_builder
|
|
84
84
|
|
|
85
85
|
def add_limit(self, count: int):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
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
|
|
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
|
-
- **
|
|
384
|
-
- **
|
|
385
|
-
- **
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|