sqlobjects 1.2.2__tar.gz → 1.2.3__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.2.2 → sqlobjects-1.2.3}/CHANGELOG.md +13 -0
- {sqlobjects-1.2.2/sqlobjects.egg-info → sqlobjects-1.2.3}/PKG-INFO +1 -1
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/pyproject.toml +1 -1
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/database/manager.py +22 -12
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/exceptions.py +13 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/function.py +10 -10
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/core.py +1 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/metadata.py +69 -208
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/queries/executor.py +15 -2
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/queryset.py +9 -6
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/session.py +9 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3/sqlobjects.egg-info}/PKG-INFO +1 -1
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/tests/test_config.py +2 -2
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/LICENSE +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/README.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/01-database-session-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/02-model-definition-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/03-query-operations-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/04-crud-operations-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/05-relationships-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/07-performance-guide.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/docs/rules/README.md +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/setup.cfg +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/cascade.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/cte.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/mixins.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/proxies.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/relations/descriptors.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/relations/prefetch.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/relations/utils.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/types/comparators.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/types/registry.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/mixins.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/model.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/objects/core.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/queries/builder.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/queries/dialect.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects.egg-info/SOURCES.txt +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.2.2 → sqlobjects-1.2.3}/sqlobjects.egg-info/top_level.txt +0 -0
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 1.2.3 (2026-02-26)
|
|
2
|
+
|
|
3
|
+
### Fix
|
|
4
|
+
|
|
5
|
+
- resolve PostgreSQL test failures and cross-test data pollution
|
|
6
|
+
- **executor**: add overloads to execute() and fix iterator type narrowing
|
|
7
|
+
- **queryset**: replace non-existent executor.session with _get_session()
|
|
8
|
+
- enhance exception handling to surface detailed SQLAlchemy errors
|
|
9
|
+
|
|
10
|
+
### Refactor
|
|
11
|
+
|
|
12
|
+
- **metadata**: simplify index handling and config parsing
|
|
13
|
+
|
|
1
14
|
## 1.2.2 (2026-02-26)
|
|
2
15
|
|
|
3
16
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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>
|
|
@@ -189,12 +189,17 @@ class Database:
|
|
|
189
189
|
>>> await db.create_tables(ObjectModel) # Create all tables
|
|
190
190
|
>>> await db.create_tables(ObjectModel, [User, Post]) # Create specific tables
|
|
191
191
|
"""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
try:
|
|
193
|
+
async with self.engine.begin() as conn:
|
|
194
|
+
if tables is None:
|
|
195
|
+
await conn.run_sync(base_class.__registry__.create_all)
|
|
196
|
+
else:
|
|
197
|
+
table_objects = [model.__table__ for model in tables]
|
|
198
|
+
await conn.run_sync(base_class.__registry__.create_all, tables=table_objects)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
from ..exceptions import convert_sqlalchemy_error
|
|
201
|
+
|
|
202
|
+
raise convert_sqlalchemy_error(e) from e
|
|
198
203
|
|
|
199
204
|
async def drop_tables(self, base_class, tables: list[type] | None = None) -> None:
|
|
200
205
|
"""Drop tables defined in the model registry of SQLObjects base class
|
|
@@ -211,12 +216,17 @@ class Database:
|
|
|
211
216
|
>>> await db.drop_tables(ObjectModel) # Drop all tables
|
|
212
217
|
>>> await db.drop_tables(ObjectModel, [User, Post]) # Drop specific tables
|
|
213
218
|
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
try:
|
|
220
|
+
async with self.engine.begin() as conn:
|
|
221
|
+
if tables is None:
|
|
222
|
+
await conn.run_sync(base_class.__registry__.drop_all)
|
|
223
|
+
else:
|
|
224
|
+
table_objects = [model.__table__ for model in tables]
|
|
225
|
+
await conn.run_sync(base_class.__registry__.drop_all, tables=table_objects)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
from ..exceptions import convert_sqlalchemy_error
|
|
228
|
+
|
|
229
|
+
raise convert_sqlalchemy_error(e) from e
|
|
220
230
|
|
|
221
231
|
async def disconnect(self) -> None:
|
|
222
232
|
"""Disconnect database and clean up resources
|
|
@@ -458,8 +458,21 @@ def convert_sqlalchemy_error(error: Exception) -> SQLObjectsError:
|
|
|
458
458
|
... except SQLAlchemyError as e:
|
|
459
459
|
... raise convert_sqlalchemy_error(e)
|
|
460
460
|
"""
|
|
461
|
+
# Build detailed error message with context
|
|
461
462
|
error_msg = str(error)
|
|
462
463
|
|
|
464
|
+
# Add SQL statement if available
|
|
465
|
+
if hasattr(error, "statement") and getattr(error, "statement", None):
|
|
466
|
+
error_msg += f"\nSQL: {error.statement}" # type: ignore[reportAttributeAccessIssue]
|
|
467
|
+
|
|
468
|
+
# Add parameters if available
|
|
469
|
+
if hasattr(error, "params") and getattr(error, "params", None):
|
|
470
|
+
error_msg += f"\nParams: {error.params}" # type: ignore[reportAttributeAccessIssue]
|
|
471
|
+
|
|
472
|
+
# Add original database error if available
|
|
473
|
+
if hasattr(error, "orig") and getattr(error, "orig", None):
|
|
474
|
+
error_msg += f"\nOriginal: {error.orig}" # type: ignore[reportAttributeAccessIssue]
|
|
475
|
+
|
|
463
476
|
if isinstance(error, SQLAIntegrityError):
|
|
464
477
|
return IntegrityError(error_msg, original_error=error)
|
|
465
478
|
elif isinstance(error, SQLAOperationalError):
|
|
@@ -432,13 +432,13 @@ class _FuncWrapper:
|
|
|
432
432
|
func = _FuncWrapper()
|
|
433
433
|
|
|
434
434
|
# Add window functions at runtime
|
|
435
|
-
func.row_number = lambda: RowNumberFunction()
|
|
436
|
-
func.rank = lambda: RankFunction()
|
|
437
|
-
func.dense_rank = lambda: DenseRankFunction()
|
|
438
|
-
func.percent_rank = lambda: PercentRankFunction()
|
|
439
|
-
func.ntile = lambda n: NtileFunction(n)
|
|
440
|
-
func.lag = lambda col, offset=1, default=None: LagFunction(col, offset, default)
|
|
441
|
-
func.lead = lambda col, offset=1, default=None: LeadFunction(col, offset, default)
|
|
442
|
-
func.first_value = lambda col: FirstValueFunction(col)
|
|
443
|
-
func.last_value = lambda col: LastValueFunction(col)
|
|
444
|
-
func.nth_value = lambda col, n: NthValueFunction(col, n)
|
|
435
|
+
func.row_number = lambda: RowNumberFunction()
|
|
436
|
+
func.rank = lambda: RankFunction()
|
|
437
|
+
func.dense_rank = lambda: DenseRankFunction()
|
|
438
|
+
func.percent_rank = lambda: PercentRankFunction()
|
|
439
|
+
func.ntile = lambda n: NtileFunction(n)
|
|
440
|
+
func.lag = lambda col, offset=1, default=None: LagFunction(col, offset, default)
|
|
441
|
+
func.lead = lambda col, offset=1, default=None: LeadFunction(col, offset, default)
|
|
442
|
+
func.first_value = lambda col: FirstValueFunction(col)
|
|
443
|
+
func.last_value = lambda col: LastValueFunction(col)
|
|
444
|
+
func.nth_value = lambda col, n: NthValueFunction(col, n)
|
|
@@ -317,6 +317,7 @@ class ColumnAttribute(ColumnAttributeFunctionMixin, Generic[T]):
|
|
|
317
317
|
"""
|
|
318
318
|
|
|
319
319
|
inherit_cache = True # make use of the cache key generated by the superclass from SQLAlchemy
|
|
320
|
+
is_clause_element = False # force SQLAlchemy to call __clause_element__() for proper coercion
|
|
320
321
|
|
|
321
322
|
def __getattr__(self, name):
|
|
322
323
|
"""Handle attribute access with proper priority.
|
|
@@ -45,25 +45,7 @@ class _RawModelConfig:
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
47
47
|
class ModelConfig:
|
|
48
|
-
"""Complete model configuration with all required fields filled.
|
|
49
|
-
|
|
50
|
-
This dataclass holds all configuration options that can be applied to a model,
|
|
51
|
-
including basic settings, database constraints, metadata, and database-specific
|
|
52
|
-
optimizations. All required fields are guaranteed to have values.
|
|
53
|
-
|
|
54
|
-
Attributes:
|
|
55
|
-
table_name: Database table name (never None after processing)
|
|
56
|
-
verbose_name: Human-readable singular name for the model (never None)
|
|
57
|
-
verbose_name_plural: Human-readable plural name for the model (never None)
|
|
58
|
-
ordering: Default ordering for queries (e.g., ['-created_at', 'name'])
|
|
59
|
-
indexes: List of database indexes to create for the table
|
|
60
|
-
constraints: List of database constraints (check, unique) for the table
|
|
61
|
-
description: Detailed description of the model's purpose (can be None)
|
|
62
|
-
db_options: Database-specific configuration options by dialect
|
|
63
|
-
custom: Custom configuration values for application-specific use
|
|
64
|
-
field_validators: Field-level validators registry
|
|
65
|
-
field_metadata: Unified field metadata information
|
|
66
|
-
"""
|
|
48
|
+
"""Complete model configuration with all required fields filled."""
|
|
67
49
|
|
|
68
50
|
table_name: str
|
|
69
51
|
verbose_name: str
|
|
@@ -425,25 +407,10 @@ class ModelProcessor(type):
|
|
|
425
407
|
|
|
426
408
|
@classmethod
|
|
427
409
|
def _integrate_field_config(mcs, cls: Any, config: ModelConfig) -> ModelConfig:
|
|
428
|
-
"""Integrate field-level configuration into model configuration - optimized version.
|
|
429
|
-
|
|
430
|
-
Args:
|
|
431
|
-
cls: Model class
|
|
432
|
-
config: Current model configuration
|
|
433
|
-
|
|
434
|
-
Returns:
|
|
435
|
-
Updated model configuration with field-level settings integrated
|
|
436
|
-
"""
|
|
437
|
-
# Collect all field configuration in single pass
|
|
438
410
|
field_indexes, field_validators, field_metadata = mcs._collect_all_field_config(cls, config.table_name)
|
|
439
|
-
|
|
440
|
-
# Merge indexes (avoid duplicates)
|
|
441
|
-
config.indexes = mcs._merge_indexes(field_indexes, config.indexes, config.table_name)
|
|
442
|
-
|
|
443
|
-
# Set validators and metadata
|
|
411
|
+
config.indexes = field_indexes + config.indexes
|
|
444
412
|
config.field_validators = field_validators
|
|
445
413
|
config.field_metadata = field_metadata
|
|
446
|
-
|
|
447
414
|
return config
|
|
448
415
|
|
|
449
416
|
@classmethod
|
|
@@ -533,82 +500,6 @@ class ModelProcessor(type):
|
|
|
533
500
|
|
|
534
501
|
return indexes, validators, metadata
|
|
535
502
|
|
|
536
|
-
@classmethod
|
|
537
|
-
def _merge_indexes(mcs, field_indexes: list[Index], table_indexes: list[Index], table_name: str) -> list[Index]:
|
|
538
|
-
"""Merge field-level and table-level indexes, avoiding duplicates.
|
|
539
|
-
|
|
540
|
-
Args:
|
|
541
|
-
field_indexes: Indexes generated from field definitions
|
|
542
|
-
table_indexes: Indexes defined at table level
|
|
543
|
-
table_name: Database table name
|
|
544
|
-
|
|
545
|
-
Returns:
|
|
546
|
-
Merged list of unique indexes
|
|
547
|
-
"""
|
|
548
|
-
|
|
549
|
-
def get_index_signature(idx): # noqa
|
|
550
|
-
if hasattr(idx, "_columns") and idx._columns: # noqa
|
|
551
|
-
columns = tuple(sorted(str(col) for col in idx._columns)) # noqa
|
|
552
|
-
return (columns, idx.unique) # noqa
|
|
553
|
-
return None
|
|
554
|
-
|
|
555
|
-
# Collect table-level index signatures
|
|
556
|
-
table_signatures = set()
|
|
557
|
-
for idx in table_indexes:
|
|
558
|
-
sig = get_index_signature(idx)
|
|
559
|
-
if sig:
|
|
560
|
-
table_signatures.add(sig)
|
|
561
|
-
|
|
562
|
-
# Filter duplicate field-level indexes
|
|
563
|
-
merged_indexes = []
|
|
564
|
-
for idx in field_indexes:
|
|
565
|
-
sig = get_index_signature(idx)
|
|
566
|
-
if sig and sig not in table_signatures:
|
|
567
|
-
merged_indexes.append(idx)
|
|
568
|
-
|
|
569
|
-
# Add table-level indexes
|
|
570
|
-
merged_indexes.extend(table_indexes)
|
|
571
|
-
|
|
572
|
-
# Normalize all index naming format
|
|
573
|
-
return mcs._normalize_all_indexes(merged_indexes, table_name)
|
|
574
|
-
|
|
575
|
-
@classmethod
|
|
576
|
-
def _normalize_all_indexes(mcs, indexes: list[Index], table_name: str) -> list[Index]:
|
|
577
|
-
"""Normalize index names that need normalization.
|
|
578
|
-
|
|
579
|
-
Only normalizes indexes with temporary names (starting with __temp__idx_).
|
|
580
|
-
|
|
581
|
-
Args:
|
|
582
|
-
indexes: List of indexes to normalize
|
|
583
|
-
table_name: Database table name
|
|
584
|
-
|
|
585
|
-
Returns:
|
|
586
|
-
List of indexes with normalized names
|
|
587
|
-
"""
|
|
588
|
-
normalized_indexes = []
|
|
589
|
-
|
|
590
|
-
for idx in indexes:
|
|
591
|
-
# Only normalize temporary names
|
|
592
|
-
if not idx.name or not idx.name.startswith(_TEMP_INDEX_PREFIX):
|
|
593
|
-
normalized_indexes.append(idx)
|
|
594
|
-
continue
|
|
595
|
-
|
|
596
|
-
# Get field name list
|
|
597
|
-
if hasattr(idx, "columns") and idx.columns:
|
|
598
|
-
field_names = "_".join(col.name for col in idx.columns) # noqa
|
|
599
|
-
elif hasattr(idx, "_columns") and idx._columns: # noqa
|
|
600
|
-
# Handle indexes not yet bound to table
|
|
601
|
-
field_names = "_".join(str(col).split(".")[-1] for col in idx._columns) # noqa
|
|
602
|
-
else:
|
|
603
|
-
normalized_indexes.append(idx)
|
|
604
|
-
continue
|
|
605
|
-
|
|
606
|
-
# Generate standardized name
|
|
607
|
-
idx.name = f"idx_{table_name}_{field_names}" # type: ignore[reportAttributeAccessIssue]
|
|
608
|
-
normalized_indexes.append(idx)
|
|
609
|
-
|
|
610
|
-
return normalized_indexes
|
|
611
|
-
|
|
612
503
|
@classmethod
|
|
613
504
|
def _register_field_validators(mcs, cls: Any, config: ModelConfig) -> None:
|
|
614
505
|
"""Register field-level validators to model class.
|
|
@@ -634,6 +525,14 @@ class ModelProcessor(type):
|
|
|
634
525
|
"""
|
|
635
526
|
from sqlalchemy import Table
|
|
636
527
|
|
|
528
|
+
# Collect fields with explicit indexes to avoid duplicates
|
|
529
|
+
indexed_fields = set()
|
|
530
|
+
for idx in config.indexes:
|
|
531
|
+
if hasattr(idx, "_columns"):
|
|
532
|
+
for col in idx._columns:
|
|
533
|
+
if isinstance(col, str):
|
|
534
|
+
indexed_fields.add(col.split(".")[-1])
|
|
535
|
+
|
|
637
536
|
# Collect column definitions and relationship fields
|
|
638
537
|
columns = []
|
|
639
538
|
relationships = {}
|
|
@@ -651,6 +550,10 @@ class ModelProcessor(type):
|
|
|
651
550
|
# Use create_table_column method to get independent Column instance
|
|
652
551
|
if hasattr(column_attr, "create_table_column"):
|
|
653
552
|
column = column_attr.create_table_column(name)
|
|
553
|
+
# Clear index attributes if field has explicit index
|
|
554
|
+
if name in indexed_fields:
|
|
555
|
+
column.index = False
|
|
556
|
+
column.unique = False
|
|
654
557
|
else:
|
|
655
558
|
# Fallback for non-ColumnAttribute fields
|
|
656
559
|
column = column_attr
|
|
@@ -685,22 +588,40 @@ class ModelProcessor(type):
|
|
|
685
588
|
|
|
686
589
|
@classmethod
|
|
687
590
|
def _post_process_table_indexes(mcs, table, table_name: str) -> None:
|
|
688
|
-
|
|
591
|
+
def col_sig(idx):
|
|
592
|
+
return tuple(sorted(col.name for col in idx.columns)) if idx.columns else None
|
|
689
593
|
|
|
690
|
-
|
|
594
|
+
full_sig_map: dict[tuple, list] = {} # (cols, unique) -> indexes
|
|
595
|
+
col_map: dict[tuple, list] = {} # cols -> indexes
|
|
691
596
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
"""
|
|
696
|
-
for idx in table.indexes:
|
|
697
|
-
# Only normalize temporary names
|
|
698
|
-
if not idx.name or not idx.name.startswith(_TEMP_INDEX_PREFIX):
|
|
597
|
+
for idx in list(table.indexes):
|
|
598
|
+
cs = col_sig(idx)
|
|
599
|
+
if cs is None:
|
|
699
600
|
continue
|
|
601
|
+
full_sig_map.setdefault((cs, getattr(idx, "unique", False)), []).append(idx)
|
|
602
|
+
col_map.setdefault(cs, []).append(idx)
|
|
603
|
+
|
|
604
|
+
to_remove: set = set()
|
|
700
605
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
606
|
+
# Remove exact duplicates: prefer explicit names over SQLAlchemy auto-generated "ix_*"
|
|
607
|
+
for indexes in full_sig_map.values():
|
|
608
|
+
if len(indexes) > 1:
|
|
609
|
+
explicit = [i for i in indexes if not (i.name and i.name.startswith("ix_"))]
|
|
610
|
+
keep = explicit[0] if explicit else indexes[0]
|
|
611
|
+
to_remove.update(i for i in indexes if i is not keep)
|
|
612
|
+
|
|
613
|
+
# Remove non-unique when unique exists on same columns
|
|
614
|
+
for indexes in col_map.values():
|
|
615
|
+
if any(getattr(i, "unique", False) for i in indexes):
|
|
616
|
+
to_remove.update(i for i in indexes if not getattr(i, "unique", False))
|
|
617
|
+
|
|
618
|
+
for idx in to_remove:
|
|
619
|
+
table.indexes.discard(idx)
|
|
620
|
+
|
|
621
|
+
# Normalize temp names after dedup
|
|
622
|
+
for idx in table.indexes:
|
|
623
|
+
if idx.name and idx.name.startswith(_TEMP_INDEX_PREFIX) and idx.columns:
|
|
624
|
+
idx.name = f"idx_{table_name}_{'_'.join(col.name for col in idx.columns)}"
|
|
704
625
|
|
|
705
626
|
@classmethod
|
|
706
627
|
def _post_process_table_constraints(mcs, table, table_name: str) -> None:
|
|
@@ -946,7 +867,7 @@ class ModelProcessor(type):
|
|
|
946
867
|
if column_attr is None or not hasattr(column_attr, "__column__"):
|
|
947
868
|
break
|
|
948
869
|
if klass is cls:
|
|
949
|
-
column_attr.__column__ = table.columns[name]
|
|
870
|
+
column_attr.__column__ = table.columns[name]
|
|
950
871
|
else:
|
|
951
872
|
new_col_attr = object.__new__(type(column_attr))
|
|
952
873
|
new_col_attr.__dict__.update(column_attr.__dict__)
|
|
@@ -1023,97 +944,37 @@ class ModelProcessor(type):
|
|
|
1023
944
|
|
|
1024
945
|
|
|
1025
946
|
def _parse_model_config(model_class: Any) -> ModelConfig:
|
|
1026
|
-
"""Parse complete configuration for a model class.
|
|
1027
|
-
|
|
1028
|
-
Args:
|
|
1029
|
-
model_class: Model class to process configuration for
|
|
1030
|
-
|
|
1031
|
-
Returns:
|
|
1032
|
-
Complete ModelConfig with all defaults filled
|
|
1033
|
-
"""
|
|
1034
947
|
config_class = getattr(model_class, "Config", None)
|
|
1035
|
-
if config_class
|
|
1036
|
-
raw_config = _parse_config_class(config_class)
|
|
1037
|
-
else:
|
|
1038
|
-
raw_config = _RawModelConfig()
|
|
1039
|
-
|
|
1040
|
-
return _fill_config_defaults(raw_config, model_class)
|
|
948
|
+
raw = _parse_config_class(config_class) if config_class else _RawModelConfig()
|
|
1041
949
|
|
|
950
|
+
table_name = raw.table_name or pluralize(to_snake_case(model_class.__name__))
|
|
951
|
+
verbose_name = raw.verbose_name or model_class.__name__
|
|
952
|
+
verbose_name_plural = raw.verbose_name_plural or pluralize(verbose_name)
|
|
1042
953
|
|
|
1043
|
-
def _parse_config_class(config_class: type) -> _RawModelConfig:
|
|
1044
|
-
"""Parse configuration from a Config inner class.
|
|
1045
|
-
|
|
1046
|
-
Args:
|
|
1047
|
-
config_class: The Config inner class to parse
|
|
1048
|
-
|
|
1049
|
-
Returns:
|
|
1050
|
-
_RawModelConfig instance with parsed configuration
|
|
1051
|
-
"""
|
|
1052
|
-
config = _RawModelConfig()
|
|
1053
|
-
|
|
1054
|
-
# Basic configuration
|
|
1055
|
-
config.table_name = getattr(config_class, "table_name", None)
|
|
1056
|
-
config.ordering = getattr(config_class, "ordering", [])
|
|
1057
|
-
|
|
1058
|
-
# Index configuration
|
|
1059
|
-
config.indexes = getattr(config_class, "indexes", [])
|
|
1060
|
-
|
|
1061
|
-
# Constraint configuration
|
|
1062
|
-
config.constraints = getattr(config_class, "constraints", [])
|
|
1063
|
-
|
|
1064
|
-
# Metadata
|
|
1065
|
-
config.verbose_name = getattr(config_class, "verbose_name", None)
|
|
1066
|
-
config.verbose_name_plural = getattr(config_class, "verbose_name_plural", None)
|
|
1067
|
-
config.description = getattr(config_class, "description", None)
|
|
1068
|
-
|
|
1069
|
-
# Database-specific configuration
|
|
1070
|
-
config.db_options = getattr(config_class, "db_options", {})
|
|
1071
|
-
|
|
1072
|
-
# Custom configuration
|
|
1073
|
-
config.custom = getattr(config_class, "custom", {})
|
|
1074
|
-
|
|
1075
|
-
return config
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
def _fill_config_defaults(config: _RawModelConfig, model_class: Any) -> ModelConfig:
|
|
1079
|
-
"""Fill default values for configuration fields that are None.
|
|
1080
|
-
|
|
1081
|
-
Args:
|
|
1082
|
-
config: _RawModelConfig instance to fill defaults for
|
|
1083
|
-
model_class: Model class to generate defaults from
|
|
1084
|
-
|
|
1085
|
-
Returns:
|
|
1086
|
-
ModelConfig instance with defaults filled
|
|
1087
|
-
"""
|
|
1088
|
-
# Fill table_name if not set
|
|
1089
|
-
table_name = config.table_name
|
|
1090
|
-
if table_name is None:
|
|
1091
|
-
snake_case_name = to_snake_case(model_class.__name__)
|
|
1092
|
-
table_name = pluralize(snake_case_name)
|
|
1093
|
-
|
|
1094
|
-
# Fill verbose_name if not set
|
|
1095
|
-
verbose_name = config.verbose_name
|
|
1096
|
-
if verbose_name is None:
|
|
1097
|
-
verbose_name = model_class.__name__
|
|
1098
|
-
|
|
1099
|
-
# Fill verbose_name_plural if not set
|
|
1100
|
-
verbose_name_plural = config.verbose_name_plural
|
|
1101
|
-
if verbose_name_plural is None:
|
|
1102
|
-
verbose_name_plural = pluralize(verbose_name)
|
|
1103
|
-
|
|
1104
|
-
# Create complete config with required fields
|
|
1105
954
|
return ModelConfig(
|
|
1106
955
|
table_name=table_name,
|
|
1107
956
|
verbose_name=verbose_name,
|
|
1108
957
|
verbose_name_plural=verbose_name_plural,
|
|
1109
|
-
ordering=
|
|
1110
|
-
indexes=
|
|
1111
|
-
constraints=
|
|
1112
|
-
description=
|
|
1113
|
-
db_options=
|
|
1114
|
-
custom=
|
|
1115
|
-
|
|
1116
|
-
|
|
958
|
+
ordering=raw.ordering,
|
|
959
|
+
indexes=raw.indexes,
|
|
960
|
+
constraints=raw.constraints,
|
|
961
|
+
description=raw.description,
|
|
962
|
+
db_options=raw.db_options,
|
|
963
|
+
custom=raw.custom,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _parse_config_class(config_class: type) -> _RawModelConfig:
|
|
968
|
+
return _RawModelConfig(
|
|
969
|
+
table_name=getattr(config_class, "table_name", None),
|
|
970
|
+
verbose_name=getattr(config_class, "verbose_name", None),
|
|
971
|
+
verbose_name_plural=getattr(config_class, "verbose_name_plural", None),
|
|
972
|
+
ordering=getattr(config_class, "ordering", []),
|
|
973
|
+
indexes=getattr(config_class, "indexes", []),
|
|
974
|
+
constraints=getattr(config_class, "constraints", []),
|
|
975
|
+
description=getattr(config_class, "description", None),
|
|
976
|
+
db_options=getattr(config_class, "db_options", {}),
|
|
977
|
+
custom=getattr(config_class, "custom", {}),
|
|
1117
978
|
)
|
|
1118
979
|
|
|
1119
980
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import gc
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import Any, TypeVar, overload
|
|
3
5
|
|
|
4
6
|
from sqlalchemy import (
|
|
5
7
|
delete,
|
|
@@ -11,6 +13,9 @@ from sqlalchemy import (
|
|
|
11
13
|
)
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
_T = TypeVar("_T")
|
|
17
|
+
|
|
18
|
+
|
|
14
19
|
class QueryExecutor:
|
|
15
20
|
"""Unified query execution engine with caching and iterator support.
|
|
16
21
|
|
|
@@ -35,6 +40,14 @@ class QueryExecutor:
|
|
|
35
40
|
readonly = query_type not in self._WRITE_TYPES
|
|
36
41
|
return get_session(self._db_or_session, readonly=readonly)
|
|
37
42
|
|
|
43
|
+
@overload
|
|
44
|
+
async def execute(
|
|
45
|
+
self, query: Any, query_type: str = ..., *, builder: Any = ..., model_class: type[_T], **kwargs: Any
|
|
46
|
+
) -> list[_T]: ...
|
|
47
|
+
@overload
|
|
48
|
+
async def execute(
|
|
49
|
+
self, query: Any, query_type: str = ..., *, builder: Any = ..., model_class: None = ..., **kwargs: Any
|
|
50
|
+
) -> list[Any] | int | bool: ...
|
|
38
51
|
async def execute(
|
|
39
52
|
self,
|
|
40
53
|
query,
|
|
@@ -91,7 +104,7 @@ class QueryExecutor:
|
|
|
91
104
|
|
|
92
105
|
return result
|
|
93
106
|
|
|
94
|
-
async def iterator(self, query, chunk_size: int = 1000):
|
|
107
|
+
async def iterator(self, query, chunk_size: int = 1000) -> AsyncGenerator[Any, None]:
|
|
95
108
|
"""Async iterator for processing large datasets in chunks."""
|
|
96
109
|
offset = 0
|
|
97
110
|
processed_chunks = 0
|
|
@@ -100,7 +113,7 @@ class QueryExecutor:
|
|
|
100
113
|
chunk_query = query.offset(offset).limit(chunk_size)
|
|
101
114
|
chunk = await self._execute_query(chunk_query, "all")
|
|
102
115
|
|
|
103
|
-
if not chunk:
|
|
116
|
+
if not isinstance(chunk, list) or not chunk:
|
|
104
117
|
break
|
|
105
118
|
|
|
106
119
|
for item in chunk:
|
|
@@ -957,11 +957,12 @@ class QuerySet(Generic[T]):
|
|
|
957
957
|
|
|
958
958
|
async def raw(self, sql: str, params: dict | None = None) -> list[T]:
|
|
959
959
|
"""Execute raw SQL query and return model instances."""
|
|
960
|
-
|
|
960
|
+
session = self._executor._get_session("all")
|
|
961
|
+
if not session:
|
|
961
962
|
return []
|
|
962
963
|
|
|
963
964
|
query = text(sql)
|
|
964
|
-
result = await
|
|
965
|
+
result = await session.execute(query, params or {})
|
|
965
966
|
|
|
966
967
|
instances = []
|
|
967
968
|
for row in result:
|
|
@@ -1007,8 +1008,9 @@ class QuerySet(Generic[T]):
|
|
|
1007
1008
|
|
|
1008
1009
|
# Get database dialect
|
|
1009
1010
|
dialect_name = "unknown"
|
|
1010
|
-
|
|
1011
|
-
|
|
1011
|
+
session = self._executor._get_session("all")
|
|
1012
|
+
if session and hasattr(session, "bind"):
|
|
1013
|
+
dialect_name = session.bind.dialect.name
|
|
1012
1014
|
|
|
1013
1015
|
# Database-specific date expression
|
|
1014
1016
|
if dialect_name == "postgresql":
|
|
@@ -1109,8 +1111,9 @@ class QuerySet(Generic[T]):
|
|
|
1109
1111
|
|
|
1110
1112
|
# Get database dialect
|
|
1111
1113
|
dialect_name = "unknown"
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
+
session = self._executor._get_session("all")
|
|
1115
|
+
if session and hasattr(session, "bind"):
|
|
1116
|
+
dialect_name = session.bind.dialect.name
|
|
1114
1117
|
|
|
1115
1118
|
# Database-specific datetime expression
|
|
1116
1119
|
if dialect_name == "postgresql":
|
|
@@ -172,6 +172,15 @@ class AsyncSession:
|
|
|
172
172
|
async def _handle_exception(self, exc: Exception):
|
|
173
173
|
"""Handle exceptions with unified error processing."""
|
|
174
174
|
await self.rollback()
|
|
175
|
+
|
|
176
|
+
# Log detailed error for debugging when echo is enabled
|
|
177
|
+
if hasattr(self.bind, "echo") and getattr(self.bind, "echo", False):
|
|
178
|
+
print(f"SQLObjects Session Error: {exc}")
|
|
179
|
+
if hasattr(exc, "statement"):
|
|
180
|
+
print(f"Statement: {exc.statement}") # type: ignore[reportAttributeAccessIssue]
|
|
181
|
+
if hasattr(exc, "params"):
|
|
182
|
+
print(f"Parameters: {exc.params}") # type: ignore[reportAttributeAccessIssue]
|
|
183
|
+
|
|
175
184
|
if isinstance(exc, SQLAlchemyError):
|
|
176
185
|
raise convert_sqlalchemy_error(exc) from exc
|
|
177
186
|
raise
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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>
|
|
@@ -44,7 +44,7 @@ class TestDatabaseConfig:
|
|
|
44
44
|
return config
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
async def
|
|
47
|
+
async def _check_database_connection(db_type: str, url: str) -> bool:
|
|
48
48
|
"""测试数据库连接"""
|
|
49
49
|
if db_type == "postgresql":
|
|
50
50
|
import asyncpg # type: ignore[reportMissingImports]
|
|
@@ -84,7 +84,7 @@ async def check_database_connection(db_type: str) -> bool:
|
|
|
84
84
|
__import__(driver)
|
|
85
85
|
|
|
86
86
|
# 测试连接
|
|
87
|
-
return await
|
|
87
|
+
return await _check_database_connection(db_type, config["url"])
|
|
88
88
|
|
|
89
89
|
except ImportError:
|
|
90
90
|
print(f"❌ {config.get('driver')} not installed for {db_type} testing") # type: ignore[reportPossiblyUnboundVariable]
|
|
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
|