sqlobjects 1.1.0__tar.gz → 1.2.1__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.1.0 → sqlobjects-1.2.1}/CHANGELOG.md +12 -0
- {sqlobjects-1.1.0/sqlobjects.egg-info → sqlobjects-1.2.1}/PKG-INFO +1 -1
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/pyproject.toml +10 -10
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/metadata.py +33 -12
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/mixins.py +1 -4
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/core.py +1 -6
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/dialect.py +5 -4
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/executor.py +20 -9
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/session.py +25 -6
- {sqlobjects-1.1.0 → sqlobjects-1.2.1/sqlobjects.egg-info}/PKG-INFO +1 -1
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/LICENSE +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/README.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/01-database-session-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/02-model-definition-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/03-query-operations-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/04-crud-operations-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/05-relationships-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/07-performance-guide.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/README.md +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/setup.cfg +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/cascade.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/database/manager.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/exceptions.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/cte.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/function.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/mixins.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/core.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/proxies.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/descriptors.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/prefetch.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/utils.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/comparators.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/registry.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/model.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/builder.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queryset.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/SOURCES.txt +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/top_level.txt +0 -0
- {sqlobjects-1.1.0 → sqlobjects-1.2.1}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlobjects"
|
|
3
|
-
version = "1.1
|
|
3
|
+
version = "1.2.1"
|
|
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"
|
|
@@ -49,22 +49,22 @@ Changelog = "https://github.com/XtraVisionsAI/sqlobjects/blob/main/CHANGELOG.md"
|
|
|
49
49
|
|
|
50
50
|
[dependency-groups]
|
|
51
51
|
dev = [
|
|
52
|
-
"pre-commit>=4.
|
|
53
|
-
"pyright>=1.1.
|
|
54
|
-
"ruff>=0.
|
|
55
|
-
"setuptools>=
|
|
52
|
+
"pre-commit>=4.5.1",
|
|
53
|
+
"pyright>=1.1.408",
|
|
54
|
+
"ruff>=0.15.2",
|
|
55
|
+
"setuptools>=82.0.0",
|
|
56
56
|
]
|
|
57
57
|
test = [
|
|
58
58
|
"aiomysql>=0.3.2",
|
|
59
|
-
"aiosqlite>=0.
|
|
60
|
-
"asyncpg>=0.
|
|
61
|
-
"pytest>=9.0.
|
|
59
|
+
"aiosqlite>=0.22.1",
|
|
60
|
+
"asyncpg>=0.31.0",
|
|
61
|
+
"pytest>=9.0.2",
|
|
62
62
|
"pytest-asyncio>=1.3.0",
|
|
63
|
-
"psutil>=7.
|
|
63
|
+
"psutil>=7.2.2", # For memory monitoring in performance tests
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
[build-system]
|
|
67
|
-
requires = ["setuptools>=
|
|
67
|
+
requires = ["setuptools>=82.0.0", "wheel"]
|
|
68
68
|
build-backend = "setuptools.build_meta"
|
|
69
69
|
|
|
70
70
|
[tool.setuptools]
|
|
@@ -2,7 +2,7 @@ import re
|
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Union, cast
|
|
4
4
|
|
|
5
|
-
from sqlalchemy import CheckConstraint, Index, UniqueConstraint
|
|
5
|
+
from sqlalchemy import CheckConstraint, ForeignKeyConstraint, Index, UniqueConstraint
|
|
6
6
|
from sqlalchemy import MetaData as SqlAlchemyMetaData
|
|
7
7
|
|
|
8
8
|
from .fields import ColumnAttribute
|
|
@@ -24,8 +24,8 @@ __all__ = [
|
|
|
24
24
|
"unique",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
_FIELD_NAME_PATTERN = re.compile(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\b")
|
|
28
|
+
_TEMP_INDEX_PREFIX = "__temp__idx_"
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
|
@@ -574,7 +574,9 @@ class ModelProcessor(type):
|
|
|
574
574
|
|
|
575
575
|
@classmethod
|
|
576
576
|
def _normalize_all_indexes(mcs, indexes: list[Index], table_name: str) -> list[Index]:
|
|
577
|
-
"""
|
|
577
|
+
"""Normalize index names that need normalization.
|
|
578
|
+
|
|
579
|
+
Only normalizes indexes with temporary names (starting with __temp__idx_).
|
|
578
580
|
|
|
579
581
|
Args:
|
|
580
582
|
indexes: List of indexes to normalize
|
|
@@ -586,6 +588,11 @@ class ModelProcessor(type):
|
|
|
586
588
|
normalized_indexes = []
|
|
587
589
|
|
|
588
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
|
+
|
|
589
596
|
# Get field name list
|
|
590
597
|
if hasattr(idx, "columns") and idx.columns:
|
|
591
598
|
field_names = "_".join(col.name for col in idx.columns) # noqa
|
|
@@ -597,10 +604,7 @@ class ModelProcessor(type):
|
|
|
597
604
|
continue
|
|
598
605
|
|
|
599
606
|
# Generate standardized name
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
# Directly modify index name (instead of rebuilding)
|
|
603
|
-
idx.name = new_name # type: ignore[reportAttributeAccessIssue]
|
|
607
|
+
idx.name = f"idx_{table_name}_{field_names}" # type: ignore[reportAttributeAccessIssue]
|
|
604
608
|
normalized_indexes.append(idx)
|
|
605
609
|
|
|
606
610
|
return normalized_indexes
|
|
@@ -683,15 +687,20 @@ class ModelProcessor(type):
|
|
|
683
687
|
def _post_process_table_indexes(mcs, table, table_name: str) -> None:
|
|
684
688
|
"""Normalize index names after table construction.
|
|
685
689
|
|
|
690
|
+
Only normalizes indexes with temporary names (starting with __temp__idx_).
|
|
691
|
+
|
|
686
692
|
Args:
|
|
687
693
|
table: SQLAlchemy Table instance
|
|
688
694
|
table_name: Database table name
|
|
689
695
|
"""
|
|
690
696
|
for idx in table.indexes:
|
|
697
|
+
# Only normalize temporary names
|
|
698
|
+
if not idx.name or not idx.name.startswith(_TEMP_INDEX_PREFIX):
|
|
699
|
+
continue
|
|
700
|
+
|
|
691
701
|
if hasattr(idx, "columns") and idx.columns:
|
|
692
702
|
field_names = "_".join(col.name for col in idx.columns)
|
|
693
|
-
|
|
694
|
-
idx.name = new_name
|
|
703
|
+
idx.name = f"idx_{table_name}_{field_names}"
|
|
695
704
|
|
|
696
705
|
@classmethod
|
|
697
706
|
def _post_process_table_constraints(mcs, table, table_name: str) -> None:
|
|
@@ -714,6 +723,20 @@ class ModelProcessor(type):
|
|
|
714
723
|
elif isinstance(cst, UniqueConstraint) and hasattr(cst, "columns"):
|
|
715
724
|
field_names = "_".join(col.name for col in cst.columns)
|
|
716
725
|
cst.name = f"uq_{table_name}_{field_names}"
|
|
726
|
+
elif isinstance(cst, ForeignKeyConstraint) and hasattr(cst, "columns"):
|
|
727
|
+
# Handle foreign key constraints
|
|
728
|
+
field_names = "_".join(col.name for col in cst.columns)
|
|
729
|
+
# Get referenced table and column names
|
|
730
|
+
if cst.elements:
|
|
731
|
+
try:
|
|
732
|
+
ref_table = cst.elements[0].column.table.name
|
|
733
|
+
ref_columns = "_".join(elem.column.name for elem in cst.elements)
|
|
734
|
+
cst.name = f"fk_{table_name}_{field_names}_{ref_table}_{ref_columns}"
|
|
735
|
+
except Exception:
|
|
736
|
+
# Fallback if reference cannot be resolved yet
|
|
737
|
+
cst.name = f"fk_{table_name}_{field_names}"
|
|
738
|
+
else:
|
|
739
|
+
cst.name = f"fk_{table_name}_{field_names}"
|
|
717
740
|
|
|
718
741
|
@classmethod
|
|
719
742
|
def _apply_dataclass_functionality(mcs, cls: Any) -> Any:
|
|
@@ -1114,11 +1137,9 @@ def index(
|
|
|
1114
1137
|
>>> index("idx_users_status", "status", postgresql_where="status = 'active'")
|
|
1115
1138
|
>>> index("idx_users_tags", "tags", postgresql_using="gin")
|
|
1116
1139
|
"""
|
|
1117
|
-
# Note: Don't auto-generate name here because table_name is needed
|
|
1118
|
-
# Actual name normalization is handled in _merge_indexes
|
|
1119
1140
|
if name is None:
|
|
1120
1141
|
field_part = "_".join(fields)
|
|
1121
|
-
name = f"
|
|
1142
|
+
name = f"{_TEMP_INDEX_PREFIX}{field_part}"
|
|
1122
1143
|
|
|
1123
1144
|
# Build dialect-specific kwargs
|
|
1124
1145
|
dialect_kwargs = {}
|
|
@@ -165,10 +165,7 @@ class SessionMixin(BaseMixin):
|
|
|
165
165
|
Returns:
|
|
166
166
|
AsyncSession instance for database operations
|
|
167
167
|
"""
|
|
168
|
-
|
|
169
|
-
if isinstance(bound_session, str):
|
|
170
|
-
return get_session(bound_session)
|
|
171
|
-
return bound_session or get_session()
|
|
168
|
+
return get_session(self._state_manager.get_bound_session())
|
|
172
169
|
|
|
173
170
|
def using(self, db_or_session: str | AsyncSession):
|
|
174
171
|
"""Return self bound to specific database/connection.
|
|
@@ -82,12 +82,7 @@ class ObjectsManager(Generic[T]):
|
|
|
82
82
|
Returns:
|
|
83
83
|
AsyncSession instance
|
|
84
84
|
"""
|
|
85
|
-
|
|
86
|
-
return get_session(readonly=readonly)
|
|
87
|
-
elif isinstance(self._db_or_session, str):
|
|
88
|
-
return get_session(self._db_or_session, readonly=readonly)
|
|
89
|
-
else:
|
|
90
|
-
return self._db_or_session
|
|
85
|
+
return get_session(self._db_or_session, readonly=readonly)
|
|
91
86
|
|
|
92
87
|
def _validate_field_names(self, **kwargs) -> None:
|
|
93
88
|
"""Validate that all field names exist on the model.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Database dialect handlers for database-specific SQL generation."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from typing import TYPE_CHECKING, Any
|
|
5
6
|
|
|
6
7
|
import sqlalchemy as sa
|
|
@@ -46,7 +47,7 @@ class BaseDialect(ABC):
|
|
|
46
47
|
pass
|
|
47
48
|
|
|
48
49
|
@abstractmethod
|
|
49
|
-
def parse_explain_result(self, rows:
|
|
50
|
+
def parse_explain_result(self, rows: Sequence) -> str:
|
|
50
51
|
"""Parse EXPLAIN result for the database."""
|
|
51
52
|
pass
|
|
52
53
|
|
|
@@ -108,7 +109,7 @@ class PostgreSQLDialect(BaseDialect):
|
|
|
108
109
|
return f"EXPLAIN ({', '.join(options)}) {sql}"
|
|
109
110
|
return f"EXPLAIN {sql}"
|
|
110
111
|
|
|
111
|
-
def parse_explain_result(self, rows:
|
|
112
|
+
def parse_explain_result(self, rows: Sequence) -> str:
|
|
112
113
|
"""Parse PostgreSQL EXPLAIN result."""
|
|
113
114
|
return "\n".join(str(row[0]) for row in rows)
|
|
114
115
|
|
|
@@ -151,7 +152,7 @@ class MySQLDialect(BaseDialect):
|
|
|
151
152
|
return f"EXPLAIN ANALYZE {sql}"
|
|
152
153
|
return f"EXPLAIN {sql}"
|
|
153
154
|
|
|
154
|
-
def parse_explain_result(self, rows:
|
|
155
|
+
def parse_explain_result(self, rows: Sequence) -> str:
|
|
155
156
|
"""Parse MySQL EXPLAIN result."""
|
|
156
157
|
return "\n".join(str(row[0]) for row in rows)
|
|
157
158
|
|
|
@@ -199,7 +200,7 @@ class SQLiteDialect(BaseDialect):
|
|
|
199
200
|
"""
|
|
200
201
|
return f"EXPLAIN QUERY PLAN {sql}"
|
|
201
202
|
|
|
202
|
-
def parse_explain_result(self, rows:
|
|
203
|
+
def parse_explain_result(self, rows: Sequence) -> str:
|
|
203
204
|
"""Parse SQLite EXPLAIN result.
|
|
204
205
|
|
|
205
206
|
SQLite EXPLAIN QUERY PLAN returns: (id, parent, notused, detail)
|
|
@@ -18,13 +18,22 @@ class QueryExecutor:
|
|
|
18
18
|
aggregations, and memory-efficient iteration for large datasets.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
_WRITE_TYPES = frozenset({"update", "delete"})
|
|
22
|
+
|
|
21
23
|
def __init__(self, session=None):
|
|
22
24
|
"""Initialize executor with optional session.
|
|
23
25
|
|
|
24
26
|
Args:
|
|
25
|
-
session: Database session
|
|
27
|
+
session: Database session, database name string, or None
|
|
26
28
|
"""
|
|
27
|
-
self.
|
|
29
|
+
self._db_or_session = session
|
|
30
|
+
|
|
31
|
+
def _get_session(self, query_type: str):
|
|
32
|
+
"""Resolve session with appropriate readonly flag."""
|
|
33
|
+
from ..session import get_session
|
|
34
|
+
|
|
35
|
+
readonly = query_type not in self._WRITE_TYPES
|
|
36
|
+
return get_session(self._db_or_session, readonly=readonly)
|
|
28
37
|
|
|
29
38
|
async def execute(
|
|
30
39
|
self,
|
|
@@ -115,23 +124,24 @@ class QueryExecutor:
|
|
|
115
124
|
Returns:
|
|
116
125
|
Query execution plan as string
|
|
117
126
|
"""
|
|
118
|
-
|
|
127
|
+
session = self._get_session("all")
|
|
128
|
+
if not session:
|
|
119
129
|
raise RuntimeError("No session available for explain operation")
|
|
120
130
|
|
|
121
131
|
# Get dialect handler
|
|
122
132
|
from .dialect import DialectHandler
|
|
123
133
|
|
|
124
|
-
dialect = DialectHandler.create(
|
|
134
|
+
dialect = DialectHandler.create(session)
|
|
125
135
|
|
|
126
136
|
# Compile query to SQL
|
|
127
|
-
compiled = query.compile(dialect=
|
|
137
|
+
compiled = query.compile(dialect=session.bind.dialect, compile_kwargs={"literal_binds": True})
|
|
128
138
|
sql_str = str(compiled)
|
|
129
139
|
|
|
130
140
|
# Build EXPLAIN query using dialect handler
|
|
131
141
|
explain_sql = dialect.build_explain_query(sql_str, analyze, verbose)
|
|
132
142
|
|
|
133
143
|
# Execute query
|
|
134
|
-
result = await
|
|
144
|
+
result = await session.execute(text(explain_sql))
|
|
135
145
|
rows = result.fetchall()
|
|
136
146
|
|
|
137
147
|
# Parse result using dialect handler
|
|
@@ -196,7 +206,8 @@ class QueryExecutor:
|
|
|
196
206
|
relationships=None,
|
|
197
207
|
):
|
|
198
208
|
"""Execute query and return appropriate result."""
|
|
199
|
-
|
|
209
|
+
session = self._get_session(query_type)
|
|
210
|
+
if not session:
|
|
200
211
|
if query_type == "all":
|
|
201
212
|
return []
|
|
202
213
|
elif query_type in ("count", "update", "delete"):
|
|
@@ -206,7 +217,7 @@ class QueryExecutor:
|
|
|
206
217
|
else: # exists
|
|
207
218
|
return False
|
|
208
219
|
|
|
209
|
-
result = await
|
|
220
|
+
result = await session.execute(query)
|
|
210
221
|
|
|
211
222
|
if query_type == "all":
|
|
212
223
|
rows = result.fetchall()
|
|
@@ -417,7 +428,7 @@ class QueryExecutor:
|
|
|
417
428
|
if not instances or not prefetch_relationships:
|
|
418
429
|
return instances
|
|
419
430
|
|
|
420
|
-
prefetch_handler = PrefetchHandler(self.
|
|
431
|
+
prefetch_handler = PrefetchHandler(self._get_session("all"))
|
|
421
432
|
return await prefetch_handler.handle_prefetch_relationships(instances, prefetch_relationships)
|
|
422
433
|
|
|
423
434
|
@staticmethod
|
|
@@ -3,7 +3,7 @@ from collections.abc import AsyncGenerator
|
|
|
3
3
|
from contextlib import asynccontextmanager
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from sqlalchemy import CursorResult
|
|
6
|
+
from sqlalchemy import CursorResult, text
|
|
7
7
|
from sqlalchemy.exc import SQLAlchemyError
|
|
8
8
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncResult
|
|
9
9
|
|
|
@@ -13,7 +13,6 @@ from .exceptions import convert_sqlalchemy_error
|
|
|
13
13
|
|
|
14
14
|
__all__ = ["AsyncSession", "ctx_session", "ctx_sessions", "get_session", "has_session"]
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
# Explicit session management (highest priority)
|
|
18
17
|
_explicit_sessions: contextvars.ContextVar[dict[str, "AsyncSession"]] = contextvars.ContextVar("explicit_sessions")
|
|
19
18
|
|
|
@@ -64,9 +63,17 @@ class AsyncSession:
|
|
|
64
63
|
return get_database(self._db_name).engine
|
|
65
64
|
|
|
66
65
|
async def execute(self, statement: Any, parameters: Any = None) -> CursorResult[Any]:
|
|
67
|
-
"""Execute statement with automatic transaction management.
|
|
66
|
+
"""Execute statement with automatic transaction management.
|
|
67
|
+
|
|
68
|
+
Supports both SQLAlchemy statement objects and raw SQL strings.
|
|
69
|
+
Raw SQL strings are automatically wrapped with text().
|
|
70
|
+
"""
|
|
68
71
|
await self._ensure_connection()
|
|
69
72
|
|
|
73
|
+
# Auto-wrap string SQL with text()
|
|
74
|
+
if isinstance(statement, str):
|
|
75
|
+
statement = text(statement)
|
|
76
|
+
|
|
70
77
|
# Auto-begin transaction for non-readonly sessions
|
|
71
78
|
if not self.readonly and self._trans is None:
|
|
72
79
|
self._trans = await self._conn.begin() # type: ignore
|
|
@@ -89,6 +96,9 @@ class AsyncSession:
|
|
|
89
96
|
async def stream(self, statement: Any, parameters: Any = None) -> AsyncResult[Any]:
|
|
90
97
|
"""Execute statement and return streaming result.
|
|
91
98
|
|
|
99
|
+
Supports both SQLAlchemy statement objects and raw SQL strings.
|
|
100
|
+
Raw SQL strings are automatically wrapped with text().
|
|
101
|
+
|
|
92
102
|
Note: stream() is not supported with auto_commit=True sessions.
|
|
93
103
|
Use explicit sessions (ctx_session) for streaming operations.
|
|
94
104
|
"""
|
|
@@ -99,6 +109,10 @@ class AsyncSession:
|
|
|
99
109
|
|
|
100
110
|
await self._ensure_connection()
|
|
101
111
|
|
|
112
|
+
# Auto-wrap string SQL with text()
|
|
113
|
+
if isinstance(statement, str):
|
|
114
|
+
statement = text(statement)
|
|
115
|
+
|
|
102
116
|
# Auto-begin transaction for non-readonly sessions
|
|
103
117
|
if not self.readonly and self._trans is None:
|
|
104
118
|
self._trans = await self._conn.begin() # type: ignore
|
|
@@ -331,11 +345,13 @@ async def ctx_sessions(*db_names: str) -> AsyncGenerator[dict[str, AsyncSession]
|
|
|
331
345
|
_SessionContextManager.clear_session(db_name)
|
|
332
346
|
|
|
333
347
|
|
|
334
|
-
def get_session(
|
|
348
|
+
def get_session(
|
|
349
|
+
db_or_name: str | AsyncSession | None = None, readonly: bool = True, auto_commit: bool = True
|
|
350
|
+
) -> AsyncSession:
|
|
335
351
|
"""Get database session with readonly optimization.
|
|
336
352
|
|
|
337
353
|
Args:
|
|
338
|
-
|
|
354
|
+
db_or_name: Database name, existing AsyncSession, or None for default database
|
|
339
355
|
readonly: True for readonly (no transaction), False for transactional
|
|
340
356
|
auto_commit: True to auto-commit transactions (ignored if readonly=True)
|
|
341
357
|
|
|
@@ -343,10 +359,13 @@ def get_session(db_name: str | None = None, readonly: bool = True, auto_commit:
|
|
|
343
359
|
AsyncSession instance
|
|
344
360
|
|
|
345
361
|
Priority:
|
|
362
|
+
- If db_or_name is an AsyncSession, return it directly
|
|
346
363
|
- First try use explicitly set session (ctx_session, ctx_sessions)
|
|
347
364
|
- Create a new AsyncSession with specified parameters if no explicit session
|
|
348
365
|
"""
|
|
349
|
-
|
|
366
|
+
if isinstance(db_or_name, AsyncSession):
|
|
367
|
+
return db_or_name
|
|
368
|
+
return _SessionContextManager.get_session(db_or_name, readonly, auto_commit)
|
|
350
369
|
|
|
351
370
|
|
|
352
371
|
def has_session(db_name: str | None = None) -> bool:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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>
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|