sqlspec 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,33 +8,19 @@ scheme-based routing, and named aliases for common configurations.
8
8
  import logging
9
9
  import re
10
10
  from pathlib import Path
11
- from typing import Any, Final, Optional, Union, cast
11
+ from typing import Any, Final, cast
12
12
 
13
13
  from mypy_extensions import mypyc_attr
14
14
 
15
15
  from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError
16
16
  from sqlspec.protocols import ObjectStoreProtocol
17
17
  from sqlspec.typing import FSSPEC_INSTALLED, OBSTORE_INSTALLED
18
+ from sqlspec.utils.type_guards import is_local_path
18
19
 
19
20
  __all__ = ("StorageRegistry", "storage_registry")
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
23
-
24
- def _is_local_uri(uri: str) -> bool:
25
- """Check if URI represents a local filesystem path."""
26
- if "://" in uri and not uri.startswith("file://"):
27
- return False
28
- windows_drive_min_length = 3
29
- return (
30
- Path(uri).exists()
31
- or Path(uri).is_absolute()
32
- or uri.startswith(("~", ".", "/"))
33
- or (len(uri) >= windows_drive_min_length and uri[1:3] == ":\\")
34
- or "/" in uri
35
- )
36
-
37
-
38
24
  SCHEME_REGEX: Final = re.compile(r"([a-zA-Z0-9+.-]+)://")
39
25
 
40
26
 
@@ -74,7 +60,7 @@ class StorageRegistry:
74
60
  def __init__(self) -> None:
75
61
  self._alias_configs: dict[str, tuple[type[ObjectStoreProtocol], str, dict[str, Any]]] = {}
76
62
  self._aliases: dict[str, dict[str, Any]] = {}
77
- self._instances: dict[Union[str, tuple[str, tuple[tuple[str, Any], ...]]], ObjectStoreProtocol] = {}
63
+ self._instances: dict[str | tuple[str, tuple[tuple[str, Any], ...]], ObjectStoreProtocol] = {}
78
64
  self._cache: dict[str, tuple[str, type[ObjectStoreProtocol]]] = {}
79
65
 
80
66
  def _make_hashable(self, obj: Any) -> Any:
@@ -88,7 +74,7 @@ class StorageRegistry:
88
74
  return obj
89
75
 
90
76
  def register_alias(
91
- self, alias: str, uri: str, *, backend: Optional[str] = None, base_path: str = "", **kwargs: Any
77
+ self, alias: str, uri: str, *, backend: str | None = None, base_path: str = "", **kwargs: Any
92
78
  ) -> None:
93
79
  """Register a named alias for a storage configuration.
94
80
 
@@ -110,9 +96,7 @@ class StorageRegistry:
110
96
  test_config["uri"] = uri
111
97
  self._aliases[alias] = test_config
112
98
 
113
- def get(
114
- self, uri_or_alias: Union[str, Path], *, backend: Optional[str] = None, **kwargs: Any
115
- ) -> ObjectStoreProtocol:
99
+ def get(self, uri_or_alias: str | Path, *, backend: str | None = None, **kwargs: Any) -> ObjectStoreProtocol:
116
100
  """Get backend instance using URI-first routing with automatic backend selection.
117
101
 
118
102
  Args:
@@ -133,11 +117,15 @@ class StorageRegistry:
133
117
  if isinstance(uri_or_alias, Path):
134
118
  uri_or_alias = f"file://{uri_or_alias.resolve()}"
135
119
 
136
- cache_key = (uri_or_alias, self._make_hashable(kwargs)) if kwargs else uri_or_alias
120
+ # Include backend in cache key to ensure different backends for same URI are cached separately
121
+ cache_params = dict(kwargs)
122
+ if backend:
123
+ cache_params["__backend__"] = backend
124
+ cache_key = (uri_or_alias, self._make_hashable(cache_params)) if cache_params else uri_or_alias
137
125
  if cache_key in self._instances:
138
126
  return self._instances[cache_key]
139
127
  scheme = self._get_scheme(uri_or_alias)
140
- if not scheme and _is_local_uri(uri_or_alias):
128
+ if not scheme and is_local_path(uri_or_alias):
141
129
  scheme = "file"
142
130
  uri_or_alias = f"file://{uri_or_alias}"
143
131
 
@@ -154,57 +142,105 @@ class StorageRegistry:
154
142
  self._instances[cache_key] = instance
155
143
  return instance
156
144
 
157
- def _resolve_from_uri(
158
- self, uri: str, *, backend_override: Optional[str] = None, **kwargs: Any
159
- ) -> ObjectStoreProtocol:
160
- """Resolve backend from URI with optional backend override."""
145
+ def _resolve_from_uri(self, uri: str, *, backend_override: str | None = None, **kwargs: Any) -> ObjectStoreProtocol:
146
+ """Resolve backend from URI with optional backend override.
147
+
148
+ Backend selection priority for local files (file:// or bare paths):
149
+ 1. obstore (if installed) - provides async I/O performance
150
+ 2. fsspec (if installed) - async wrapper fallback
151
+ 3. local (always available) - zero-dependency sync backend
152
+
153
+ For cloud storage, prefer obstore over fsspec when available.
154
+
155
+ Args:
156
+ uri: Storage URI to resolve.
157
+ backend_override: Force specific backend type.
158
+ **kwargs: Additional backend configuration.
159
+
160
+ Returns:
161
+ Configured backend instance.
162
+
163
+ Raises:
164
+ MissingDependencyError: No backend available for URI scheme.
165
+ """
161
166
  if backend_override:
162
167
  return self._create_backend(backend_override, uri, **kwargs)
168
+
163
169
  scheme = self._get_scheme(uri)
164
170
 
165
- # For local files, prefer LocalStore first
171
+ # NEW: Prefer obstore for local files when available
166
172
  if scheme in {None, "file"}:
173
+ # Try obstore first for async performance
174
+ if OBSTORE_INSTALLED:
175
+ try:
176
+ return self._create_backend("obstore", uri, **kwargs)
177
+ except (ValueError, ImportError, NotImplementedError):
178
+ pass
179
+
180
+ # Fallback to fsspec if available
181
+ if FSSPEC_INSTALLED:
182
+ try:
183
+ return self._create_backend("fsspec", uri, **kwargs)
184
+ except (ValueError, ImportError, NotImplementedError):
185
+ pass
186
+
187
+ # Final fallback: local zero-dependency backend
167
188
  return self._create_backend("local", uri, **kwargs)
168
189
 
169
- # Try ObStore first if available and appropriate
190
+ # For cloud schemes, prefer obstore over fsspec
170
191
  if scheme not in FSSPEC_ONLY_SCHEMES and OBSTORE_INSTALLED:
171
192
  try:
172
193
  return self._create_backend("obstore", uri, **kwargs)
173
194
  except (ValueError, ImportError, NotImplementedError):
174
195
  pass
175
196
 
176
- # Try FSSpec if available
197
+ # Try fsspec if available
177
198
  if FSSPEC_INSTALLED:
178
199
  try:
179
200
  return self._create_backend("fsspec", uri, **kwargs)
180
201
  except (ValueError, ImportError, NotImplementedError):
181
202
  pass
182
203
 
183
- # For cloud schemes without backends, provide helpful error
204
+ # No backend available
184
205
  msg = f"No backend available for URI scheme '{scheme}'. Install obstore or fsspec for cloud storage support."
185
206
  raise MissingDependencyError(msg)
186
207
 
187
208
  def _determine_backend_class(self, uri: str) -> type[ObjectStoreProtocol]:
188
- """Determine the backend class for a URI based on availability."""
209
+ """Determine the backend class for a URI based on availability.
210
+
211
+ Args:
212
+ uri: Storage URI to analyze.
213
+
214
+ Returns:
215
+ Backend class type to use.
216
+
217
+ Raises:
218
+ MissingDependencyError: No backend available for URI scheme.
219
+ """
189
220
  scheme = self._get_scheme(uri)
190
221
 
191
- # For local files, always use LocalStore
222
+ # NEW: For local files, prefer obstore > fsspec > local
192
223
  if scheme in {None, "file"}:
224
+ if OBSTORE_INSTALLED:
225
+ return self._get_backend_class("obstore")
226
+ if FSSPEC_INSTALLED:
227
+ return self._get_backend_class("fsspec")
193
228
  return self._get_backend_class("local")
194
229
 
195
230
  # FSSpec-only schemes require FSSpec
196
- if scheme in FSSPEC_ONLY_SCHEMES and FSSPEC_INSTALLED:
231
+ if scheme in FSSPEC_ONLY_SCHEMES:
232
+ if not FSSPEC_INSTALLED:
233
+ msg = f"Scheme '{scheme}' requires fsspec. Install with: pip install fsspec"
234
+ raise MissingDependencyError(msg)
197
235
  return self._get_backend_class("fsspec")
198
236
 
199
- # Prefer ObStore for cloud storage if available
237
+ # For cloud schemes, prefer obstore
200
238
  if OBSTORE_INSTALLED:
201
239
  return self._get_backend_class("obstore")
202
240
 
203
- # Fall back to FSSpec if available
204
241
  if FSSPEC_INSTALLED:
205
242
  return self._get_backend_class("fsspec")
206
243
 
207
- # For cloud schemes without backends, provide helpful error
208
244
  msg = f"No backend available for URI scheme '{scheme}'. Install obstore or fsspec for cloud storage support."
209
245
  raise MissingDependencyError(msg)
210
246
 
@@ -229,7 +265,7 @@ class StorageRegistry:
229
265
  """Create backend instance for URI."""
230
266
  return self._get_backend_class(backend_type)(uri, **kwargs)
231
267
 
232
- def _get_scheme(self, uri: str) -> Optional[str]:
268
+ def _get_scheme(self, uri: str) -> str | None:
233
269
  """Extract the scheme from a URI using regex."""
234
270
  if not uri:
235
271
  return None
@@ -244,7 +280,7 @@ class StorageRegistry:
244
280
  """List all registered aliases."""
245
281
  return list(self._alias_configs.keys())
246
282
 
247
- def clear_cache(self, uri_or_alias: Optional[str] = None) -> None:
283
+ def clear_cache(self, uri_or_alias: str | None = None) -> None:
248
284
  """Clear resolved backend cache."""
249
285
  if uri_or_alias:
250
286
  self._instances.pop(uri_or_alias, None)
sqlspec/typing.py CHANGED
@@ -1,9 +1,9 @@
1
1
  # pyright: ignore[reportAttributeAccessIssue]
2
- from collections.abc import Iterator, Mapping
2
+ from collections.abc import Iterator
3
3
  from functools import lru_cache
4
- from typing import TYPE_CHECKING, Annotated, Any, Protocol, Union
4
+ from typing import Annotated, Any, Protocol, TypeAlias, _TypedDict # pyright: ignore
5
5
 
6
- from typing_extensions import TypeAlias, TypeVar
6
+ from typing_extensions import TypeVar
7
7
 
8
8
  from sqlspec._typing import (
9
9
  AIOSQL_INSTALLED,
@@ -15,6 +15,7 @@ from sqlspec._typing import (
15
15
  NUMPY_INSTALLED,
16
16
  OBSTORE_INSTALLED,
17
17
  OPENTELEMETRY_INSTALLED,
18
+ ORJSON_INSTALLED,
18
19
  PGVECTOR_INSTALLED,
19
20
  PROMETHEUS_INSTALLED,
20
21
  PYARROW_INSTALLED,
@@ -22,7 +23,6 @@ from sqlspec._typing import (
22
23
  UNSET,
23
24
  AiosqlAsyncProtocol,
24
25
  AiosqlParamType,
25
- AiosqlProtocol,
26
26
  AiosqlSQLOperationType,
27
27
  AiosqlSyncProtocol,
28
28
  ArrowRecordBatch,
@@ -40,6 +40,7 @@ from sqlspec._typing import (
40
40
  FailFast,
41
41
  Gauge,
42
42
  Histogram,
43
+ NumpyArray,
43
44
  Span,
44
45
  Status,
45
46
  StatusCode,
@@ -60,9 +61,6 @@ from sqlspec._typing import (
60
61
  trace,
61
62
  )
62
63
 
63
- if TYPE_CHECKING:
64
- from collections.abc import Sequence
65
-
66
64
 
67
65
  class DictLike(Protocol):
68
66
  """A protocol for objects that behave like a dictionary for reading."""
@@ -86,30 +84,22 @@ PoolT = TypeVar("PoolT")
86
84
 
87
85
  :class:`~sqlspec.typing.PoolT`
88
86
  """
89
- PoolT_co = TypeVar("PoolT_co", covariant=True)
90
- """Type variable for covariant pool types.
91
-
92
- :class:`~sqlspec.typing.PoolT_co`
93
- """
94
- ModelT = TypeVar("ModelT", bound="Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]")
95
- """Type variable for model types.
87
+ SchemaT = TypeVar("SchemaT", default=dict[str, Any])
88
+ """Type variable for schema types (models, TypedDict, dataclasses, etc.).
96
89
 
97
- :class:`DictLike` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`DataclassProtocol` | :class:`AttrsInstance`
90
+ Unbounded TypeVar for use with schema_type parameter in driver methods.
91
+ Supports all schema types including TypedDict which cannot be bounded to a class hierarchy.
98
92
  """
99
- RowT = TypeVar("RowT", bound="dict[str, Any]")
100
-
101
93
 
102
- DictRow: TypeAlias = "dict[str, Any]"
103
- """Type variable for DictRow types."""
104
- TupleRow: TypeAlias = "tuple[Any, ...]"
105
- """Type variable for TupleRow types."""
106
94
 
107
- SupportedSchemaModel: TypeAlias = "Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]"
95
+ SupportedSchemaModel: TypeAlias = (
96
+ DictLike | StructStub | BaseModelStub | DataclassProtocol | AttrsInstanceStub | _TypedDict
97
+ )
108
98
  """Type alias for pydantic or msgspec models.
109
99
 
110
100
  :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`DataclassProtocol` | :class:`AttrsInstance`
111
101
  """
112
- StatementParameters: TypeAlias = "Union[Any, dict[str, Any], list[Any], tuple[Any, ...], None]"
102
+ StatementParameters: TypeAlias = "Any | dict[str, Any] | list[Any] | tuple[Any, ...] | None"
113
103
  """Type alias for statement parameters.
114
104
 
115
105
  Represents:
@@ -118,40 +108,6 @@ Represents:
118
108
  - :type:`tuple[Any, ...]`
119
109
  - :type:`None`
120
110
  """
121
- ModelDTOT = TypeVar("ModelDTOT", bound="SupportedSchemaModel")
122
- """Type variable for model DTOs.
123
-
124
- :class:`msgspec.Struct`|:class:`pydantic.BaseModel`
125
- """
126
- PydanticOrMsgspecT = SupportedSchemaModel
127
- """Type alias for pydantic or msgspec models.
128
-
129
- :class:`msgspec.Struct` or :class:`pydantic.BaseModel`
130
- """
131
- ModelDict: TypeAlias = (
132
- "Union[dict[str, Any], Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub], Any]"
133
- )
134
- """Type alias for model dictionaries.
135
-
136
- Represents:
137
- - :type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`
138
- """
139
- ModelDictList: TypeAlias = (
140
- "Sequence[Union[dict[str, Any], Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]]]"
141
- )
142
- """Type alias for model dictionary lists.
143
-
144
- A list or sequence of any of the following:
145
- - :type:`Sequence`[:type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`]
146
-
147
- """
148
- BulkModelDict: TypeAlias = "Union[Sequence[Union[dict[str, Any], Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]]], Any]"
149
- """Type alias for bulk model dictionaries.
150
-
151
- Represents:
152
- - :type:`Sequence`[:type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`]
153
- - :class:`DTOData`[:type:`list[ModelT]`]
154
- """
155
111
 
156
112
 
157
113
  @lru_cache(typed=True)
@@ -169,18 +125,6 @@ def get_type_adapter(f: "type[T]") -> Any:
169
125
  return TypeAdapter(f)
170
126
 
171
127
 
172
- def MixinOf(base: type[T]) -> type[T]: # noqa: N802
173
- """Useful function to make mixins with baseclass type hint
174
-
175
- ```
176
- class StorageMixin(MixinOf(DriverProtocol)): ...
177
- ```
178
- """
179
- if TYPE_CHECKING:
180
- return base
181
- return type("<MixinOf>", (base,), {})
182
-
183
-
184
128
  __all__ = (
185
129
  "AIOSQL_INSTALLED",
186
130
  "ATTRS_INSTALLED",
@@ -191,6 +135,7 @@ __all__ = (
191
135
  "NUMPY_INSTALLED",
192
136
  "OBSTORE_INSTALLED",
193
137
  "OPENTELEMETRY_INSTALLED",
138
+ "ORJSON_INSTALLED",
194
139
  "PGVECTOR_INSTALLED",
195
140
  "PROMETHEUS_INSTALLED",
196
141
  "PYARROW_INSTALLED",
@@ -199,38 +144,26 @@ __all__ = (
199
144
  "UNSET",
200
145
  "AiosqlAsyncProtocol",
201
146
  "AiosqlParamType",
202
- "AiosqlProtocol",
203
147
  "AiosqlSQLOperationType",
204
148
  "AiosqlSyncProtocol",
205
149
  "ArrowRecordBatch",
206
150
  "ArrowTable",
207
151
  "AttrsInstance",
208
152
  "BaseModel",
209
- "BulkModelDict",
210
153
  "ConnectionT",
211
154
  "Counter",
212
155
  "DTOData",
213
156
  "DataclassProtocol",
214
157
  "DictLike",
215
- "DictRow",
216
158
  "Empty",
217
159
  "EmptyEnum",
218
160
  "EmptyType",
219
161
  "FailFast",
220
162
  "Gauge",
221
163
  "Histogram",
222
- "Mapping",
223
- "MixinOf",
224
- "ModelDTOT",
225
- "ModelDict",
226
- "ModelDict",
227
- "ModelDictList",
228
- "ModelDictList",
229
- "ModelT",
164
+ "NumpyArray",
230
165
  "PoolT",
231
- "PoolT_co",
232
- "PydanticOrMsgspecT",
233
- "RowT",
166
+ "SchemaT",
234
167
  "Span",
235
168
  "StatementParameters",
236
169
  "Status",
@@ -238,7 +171,6 @@ __all__ = (
238
171
  "Struct",
239
172
  "SupportedSchemaModel",
240
173
  "Tracer",
241
- "TupleRow",
242
174
  "TypeAdapter",
243
175
  "UnsetType",
244
176
  "aiosql",
@@ -0,0 +1,153 @@
1
+ """Configuration resolver for SQLSpec CLI.
2
+
3
+ This module provides utilities for resolving configuration objects from dotted paths,
4
+ with support for both direct config instances and callable functions that return configs.
5
+ Supports both synchronous and asynchronous callable functions.
6
+ """
7
+
8
+ import inspect
9
+ from collections.abc import Sequence
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ from sqlspec.exceptions import ConfigResolverError
13
+ from sqlspec.utils.module_loader import import_string
14
+ from sqlspec.utils.sync_tools import async_, await_
15
+
16
+ if TYPE_CHECKING:
17
+ from sqlspec.config import AsyncDatabaseConfig, SyncDatabaseConfig
18
+
19
+ __all__ = ("resolve_config_async", "resolve_config_sync")
20
+
21
+
22
+ async def resolve_config_async(
23
+ config_path: str,
24
+ ) -> "list[AsyncDatabaseConfig | SyncDatabaseConfig] | AsyncDatabaseConfig | SyncDatabaseConfig":
25
+ """Resolve config from dotted path, handling callables and direct instances.
26
+
27
+ This is the async-first version that handles both sync and async callables efficiently.
28
+
29
+ Args:
30
+ config_path: Dotted path to config object or callable function.
31
+
32
+ Returns:
33
+ Resolved config instance or list of config instances.
34
+
35
+ Raises:
36
+ ConfigResolverError: If config resolution fails.
37
+ """
38
+ try:
39
+ config_obj = import_string(config_path)
40
+ except ImportError as e:
41
+ msg = f"Failed to import config from path '{config_path}': {e}"
42
+ raise ConfigResolverError(msg) from e
43
+
44
+ if not callable(config_obj):
45
+ return _validate_config_result(config_obj, config_path)
46
+
47
+ try:
48
+ if inspect.iscoroutinefunction(config_obj):
49
+ result = await config_obj()
50
+ else:
51
+ result = await async_(config_obj)()
52
+ except Exception as e:
53
+ msg = f"Failed to execute callable config '{config_path}': {e}"
54
+ raise ConfigResolverError(msg) from e
55
+
56
+ return _validate_config_result(result, config_path)
57
+
58
+
59
+ def resolve_config_sync(
60
+ config_path: str,
61
+ ) -> "list[AsyncDatabaseConfig | SyncDatabaseConfig] | AsyncDatabaseConfig | SyncDatabaseConfig":
62
+ """Synchronous wrapper for resolve_config.
63
+
64
+ Args:
65
+ config_path: Dotted path to config object or callable function.
66
+
67
+ Returns:
68
+ Resolved config instance or list of config instances.
69
+ """
70
+ try:
71
+ config_obj = import_string(config_path)
72
+ except ImportError as e:
73
+ msg = f"Failed to import config from path '{config_path}': {e}"
74
+ raise ConfigResolverError(msg) from e
75
+
76
+ if not callable(config_obj):
77
+ return _validate_config_result(config_obj, config_path)
78
+
79
+ try:
80
+ if inspect.iscoroutinefunction(config_obj):
81
+ result = await_(config_obj, raise_sync_error=False)()
82
+ else:
83
+ result = config_obj()
84
+ except Exception as e:
85
+ msg = f"Failed to execute callable config '{config_path}': {e}"
86
+ raise ConfigResolverError(msg) from e
87
+
88
+ return _validate_config_result(result, config_path)
89
+
90
+
91
+ def _validate_config_result(
92
+ config_result: Any, config_path: str
93
+ ) -> "list[AsyncDatabaseConfig | SyncDatabaseConfig] | AsyncDatabaseConfig | SyncDatabaseConfig":
94
+ """Validate that the config result is a valid config or list of configs.
95
+
96
+ Args:
97
+ config_result: The result from config resolution.
98
+ config_path: Original config path for error messages.
99
+
100
+ Returns:
101
+ Validated config result.
102
+
103
+ Raises:
104
+ ConfigResolverError: If config result is invalid.
105
+ """
106
+ if config_result is None:
107
+ msg = f"Config '{config_path}' resolved to None. Expected config instance or list of configs."
108
+ raise ConfigResolverError(msg)
109
+
110
+ if isinstance(config_result, Sequence) and not isinstance(config_result, str):
111
+ if not config_result:
112
+ msg = f"Config '{config_path}' resolved to empty list. Expected at least one config."
113
+ raise ConfigResolverError(msg)
114
+
115
+ for i, config in enumerate(config_result):
116
+ if not _is_valid_config(config):
117
+ msg = f"Config '{config_path}' returned invalid config at index {i}. Expected database config instance."
118
+ raise ConfigResolverError(msg)
119
+
120
+ return cast("list[AsyncDatabaseConfig | SyncDatabaseConfig]", list(config_result))
121
+
122
+ if not _is_valid_config(config_result):
123
+ msg = f"Config '{config_path}' returned invalid type '{type(config_result).__name__}'. Expected database config instance or list."
124
+ raise ConfigResolverError(msg)
125
+
126
+ return cast("AsyncDatabaseConfig | SyncDatabaseConfig", config_result)
127
+
128
+
129
+ def _is_valid_config(config: Any) -> bool:
130
+ """Check if an object is a valid SQLSpec database config.
131
+
132
+ Args:
133
+ config: Object to validate.
134
+
135
+ Returns:
136
+ True if object appears to be a valid config.
137
+ """
138
+ # Check for litestar extension DatabaseConfig wrapper
139
+ nested_config = getattr(config, "config", None)
140
+ if nested_config is not None and hasattr(nested_config, "migration_config"):
141
+ return True
142
+
143
+ # Check for direct database config with migration support
144
+ migration_config = getattr(config, "migration_config", None)
145
+ if migration_config is not None:
146
+ # Modern SQLSpec config with pool_config
147
+ if hasattr(config, "pool_config"):
148
+ return True
149
+ # Legacy config with database_url and bind_key
150
+ if hasattr(config, "database_url") and hasattr(config, "bind_key"):
151
+ return True
152
+
153
+ return False
@@ -4,15 +4,14 @@ This module provides utilities for tracking correlation IDs across
4
4
  database operations, enabling distributed tracing and debugging.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import uuid
8
+ from collections.abc import Generator
10
9
  from contextlib import contextmanager
11
10
  from contextvars import ContextVar
12
11
  from typing import TYPE_CHECKING, Any
13
12
 
14
13
  if TYPE_CHECKING:
15
- from collections.abc import Generator, MutableMapping
14
+ from collections.abc import MutableMapping
16
15
  from logging import LoggerAdapter
17
16
 
18
17
  __all__ = ("CorrelationContext", "correlation_context", "get_correlation_adapter")
@@ -115,7 +114,7 @@ def correlation_context(correlation_id: str | None = None) -> Generator[str, Non
115
114
  yield cid
116
115
 
117
116
 
118
- def get_correlation_adapter(logger: Any) -> LoggerAdapter:
117
+ def get_correlation_adapter(logger: Any) -> "LoggerAdapter":
119
118
  """Get a logger adapter that automatically includes correlation ID.
120
119
 
121
120
  Args:
@@ -129,7 +128,7 @@ def get_correlation_adapter(logger: Any) -> LoggerAdapter:
129
128
  class CorrelationAdapter(LoggerAdapter):
130
129
  """Logger adapter that adds correlation ID to all logs."""
131
130
 
132
- def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, dict[str, Any]]:
131
+ def process(self, msg: str, kwargs: "MutableMapping[str, Any]") -> tuple[str, dict[str, Any]]:
133
132
  """Add correlation ID to the log record.
134
133
 
135
134
  Args:
@@ -5,7 +5,8 @@ field name conversion when mapping database results to schema objects.
5
5
  Used primarily for msgspec field name conversion with rename configurations.
6
6
  """
7
7
 
8
- from typing import Any, Callable, Union
8
+ from collections.abc import Callable
9
+ from typing import Any
9
10
 
10
11
  __all__ = ("transform_dict_keys",)
11
12
 
@@ -30,7 +31,7 @@ def _safe_convert_key(key: Any, converter: Callable[[str], str]) -> Any:
30
31
  return key
31
32
 
32
33
 
33
- def transform_dict_keys(data: Union[dict, list, Any], converter: Callable[[str], str]) -> Union[dict, list, Any]:
34
+ def transform_dict_keys(data: dict | list | Any, converter: Callable[[str], str]) -> dict | list | Any:
34
35
  """Transform dictionary keys using the provided converter function.
35
36
 
36
37
  Recursively transforms all dictionary keys in a data structure using
@@ -5,8 +5,9 @@ Used to communicate API changes and migration paths to users.
5
5
  """
6
6
 
7
7
  import inspect
8
+ from collections.abc import Callable
8
9
  from functools import wraps
9
- from typing import Callable, Literal, Optional
10
+ from typing import Literal
10
11
  from warnings import warn
11
12
 
12
13
  from typing_extensions import ParamSpec, TypeVar
@@ -24,9 +25,9 @@ def warn_deprecation(
24
25
  deprecated_name: str,
25
26
  kind: DeprecatedKind,
26
27
  *,
27
- removal_in: Optional[str] = None,
28
- alternative: Optional[str] = None,
29
- info: Optional[str] = None,
28
+ removal_in: str | None = None,
29
+ alternative: str | None = None,
30
+ info: str | None = None,
30
31
  pending: bool = False,
31
32
  ) -> None:
32
33
  """Warn about a call to a deprecated function.
@@ -72,11 +73,11 @@ def warn_deprecation(
72
73
  def deprecated(
73
74
  version: str,
74
75
  *,
75
- removal_in: Optional[str] = None,
76
- alternative: Optional[str] = None,
77
- info: Optional[str] = None,
76
+ removal_in: str | None = None,
77
+ alternative: str | None = None,
78
+ info: str | None = None,
78
79
  pending: bool = False,
79
- kind: Optional[Literal["function", "method", "classmethod", "property"]] = None,
80
+ kind: Literal["function", "method", "classmethod", "property"] | None = None,
80
81
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
81
82
  """Create a decorator wrapping a function, method or property with a deprecation warning.
82
83