sqlspec 0.12.1__py3-none-any.whl → 0.13.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 (113) hide show
  1. sqlspec/_sql.py +21 -180
  2. sqlspec/adapters/adbc/config.py +10 -12
  3. sqlspec/adapters/adbc/driver.py +120 -118
  4. sqlspec/adapters/aiosqlite/config.py +3 -3
  5. sqlspec/adapters/aiosqlite/driver.py +116 -141
  6. sqlspec/adapters/asyncmy/config.py +3 -4
  7. sqlspec/adapters/asyncmy/driver.py +123 -135
  8. sqlspec/adapters/asyncpg/config.py +3 -7
  9. sqlspec/adapters/asyncpg/driver.py +98 -140
  10. sqlspec/adapters/bigquery/config.py +4 -5
  11. sqlspec/adapters/bigquery/driver.py +231 -181
  12. sqlspec/adapters/duckdb/config.py +3 -6
  13. sqlspec/adapters/duckdb/driver.py +132 -124
  14. sqlspec/adapters/oracledb/config.py +6 -5
  15. sqlspec/adapters/oracledb/driver.py +242 -259
  16. sqlspec/adapters/psqlpy/config.py +3 -7
  17. sqlspec/adapters/psqlpy/driver.py +118 -93
  18. sqlspec/adapters/psycopg/config.py +34 -30
  19. sqlspec/adapters/psycopg/driver.py +342 -214
  20. sqlspec/adapters/sqlite/config.py +3 -3
  21. sqlspec/adapters/sqlite/driver.py +150 -104
  22. sqlspec/config.py +0 -4
  23. sqlspec/driver/_async.py +89 -98
  24. sqlspec/driver/_common.py +52 -17
  25. sqlspec/driver/_sync.py +81 -105
  26. sqlspec/driver/connection.py +207 -0
  27. sqlspec/driver/mixins/_csv_writer.py +91 -0
  28. sqlspec/driver/mixins/_pipeline.py +38 -49
  29. sqlspec/driver/mixins/_result_utils.py +27 -9
  30. sqlspec/driver/mixins/_storage.py +149 -216
  31. sqlspec/driver/mixins/_type_coercion.py +3 -4
  32. sqlspec/driver/parameters.py +138 -0
  33. sqlspec/exceptions.py +10 -2
  34. sqlspec/extensions/aiosql/adapter.py +0 -10
  35. sqlspec/extensions/litestar/handlers.py +0 -1
  36. sqlspec/extensions/litestar/plugin.py +0 -3
  37. sqlspec/extensions/litestar/providers.py +0 -14
  38. sqlspec/loader.py +31 -118
  39. sqlspec/protocols.py +542 -0
  40. sqlspec/service/__init__.py +3 -2
  41. sqlspec/service/_util.py +147 -0
  42. sqlspec/service/base.py +1116 -9
  43. sqlspec/statement/builder/__init__.py +42 -32
  44. sqlspec/statement/builder/_ddl_utils.py +0 -10
  45. sqlspec/statement/builder/_parsing_utils.py +10 -4
  46. sqlspec/statement/builder/base.py +70 -23
  47. sqlspec/statement/builder/column.py +283 -0
  48. sqlspec/statement/builder/ddl.py +102 -65
  49. sqlspec/statement/builder/delete.py +23 -7
  50. sqlspec/statement/builder/insert.py +29 -15
  51. sqlspec/statement/builder/merge.py +4 -4
  52. sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
  53. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
  54. sqlspec/statement/builder/mixins/_delete_from.py +1 -1
  55. sqlspec/statement/builder/mixins/_from.py +10 -8
  56. sqlspec/statement/builder/mixins/_group_by.py +0 -1
  57. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
  58. sqlspec/statement/builder/mixins/_insert_values.py +0 -2
  59. sqlspec/statement/builder/mixins/_join.py +20 -13
  60. sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
  61. sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
  62. sqlspec/statement/builder/mixins/_order_by.py +2 -2
  63. sqlspec/statement/builder/mixins/_pivot.py +4 -7
  64. sqlspec/statement/builder/mixins/_select_columns.py +6 -5
  65. sqlspec/statement/builder/mixins/_unpivot.py +6 -9
  66. sqlspec/statement/builder/mixins/_update_from.py +2 -1
  67. sqlspec/statement/builder/mixins/_update_set.py +11 -8
  68. sqlspec/statement/builder/mixins/_where.py +61 -34
  69. sqlspec/statement/builder/select.py +32 -17
  70. sqlspec/statement/builder/update.py +25 -11
  71. sqlspec/statement/filters.py +39 -14
  72. sqlspec/statement/parameter_manager.py +220 -0
  73. sqlspec/statement/parameters.py +210 -79
  74. sqlspec/statement/pipelines/__init__.py +166 -23
  75. sqlspec/statement/pipelines/analyzers/_analyzer.py +22 -25
  76. sqlspec/statement/pipelines/context.py +35 -39
  77. sqlspec/statement/pipelines/transformers/__init__.py +2 -3
  78. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
  79. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +667 -43
  80. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
  81. sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
  82. sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
  83. sqlspec/statement/pipelines/validators/_performance.py +38 -23
  84. sqlspec/statement/pipelines/validators/_security.py +39 -62
  85. sqlspec/statement/result.py +37 -129
  86. sqlspec/statement/splitter.py +0 -12
  87. sqlspec/statement/sql.py +885 -379
  88. sqlspec/statement/sql_compiler.py +140 -0
  89. sqlspec/storage/__init__.py +10 -2
  90. sqlspec/storage/backends/fsspec.py +82 -35
  91. sqlspec/storage/backends/obstore.py +66 -49
  92. sqlspec/storage/capabilities.py +101 -0
  93. sqlspec/storage/registry.py +56 -83
  94. sqlspec/typing.py +6 -434
  95. sqlspec/utils/cached_property.py +25 -0
  96. sqlspec/utils/correlation.py +0 -2
  97. sqlspec/utils/logging.py +0 -6
  98. sqlspec/utils/sync_tools.py +0 -4
  99. sqlspec/utils/text.py +0 -5
  100. sqlspec/utils/type_guards.py +892 -0
  101. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
  102. sqlspec-0.13.0.dist-info/RECORD +150 -0
  103. sqlspec/statement/builder/protocols.py +0 -20
  104. sqlspec/statement/pipelines/base.py +0 -315
  105. sqlspec/statement/pipelines/result_types.py +0 -41
  106. sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
  107. sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
  108. sqlspec/statement/pipelines/validators/base.py +0 -67
  109. sqlspec/storage/protocol.py +0 -170
  110. sqlspec-0.12.1.dist-info/RECORD +0 -145
  111. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
  112. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
  113. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
@@ -8,13 +8,13 @@ This module provides a flexible, lazy-loading storage registry that supports:
8
8
  - Automatic instrumentation integration
9
9
  """
10
10
 
11
- # TODO: TRY300 - Review try-except patterns for else block opportunities
12
11
  import logging
13
12
  from pathlib import Path
14
13
  from typing import Any, Optional, TypeVar, Union, cast
15
14
 
16
15
  from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError
17
- from sqlspec.storage.protocol import ObjectStoreProtocol
16
+ from sqlspec.protocols import ObjectStoreProtocol
17
+ from sqlspec.storage.capabilities import StorageCapabilities
18
18
  from sqlspec.typing import FSSPEC_INSTALLED, OBSTORE_INSTALLED
19
19
 
20
20
  __all__ = ("StorageRegistry", "storage_registry")
@@ -58,9 +58,7 @@ class StorageRegistry:
58
58
  """
59
59
 
60
60
  def __init__(self) -> None:
61
- # Named aliases (secondary feature) - internal storage
62
61
  self._alias_configs: dict[str, tuple[type[ObjectStoreProtocol], str, dict[str, Any]]] = {}
63
- # Expose configs for testing compatibility
64
62
  self._aliases: dict[str, dict[str, Any]] = {}
65
63
  self._instances: dict[Union[str, tuple[str, tuple[tuple[str, Any], ...]]], ObjectStoreProtocol] = {}
66
64
 
@@ -85,21 +83,17 @@ class StorageRegistry:
85
83
  **kwargs: Backend-specific configuration options
86
84
  """
87
85
  if backend is None:
88
- # Auto-detect from URI using new intelligent selection
89
86
  backend = self._determine_backend_class(uri)
90
87
 
91
88
  config = config or {}
92
89
  config.update(kwargs)
93
90
 
94
- # Store the actual config that will be passed to backend
95
91
  backend_config = dict(config)
96
92
  if base_path:
97
93
  backend_config["base_path"] = base_path
98
94
 
99
- # Store backend class, URI, and config separately
100
95
  self._alias_configs[alias] = (backend, uri, backend_config)
101
96
 
102
- # Store config with URI for test compatibility
103
97
  test_config = dict(backend_config)
104
98
  test_config["uri"] = uri
105
99
  self._aliases[alias] = test_config
@@ -117,72 +111,35 @@ class StorageRegistry:
117
111
  Raises:
118
112
  ImproperConfigurationError: If alias not found or invalid input
119
113
  """
120
- # Handle None case - raise AttributeError for test compatibility
121
- if uri_or_alias is None:
122
- msg = "uri_or_alias cannot be None"
123
- raise AttributeError(msg)
124
-
125
- # Handle empty string
126
114
  if not uri_or_alias:
127
- msg = "Unknown storage alias: ''"
115
+ msg = "URI or alias cannot be empty."
128
116
  raise ImproperConfigurationError(msg)
129
117
 
130
- # Handle Path objects - convert to file:// URI
131
118
  if isinstance(uri_or_alias, Path):
132
119
  uri_or_alias = f"file://{uri_or_alias.resolve()}"
133
120
 
134
- # Check cache first
135
- cache_key: Union[str, tuple[str, tuple[tuple[str, Any], ...]]] = (
136
- (uri_or_alias, tuple(sorted(kwargs.items()))) if kwargs else uri_or_alias
137
- )
121
+ cache_key = (uri_or_alias, tuple(sorted(kwargs.items()))) if kwargs else uri_or_alias
138
122
  if cache_key in self._instances:
139
123
  return self._instances[cache_key]
140
124
 
141
- # PRIMARY: Try URI-first routing
142
125
  if "://" in uri_or_alias:
143
- backend = self._resolve_from_uri(uri_or_alias, **kwargs)
144
- # Cache the instance for future use
145
- self._instances[cache_key] = backend
146
- return backend
147
-
148
- # SECONDARY: Check if it's a registered alias
149
- if uri_or_alias in self._alias_configs:
126
+ instance = self._resolve_from_uri(uri_or_alias, **kwargs)
127
+ elif uri_or_alias in self._alias_configs:
150
128
  backend_cls, stored_uri, config = self._alias_configs[uri_or_alias]
151
- # Merge kwargs with alias config (kwargs override)
152
- merged_config = dict(config)
153
- merged_config.update(kwargs)
154
- # URI is passed as first positional arg
129
+ merged_config = {**config, **kwargs}
155
130
  instance = backend_cls(stored_uri, **merged_config)
156
- self._instances[cache_key] = instance
157
- return instance
131
+ else:
132
+ msg = f"Unknown storage alias or invalid URI: '{uri_or_alias}'"
133
+ raise ImproperConfigurationError(msg)
158
134
 
159
- # Not a URI and not an alias
160
- msg = f"Unknown storage alias: '{uri_or_alias}'"
161
- raise ImproperConfigurationError(msg)
135
+ self._instances[cache_key] = instance
136
+ return instance
162
137
 
163
138
  def _resolve_from_uri(self, uri: str, **kwargs: Any) -> ObjectStoreProtocol:
164
- """Resolve backend from URI.
165
-
166
- Tries ObStore first for supported schemes, then falls back to FSSpec.
167
-
168
- Args:
169
- uri: URI to resolve backend for
170
- **kwargs: Additional backend-specific configuration
171
-
172
- Returns:
173
- Backend instance
174
-
175
- Raises:
176
- MissingDependencyError: If no suitable backend can be created
177
- """
178
- # Schemes that ObStore doesn't support
179
-
180
- # Extract scheme
139
+ """Resolve backend from URI, trying ObStore first, then FSSpec."""
181
140
  scheme = self._get_scheme(uri)
182
-
183
141
  last_exc: Optional[Exception] = None
184
142
 
185
- # If scheme is FSSpec-only, skip ObStore
186
143
  if scheme not in FSSPEC_ONLY_SCHEMES and OBSTORE_INSTALLED:
187
144
  try:
188
145
  return self._create_backend("obstore", uri, **kwargs)
@@ -197,13 +154,14 @@ class StorageRegistry:
197
154
  logger.debug("FSSpec backend failed for %s: %s", uri, e)
198
155
  last_exc = e
199
156
 
200
- msg = f"No storage backend available for URI '{uri}'. Install 'obstore' or 'fsspec' and ensure dependencies for your filesystem are installed."
157
+ msg = f"No storage backend available for URI '{uri}'. Install 'obstore' or 'fsspec' and required dependencies."
201
158
  raise MissingDependencyError(msg) from last_exc
202
159
 
203
160
  def _determine_backend_class(self, uri: str) -> type[ObjectStoreProtocol]:
204
- """Determine the best backend class for a URI based on availability.
161
+ """Determine the best backend class for a URI based on availability and capabilities.
205
162
 
206
- Prefers ObStore, falls back to FSSpec.
163
+ Prefers ObStore for its superior performance and native capabilities,
164
+ falls back to FSSpec for extended protocol support.
207
165
 
208
166
  Args:
209
167
  uri: URI to determine backend for.
@@ -211,12 +169,20 @@ class StorageRegistry:
211
169
  Returns:
212
170
  Backend class (not instance)
213
171
  """
172
+ scheme = self._get_scheme(uri)
173
+
174
+ # Check if scheme requires FSSpec (not supported by ObStore)
175
+ if scheme in FSSPEC_ONLY_SCHEMES and FSSPEC_INSTALLED:
176
+ return self._get_backend_class("fsspec")
177
+
178
+ # Prefer ObStore for its superior performance
214
179
  if OBSTORE_INSTALLED:
215
180
  return self._get_backend_class("obstore")
181
+ # Could check capabilities here if needed
182
+
216
183
  if FSSPEC_INSTALLED:
217
184
  return self._get_backend_class("fsspec")
218
185
 
219
- scheme = uri.split("://", maxsplit=1)[0].lower()
220
186
  msg = f"No backend available for URI scheme '{scheme}'. Install obstore or fsspec."
221
187
  raise MissingDependencyError(msg)
222
188
 
@@ -244,35 +210,14 @@ class StorageRegistry:
244
210
  raise ValueError(msg)
245
211
 
246
212
  def _create_backend(self, backend_type: str, uri: str, **kwargs: Any) -> ObjectStoreProtocol:
247
- """Create backend instance for URI.
248
-
249
- Args:
250
- backend_type: Backend type ('obstore' or 'fsspec')
251
- uri: URI to create backend for
252
- **kwargs: Additional backend-specific configuration
253
-
254
- Returns:
255
- Backend instance
256
- """
213
+ """Create backend instance for URI."""
257
214
  backend_cls = self._get_backend_class(backend_type)
258
- # Both backends accept URI as first positional parameter
259
215
  return backend_cls(uri, **kwargs)
260
216
 
261
217
  def _get_scheme(self, uri: str) -> str:
262
- """Extract scheme from URI.
263
-
264
- Args:
265
- uri: URI to extract scheme from
266
-
267
- Returns:
268
- Scheme (e.g., 's3', 'gs', 'file')
269
- """
270
- # Handle file paths without explicit file:// scheme
218
+ """Extract scheme from URI."""
271
219
  if not uri or "://" not in uri:
272
- # Local path (absolute or relative)
273
220
  return "file"
274
-
275
- # Extract scheme from URI
276
221
  return uri.split("://", maxsplit=1)[0].lower()
277
222
 
278
223
  # Utility methods
@@ -310,6 +255,34 @@ class StorageRegistry:
310
255
  self._alias_configs.clear()
311
256
  self._aliases.clear()
312
257
 
258
+ def get_backend_capabilities(self, uri_or_alias: Union[str, Path]) -> "StorageCapabilities":
259
+ """Get capabilities for a backend without creating an instance.
260
+
261
+ Args:
262
+ uri_or_alias: URI or alias to check capabilities for
263
+
264
+ Returns:
265
+ StorageCapabilities object describing backend capabilities
266
+ """
267
+ if isinstance(uri_or_alias, Path):
268
+ uri_or_alias = f"file://{uri_or_alias.resolve()}"
269
+
270
+ if "://" in uri_or_alias:
271
+ backend_cls = self._determine_backend_class(uri_or_alias)
272
+ elif uri_or_alias in self._alias_configs:
273
+ backend_cls, _, _ = self._alias_configs[uri_or_alias]
274
+ else:
275
+ msg = f"Unknown storage alias or invalid URI: '{uri_or_alias}'"
276
+ raise ImproperConfigurationError(msg)
277
+
278
+ # Get capabilities from the backend class
279
+ if hasattr(backend_cls, "capabilities"):
280
+ return backend_cls.capabilities
281
+
282
+ # Default capabilities if not defined
283
+
284
+ return StorageCapabilities()
285
+
313
286
 
314
287
  # Global registry instance
315
288
  storage_registry = StorageRegistry()