sqlspec 0.25.0__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 (84) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +12 -50
  3. sqlspec/_typing.py +9 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +127 -18
  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 +63 -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 +6 -0
  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/builder/_base.py +82 -42
  45. sqlspec/builder/_column.py +57 -24
  46. sqlspec/builder/_ddl.py +84 -34
  47. sqlspec/builder/_insert.py +30 -52
  48. sqlspec/builder/_parsing_utils.py +104 -8
  49. sqlspec/builder/_select.py +147 -2
  50. sqlspec/builder/mixins/_cte_and_set_ops.py +1 -2
  51. sqlspec/builder/mixins/_join_operations.py +14 -30
  52. sqlspec/builder/mixins/_merge_operations.py +167 -61
  53. sqlspec/builder/mixins/_order_limit_operations.py +3 -10
  54. sqlspec/builder/mixins/_select_operations.py +3 -9
  55. sqlspec/builder/mixins/_update_operations.py +3 -22
  56. sqlspec/builder/mixins/_where_clause.py +4 -10
  57. sqlspec/cli.py +246 -140
  58. sqlspec/config.py +33 -19
  59. sqlspec/core/cache.py +2 -2
  60. sqlspec/core/compiler.py +56 -1
  61. sqlspec/core/parameters.py +7 -3
  62. sqlspec/core/statement.py +5 -0
  63. sqlspec/core/type_conversion.py +234 -0
  64. sqlspec/driver/__init__.py +6 -3
  65. sqlspec/driver/_async.py +106 -3
  66. sqlspec/driver/_common.py +156 -4
  67. sqlspec/driver/_sync.py +106 -3
  68. sqlspec/exceptions.py +5 -0
  69. sqlspec/migrations/__init__.py +4 -3
  70. sqlspec/migrations/base.py +153 -14
  71. sqlspec/migrations/commands.py +34 -96
  72. sqlspec/migrations/context.py +145 -0
  73. sqlspec/migrations/loaders.py +25 -8
  74. sqlspec/migrations/runner.py +352 -82
  75. sqlspec/typing.py +2 -0
  76. sqlspec/utils/config_resolver.py +153 -0
  77. sqlspec/utils/serializers.py +50 -2
  78. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  79. sqlspec-0.26.0.dist-info/RECORD +157 -0
  80. sqlspec-0.25.0.dist-info/RECORD +0 -139
  81. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  82. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  83. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  84. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,134 @@
1
+ """PostgreSQL-specific data dictionary for metadata queries via asyncpg."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Callable, Optional, cast
5
+
6
+ from sqlspec.driver import AsyncDataDictionaryBase, AsyncDriverAdapterBase, VersionInfo
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.adapters.asyncpg.driver import AsyncpgDriver
11
+
12
+ logger = get_logger("adapters.asyncpg.data_dictionary")
13
+
14
+ # Compiled regex patterns
15
+ POSTGRES_VERSION_PATTERN = re.compile(r"PostgreSQL (\d+)\.(\d+)(?:\.(\d+))?")
16
+
17
+ __all__ = ("PostgresAsyncDataDictionary",)
18
+
19
+
20
+ class PostgresAsyncDataDictionary(AsyncDataDictionaryBase):
21
+ """PostgreSQL-specific async data dictionary."""
22
+
23
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "Optional[VersionInfo]":
24
+ """Get PostgreSQL database version information.
25
+
26
+ Args:
27
+ driver: Async database driver instance
28
+
29
+ Returns:
30
+ PostgreSQL version information or None if detection fails
31
+ """
32
+ asyncpg_driver = cast("AsyncpgDriver", driver)
33
+ version_str = await asyncpg_driver.select_value("SELECT version()")
34
+ if not version_str:
35
+ logger.warning("No PostgreSQL version information found")
36
+ return None
37
+
38
+ # Parse version like "PostgreSQL 15.3 on x86_64-pc-linux-gnu..."
39
+ version_match = POSTGRES_VERSION_PATTERN.search(str(version_str))
40
+ if not version_match:
41
+ logger.warning("Could not parse PostgreSQL version: %s", version_str)
42
+ return None
43
+
44
+ major = int(version_match.group(1))
45
+ minor = int(version_match.group(2))
46
+ patch = int(version_match.group(3)) if version_match.group(3) else 0
47
+
48
+ version_info = VersionInfo(major, minor, patch)
49
+ logger.debug("Detected PostgreSQL version: %s", version_info)
50
+ return version_info
51
+
52
+ async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
53
+ """Check if PostgreSQL database supports a specific feature.
54
+
55
+ Args:
56
+ driver: Async database driver instance
57
+ feature: Feature name to check
58
+
59
+ Returns:
60
+ True if feature is supported, False otherwise
61
+ """
62
+ version_info = await self.get_version(driver)
63
+ if not version_info:
64
+ return False
65
+
66
+ feature_checks: dict[str, Callable[..., bool]] = {
67
+ "supports_json": lambda v: v >= VersionInfo(9, 2, 0),
68
+ "supports_jsonb": lambda v: v >= VersionInfo(9, 4, 0),
69
+ "supports_uuid": lambda _: True, # UUID extension widely available
70
+ "supports_arrays": lambda _: True, # PostgreSQL has excellent array support
71
+ "supports_returning": lambda v: v >= VersionInfo(8, 2, 0),
72
+ "supports_upsert": lambda v: v >= VersionInfo(9, 5, 0), # ON CONFLICT
73
+ "supports_window_functions": lambda v: v >= VersionInfo(8, 4, 0),
74
+ "supports_cte": lambda v: v >= VersionInfo(8, 4, 0),
75
+ "supports_transactions": lambda _: True,
76
+ "supports_prepared_statements": lambda _: True,
77
+ "supports_schemas": lambda _: True,
78
+ "supports_partitioning": lambda v: v >= VersionInfo(10, 0, 0),
79
+ }
80
+
81
+ if feature in feature_checks:
82
+ return bool(feature_checks[feature](version_info))
83
+
84
+ return False
85
+
86
+ async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
87
+ """Get optimal PostgreSQL type for a category.
88
+
89
+ Args:
90
+ driver: Async database driver instance
91
+ type_category: Type category
92
+
93
+ Returns:
94
+ PostgreSQL-specific type name
95
+ """
96
+ version_info = await self.get_version(driver)
97
+
98
+ if type_category == "json":
99
+ if version_info and version_info >= VersionInfo(9, 4, 0):
100
+ return "JSONB" # Prefer JSONB over JSON
101
+ if version_info and version_info >= VersionInfo(9, 2, 0):
102
+ return "JSON"
103
+ return "TEXT"
104
+
105
+ type_map = {
106
+ "uuid": "UUID",
107
+ "boolean": "BOOLEAN",
108
+ "timestamp": "TIMESTAMP WITH TIME ZONE",
109
+ "text": "TEXT",
110
+ "blob": "BYTEA",
111
+ "array": "ARRAY",
112
+ }
113
+ return type_map.get(type_category, "TEXT")
114
+
115
+ def list_available_features(self) -> "list[str]":
116
+ """List available PostgreSQL feature flags.
117
+
118
+ Returns:
119
+ List of supported feature names
120
+ """
121
+ return [
122
+ "supports_json",
123
+ "supports_jsonb",
124
+ "supports_uuid",
125
+ "supports_arrays",
126
+ "supports_returning",
127
+ "supports_upsert",
128
+ "supports_window_functions",
129
+ "supports_cte",
130
+ "supports_transactions",
131
+ "supports_prepared_statements",
132
+ "supports_schemas",
133
+ "supports_partitioning",
134
+ ]
@@ -4,6 +4,7 @@ Provides async PostgreSQL connectivity with parameter processing, resource manag
4
4
  PostgreSQL COPY operation support, and transaction management.
5
5
  """
6
6
 
7
+ import datetime
7
8
  import re
8
9
  from typing import TYPE_CHECKING, Any, Final, Optional
9
10
 
@@ -23,6 +24,7 @@ if TYPE_CHECKING:
23
24
  from sqlspec.core.result import SQLResult
24
25
  from sqlspec.core.statement import SQL
25
26
  from sqlspec.driver import ExecutionResult
27
+ from sqlspec.driver._async import AsyncDataDictionaryBase
26
28
 
27
29
  __all__ = ("AsyncpgCursor", "AsyncpgDriver", "AsyncpgExceptionHandler", "asyncpg_statement_config")
28
30
 
@@ -36,7 +38,7 @@ asyncpg_statement_config = StatementConfig(
36
38
  supported_parameter_styles={ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_PYFORMAT},
37
39
  default_execution_parameter_style=ParameterStyle.NUMERIC,
38
40
  supported_execution_parameter_styles={ParameterStyle.NUMERIC},
39
- type_coercion_map={},
41
+ type_coercion_map={datetime.datetime: lambda x: x, datetime.date: lambda x: x, datetime.time: lambda x: x},
40
42
  has_native_list_expansion=True,
41
43
  needs_static_script_compilation=False,
42
44
  preserve_parameter_format=True,
@@ -63,8 +65,7 @@ class AsyncpgCursor:
63
65
  async def __aenter__(self) -> "AsyncpgConnection":
64
66
  return self.connection
65
67
 
66
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
67
- _ = (exc_type, exc_val, exc_tb)
68
+ async def __aexit__(self, *_: Any) -> None: ...
68
69
 
69
70
 
70
71
  class AsyncpgExceptionHandler:
@@ -104,7 +105,7 @@ class AsyncpgDriver(AsyncDriverAdapterBase):
104
105
  and caching, and parameter processing with type coercion.
105
106
  """
106
107
 
107
- __slots__ = ()
108
+ __slots__ = ("_data_dictionary",)
108
109
  dialect = "postgres"
109
110
 
110
111
  def __init__(
@@ -123,6 +124,7 @@ class AsyncpgDriver(AsyncDriverAdapterBase):
123
124
  )
124
125
 
125
126
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
127
+ self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
126
128
 
127
129
  def with_cursor(self, connection: "AsyncpgConnection") -> "AsyncpgCursor":
128
130
  """Create context manager for AsyncPG cursor."""
@@ -311,3 +313,16 @@ class AsyncpgDriver(AsyncDriverAdapterBase):
311
313
  except asyncpg.PostgresError as e:
312
314
  msg = f"Failed to commit async transaction: {e}"
313
315
  raise SQLSpecError(msg) from e
316
+
317
+ @property
318
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
319
+ """Get the data dictionary for this driver.
320
+
321
+ Returns:
322
+ Data dictionary instance for metadata queries
323
+ """
324
+ if self._data_dictionary is None:
325
+ from sqlspec.adapters.asyncpg.data_dictionary import PostgresAsyncDataDictionary
326
+
327
+ self._data_dictionary = PostgresAsyncDataDictionary()
328
+ return self._data_dictionary
@@ -94,6 +94,7 @@ class BigQueryConfig(NoPoolSyncConfig[BigQueryConnection, BigQueryDriver]):
94
94
  migration_config: Optional[dict[str, Any]] = None,
95
95
  statement_config: "Optional[StatementConfig]" = None,
96
96
  driver_features: "Optional[Union[BigQueryDriverFeatures, dict[str, Any]]]" = None,
97
+ bind_key: "Optional[str]" = None,
97
98
  ) -> None:
98
99
  """Initialize BigQuery configuration.
99
100
 
@@ -102,6 +103,7 @@ class BigQueryConfig(NoPoolSyncConfig[BigQueryConnection, BigQueryDriver]):
102
103
  migration_config: Migration configuration
103
104
  statement_config: Statement configuration override
104
105
  driver_features: BigQuery-specific driver features
106
+ bind_key: Optional unique identifier for this configuration
105
107
  """
106
108
 
107
109
  self.connection_config: dict[str, Any] = dict(connection_config) if connection_config else {}
@@ -124,6 +126,7 @@ class BigQueryConfig(NoPoolSyncConfig[BigQueryConnection, BigQueryDriver]):
124
126
  migration_config=migration_config,
125
127
  statement_config=statement_config,
126
128
  driver_features=self.driver_features,
129
+ bind_key=bind_key,
127
130
  )
128
131
 
129
132
  def _setup_default_job_config(self) -> None:
@@ -0,0 +1,109 @@
1
+ """BigQuery-specific data dictionary for metadata queries."""
2
+
3
+ from typing import Optional
4
+
5
+ from sqlspec.driver import SyncDataDictionaryBase, SyncDriverAdapterBase, VersionInfo
6
+ from sqlspec.utils.logging import get_logger
7
+
8
+ logger = get_logger("adapters.bigquery.data_dictionary")
9
+
10
+ __all__ = ("BigQuerySyncDataDictionary",)
11
+
12
+
13
+ class BigQuerySyncDataDictionary(SyncDataDictionaryBase):
14
+ """BigQuery-specific sync data dictionary."""
15
+
16
+ def get_version(self, driver: SyncDriverAdapterBase) -> "Optional[VersionInfo]":
17
+ """Get BigQuery version information.
18
+
19
+ BigQuery is a cloud service without traditional versioning.
20
+ Returns a fixed version to indicate feature availability.
21
+
22
+ Args:
23
+ driver: BigQuery driver instance
24
+
25
+ Returns:
26
+ Fixed version info indicating current BigQuery capabilities
27
+ """
28
+ # BigQuery is a cloud service - return a fixed version
29
+ # indicating modern feature support
30
+ logger.debug("BigQuery cloud service - using fixed version")
31
+ return VersionInfo(1, 0, 0)
32
+
33
+ def get_feature_flag(self, driver: SyncDriverAdapterBase, feature: str) -> bool:
34
+ """Check if BigQuery supports a specific feature.
35
+
36
+ Args:
37
+ driver: BigQuery driver instance
38
+ feature: Feature name to check
39
+
40
+ Returns:
41
+ True if feature is supported, False otherwise
42
+ """
43
+ # BigQuery feature support based on current capabilities
44
+ feature_checks = {
45
+ "supports_json": True, # Native JSON type
46
+ "supports_arrays": True, # ARRAY types
47
+ "supports_structs": True, # STRUCT types
48
+ "supports_geography": True, # GEOGRAPHY type
49
+ "supports_returning": False, # No RETURNING clause
50
+ "supports_upsert": True, # MERGE statement
51
+ "supports_window_functions": True,
52
+ "supports_cte": True,
53
+ "supports_transactions": True, # Multi-statement transactions
54
+ "supports_prepared_statements": True,
55
+ "supports_schemas": True, # Datasets and projects
56
+ "supports_partitioning": True, # Table partitioning
57
+ "supports_clustering": True, # Table clustering
58
+ "supports_uuid": False, # No native UUID, use STRING
59
+ }
60
+
61
+ return feature_checks.get(feature, False)
62
+
63
+ def get_optimal_type(self, driver: SyncDriverAdapterBase, type_category: str) -> str:
64
+ """Get optimal BigQuery type for a category.
65
+
66
+ Args:
67
+ driver: BigQuery driver instance
68
+ type_category: Type category
69
+
70
+ Returns:
71
+ BigQuery-specific type name
72
+ """
73
+ type_map = {
74
+ "json": "JSON",
75
+ "uuid": "STRING",
76
+ "boolean": "BOOL",
77
+ "timestamp": "TIMESTAMP",
78
+ "text": "STRING",
79
+ "blob": "BYTES",
80
+ "array": "ARRAY",
81
+ "struct": "STRUCT",
82
+ "geography": "GEOGRAPHY",
83
+ "numeric": "NUMERIC",
84
+ "bignumeric": "BIGNUMERIC",
85
+ }
86
+ return type_map.get(type_category, "STRING")
87
+
88
+ def list_available_features(self) -> "list[str]":
89
+ """List available BigQuery feature flags.
90
+
91
+ Returns:
92
+ List of supported feature names
93
+ """
94
+ return [
95
+ "supports_json",
96
+ "supports_arrays",
97
+ "supports_structs",
98
+ "supports_geography",
99
+ "supports_returning",
100
+ "supports_upsert",
101
+ "supports_window_functions",
102
+ "supports_cte",
103
+ "supports_transactions",
104
+ "supports_prepared_statements",
105
+ "supports_schemas",
106
+ "supports_partitioning",
107
+ "supports_clustering",
108
+ "supports_uuid",
109
+ ]
@@ -15,6 +15,7 @@ from google.cloud.bigquery import ArrayQueryParameter, QueryJob, QueryJobConfig,
15
15
  from google.cloud.exceptions import GoogleCloudError
16
16
 
17
17
  from sqlspec.adapters.bigquery._types import BigQueryConnection
18
+ from sqlspec.adapters.bigquery.type_converter import BigQueryTypeConverter
18
19
  from sqlspec.core.cache import get_cache_config
19
20
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
20
21
  from sqlspec.core.statement import StatementConfig
@@ -28,11 +29,14 @@ if TYPE_CHECKING:
28
29
 
29
30
  from sqlspec.core.result import SQLResult
30
31
  from sqlspec.core.statement import SQL
32
+ from sqlspec.driver._sync import SyncDataDictionaryBase
31
33
 
32
34
  logger = logging.getLogger(__name__)
33
35
 
34
36
  __all__ = ("BigQueryCursor", "BigQueryDriver", "BigQueryExceptionHandler", "bigquery_statement_config")
35
37
 
38
+ _type_converter = BigQueryTypeConverter()
39
+
36
40
 
37
41
  _BQ_TYPE_MAP: dict[type, tuple[str, Optional[str]]] = {
38
42
  bool: ("BOOL", None),
@@ -134,7 +138,7 @@ bigquery_type_coercion_map = {
134
138
  bool: lambda x: x,
135
139
  int: lambda x: x,
136
140
  float: lambda x: x,
137
- str: lambda x: x,
141
+ str: _type_converter.convert_if_detected,
138
142
  bytes: lambda x: x,
139
143
  datetime.datetime: lambda x: x,
140
144
  datetime.date: lambda x: x,
@@ -177,7 +181,7 @@ class BigQueryCursor:
177
181
  def __enter__(self) -> "BigQueryConnection":
178
182
  return self.connection
179
183
 
180
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
184
+ def __exit__(self, *_: Any) -> None:
181
185
  """Clean up cursor resources including active QueryJobs."""
182
186
  if self.job is not None:
183
187
  try:
@@ -230,7 +234,7 @@ class BigQueryDriver(SyncDriverAdapterBase):
230
234
  type coercion, error handling, and query job management.
231
235
  """
232
236
 
233
- __slots__ = ("_default_query_job_config",)
237
+ __slots__ = ("_data_dictionary", "_default_query_job_config")
234
238
  dialect = "bigquery"
235
239
 
236
240
  def __init__(
@@ -252,6 +256,7 @@ class BigQueryDriver(SyncDriverAdapterBase):
252
256
  self._default_query_job_config: Optional[QueryJobConfig] = (driver_features or {}).get(
253
257
  "default_query_job_config"
254
258
  )
259
+ self._data_dictionary: Optional[SyncDataDictionaryBase] = None
255
260
 
256
261
  def with_cursor(self, connection: "BigQueryConnection") -> "BigQueryCursor":
257
262
  """Create context manager for cursor management.
@@ -532,3 +537,16 @@ class BigQueryDriver(SyncDriverAdapterBase):
532
537
  cursor.job.result()
533
538
  affected_rows = cursor.job.num_dml_affected_rows or 0
534
539
  return self.create_execution_result(cursor, rowcount_override=affected_rows)
540
+
541
+ @property
542
+ def data_dictionary(self) -> "SyncDataDictionaryBase":
543
+ """Get the data dictionary for this driver.
544
+
545
+ Returns:
546
+ Data dictionary instance for metadata queries
547
+ """
548
+ if self._data_dictionary is None:
549
+ from sqlspec.adapters.bigquery.data_dictionary import BigQuerySyncDataDictionary
550
+
551
+ self._data_dictionary = BigQuerySyncDataDictionary()
552
+ return self._data_dictionary
@@ -0,0 +1,93 @@
1
+ """BigQuery-specific type conversion with UUID support.
2
+
3
+ Provides specialized type handling for BigQuery, including UUID support
4
+ for the native BigQuery driver.
5
+ """
6
+
7
+ from typing import Any, Final, Optional
8
+ from uuid import UUID
9
+
10
+ from sqlspec.core.type_conversion import BaseTypeConverter, convert_uuid
11
+
12
+ try:
13
+ from google.cloud.bigquery import ScalarQueryParameter
14
+ except ImportError:
15
+ ScalarQueryParameter = None # type: ignore[assignment,misc]
16
+
17
+ # Enhanced BigQuery type mapping with UUID support
18
+ BQ_TYPE_MAP: Final[dict[str, str]] = {
19
+ "str": "STRING",
20
+ "int": "INT64",
21
+ "float": "FLOAT64",
22
+ "bool": "BOOL",
23
+ "datetime": "DATETIME",
24
+ "date": "DATE",
25
+ "time": "TIME",
26
+ "UUID": "STRING", # UUID as STRING in BigQuery
27
+ "uuid": "STRING",
28
+ "Decimal": "NUMERIC",
29
+ "bytes": "BYTES",
30
+ "list": "ARRAY",
31
+ "dict": "STRUCT",
32
+ }
33
+
34
+
35
+ class BigQueryTypeConverter(BaseTypeConverter):
36
+ """BigQuery-specific type conversion with UUID support.
37
+
38
+ Extends the base TypeDetector with BigQuery-specific functionality
39
+ including UUID parameter handling for the native BigQuery driver.
40
+ """
41
+
42
+ __slots__ = ()
43
+
44
+ def create_parameter(self, name: str, value: Any) -> Optional[Any]:
45
+ """Create BigQuery parameter with proper type mapping.
46
+
47
+ Args:
48
+ name: Parameter name.
49
+ value: Parameter value.
50
+
51
+ Returns:
52
+ ScalarQueryParameter for native BigQuery driver, None if not available.
53
+ """
54
+ if ScalarQueryParameter is None:
55
+ return None
56
+
57
+ if isinstance(value, UUID):
58
+ return ScalarQueryParameter(name, "STRING", str(value))
59
+
60
+ if isinstance(value, str):
61
+ detected_type = self.detect_type(value)
62
+ if detected_type == "uuid":
63
+ uuid_obj = convert_uuid(value)
64
+ return ScalarQueryParameter(name, "STRING", str(uuid_obj))
65
+
66
+ # Handle other types
67
+ param_type = BQ_TYPE_MAP.get(type(value).__name__, "STRING")
68
+ return ScalarQueryParameter(name, param_type, value)
69
+
70
+ def convert_bigquery_value(self, value: Any, column_type: str) -> Any:
71
+ """Convert BigQuery value based on column type.
72
+
73
+ Args:
74
+ value: Value to convert.
75
+ column_type: BigQuery column type.
76
+
77
+ Returns:
78
+ Converted value appropriate for the column type.
79
+ """
80
+ if column_type == "STRING" and isinstance(value, str):
81
+ # Try to detect if this is a special type
82
+ detected_type = self.detect_type(value)
83
+ if detected_type:
84
+ try:
85
+ return self.convert_value(value, detected_type)
86
+ except Exception:
87
+ # If conversion fails, return original value
88
+ return value
89
+
90
+ return value
91
+
92
+
93
+ __all__ = ("BQ_TYPE_MAP", "BigQueryTypeConverter")
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from duckdb import DuckDBPyConnection
3
+ from duckdb import DuckDBPyConnection # type: ignore[import-untyped]
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from typing_extensions import TypeAlias
@@ -149,6 +149,7 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
149
149
  migration_config: Optional[dict[str, Any]] = None,
150
150
  statement_config: "Optional[StatementConfig]" = None,
151
151
  driver_features: "Optional[Union[DuckDBDriverFeatures, dict[str, Any]]]" = None,
152
+ bind_key: "Optional[str]" = None,
152
153
  ) -> None:
153
154
  """Initialize DuckDB configuration."""
154
155
  if pool_config is None:
@@ -160,6 +161,7 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
160
161
  pool_config["database"] = ":memory:shared_db"
161
162
 
162
163
  super().__init__(
164
+ bind_key=bind_key,
163
165
  pool_config=dict(pool_config),
164
166
  pool_instance=pool_instance,
165
167
  migration_config=migration_config,
@@ -0,0 +1,124 @@
1
+ """DuckDB-specific data dictionary for metadata queries."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Callable, Optional, cast
5
+
6
+ from sqlspec.driver import SyncDataDictionaryBase, SyncDriverAdapterBase, VersionInfo
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.adapters.duckdb.driver import DuckDBDriver
11
+
12
+ logger = get_logger("adapters.duckdb.data_dictionary")
13
+
14
+ # Compiled regex patterns
15
+ DUCKDB_VERSION_PATTERN = re.compile(r"v?(\d+)\.(\d+)\.(\d+)")
16
+
17
+ __all__ = ("DuckDBSyncDataDictionary",)
18
+
19
+
20
+ class DuckDBSyncDataDictionary(SyncDataDictionaryBase):
21
+ """DuckDB-specific sync data dictionary."""
22
+
23
+ def get_version(self, driver: SyncDriverAdapterBase) -> "Optional[VersionInfo]":
24
+ """Get DuckDB database version information.
25
+
26
+ Args:
27
+ driver: DuckDB driver instance
28
+
29
+ Returns:
30
+ DuckDB version information or None if detection fails
31
+ """
32
+ version_str = cast("DuckDBDriver", driver).select_value("SELECT version()")
33
+ if not version_str:
34
+ logger.warning("No DuckDB version information found")
35
+ return None
36
+
37
+ # Parse version like "v0.9.2" or "0.9.2"
38
+ version_match = DUCKDB_VERSION_PATTERN.search(str(version_str))
39
+ if not version_match:
40
+ logger.warning("Could not parse DuckDB version: %s", version_str)
41
+ return None
42
+
43
+ major, minor, patch = map(int, version_match.groups())
44
+ version_info = VersionInfo(major, minor, patch)
45
+ logger.debug("Detected DuckDB version: %s", version_info)
46
+ return version_info
47
+
48
+ def get_feature_flag(self, driver: SyncDriverAdapterBase, feature: str) -> bool:
49
+ """Check if DuckDB database supports a specific feature.
50
+
51
+ Args:
52
+ driver: DuckDB driver instance
53
+ feature: Feature name to check
54
+
55
+ Returns:
56
+ True if feature is supported, False otherwise
57
+ """
58
+ version_info = self.get_version(driver)
59
+ if not version_info:
60
+ return False
61
+
62
+ feature_checks: dict[str, Callable[..., bool]] = {
63
+ "supports_json": lambda _: True, # DuckDB has excellent JSON support
64
+ "supports_arrays": lambda _: True, # LIST type
65
+ "supports_maps": lambda _: True, # MAP type
66
+ "supports_structs": lambda _: True, # STRUCT type
67
+ "supports_returning": lambda v: v >= VersionInfo(0, 8, 0),
68
+ "supports_upsert": lambda v: v >= VersionInfo(0, 8, 0),
69
+ "supports_window_functions": lambda _: True,
70
+ "supports_cte": lambda _: True,
71
+ "supports_transactions": lambda _: True,
72
+ "supports_prepared_statements": lambda _: True,
73
+ "supports_schemas": lambda _: True,
74
+ "supports_uuid": lambda _: True,
75
+ }
76
+
77
+ if feature in feature_checks:
78
+ return bool(feature_checks[feature](version_info))
79
+
80
+ return False
81
+
82
+ def get_optimal_type(self, driver: SyncDriverAdapterBase, type_category: str) -> str: # pyright: ignore
83
+ """Get optimal DuckDB type for a category.
84
+
85
+ Args:
86
+ driver: DuckDB driver instance
87
+ type_category: Type category
88
+
89
+ Returns:
90
+ DuckDB-specific type name
91
+ """
92
+ type_map = {
93
+ "json": "JSON",
94
+ "uuid": "UUID",
95
+ "boolean": "BOOLEAN",
96
+ "timestamp": "TIMESTAMP",
97
+ "text": "TEXT",
98
+ "blob": "BLOB",
99
+ "array": "LIST",
100
+ "map": "MAP",
101
+ "struct": "STRUCT",
102
+ }
103
+ return type_map.get(type_category, "VARCHAR")
104
+
105
+ def list_available_features(self) -> "list[str]":
106
+ """List available DuckDB feature flags.
107
+
108
+ Returns:
109
+ List of supported feature names
110
+ """
111
+ return [
112
+ "supports_json",
113
+ "supports_arrays",
114
+ "supports_maps",
115
+ "supports_structs",
116
+ "supports_returning",
117
+ "supports_upsert",
118
+ "supports_window_functions",
119
+ "supports_cte",
120
+ "supports_transactions",
121
+ "supports_prepared_statements",
122
+ "supports_schemas",
123
+ "supports_uuid",
124
+ ]