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.

Files changed (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {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
- from typing import TYPE_CHECKING, Any, Final, NamedTuple, Optional, Union, cast
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 get_cache_config, sql_cache
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._positional_parameters, *parameters)
356
+ (*sql_statement.positional_parameters, *parameters)
210
357
  if parameters
211
- else sql_statement._positional_parameters
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._positional_parameters, *parameters) if parameters else statement._positional_parameters
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._raw_sql or statement.sql
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._named_parameters:
240
- new_sql = SQL(sql_text, statement_config=statement_config, **statement._named_parameters)
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._positional_parameters, statement_config=statement_config)
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, parameters: Any, statement_config: "StatementConfig", is_many: bool = False
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
- cached_result = sql_cache.get(cache_key)
417
- if cached_result is not None:
418
- return cached_result
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
- sql_cache.set(cache_key, (compiled_sql, prepared_parameters))
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._positional_parameters, statement_config=original_sql.statement_config)
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._positional_parameters, statement_config=original_sql.statement_config)
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._raw_sql, parameters, statement_config=config, is_many=True, **kwargs)
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._raw_sql, parameters, statement_config=config, is_many=True, **kwargs)
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
- try:
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 isinstance(value, target_type):
84
- return value
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 CacheKey, get_cache_config, get_default_cache
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
- cache_key = CacheKey((cache_key_str,))
442
- unified_cache = get_default_cache()
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
- unified_cache.put(cache_key, cached_file_data)
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
- unified_cache = get_default_cache()
596
- unified_cache.clear()
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
- unified_cache = get_default_cache()
603
- unified_cache.clear()
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.
@@ -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, MigrationCommands, SyncMigrationCommands
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",