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
|
@@ -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,
|
|
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:
|
|
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,
|
|
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")
|
|
@@ -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
|
+
]
|