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/driver/_common.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Common driver attributes and utilities."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import re
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Final, NamedTuple, Optional, TypeVar, Union, cast
|
|
4
6
|
|
|
5
7
|
from mypy_extensions import trait
|
|
6
8
|
from sqlglot import exp
|
|
7
9
|
|
|
8
10
|
from sqlspec.builder import QueryBuilder
|
|
9
11
|
from sqlspec.core import SQL, ParameterStyle, SQLResult, Statement, StatementConfig, TypedParameter
|
|
10
|
-
from sqlspec.core.cache import
|
|
12
|
+
from sqlspec.core.cache import CachedStatement, get_cache, get_cache_config
|
|
11
13
|
from sqlspec.core.splitter import split_sql_script
|
|
12
14
|
from sqlspec.exceptions import ImproperConfigurationError
|
|
13
15
|
from sqlspec.utils.logging import get_logger
|
|
@@ -25,13 +27,158 @@ __all__ = (
|
|
|
25
27
|
"EXEC_ROWCOUNT_OVERRIDE",
|
|
26
28
|
"EXEC_SPECIAL_DATA",
|
|
27
29
|
"CommonDriverAttributesMixin",
|
|
30
|
+
"DataDictionaryMixin",
|
|
28
31
|
"ExecutionResult",
|
|
29
32
|
"ScriptExecutionResult",
|
|
33
|
+
"VersionInfo",
|
|
30
34
|
)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
logger = get_logger("driver")
|
|
34
38
|
|
|
39
|
+
DriverT = TypeVar("DriverT")
|
|
40
|
+
VERSION_GROUPS_MIN_FOR_MINOR = 1
|
|
41
|
+
VERSION_GROUPS_MIN_FOR_PATCH = 2
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VersionInfo:
|
|
45
|
+
"""Database version information."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, major: int, minor: int = 0, patch: int = 0) -> None:
|
|
48
|
+
"""Initialize version info.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
major: Major version number
|
|
52
|
+
minor: Minor version number
|
|
53
|
+
patch: Patch version number
|
|
54
|
+
"""
|
|
55
|
+
self.major = major
|
|
56
|
+
self.minor = minor
|
|
57
|
+
self.patch = patch
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def version_tuple(self) -> "tuple[int, int, int]":
|
|
61
|
+
"""Get version as tuple for comparison."""
|
|
62
|
+
return (self.major, self.minor, self.patch)
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
"""String representation of version info."""
|
|
66
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
"""Detailed string representation."""
|
|
70
|
+
return f"VersionInfo({self.major}, {self.minor}, {self.patch})"
|
|
71
|
+
|
|
72
|
+
def __eq__(self, other: object) -> bool:
|
|
73
|
+
"""Check version equality."""
|
|
74
|
+
if not isinstance(other, VersionInfo):
|
|
75
|
+
return NotImplemented
|
|
76
|
+
return self.version_tuple == other.version_tuple
|
|
77
|
+
|
|
78
|
+
def __lt__(self, other: "VersionInfo") -> bool:
|
|
79
|
+
"""Check if this version is less than another."""
|
|
80
|
+
return self.version_tuple < other.version_tuple
|
|
81
|
+
|
|
82
|
+
def __le__(self, other: "VersionInfo") -> bool:
|
|
83
|
+
"""Check if this version is less than or equal to another."""
|
|
84
|
+
return self.version_tuple <= other.version_tuple
|
|
85
|
+
|
|
86
|
+
def __gt__(self, other: "VersionInfo") -> bool:
|
|
87
|
+
"""Check if this version is greater than another."""
|
|
88
|
+
return self.version_tuple > other.version_tuple
|
|
89
|
+
|
|
90
|
+
def __ge__(self, other: "VersionInfo") -> bool:
|
|
91
|
+
"""Check if this version is greater than or equal to another."""
|
|
92
|
+
return self.version_tuple >= other.version_tuple
|
|
93
|
+
|
|
94
|
+
def __hash__(self) -> int:
|
|
95
|
+
"""Make VersionInfo hashable based on version tuple."""
|
|
96
|
+
return hash(self.version_tuple)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@trait
|
|
100
|
+
class DataDictionaryMixin:
|
|
101
|
+
"""Mixin providing common data dictionary functionality."""
|
|
102
|
+
|
|
103
|
+
def parse_version_string(self, version_str: str) -> "Optional[VersionInfo]":
|
|
104
|
+
"""Parse version string into VersionInfo.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
version_str: Raw version string from database
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
VersionInfo instance or None if parsing fails
|
|
111
|
+
"""
|
|
112
|
+
# Try common version patterns
|
|
113
|
+
patterns = [
|
|
114
|
+
r"(\d+)\.(\d+)\.(\d+)", # x.y.z
|
|
115
|
+
r"(\d+)\.(\d+)", # x.y
|
|
116
|
+
r"(\d+)", # x
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
for pattern in patterns:
|
|
120
|
+
match = re.search(pattern, version_str)
|
|
121
|
+
if match:
|
|
122
|
+
groups = match.groups()
|
|
123
|
+
|
|
124
|
+
major = int(groups[0])
|
|
125
|
+
minor = int(groups[1]) if len(groups) > VERSION_GROUPS_MIN_FOR_MINOR else 0
|
|
126
|
+
patch = int(groups[2]) if len(groups) > VERSION_GROUPS_MIN_FOR_PATCH else 0
|
|
127
|
+
return VersionInfo(major, minor, patch)
|
|
128
|
+
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def detect_version_with_queries(self, driver: Any, queries: "list[str]") -> "Optional[VersionInfo]":
|
|
132
|
+
"""Try multiple version queries to detect database version.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
driver: Database driver instance
|
|
136
|
+
queries: List of SQL queries to try
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Version information or None if detection fails
|
|
140
|
+
"""
|
|
141
|
+
for query in queries:
|
|
142
|
+
with suppress(Exception):
|
|
143
|
+
result = driver.execute(query)
|
|
144
|
+
if result.data:
|
|
145
|
+
version_str = str(result.data[0])
|
|
146
|
+
if isinstance(result.data[0], dict):
|
|
147
|
+
version_str = str(next(iter(result.data[0].values())))
|
|
148
|
+
elif isinstance(result.data[0], (list, tuple)):
|
|
149
|
+
version_str = str(result.data[0][0])
|
|
150
|
+
|
|
151
|
+
parsed_version = self.parse_version_string(version_str)
|
|
152
|
+
if parsed_version:
|
|
153
|
+
logger.debug("Detected database version: %s", parsed_version)
|
|
154
|
+
return parsed_version
|
|
155
|
+
|
|
156
|
+
logger.warning("Could not detect database version")
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def get_default_type_mapping(self) -> "dict[str, str]":
|
|
160
|
+
"""Get default type mappings for common categories.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary mapping type categories to generic SQL types
|
|
164
|
+
"""
|
|
165
|
+
return {
|
|
166
|
+
"json": "TEXT",
|
|
167
|
+
"uuid": "VARCHAR(36)",
|
|
168
|
+
"boolean": "INTEGER",
|
|
169
|
+
"timestamp": "TIMESTAMP",
|
|
170
|
+
"text": "TEXT",
|
|
171
|
+
"blob": "BLOB",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def get_default_features(self) -> "list[str]":
|
|
175
|
+
"""Get default feature flags supported by most databases.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
List of commonly supported feature names
|
|
179
|
+
"""
|
|
180
|
+
return ["supports_transactions", "supports_prepared_statements"]
|
|
181
|
+
|
|
35
182
|
|
|
36
183
|
class ScriptExecutionResult(NamedTuple):
|
|
37
184
|
"""Result from script execution with statement count information."""
|
|
@@ -206,16 +353,16 @@ class CommonDriverAttributesMixin:
|
|
|
206
353
|
sql_statement = statement.to_statement(statement_config)
|
|
207
354
|
if parameters or kwargs:
|
|
208
355
|
merged_parameters = (
|
|
209
|
-
(*sql_statement.
|
|
356
|
+
(*sql_statement.positional_parameters, *parameters)
|
|
210
357
|
if parameters
|
|
211
|
-
else sql_statement.
|
|
358
|
+
else sql_statement.positional_parameters
|
|
212
359
|
)
|
|
213
360
|
return SQL(sql_statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
|
|
214
361
|
return sql_statement
|
|
215
362
|
if isinstance(statement, SQL):
|
|
216
363
|
if parameters or kwargs:
|
|
217
364
|
merged_parameters = (
|
|
218
|
-
(*statement.
|
|
365
|
+
(*statement.positional_parameters, *parameters) if parameters else statement.positional_parameters
|
|
219
366
|
)
|
|
220
367
|
return SQL(statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
|
|
221
368
|
needs_rebuild = False
|
|
@@ -232,14 +379,14 @@ class CommonDriverAttributesMixin:
|
|
|
232
379
|
needs_rebuild = True
|
|
233
380
|
|
|
234
381
|
if needs_rebuild:
|
|
235
|
-
sql_text = statement.
|
|
382
|
+
sql_text = statement.raw_sql or statement.sql
|
|
236
383
|
|
|
237
384
|
if statement.is_many and statement.parameters:
|
|
238
385
|
new_sql = SQL(sql_text, statement.parameters, statement_config=statement_config, is_many=True)
|
|
239
|
-
elif statement.
|
|
240
|
-
new_sql = SQL(sql_text, statement_config=statement_config, **statement.
|
|
386
|
+
elif statement.named_parameters:
|
|
387
|
+
new_sql = SQL(sql_text, statement_config=statement_config, **statement.named_parameters)
|
|
241
388
|
else:
|
|
242
|
-
new_sql = SQL(sql_text, *statement.
|
|
389
|
+
new_sql = SQL(sql_text, *statement.positional_parameters, statement_config=statement_config)
|
|
243
390
|
|
|
244
391
|
return new_sql
|
|
245
392
|
return statement
|
|
@@ -270,7 +417,11 @@ class CommonDriverAttributesMixin:
|
|
|
270
417
|
]
|
|
271
418
|
|
|
272
419
|
def prepare_driver_parameters(
|
|
273
|
-
self,
|
|
420
|
+
self,
|
|
421
|
+
parameters: Any,
|
|
422
|
+
statement_config: "StatementConfig",
|
|
423
|
+
is_many: bool = False,
|
|
424
|
+
prepared_statement: Optional[Any] = None, # pyright: ignore[reportUnusedParameter]
|
|
274
425
|
) -> Any:
|
|
275
426
|
"""Prepare parameters for database driver consumption.
|
|
276
427
|
|
|
@@ -281,6 +432,7 @@ class CommonDriverAttributesMixin:
|
|
|
281
432
|
parameters: Parameters in any format (dict, list, tuple, scalar, TypedParameter)
|
|
282
433
|
statement_config: Statement configuration for parameter style detection
|
|
283
434
|
is_many: If True, handle as executemany parameter sequence
|
|
435
|
+
prepared_statement: Optional prepared statement containing metadata for parameter processing
|
|
284
436
|
|
|
285
437
|
Returns:
|
|
286
438
|
Parameters with TypedParameter objects unwrapped to primitive values
|
|
@@ -413,15 +565,16 @@ class CommonDriverAttributesMixin:
|
|
|
413
565
|
cache_key = None
|
|
414
566
|
if cache_config.compiled_cache_enabled and statement_config.enable_caching:
|
|
415
567
|
cache_key = self._generate_compilation_cache_key(statement, statement_config, flatten_single_parameters)
|
|
416
|
-
|
|
417
|
-
if
|
|
418
|
-
|
|
568
|
+
cache = get_cache()
|
|
569
|
+
cached_result = cache.get("statement", cache_key, str(statement.dialect) if statement.dialect else None)
|
|
570
|
+
if cached_result is not None and isinstance(cached_result, CachedStatement):
|
|
571
|
+
return cached_result.compiled_sql, cached_result.parameters
|
|
419
572
|
|
|
420
573
|
prepared_statement = self.prepare_statement(statement, statement_config=statement_config)
|
|
421
574
|
compiled_sql, execution_parameters = prepared_statement.compile()
|
|
422
575
|
|
|
423
576
|
prepared_parameters = self.prepare_driver_parameters(
|
|
424
|
-
execution_parameters, statement_config, is_many=statement.is_many
|
|
577
|
+
execution_parameters, statement_config, is_many=statement.is_many, prepared_statement=statement
|
|
425
578
|
)
|
|
426
579
|
|
|
427
580
|
if statement_config.parameter_config.output_transformer:
|
|
@@ -430,7 +583,23 @@ class CommonDriverAttributesMixin:
|
|
|
430
583
|
)
|
|
431
584
|
|
|
432
585
|
if cache_key is not None:
|
|
433
|
-
|
|
586
|
+
cache = get_cache()
|
|
587
|
+
cached_statement = CachedStatement(
|
|
588
|
+
compiled_sql=compiled_sql,
|
|
589
|
+
parameters=tuple(prepared_parameters)
|
|
590
|
+
if isinstance(prepared_parameters, list)
|
|
591
|
+
else (
|
|
592
|
+
prepared_parameters
|
|
593
|
+
if prepared_parameters is None or isinstance(prepared_parameters, dict)
|
|
594
|
+
else (
|
|
595
|
+
tuple(prepared_parameters)
|
|
596
|
+
if not isinstance(prepared_parameters, tuple)
|
|
597
|
+
else prepared_parameters
|
|
598
|
+
)
|
|
599
|
+
),
|
|
600
|
+
expression=statement.expression,
|
|
601
|
+
)
|
|
602
|
+
cache.put("statement", cache_key, cached_statement, str(statement.dialect) if statement.dialect else None)
|
|
434
603
|
|
|
435
604
|
return compiled_sql, prepared_parameters
|
|
436
605
|
|
|
@@ -562,8 +731,8 @@ class CommonDriverAttributesMixin:
|
|
|
562
731
|
count_expr.set("limit", None)
|
|
563
732
|
count_expr.set("offset", None)
|
|
564
733
|
|
|
565
|
-
return SQL(count_expr, *original_sql.
|
|
734
|
+
return SQL(count_expr, *original_sql.positional_parameters, statement_config=original_sql.statement_config)
|
|
566
735
|
|
|
567
736
|
subquery = cast("exp.Select", expr).subquery(alias="total_query")
|
|
568
737
|
count_expr = exp.select(exp.Count(this=exp.Star())).from_(subquery)
|
|
569
|
-
return SQL(count_expr, *original_sql.
|
|
738
|
+
return SQL(count_expr, *original_sql.positional_parameters, statement_config=original_sql.statement_config)
|
sqlspec/driver/_sync.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Synchronous 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
|
|
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__ = ("SyncDriverAdapterBase",)
|
|
24
|
+
__all__ = ("SyncDataDictionaryBase", "SyncDriverAdapterBase", "SyncDriverT")
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
EMPTY_FILTERS: Final["list[StatementFilter]"] = []
|
|
28
28
|
|
|
29
|
+
SyncDriverT = TypeVar("SyncDriverT", bound="SyncDriverAdapterBase")
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
class SyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, ToSchemaMixin):
|
|
31
33
|
"""Base class for synchronous database drivers."""
|
|
32
34
|
|
|
33
35
|
__slots__ = ()
|
|
34
36
|
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def data_dictionary(self) -> "SyncDataDictionaryBase":
|
|
40
|
+
"""Get the data dictionary for this driver.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Data dictionary instance for metadata queries
|
|
44
|
+
"""
|
|
45
|
+
|
|
35
46
|
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 SyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, ToS
|
|
|
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 self.dispatch_statement_execution(statement=sql_statement, connection=self.connection)
|
|
195
206
|
|
|
@@ -488,3 +499,95 @@ class SyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, ToS
|
|
|
488
499
|
def _raise_cannot_extract_value_from_row_type(self, type_name: str) -> NoReturn:
|
|
489
500
|
msg = f"Cannot extract value from row type {type_name}"
|
|
490
501
|
raise TypeError(msg)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class SyncDataDictionaryBase(DataDictionaryMixin):
|
|
505
|
+
"""Base class for synchronous data dictionary implementations."""
|
|
506
|
+
|
|
507
|
+
@abstractmethod
|
|
508
|
+
def get_version(self, driver: "SyncDriverAdapterBase") -> "Optional[VersionInfo]":
|
|
509
|
+
"""Get database version information.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
driver: Sync database driver instance
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Version information or None if detection fails
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
@abstractmethod
|
|
519
|
+
def get_feature_flag(self, driver: "SyncDriverAdapterBase", feature: str) -> bool:
|
|
520
|
+
"""Check if database supports a specific feature.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
driver: Sync database driver instance
|
|
524
|
+
feature: Feature name to check
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
True if feature is supported, False otherwise
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
@abstractmethod
|
|
531
|
+
def get_optimal_type(self, driver: "SyncDriverAdapterBase", type_category: str) -> str:
|
|
532
|
+
"""Get optimal database type for a category.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
driver: Sync database driver instance
|
|
536
|
+
type_category: Type category (e.g., 'json', 'uuid', 'boolean')
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Database-specific type name
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
def get_tables(self, driver: "SyncDriverAdapterBase", schema: "Optional[str]" = None) -> "list[str]":
|
|
543
|
+
"""Get list of tables in schema.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
driver: Sync database driver instance
|
|
547
|
+
schema: Schema name (None for default)
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
List of table names
|
|
551
|
+
"""
|
|
552
|
+
_ = driver, schema
|
|
553
|
+
return []
|
|
554
|
+
|
|
555
|
+
def get_columns(
|
|
556
|
+
self, driver: "SyncDriverAdapterBase", table: str, schema: "Optional[str]" = None
|
|
557
|
+
) -> "list[dict[str, Any]]":
|
|
558
|
+
"""Get column information for a table.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
driver: Sync database driver instance
|
|
562
|
+
table: Table name
|
|
563
|
+
schema: Schema name (None for default)
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
List of column metadata dictionaries
|
|
567
|
+
"""
|
|
568
|
+
_ = driver, table, schema
|
|
569
|
+
return []
|
|
570
|
+
|
|
571
|
+
def get_indexes(
|
|
572
|
+
self, driver: "SyncDriverAdapterBase", table: str, schema: "Optional[str]" = None
|
|
573
|
+
) -> "list[dict[str, Any]]":
|
|
574
|
+
"""Get index information for a table.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
driver: Sync database driver instance
|
|
578
|
+
table: Table name
|
|
579
|
+
schema: Schema name (None for default)
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
List of index metadata dictionaries
|
|
583
|
+
"""
|
|
584
|
+
_ = driver, table, schema
|
|
585
|
+
return []
|
|
586
|
+
|
|
587
|
+
def list_available_features(self) -> "list[str]":
|
|
588
|
+
"""List all features that can be checked via get_feature_flag.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
List of feature names this data dictionary supports
|
|
592
|
+
"""
|
|
593
|
+
return self.get_default_features()
|
|
@@ -15,6 +15,7 @@ from mypy_extensions import trait
|
|
|
15
15
|
from sqlspec.exceptions import SQLSpecError
|
|
16
16
|
from sqlspec.typing import (
|
|
17
17
|
CATTRS_INSTALLED,
|
|
18
|
+
NUMPY_INSTALLED,
|
|
18
19
|
ModelDTOT,
|
|
19
20
|
ModelT,
|
|
20
21
|
attrs_asdict,
|
|
@@ -41,12 +42,36 @@ logger = logging.getLogger(__name__)
|
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
_DATETIME_TYPES: Final[set[type]] = {datetime.datetime, datetime.date, datetime.time}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_list_type_target(target_type: Any) -> bool:
|
|
48
|
+
"""Check if target type is a list type (e.g., list[float])."""
|
|
49
|
+
try:
|
|
50
|
+
return hasattr(target_type, "__origin__") and target_type.__origin__ is list
|
|
51
|
+
except (AttributeError, TypeError):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _convert_numpy_to_list(target_type: Any, value: Any) -> Any:
|
|
56
|
+
"""Convert numpy array to list if target is a list type."""
|
|
57
|
+
if not NUMPY_INSTALLED:
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
import numpy as np
|
|
61
|
+
|
|
62
|
+
if isinstance(value, np.ndarray) and _is_list_type_target(target_type):
|
|
63
|
+
return value.tolist()
|
|
64
|
+
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
44
68
|
_DEFAULT_TYPE_DECODERS: Final[list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]] = [
|
|
45
69
|
(lambda x: x is UUID, lambda t, v: t(v.hex)),
|
|
46
70
|
(lambda x: x is datetime.datetime, lambda t, v: t(v.isoformat())),
|
|
47
71
|
(lambda x: x is datetime.date, lambda t, v: t(v.isoformat())),
|
|
48
72
|
(lambda x: x is datetime.time, lambda t, v: t(v.isoformat())),
|
|
49
73
|
(lambda x: x is Enum, lambda t, v: t(v.value)),
|
|
74
|
+
(_is_list_type_target, _convert_numpy_to_list),
|
|
50
75
|
]
|
|
51
76
|
|
|
52
77
|
|
|
@@ -63,6 +88,13 @@ def _default_msgspec_deserializer(
|
|
|
63
88
|
Returns:
|
|
64
89
|
Converted value or original value if conversion not applicable
|
|
65
90
|
"""
|
|
91
|
+
# Handle numpy arrays first for list types
|
|
92
|
+
if NUMPY_INSTALLED:
|
|
93
|
+
import numpy as np
|
|
94
|
+
|
|
95
|
+
if isinstance(value, np.ndarray) and _is_list_type_target(target_type):
|
|
96
|
+
return value.tolist()
|
|
97
|
+
|
|
66
98
|
if type_decoders:
|
|
67
99
|
for predicate, decoder in type_decoders:
|
|
68
100
|
if predicate(target_type):
|
|
@@ -71,17 +103,19 @@ def _default_msgspec_deserializer(
|
|
|
71
103
|
if target_type is UUID and isinstance(value, UUID):
|
|
72
104
|
return value.hex
|
|
73
105
|
|
|
74
|
-
if target_type in _DATETIME_TYPES:
|
|
75
|
-
|
|
76
|
-
return value.isoformat()
|
|
77
|
-
except AttributeError:
|
|
78
|
-
pass
|
|
106
|
+
if target_type in _DATETIME_TYPES and hasattr(value, "isoformat"):
|
|
107
|
+
return value.isoformat() # pyright: ignore
|
|
79
108
|
|
|
80
109
|
if isinstance(target_type, type) and issubclass(target_type, Enum) and isinstance(value, Enum):
|
|
81
110
|
return value.value
|
|
82
111
|
|
|
83
|
-
if
|
|
84
|
-
|
|
112
|
+
# Check if value is already the correct type (but avoid parameterized generics)
|
|
113
|
+
try:
|
|
114
|
+
if isinstance(target_type, type) and isinstance(value, target_type):
|
|
115
|
+
return value
|
|
116
|
+
except TypeError:
|
|
117
|
+
# Handle parameterized generics like list[int] which can't be used with isinstance
|
|
118
|
+
pass
|
|
85
119
|
|
|
86
120
|
if isinstance(target_type, type):
|
|
87
121
|
try:
|
|
@@ -190,6 +224,25 @@ class ToSchemaMixin:
|
|
|
190
224
|
logger.debug("Field name transformation failed for msgspec schema: %s", e)
|
|
191
225
|
transformed_data = data
|
|
192
226
|
|
|
227
|
+
# Pre-process numpy arrays to lists before msgspec conversion
|
|
228
|
+
if NUMPY_INSTALLED:
|
|
229
|
+
try:
|
|
230
|
+
import numpy as np
|
|
231
|
+
|
|
232
|
+
def _convert_numpy_arrays_in_data(obj: Any) -> Any:
|
|
233
|
+
"""Recursively convert numpy arrays to lists in data structures."""
|
|
234
|
+
if isinstance(obj, np.ndarray):
|
|
235
|
+
return obj.tolist()
|
|
236
|
+
if isinstance(obj, dict):
|
|
237
|
+
return {k: _convert_numpy_arrays_in_data(v) for k, v in obj.items()}
|
|
238
|
+
if isinstance(obj, (list, tuple)):
|
|
239
|
+
return type(obj)(_convert_numpy_arrays_in_data(item) for item in obj)
|
|
240
|
+
return obj
|
|
241
|
+
|
|
242
|
+
transformed_data = _convert_numpy_arrays_in_data(transformed_data)
|
|
243
|
+
except ImportError:
|
|
244
|
+
pass
|
|
245
|
+
|
|
193
246
|
if not isinstance(transformed_data, Sequence):
|
|
194
247
|
return convert(obj=transformed_data, type=schema_type, from_attributes=True, dec_hook=deserializer)
|
|
195
248
|
return convert(obj=transformed_data, type=list[schema_type], from_attributes=True, dec_hook=deserializer) # type: ignore[valid-type]
|
sqlspec/exceptions.py
CHANGED
|
@@ -3,6 +3,7 @@ from contextlib import contextmanager
|
|
|
3
3
|
from typing import Any, Optional, Union
|
|
4
4
|
|
|
5
5
|
__all__ = (
|
|
6
|
+
"ConfigResolverError",
|
|
6
7
|
"FileNotFoundInStorageError",
|
|
7
8
|
"ImproperConfigurationError",
|
|
8
9
|
"IntegrityError",
|
|
@@ -69,6 +70,10 @@ class BackendNotRegisteredError(SQLSpecError):
|
|
|
69
70
|
super().__init__(f"Storage backend '{backend_key}' is not registered. Please register it before use.")
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
class ConfigResolverError(SQLSpecError):
|
|
74
|
+
"""Exception raised when config resolution fails."""
|
|
75
|
+
|
|
76
|
+
|
|
72
77
|
class SQLParsingError(SQLSpecError):
|
|
73
78
|
"""Issues parsing SQL statements."""
|
|
74
79
|
|
sqlspec/loader.py
CHANGED
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Final, Optional, Union
|
|
13
13
|
from urllib.parse import unquote, urlparse
|
|
14
14
|
|
|
15
|
-
from sqlspec.core.cache import
|
|
15
|
+
from sqlspec.core.cache import get_cache, get_cache_config
|
|
16
16
|
from sqlspec.core.statement import SQL
|
|
17
17
|
from sqlspec.exceptions import SQLFileNotFoundError, SQLFileParseError, StorageOperationFailedError
|
|
18
18
|
from sqlspec.storage.registry import storage_registry as default_storage_registry
|
|
@@ -438,9 +438,8 @@ class SQLFileLoader:
|
|
|
438
438
|
return
|
|
439
439
|
|
|
440
440
|
cache_key_str = self._generate_file_cache_key(file_path)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
cached_file = unified_cache.get(cache_key)
|
|
441
|
+
cache = get_cache()
|
|
442
|
+
cached_file = cache.get("file", cache_key_str)
|
|
444
443
|
|
|
445
444
|
if (
|
|
446
445
|
cached_file is not None
|
|
@@ -475,7 +474,7 @@ class SQLFileLoader:
|
|
|
475
474
|
file_statements[stored_name] = self._queries[query_name]
|
|
476
475
|
|
|
477
476
|
cached_file_data = CachedSQLFile(sql_file=sql_file, parsed_statements=file_statements)
|
|
478
|
-
|
|
477
|
+
cache.put("file", cache_key_str, cached_file_data)
|
|
479
478
|
|
|
480
479
|
def _load_file_without_cache(self, file_path: Union[str, Path], namespace: Optional[str]) -> None:
|
|
481
480
|
"""Load a single SQL file without using cache.
|
|
@@ -592,15 +591,15 @@ class SQLFileLoader:
|
|
|
592
591
|
|
|
593
592
|
cache_config = get_cache_config()
|
|
594
593
|
if cache_config.compiled_cache_enabled:
|
|
595
|
-
|
|
596
|
-
|
|
594
|
+
cache = get_cache()
|
|
595
|
+
cache.clear()
|
|
597
596
|
|
|
598
597
|
def clear_file_cache(self) -> None:
|
|
599
598
|
"""Clear the file cache only, keeping loaded queries."""
|
|
600
599
|
cache_config = get_cache_config()
|
|
601
600
|
if cache_config.compiled_cache_enabled:
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
cache = get_cache()
|
|
602
|
+
cache.clear()
|
|
604
603
|
|
|
605
604
|
def get_query_text(self, name: str) -> str:
|
|
606
605
|
"""Get raw SQL text for a query.
|
sqlspec/migrations/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ A native migration system for SQLSpec that leverages the SQLFileLoader
|
|
|
4
4
|
and driver system for database versioning.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from sqlspec.migrations.commands import AsyncMigrationCommands,
|
|
7
|
+
from sqlspec.migrations.commands import AsyncMigrationCommands, SyncMigrationCommands, create_migration_commands
|
|
8
8
|
from sqlspec.migrations.loaders import (
|
|
9
9
|
BaseMigrationLoader,
|
|
10
10
|
MigrationLoadError,
|
|
@@ -12,7 +12,7 @@ from sqlspec.migrations.loaders import (
|
|
|
12
12
|
SQLFileLoader,
|
|
13
13
|
get_migration_loader,
|
|
14
14
|
)
|
|
15
|
-
from sqlspec.migrations.runner import AsyncMigrationRunner, SyncMigrationRunner
|
|
15
|
+
from sqlspec.migrations.runner import AsyncMigrationRunner, SyncMigrationRunner, create_migration_runner
|
|
16
16
|
from sqlspec.migrations.tracker import AsyncMigrationTracker, SyncMigrationTracker
|
|
17
17
|
from sqlspec.migrations.utils import create_migration_file, drop_all, get_author
|
|
18
18
|
|
|
@@ -21,14 +21,15 @@ __all__ = (
|
|
|
21
21
|
"AsyncMigrationRunner",
|
|
22
22
|
"AsyncMigrationTracker",
|
|
23
23
|
"BaseMigrationLoader",
|
|
24
|
-
"MigrationCommands",
|
|
25
24
|
"MigrationLoadError",
|
|
26
25
|
"PythonFileLoader",
|
|
27
26
|
"SQLFileLoader",
|
|
28
27
|
"SyncMigrationCommands",
|
|
29
28
|
"SyncMigrationRunner",
|
|
30
29
|
"SyncMigrationTracker",
|
|
30
|
+
"create_migration_commands",
|
|
31
31
|
"create_migration_file",
|
|
32
|
+
"create_migration_runner",
|
|
32
33
|
"drop_all",
|
|
33
34
|
"get_author",
|
|
34
35
|
"get_migration_loader",
|