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