sqlobjects 1.2.0__tar.gz → 1.2.2__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.2.0 → sqlobjects-1.2.2}/CHANGELOG.md +12 -0
  2. {sqlobjects-1.2.0/sqlobjects.egg-info → sqlobjects-1.2.2}/PKG-INFO +1 -1
  3. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/pyproject.toml +1 -1
  4. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/metadata.py +18 -6
  5. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/mixins.py +1 -4
  6. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/objects/core.py +1 -6
  7. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/queries/dialect.py +5 -4
  8. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/queries/executor.py +20 -9
  9. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/session.py +8 -3
  10. {sqlobjects-1.2.0 → sqlobjects-1.2.2/sqlobjects.egg-info}/PKG-INFO +1 -1
  11. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/LICENSE +0 -0
  12. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/README.md +0 -0
  13. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/01-database-session-guide.md +0 -0
  14. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/02-model-definition-guide.md +0 -0
  15. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/03-query-operations-guide.md +0 -0
  16. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/04-crud-operations-guide.md +0 -0
  17. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/05-relationships-guide.md +0 -0
  18. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/06-validation-signals-guide.md +0 -0
  19. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/07-performance-guide.md +0 -0
  20. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/docs/rules/README.md +0 -0
  21. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/setup.cfg +0 -0
  22. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/__init__.py +0 -0
  23. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/_install_rules.py +0 -0
  24. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/cascade.py +0 -0
  25. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/database/__init__.py +0 -0
  26. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/database/config.py +0 -0
  27. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/database/manager.py +0 -0
  28. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/exceptions.py +0 -0
  29. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/__init__.py +0 -0
  30. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/aggregate.py +0 -0
  31. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/base.py +0 -0
  32. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/cte.py +0 -0
  33. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/explain.py +0 -0
  34. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/function.py +0 -0
  35. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/mixins.py +0 -0
  36. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/scalar.py +0 -0
  37. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/subquery.py +0 -0
  38. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/terminal.py +0 -0
  39. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/expressions/window.py +0 -0
  40. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/__init__.py +0 -0
  41. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/core.py +0 -0
  42. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/functions.py +0 -0
  43. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/proxies.py +0 -0
  44. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/relations/__init__.py +0 -0
  45. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/relations/descriptors.py +0 -0
  46. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/relations/managers.py +0 -0
  47. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/relations/prefetch.py +0 -0
  48. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/relations/utils.py +0 -0
  50. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/shortcuts.py +0 -0
  51. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/types/__init__.py +0 -0
  52. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/types/base.py +0 -0
  53. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/types/comparators.py +0 -0
  54. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/types/registry.py +0 -0
  55. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/fields/utils.py +0 -0
  56. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/internal/__init__.py +0 -0
  57. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/internal/operations.py +0 -0
  58. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/internal/results.py +0 -0
  59. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/model.py +0 -0
  60. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/objects/__init__.py +0 -0
  61. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/objects/bulk.py +0 -0
  62. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/objects/upsert.py +0 -0
  63. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/queries/__init__.py +0 -0
  64. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/queries/builder.py +0 -0
  65. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/queryset.py +0 -0
  66. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/signals.py +0 -0
  67. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/utils/__init__.py +0 -0
  68. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/utils/inspect.py +0 -0
  69. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/utils/naming.py +0 -0
  70. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/utils/pattern.py +0 -0
  71. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects/validators.py +0 -0
  72. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects.egg-info/SOURCES.txt +0 -0
  73. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects.egg-info/dependency_links.txt +0 -0
  74. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects.egg-info/entry_points.txt +0 -0
  75. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects.egg-info/requires.txt +0 -0
  76. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/sqlobjects.egg-info/top_level.txt +0 -0
  77. {sqlobjects-1.2.0 → sqlobjects-1.2.2}/tests/test_config.py +0 -0
@@ -1,3 +1,15 @@
1
+ ## 1.2.2 (2026-02-26)
2
+
3
+ ### Fix
4
+
5
+ - clone Column descriptor per subclass to prevent shared ColumnAttribute binding
6
+
7
+ ## 1.2.1 (2026-02-25)
8
+
9
+ ### Refactor
10
+
11
+ - centralize session resolution in get_session()
12
+
1
13
  ## 1.2.0 (2026-02-25)
2
14
 
3
15
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.2.0
3
+ Version: 1.2.2
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.2.0"
3
+ version = "1.2.2"
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"
@@ -938,13 +938,25 @@ class ModelProcessor(type):
938
938
  cls: Model class
939
939
  table: SQLAlchemy Table instance
940
940
  """
941
- for name, field_def in mcs._get_fields(cls).items():
942
- if is_field_definition(field_def) and not getattr(field_def, "_is_relationship", False):
943
- column_attr = get_column_from_field(field_def)
944
- if column_attr is not None and hasattr(column_attr, "__column__"):
945
- # Update the ColumnAttribute's internal column to reference the table column
946
- if name in table.columns:
941
+ for name in table.columns.keys():
942
+ for klass in cls.__mro__:
943
+ descriptor = klass.__dict__.get(name)
944
+ if descriptor is not None and hasattr(descriptor, "_column_attribute"):
945
+ column_attr = descriptor._column_attribute
946
+ if column_attr is None or not hasattr(column_attr, "__column__"):
947
+ break
948
+ if klass is cls:
947
949
  column_attr.__column__ = table.columns[name] # type: ignore[reportAttributeAccessIssue]
950
+ else:
951
+ new_col_attr = object.__new__(type(column_attr))
952
+ new_col_attr.__dict__.update(column_attr.__dict__)
953
+ new_col_attr.__column__ = table.columns[name]
954
+ new_col_attr.model_class = cls
955
+ new_descriptor = object.__new__(type(descriptor))
956
+ new_descriptor.__dict__.update(descriptor.__dict__)
957
+ new_descriptor._column_attribute = new_col_attr
958
+ type.__setattr__(cls, name, new_descriptor)
959
+ break
948
960
 
949
961
  @classmethod
950
962
  def _initialize_field_cache(mcs, cls: Any) -> None:
@@ -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
@@ -345,11 +345,13 @@ async def ctx_sessions(*db_names: str) -> AsyncGenerator[dict[str, AsyncSession]
345
345
  _SessionContextManager.clear_session(db_name)
346
346
 
347
347
 
348
- 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:
349
351
  """Get database session with readonly optimization.
350
352
 
351
353
  Args:
352
- db_name: Database name (uses default database if None)
354
+ db_or_name: Database name, existing AsyncSession, or None for default database
353
355
  readonly: True for readonly (no transaction), False for transactional
354
356
  auto_commit: True to auto-commit transactions (ignored if readonly=True)
355
357
 
@@ -357,10 +359,13 @@ def get_session(db_name: str | None = None, readonly: bool = True, auto_commit:
357
359
  AsyncSession instance
358
360
 
359
361
  Priority:
362
+ - If db_or_name is an AsyncSession, return it directly
360
363
  - First try use explicitly set session (ctx_session, ctx_sessions)
361
364
  - Create a new AsyncSession with specified parameters if no explicit session
362
365
  """
363
- 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)
364
369
 
365
370
 
366
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.2.0
3
+ Version: 1.2.2
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