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.
Files changed (77) hide show
  1. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/CHANGELOG.md +12 -0
  2. {sqlobjects-1.1.0/sqlobjects.egg-info → sqlobjects-1.2.1}/PKG-INFO +1 -1
  3. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/pyproject.toml +10 -10
  4. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/metadata.py +33 -12
  5. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/mixins.py +1 -4
  6. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/core.py +1 -6
  7. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/dialect.py +5 -4
  8. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/executor.py +20 -9
  9. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/session.py +25 -6
  10. {sqlobjects-1.1.0 → sqlobjects-1.2.1/sqlobjects.egg-info}/PKG-INFO +1 -1
  11. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/LICENSE +0 -0
  12. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/README.md +0 -0
  13. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/01-database-session-guide.md +0 -0
  14. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/02-model-definition-guide.md +0 -0
  15. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/03-query-operations-guide.md +0 -0
  16. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/04-crud-operations-guide.md +0 -0
  17. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/05-relationships-guide.md +0 -0
  18. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/06-validation-signals-guide.md +0 -0
  19. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/07-performance-guide.md +0 -0
  20. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/docs/rules/README.md +0 -0
  21. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/setup.cfg +0 -0
  22. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/__init__.py +0 -0
  23. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/_install_rules.py +0 -0
  24. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/cascade.py +0 -0
  25. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/database/__init__.py +0 -0
  26. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/database/config.py +0 -0
  27. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/database/manager.py +0 -0
  28. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/exceptions.py +0 -0
  29. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/__init__.py +0 -0
  30. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/aggregate.py +0 -0
  31. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/base.py +0 -0
  32. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/cte.py +0 -0
  33. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/explain.py +0 -0
  34. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/function.py +0 -0
  35. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/mixins.py +0 -0
  36. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/scalar.py +0 -0
  37. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/subquery.py +0 -0
  38. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/terminal.py +0 -0
  39. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/expressions/window.py +0 -0
  40. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/__init__.py +0 -0
  41. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/core.py +0 -0
  42. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/functions.py +0 -0
  43. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/proxies.py +0 -0
  44. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/__init__.py +0 -0
  45. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/descriptors.py +0 -0
  46. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/managers.py +0 -0
  47. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/prefetch.py +0 -0
  48. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/relations/utils.py +0 -0
  50. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/shortcuts.py +0 -0
  51. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/__init__.py +0 -0
  52. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/base.py +0 -0
  53. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/comparators.py +0 -0
  54. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/types/registry.py +0 -0
  55. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/fields/utils.py +0 -0
  56. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/internal/__init__.py +0 -0
  57. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/internal/operations.py +0 -0
  58. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/internal/results.py +0 -0
  59. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/model.py +0 -0
  60. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/__init__.py +0 -0
  61. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/bulk.py +0 -0
  62. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/objects/upsert.py +0 -0
  63. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/__init__.py +0 -0
  64. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queries/builder.py +0 -0
  65. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/queryset.py +0 -0
  66. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/signals.py +0 -0
  67. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/__init__.py +0 -0
  68. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/inspect.py +0 -0
  69. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/naming.py +0 -0
  70. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/utils/pattern.py +0 -0
  71. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects/validators.py +0 -0
  72. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/SOURCES.txt +0 -0
  73. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/dependency_links.txt +0 -0
  74. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/entry_points.txt +0 -0
  75. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/requires.txt +0 -0
  76. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/sqlobjects.egg-info/top_level.txt +0 -0
  77. {sqlobjects-1.1.0 → sqlobjects-1.2.1}/tests/test_config.py +0 -0
@@ -1,3 +1,15 @@
1
+ ## 1.2.1 (2026-02-25)
2
+
3
+ ### Refactor
4
+
5
+ - centralize session resolution in get_session()
6
+
7
+ ## 1.2.0 (2026-02-25)
8
+
9
+ ### Feat
10
+
11
+ - **metadata**: improve constraint and index naming conventions
12
+
1
13
  ## 1.1.0 (2026-02-14)
2
14
 
3
15
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.1.0
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.0"
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.4.0",
53
- "pyright>=1.1.407",
54
- "ruff>=0.14.5",
55
- "setuptools>=80.9.0",
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.20.0",
60
- "asyncpg>=0.30.0",
61
- "pytest>=9.0.1",
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.1.3", # For memory monitoring in performance tests
63
+ "psutil>=7.2.2", # For memory monitoring in performance tests
64
64
  ]
65
65
 
66
66
  [build-system]
67
- requires = ["setuptools>=61.0", "wheel"]
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
- """Force uniform naming format for all indexes.
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
- new_name = f"idx_{table_name}_{field_names}"
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
- new_name = f"idx_{table_name}_{field_names}"
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"idx_{field_part}" # Temporary name, will be replaced later
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
- bound_session = self._state_manager.get_bound_session()
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
- if self._db_or_session is None:
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: list) -> str:
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: list) -> str:
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: list) -> str:
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: list) -> str:
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 for query execution
27
+ session: Database session, database name string, or None
26
28
  """
27
- self.session = session
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
- if not self.session:
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(self.session)
134
+ dialect = DialectHandler.create(session)
125
135
 
126
136
  # Compile query to SQL
127
- compiled = query.compile(dialect=self.session.bind.dialect, compile_kwargs={"literal_binds": True})
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 self.session.execute(text(explain_sql))
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
- if not self.session:
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 self.session.execute(query)
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.session)
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(db_name: str | None = None, readonly: bool = True, auto_commit: bool = True) -> AsyncSession:
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
- db_name: Database name (uses default database if None)
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
- return _SessionContextManager.get_session(db_name, readonly, auto_commit)
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.0
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