sqlspec 0.24.1__py3-none-any.whl → 0.26.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.
- sqlspec/_serialization.py +223 -21
- sqlspec/_sql.py +20 -62
- sqlspec/_typing.py +11 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +129 -20
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +68 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +8 -4
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/base.py +3 -4
- sqlspec/builder/_base.py +130 -48
- sqlspec/builder/_column.py +66 -24
- sqlspec/builder/_ddl.py +91 -41
- sqlspec/builder/_insert.py +40 -58
- sqlspec/builder/_parsing_utils.py +127 -12
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
- sqlspec/builder/mixins/_delete_operations.py +12 -7
- sqlspec/builder/mixins/_insert_operations.py +50 -36
- sqlspec/builder/mixins/_join_operations.py +15 -30
- sqlspec/builder/mixins/_merge_operations.py +210 -78
- sqlspec/builder/mixins/_order_limit_operations.py +4 -10
- sqlspec/builder/mixins/_pivot_operations.py +1 -0
- sqlspec/builder/mixins/_select_operations.py +44 -22
- sqlspec/builder/mixins/_update_operations.py +30 -37
- sqlspec/builder/mixins/_where_clause.py +52 -70
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/__init__.py +3 -2
- sqlspec/core/cache.py +298 -352
- sqlspec/core/compiler.py +61 -4
- sqlspec/core/filters.py +246 -213
- sqlspec/core/hashing.py +9 -11
- sqlspec/core/parameters.py +27 -10
- sqlspec/core/statement.py +72 -12
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +108 -5
- sqlspec/driver/_common.py +186 -17
- sqlspec/driver/_sync.py +108 -5
- sqlspec/driver/mixins/_result_tools.py +60 -7
- sqlspec/exceptions.py +5 -0
- sqlspec/loader.py +8 -9
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/storage/backends/fsspec.py +1 -0
- sqlspec/typing.py +4 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.24.1.dist-info/RECORD +0 -139
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/core/hashing.py
CHANGED
|
@@ -185,26 +185,24 @@ def hash_sql_statement(statement: "SQL") -> str:
|
|
|
185
185
|
"""
|
|
186
186
|
from sqlspec.utils.type_guards import is_expression
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
else:
|
|
191
|
-
expr_hash = hash(statement._raw_sql)
|
|
188
|
+
stmt_expr = statement.statement_expression
|
|
189
|
+
expr_hash = hash_expression(stmt_expr) if is_expression(stmt_expr) else hash(statement.raw_sql)
|
|
192
190
|
|
|
193
191
|
param_hash = hash_parameters(
|
|
194
|
-
positional_parameters=statement.
|
|
195
|
-
named_parameters=statement.
|
|
196
|
-
original_parameters=statement.
|
|
192
|
+
positional_parameters=statement.positional_parameters,
|
|
193
|
+
named_parameters=statement.named_parameters,
|
|
194
|
+
original_parameters=statement.original_parameters,
|
|
197
195
|
)
|
|
198
196
|
|
|
199
|
-
filter_hash = hash_filters(statement.
|
|
197
|
+
filter_hash = hash_filters(statement.filters)
|
|
200
198
|
|
|
201
199
|
state_components = [
|
|
202
200
|
expr_hash,
|
|
203
201
|
param_hash,
|
|
204
202
|
filter_hash,
|
|
205
|
-
hash(statement.
|
|
206
|
-
hash(statement.
|
|
207
|
-
hash(statement.
|
|
203
|
+
hash(statement.dialect),
|
|
204
|
+
hash(statement.is_many),
|
|
205
|
+
hash(statement.is_script),
|
|
208
206
|
]
|
|
209
207
|
|
|
210
208
|
return f"sql:{hash(tuple(state_components))}"
|
sqlspec/core/parameters.py
CHANGED
|
@@ -619,7 +619,9 @@ class ParameterConverter:
|
|
|
619
619
|
|
|
620
620
|
return converted_sql
|
|
621
621
|
|
|
622
|
-
def _convert_sequence_to_dict(
|
|
622
|
+
def _convert_sequence_to_dict(
|
|
623
|
+
self, parameters: "Sequence[Any]", param_info: "list[ParameterInfo]"
|
|
624
|
+
) -> "dict[str, Any]":
|
|
623
625
|
"""Convert sequence parameters to dictionary for named styles.
|
|
624
626
|
|
|
625
627
|
Args:
|
|
@@ -637,7 +639,7 @@ class ParameterConverter:
|
|
|
637
639
|
return param_dict
|
|
638
640
|
|
|
639
641
|
def _extract_param_value_mixed_styles(
|
|
640
|
-
self, param: ParameterInfo, parameters: Mapping, param_keys: "list[str]"
|
|
642
|
+
self, param: ParameterInfo, parameters: "Mapping[str, Any]", param_keys: "list[str]"
|
|
641
643
|
) -> "tuple[Any, bool]":
|
|
642
644
|
"""Extract parameter value for mixed style parameters.
|
|
643
645
|
|
|
@@ -670,7 +672,9 @@ class ParameterConverter:
|
|
|
670
672
|
|
|
671
673
|
return None, False
|
|
672
674
|
|
|
673
|
-
def _extract_param_value_single_style(
|
|
675
|
+
def _extract_param_value_single_style(
|
|
676
|
+
self, param: ParameterInfo, parameters: "Mapping[str, Any]"
|
|
677
|
+
) -> "tuple[Any, bool]":
|
|
674
678
|
"""Extract parameter value for single style parameters.
|
|
675
679
|
|
|
676
680
|
Args:
|
|
@@ -755,17 +759,30 @@ class ParameterConverter:
|
|
|
755
759
|
parameter_styles = {p.style for p in param_info}
|
|
756
760
|
has_mixed_styles = len(parameter_styles) > 1
|
|
757
761
|
|
|
762
|
+
# Build unique parameter mapping to avoid duplicates when same parameter appears multiple times
|
|
763
|
+
unique_params: dict[str, Any] = {}
|
|
764
|
+
param_order: list[str] = []
|
|
765
|
+
|
|
758
766
|
if has_mixed_styles:
|
|
759
767
|
param_keys = list(parameters.keys())
|
|
760
768
|
for param in param_info:
|
|
761
|
-
|
|
762
|
-
if
|
|
763
|
-
|
|
769
|
+
param_key = param.placeholder_text
|
|
770
|
+
if param_key not in unique_params:
|
|
771
|
+
value, found = self._extract_param_value_mixed_styles(param, parameters, param_keys)
|
|
772
|
+
if found:
|
|
773
|
+
unique_params[param_key] = value
|
|
774
|
+
param_order.append(param_key)
|
|
764
775
|
else:
|
|
765
776
|
for param in param_info:
|
|
766
|
-
|
|
767
|
-
if
|
|
768
|
-
|
|
777
|
+
param_key = param.placeholder_text
|
|
778
|
+
if param_key not in unique_params:
|
|
779
|
+
value, found = self._extract_param_value_single_style(param, parameters)
|
|
780
|
+
if found:
|
|
781
|
+
unique_params[param_key] = value
|
|
782
|
+
param_order.append(param_key)
|
|
783
|
+
|
|
784
|
+
# Build parameter values list from unique parameters in order
|
|
785
|
+
param_values = [unique_params[param_key] for param_key in param_order]
|
|
769
786
|
|
|
770
787
|
if preserve_parameter_format and original_parameters is not None:
|
|
771
788
|
return self._preserve_original_format(param_values, original_parameters)
|
|
@@ -1064,7 +1081,7 @@ class ParameterProcessor:
|
|
|
1064
1081
|
|
|
1065
1082
|
return processed_sql, processed_parameters
|
|
1066
1083
|
|
|
1067
|
-
def
|
|
1084
|
+
def get_sqlglot_compatible_sql(
|
|
1068
1085
|
self, sql: str, parameters: Any, config: ParameterStyleConfig, dialect: Optional[str] = None
|
|
1069
1086
|
) -> "tuple[str, Any]":
|
|
1070
1087
|
"""Get SQL normalized for parsing only (Phase 1 only).
|
sqlspec/core/statement.py
CHANGED
|
@@ -18,6 +18,7 @@ from sqlspec.utils.type_guards import is_statement_filter, supports_where
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from sqlglot.dialects.dialect import DialectType
|
|
20
20
|
|
|
21
|
+
from sqlspec.core.cache import FiltersView
|
|
21
22
|
from sqlspec.core.filters import StatementFilter
|
|
22
23
|
|
|
23
24
|
|
|
@@ -58,6 +59,7 @@ PROCESSED_STATE_SLOTS: Final = (
|
|
|
58
59
|
"execution_parameters",
|
|
59
60
|
"parsed_expression",
|
|
60
61
|
"operation_type",
|
|
62
|
+
"parameter_casts",
|
|
61
63
|
"validation_errors",
|
|
62
64
|
"is_many",
|
|
63
65
|
)
|
|
@@ -80,6 +82,7 @@ class ProcessedState:
|
|
|
80
82
|
execution_parameters: Any,
|
|
81
83
|
parsed_expression: "Optional[exp.Expression]" = None,
|
|
82
84
|
operation_type: "OperationType" = "UNKNOWN",
|
|
85
|
+
parameter_casts: "Optional[dict[int, str]]" = None,
|
|
83
86
|
validation_errors: "Optional[list[str]]" = None,
|
|
84
87
|
is_many: bool = False,
|
|
85
88
|
) -> None:
|
|
@@ -87,6 +90,7 @@ class ProcessedState:
|
|
|
87
90
|
self.execution_parameters = execution_parameters
|
|
88
91
|
self.parsed_expression = parsed_expression
|
|
89
92
|
self.operation_type = operation_type
|
|
93
|
+
self.parameter_casts = parameter_casts or {}
|
|
90
94
|
self.validation_errors = validation_errors or []
|
|
91
95
|
self.is_many = is_many
|
|
92
96
|
|
|
@@ -161,14 +165,14 @@ class SQL:
|
|
|
161
165
|
self._process_parameters(*parameters, **kwargs)
|
|
162
166
|
|
|
163
167
|
def _create_auto_config(
|
|
164
|
-
self,
|
|
168
|
+
self, _statement: "Union[str, exp.Expression, 'SQL']", _parameters: tuple, _kwargs: dict[str, Any]
|
|
165
169
|
) -> "StatementConfig":
|
|
166
170
|
"""Create default StatementConfig when none provided.
|
|
167
171
|
|
|
168
172
|
Args:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
_statement: The SQL statement (unused)
|
|
174
|
+
_parameters: Statement parameters (unused)
|
|
175
|
+
_kwargs: Additional keyword arguments (unused)
|
|
172
176
|
|
|
173
177
|
Returns:
|
|
174
178
|
Default StatementConfig instance
|
|
@@ -196,14 +200,14 @@ class SQL:
|
|
|
196
200
|
Args:
|
|
197
201
|
sql_obj: Existing SQL object to copy from
|
|
198
202
|
"""
|
|
199
|
-
self._raw_sql = sql_obj.
|
|
200
|
-
self._filters = sql_obj.
|
|
201
|
-
self._named_parameters = sql_obj.
|
|
202
|
-
self._positional_parameters = sql_obj.
|
|
203
|
-
self._is_many = sql_obj.
|
|
204
|
-
self._is_script = sql_obj.
|
|
205
|
-
if sql_obj.
|
|
206
|
-
self._processed_state = sql_obj.
|
|
203
|
+
self._raw_sql = sql_obj.raw_sql
|
|
204
|
+
self._filters = sql_obj.filters.copy()
|
|
205
|
+
self._named_parameters = sql_obj.named_parameters.copy()
|
|
206
|
+
self._positional_parameters = sql_obj.positional_parameters.copy()
|
|
207
|
+
self._is_many = sql_obj.is_many
|
|
208
|
+
self._is_script = sql_obj.is_script
|
|
209
|
+
if sql_obj.is_processed:
|
|
210
|
+
self._processed_state = sql_obj.get_processed_state()
|
|
207
211
|
|
|
208
212
|
def _should_auto_detect_many(self, parameters: tuple) -> bool:
|
|
209
213
|
"""Detect execute_many mode from parameter structure.
|
|
@@ -270,6 +274,15 @@ class SQL:
|
|
|
270
274
|
"""Get the raw SQL string."""
|
|
271
275
|
return self._raw_sql
|
|
272
276
|
|
|
277
|
+
@property
|
|
278
|
+
def raw_sql(self) -> str:
|
|
279
|
+
"""Get raw SQL string (public API).
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The raw SQL string
|
|
283
|
+
"""
|
|
284
|
+
return self._raw_sql
|
|
285
|
+
|
|
273
286
|
@property
|
|
274
287
|
def parameters(self) -> Any:
|
|
275
288
|
"""Get the original parameters."""
|
|
@@ -277,6 +290,21 @@ class SQL:
|
|
|
277
290
|
return self._named_parameters
|
|
278
291
|
return self._positional_parameters or []
|
|
279
292
|
|
|
293
|
+
@property
|
|
294
|
+
def positional_parameters(self) -> "list[Any]":
|
|
295
|
+
"""Get positional parameters (public API)."""
|
|
296
|
+
return self._positional_parameters or []
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def named_parameters(self) -> "dict[str, Any]":
|
|
300
|
+
"""Get named parameters (public API)."""
|
|
301
|
+
return self._named_parameters
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def original_parameters(self) -> Any:
|
|
305
|
+
"""Get original parameters (public API)."""
|
|
306
|
+
return self._original_parameters
|
|
307
|
+
|
|
280
308
|
@property
|
|
281
309
|
def operation_type(self) -> "OperationType":
|
|
282
310
|
"""SQL operation type."""
|
|
@@ -301,6 +329,25 @@ class SQL:
|
|
|
301
329
|
"""Applied filters."""
|
|
302
330
|
return self._filters.copy()
|
|
303
331
|
|
|
332
|
+
def get_filters_view(self) -> "FiltersView":
|
|
333
|
+
"""Get zero-copy filters view (public API).
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Read-only view of filters without copying
|
|
337
|
+
"""
|
|
338
|
+
from sqlspec.core.cache import FiltersView
|
|
339
|
+
|
|
340
|
+
return FiltersView(self._filters)
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def is_processed(self) -> bool:
|
|
344
|
+
"""Check if SQL has been processed (public API)."""
|
|
345
|
+
return self._processed_state is not Empty
|
|
346
|
+
|
|
347
|
+
def get_processed_state(self) -> Any:
|
|
348
|
+
"""Get processed state (public API)."""
|
|
349
|
+
return self._processed_state
|
|
350
|
+
|
|
304
351
|
@property
|
|
305
352
|
def dialect(self) -> "Optional[str]":
|
|
306
353
|
"""SQL dialect."""
|
|
@@ -311,6 +358,17 @@ class SQL:
|
|
|
311
358
|
"""Internal SQLGlot expression."""
|
|
312
359
|
return self.expression
|
|
313
360
|
|
|
361
|
+
@property
|
|
362
|
+
def statement_expression(self) -> "Optional[exp.Expression]":
|
|
363
|
+
"""Get parsed statement expression (public API).
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Parsed SQLGlot expression or None if not parsed
|
|
367
|
+
"""
|
|
368
|
+
if self._processed_state is not Empty:
|
|
369
|
+
return self._processed_state.parsed_expression
|
|
370
|
+
return None
|
|
371
|
+
|
|
314
372
|
@property
|
|
315
373
|
def is_many(self) -> bool:
|
|
316
374
|
"""Check if this is execute_many."""
|
|
@@ -392,6 +450,7 @@ class SQL:
|
|
|
392
450
|
execution_parameters=compiled_result.execution_parameters,
|
|
393
451
|
parsed_expression=compiled_result.expression,
|
|
394
452
|
operation_type=compiled_result.operation_type,
|
|
453
|
+
parameter_casts=compiled_result.parameter_casts,
|
|
395
454
|
validation_errors=[],
|
|
396
455
|
is_many=self._is_many,
|
|
397
456
|
)
|
|
@@ -403,6 +462,7 @@ class SQL:
|
|
|
403
462
|
compiled_sql=self._raw_sql,
|
|
404
463
|
execution_parameters=self._named_parameters or self._positional_parameters,
|
|
405
464
|
operation_type="UNKNOWN",
|
|
465
|
+
parameter_casts={},
|
|
406
466
|
is_many=self._is_many,
|
|
407
467
|
)
|
|
408
468
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Centralized type conversion and detection for SQLSpec.
|
|
2
|
+
|
|
3
|
+
Provides unified type detection and conversion utilities for all database
|
|
4
|
+
adapters, with MyPyC-compatible optimizations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from datetime import date, datetime, time, timezone
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from typing import Any, Callable, Final, Optional
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from sqlspec._serialization import decode_json
|
|
14
|
+
|
|
15
|
+
# MyPyC-compatible pre-compiled patterns
|
|
16
|
+
SPECIAL_TYPE_REGEX: Final[re.Pattern[str]] = re.compile(
|
|
17
|
+
r"^(?:"
|
|
18
|
+
r"(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|"
|
|
19
|
+
r"(?P<iso_datetime>\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)|"
|
|
20
|
+
r"(?P<iso_date>\d{4}-\d{2}-\d{2})|"
|
|
21
|
+
r"(?P<iso_time>\d{2}:\d{2}:\d{2}(?:\.\d+)?)|"
|
|
22
|
+
r"(?P<json>[\[{].*[\]}])|"
|
|
23
|
+
r"(?P<ipv4>(?:\d{1,3}\.){3}\d{1,3})|"
|
|
24
|
+
r"(?P<ipv6>(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4})|"
|
|
25
|
+
r"(?P<mac>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})"
|
|
26
|
+
r")$",
|
|
27
|
+
re.IGNORECASE | re.DOTALL,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseTypeConverter:
|
|
32
|
+
"""Universal type detection and conversion for all adapters.
|
|
33
|
+
|
|
34
|
+
Provides centralized type detection and conversion functionality
|
|
35
|
+
that can be used across all database adapters to ensure consistent
|
|
36
|
+
behavior. Users can extend this class for custom type conversion needs.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ()
|
|
40
|
+
|
|
41
|
+
def detect_type(self, value: str) -> Optional[str]:
|
|
42
|
+
"""Detect special types from string values.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value: String value to analyze.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Type name if detected, None otherwise.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(value, str): # pyright: ignore
|
|
51
|
+
return None
|
|
52
|
+
if not value:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
match = SPECIAL_TYPE_REGEX.match(value)
|
|
56
|
+
if not match:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
return next((k for k, v in match.groupdict().items() if v), None)
|
|
60
|
+
|
|
61
|
+
def convert_value(self, value: str, detected_type: str) -> Any:
|
|
62
|
+
"""Convert string value to appropriate Python type.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
value: String value to convert.
|
|
66
|
+
detected_type: Detected type name.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Converted value in appropriate Python type.
|
|
70
|
+
"""
|
|
71
|
+
converter = _TYPE_CONVERTERS.get(detected_type)
|
|
72
|
+
if converter:
|
|
73
|
+
return converter(value)
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
def convert_if_detected(self, value: Any) -> Any:
|
|
77
|
+
"""Convert value only if special type detected, else return original.
|
|
78
|
+
|
|
79
|
+
This method provides performance optimization by avoiding expensive
|
|
80
|
+
regex operations on plain strings that don't contain special characters.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
value: Value to potentially convert.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Converted value if special type detected, original value otherwise.
|
|
87
|
+
"""
|
|
88
|
+
if not isinstance(value, str):
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
# Quick pre-check for performance - avoid regex on plain strings
|
|
92
|
+
if not any(c in value for c in ["{", "[", "-", ":", "T"]):
|
|
93
|
+
return value # Skip regex entirely for "hello world" etc.
|
|
94
|
+
|
|
95
|
+
detected_type = self.detect_type(value)
|
|
96
|
+
if detected_type:
|
|
97
|
+
try:
|
|
98
|
+
return self.convert_value(value, detected_type)
|
|
99
|
+
except Exception:
|
|
100
|
+
# If conversion fails, return original value
|
|
101
|
+
return value
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def convert_uuid(value: str) -> UUID:
|
|
106
|
+
"""Convert UUID string to UUID object.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
value: UUID string.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
UUID object.
|
|
113
|
+
"""
|
|
114
|
+
return UUID(value)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def convert_iso_datetime(value: str) -> datetime:
|
|
118
|
+
"""Convert ISO 8601 datetime string to datetime object.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
value: ISO datetime string.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
datetime object.
|
|
125
|
+
"""
|
|
126
|
+
# Handle various ISO formats with timezone
|
|
127
|
+
if value.endswith("Z"):
|
|
128
|
+
value = value[:-1] + "+00:00"
|
|
129
|
+
|
|
130
|
+
# Replace space with T for standard ISO format
|
|
131
|
+
if " " in value and "T" not in value:
|
|
132
|
+
value = value.replace(" ", "T")
|
|
133
|
+
|
|
134
|
+
return datetime.fromisoformat(value)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def convert_iso_date(value: str) -> date:
|
|
138
|
+
"""Convert ISO date string to date object.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
value: ISO date string.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
date object.
|
|
145
|
+
"""
|
|
146
|
+
return date.fromisoformat(value)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def convert_iso_time(value: str) -> time:
|
|
150
|
+
"""Convert ISO time string to time object.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
value: ISO time string.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
time object.
|
|
157
|
+
"""
|
|
158
|
+
return time.fromisoformat(value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def convert_json(value: str) -> Any:
|
|
162
|
+
"""Convert JSON string to Python object.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
value: JSON string.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Decoded Python object.
|
|
169
|
+
"""
|
|
170
|
+
return decode_json(value)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def convert_decimal(value: str) -> Decimal:
|
|
174
|
+
"""Convert string to Decimal for precise arithmetic.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
value: Decimal string.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Decimal object.
|
|
181
|
+
"""
|
|
182
|
+
return Decimal(value)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# Converter registry
|
|
186
|
+
_TYPE_CONVERTERS: Final[dict[str, Callable[[str], Any]]] = {
|
|
187
|
+
"uuid": convert_uuid,
|
|
188
|
+
"iso_datetime": convert_iso_datetime,
|
|
189
|
+
"iso_date": convert_iso_date,
|
|
190
|
+
"iso_time": convert_iso_time,
|
|
191
|
+
"json": convert_json,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def format_datetime_rfc3339(dt: datetime) -> str:
|
|
196
|
+
"""Format datetime as RFC 3339 compliant string.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
dt: datetime object.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
RFC 3339 formatted datetime string.
|
|
203
|
+
"""
|
|
204
|
+
if dt.tzinfo is None:
|
|
205
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
206
|
+
return dt.isoformat()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def parse_datetime_rfc3339(dt_str: str) -> datetime:
|
|
210
|
+
"""Parse RFC 3339 datetime string.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
dt_str: RFC 3339 datetime string.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
datetime object.
|
|
217
|
+
"""
|
|
218
|
+
# Handle Z suffix
|
|
219
|
+
if dt_str.endswith("Z"):
|
|
220
|
+
dt_str = dt_str[:-1] + "+00:00"
|
|
221
|
+
return datetime.fromisoformat(dt_str)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = (
|
|
225
|
+
"BaseTypeConverter",
|
|
226
|
+
"convert_decimal",
|
|
227
|
+
"convert_iso_date",
|
|
228
|
+
"convert_iso_datetime",
|
|
229
|
+
"convert_iso_time",
|
|
230
|
+
"convert_json",
|
|
231
|
+
"convert_uuid",
|
|
232
|
+
"format_datetime_rfc3339",
|
|
233
|
+
"parse_datetime_rfc3339",
|
|
234
|
+
)
|
sqlspec/driver/__init__.py
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
5
|
from sqlspec.driver import mixins
|
|
6
|
-
from sqlspec.driver._async import AsyncDriverAdapterBase
|
|
7
|
-
from sqlspec.driver._common import CommonDriverAttributesMixin, ExecutionResult
|
|
8
|
-
from sqlspec.driver._sync import SyncDriverAdapterBase
|
|
6
|
+
from sqlspec.driver._async import AsyncDataDictionaryBase, AsyncDriverAdapterBase
|
|
7
|
+
from sqlspec.driver._common import CommonDriverAttributesMixin, ExecutionResult, VersionInfo
|
|
8
|
+
from sqlspec.driver._sync import SyncDataDictionaryBase, SyncDriverAdapterBase
|
|
9
9
|
|
|
10
10
|
__all__ = (
|
|
11
|
+
"AsyncDataDictionaryBase",
|
|
11
12
|
"AsyncDriverAdapterBase",
|
|
12
13
|
"CommonDriverAttributesMixin",
|
|
13
14
|
"DriverAdapterProtocol",
|
|
14
15
|
"ExecutionResult",
|
|
16
|
+
"SyncDataDictionaryBase",
|
|
15
17
|
"SyncDriverAdapterBase",
|
|
18
|
+
"VersionInfo",
|
|
16
19
|
"mixins",
|
|
17
20
|
)
|
|
18
21
|
|
sqlspec/driver/_async.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Asynchronous driver protocol implementation."""
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Final, NoReturn, Optional, Union, cast, overload
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Final, NoReturn, Optional, TypeVar, Union, cast, overload
|
|
5
5
|
|
|
6
6
|
from sqlspec.core import SQL, Statement
|
|
7
|
-
from sqlspec.driver._common import CommonDriverAttributesMixin, ExecutionResult
|
|
7
|
+
from sqlspec.driver._common import CommonDriverAttributesMixin, DataDictionaryMixin, ExecutionResult, VersionInfo
|
|
8
8
|
from sqlspec.driver.mixins import SQLTranslatorMixin, ToSchemaMixin
|
|
9
9
|
from sqlspec.exceptions import NotFoundError
|
|
10
10
|
from sqlspec.utils.logging import get_logger
|
|
@@ -21,17 +21,28 @@ if TYPE_CHECKING:
|
|
|
21
21
|
_LOGGER_NAME: Final[str] = "sqlspec"
|
|
22
22
|
logger = get_logger(_LOGGER_NAME)
|
|
23
23
|
|
|
24
|
-
__all__ = ("AsyncDriverAdapterBase",)
|
|
24
|
+
__all__ = ("AsyncDataDictionaryBase", "AsyncDriverAdapterBase", "AsyncDriverT")
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
EMPTY_FILTERS: Final["list[StatementFilter]"] = []
|
|
28
28
|
|
|
29
|
+
AsyncDriverT = TypeVar("AsyncDriverT", bound="AsyncDriverAdapterBase")
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, ToSchemaMixin):
|
|
31
33
|
"""Base class for asynchronous database drivers."""
|
|
32
34
|
|
|
33
35
|
__slots__ = ()
|
|
34
36
|
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def data_dictionary(self) -> "AsyncDataDictionaryBase":
|
|
40
|
+
"""Get the data dictionary for this driver.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Data dictionary instance for metadata queries
|
|
44
|
+
"""
|
|
45
|
+
|
|
35
46
|
async def dispatch_statement_execution(self, statement: "SQL", connection: "Any") -> "SQLResult":
|
|
36
47
|
"""Central execution dispatcher using the Template Method Pattern.
|
|
37
48
|
|
|
@@ -186,10 +197,10 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
|
|
|
186
197
|
config = statement_config or self.statement_config
|
|
187
198
|
|
|
188
199
|
if isinstance(statement, SQL):
|
|
189
|
-
sql_statement = SQL(statement.
|
|
200
|
+
sql_statement = SQL(statement.raw_sql, parameters, statement_config=config, is_many=True, **kwargs)
|
|
190
201
|
else:
|
|
191
202
|
base_statement = self.prepare_statement(statement, filters, statement_config=config, kwargs=kwargs)
|
|
192
|
-
sql_statement = SQL(base_statement.
|
|
203
|
+
sql_statement = SQL(base_statement.raw_sql, parameters, statement_config=config, is_many=True, **kwargs)
|
|
193
204
|
|
|
194
205
|
return await self.dispatch_statement_execution(statement=sql_statement, connection=self.connection)
|
|
195
206
|
|
|
@@ -487,3 +498,95 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
|
|
|
487
498
|
def _raise_cannot_extract_value_from_row_type(self, type_name: str) -> NoReturn:
|
|
488
499
|
msg = f"Cannot extract value from row type {type_name}"
|
|
489
500
|
raise TypeError(msg)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class AsyncDataDictionaryBase(DataDictionaryMixin):
|
|
504
|
+
"""Base class for asynchronous data dictionary implementations."""
|
|
505
|
+
|
|
506
|
+
@abstractmethod
|
|
507
|
+
async def get_version(self, driver: "AsyncDriverAdapterBase") -> "Optional[VersionInfo]":
|
|
508
|
+
"""Get database version information.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
driver: Async database driver instance
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Version information or None if detection fails
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
@abstractmethod
|
|
518
|
+
async def get_feature_flag(self, driver: "AsyncDriverAdapterBase", feature: str) -> bool:
|
|
519
|
+
"""Check if database supports a specific feature.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
driver: Async database driver instance
|
|
523
|
+
feature: Feature name to check
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
True if feature is supported, False otherwise
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
@abstractmethod
|
|
530
|
+
async def get_optimal_type(self, driver: "AsyncDriverAdapterBase", type_category: str) -> str:
|
|
531
|
+
"""Get optimal database type for a category.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
driver: Async database driver instance
|
|
535
|
+
type_category: Type category (e.g., 'json', 'uuid', 'boolean')
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Database-specific type name
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
async def get_tables(self, driver: "AsyncDriverAdapterBase", schema: "Optional[str]" = None) -> "list[str]":
|
|
542
|
+
"""Get list of tables in schema.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
driver: Async database driver instance
|
|
546
|
+
schema: Schema name (None for default)
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
List of table names
|
|
550
|
+
"""
|
|
551
|
+
_ = driver, schema
|
|
552
|
+
return []
|
|
553
|
+
|
|
554
|
+
async def get_columns(
|
|
555
|
+
self, driver: "AsyncDriverAdapterBase", table: str, schema: "Optional[str]" = None
|
|
556
|
+
) -> "list[dict[str, Any]]":
|
|
557
|
+
"""Get column information for a table.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
driver: Async database driver instance
|
|
561
|
+
table: Table name
|
|
562
|
+
schema: Schema name (None for default)
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
List of column metadata dictionaries
|
|
566
|
+
"""
|
|
567
|
+
_ = driver, table, schema
|
|
568
|
+
return []
|
|
569
|
+
|
|
570
|
+
async def get_indexes(
|
|
571
|
+
self, driver: "AsyncDriverAdapterBase", table: str, schema: "Optional[str]" = None
|
|
572
|
+
) -> "list[dict[str, Any]]":
|
|
573
|
+
"""Get index information for a table.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
driver: Async database driver instance
|
|
577
|
+
table: Table name
|
|
578
|
+
schema: Schema name (None for default)
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
List of index metadata dictionaries
|
|
582
|
+
"""
|
|
583
|
+
_ = driver, table, schema
|
|
584
|
+
return []
|
|
585
|
+
|
|
586
|
+
def list_available_features(self) -> "list[str]":
|
|
587
|
+
"""List all features that can be checked via get_feature_flag.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
List of feature names this data dictionary supports
|
|
591
|
+
"""
|
|
592
|
+
return self.get_default_features()
|