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
@@ -9,7 +9,7 @@ from typing import Any, Final, Optional, Union, cast
9
9
 
10
10
  from sqlglot import exp, maybe_parse, parse_one
11
11
 
12
- from sqlspec.core.parameters import ParameterStyle
12
+ from sqlspec.core.parameters import ParameterStyle, ParameterValidator
13
13
  from sqlspec.utils.type_guards import (
14
14
  has_expression_and_parameters,
15
15
  has_expression_and_sql,
@@ -63,9 +63,7 @@ def parse_column_expression(
63
63
  if isinstance(column_input, exp.Expression):
64
64
  return column_input
65
65
 
66
- # Handle SQL objects (from sql.raw with parameters)
67
66
  if has_expression_and_sql(column_input):
68
- # This is likely a SQL object
69
67
  expression = getattr(column_input, "expression", None)
70
68
  if expression is not None and isinstance(expression, exp.Expression):
71
69
  # Merge parameters from SQL object into builder if available
@@ -74,9 +72,7 @@ def parse_column_expression(
74
72
  for param_name, param_value in sql_parameters.items():
75
73
  builder.add_parameter(param_value, name=param_name)
76
74
  return cast("exp.Expression", expression)
77
- # If expression is None, fall back to parsing the raw SQL
78
75
  sql_text = getattr(column_input, "sql", "")
79
- # Merge parameters even when parsing raw SQL
80
76
  if builder and has_expression_and_parameters(column_input) and hasattr(builder, "add_parameter"):
81
77
  sql_parameters = getattr(column_input, "parameters", {})
82
78
  for param_name, param_value in sql_parameters.items():
@@ -175,8 +171,6 @@ def parse_condition_expression(
175
171
 
176
172
  # Convert database-specific parameter styles to SQLGlot-compatible format
177
173
  # This ensures that placeholders like $1, %s, :1 are properly recognized as parameters
178
- from sqlspec.core.parameters import ParameterValidator
179
-
180
174
  validator = ParameterValidator()
181
175
  param_info = validator.extract_parameters(condition_input)
182
176
 
@@ -205,4 +199,106 @@ def parse_condition_expression(
205
199
  return exp.condition(condition_input)
206
200
 
207
201
 
208
- __all__ = ("parse_column_expression", "parse_condition_expression", "parse_order_expression", "parse_table_expression")
202
+ def extract_sql_object_expression(value: Any, builder: Optional[Any] = None) -> exp.Expression:
203
+ """Extract SQLGlot expression from SQL object value with parameter merging.
204
+
205
+ Handles the common pattern of:
206
+ 1. Check if value has expression and SQL attributes
207
+ 2. Try to get expression first, merge parameters if available
208
+ 3. Fall back to parsing raw SQL text if expression is None
209
+ 4. Merge parameters in both cases
210
+ 5. Handle callable SQL text
211
+
212
+ This consolidates duplicated logic across builder files that process
213
+ SQL objects (like those from sql.raw() calls).
214
+
215
+ Args:
216
+ value: The SQL object value to process
217
+ builder: Optional builder instance for parameter merging (must have add_parameter method)
218
+
219
+ Returns:
220
+ SQLGlot Expression extracted from the SQL object
221
+
222
+ Raises:
223
+ ValueError: If the value doesn't appear to be a SQL object
224
+ """
225
+ if not has_expression_and_sql(value):
226
+ msg = f"Value does not have both expression and sql attributes: {type(value)}"
227
+ raise ValueError(msg)
228
+
229
+ # Try expression attribute first
230
+ expression = getattr(value, "expression", None)
231
+ if expression is not None and isinstance(expression, exp.Expression):
232
+ # Merge parameters if available and builder supports it
233
+ if builder and hasattr(value, "parameters") and hasattr(builder, "add_parameter"):
234
+ sql_parameters = getattr(value, "parameters", {})
235
+ for param_name, param_value in sql_parameters.items():
236
+ builder.add_parameter(param_value, name=param_name)
237
+ return cast("exp.Expression", expression)
238
+
239
+ # Fall back to parsing raw SQL text
240
+ sql_text = getattr(value, "sql", "")
241
+
242
+ # Merge parameters even when parsing raw SQL
243
+ if builder and hasattr(value, "parameters") and hasattr(builder, "add_parameter"):
244
+ sql_parameters = getattr(value, "parameters", {})
245
+ for param_name, param_value in sql_parameters.items():
246
+ builder.add_parameter(param_value, name=param_name)
247
+
248
+ # Handle callable SQL text
249
+ if callable(sql_text):
250
+ sql_text = str(value)
251
+
252
+ # Parse SQL text and return as expression
253
+ return exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
254
+
255
+
256
+ def extract_expression(value: Any) -> exp.Expression:
257
+ """Extract SQLGlot expression from value, handling wrapper types.
258
+
259
+ Args:
260
+ value: String, SQLGlot expression, or wrapper type.
261
+
262
+ Returns:
263
+ Raw SQLGlot expression.
264
+ """
265
+ from sqlspec.builder._column import Column
266
+ from sqlspec.builder._expression_wrappers import ExpressionWrapper
267
+ from sqlspec.builder.mixins._select_operations import Case
268
+
269
+ if isinstance(value, str):
270
+ return exp.column(value)
271
+ if isinstance(value, Column):
272
+ return value.sqlglot_expression
273
+ if isinstance(value, ExpressionWrapper):
274
+ return value.expression
275
+ if isinstance(value, Case):
276
+ return exp.Case(ifs=value.conditions, default=value.default)
277
+ if isinstance(value, exp.Expression):
278
+ return value
279
+ return exp.convert(value)
280
+
281
+
282
+ def to_expression(value: Any) -> exp.Expression:
283
+ """Convert a Python value to a raw SQLGlot expression.
284
+
285
+ Args:
286
+ value: Python value or SQLGlot expression to convert.
287
+
288
+ Returns:
289
+ Raw SQLGlot expression.
290
+ """
291
+ if isinstance(value, exp.Expression):
292
+ return value
293
+ return exp.convert(value)
294
+
295
+
296
+ __all__ = (
297
+ "extract_expression",
298
+ "extract_sql_object_expression",
299
+ "parse_column_expression",
300
+ "parse_condition_expression",
301
+ "parse_order_expression",
302
+ "parse_table_expression",
303
+ "to_expression",
304
+ )
@@ -5,7 +5,7 @@ parameter binding and validation.
5
5
  """
6
6
 
7
7
  import re
8
- from typing import Any, Callable, Final, Optional, Union
8
+ from typing import Any, Callable, Final, Optional, Union, cast
9
9
 
10
10
  from sqlglot import exp
11
11
  from typing_extensions import Self
@@ -24,6 +24,7 @@ from sqlspec.builder.mixins import (
24
24
  WhereClauseMixin,
25
25
  )
26
26
  from sqlspec.core.result import SQLResult
27
+ from sqlspec.exceptions import SQLBuilderError
27
28
 
28
29
  __all__ = ("Select",)
29
30
 
@@ -73,7 +74,6 @@ class Select(
73
74
  """
74
75
  super().__init__(**kwargs)
75
76
 
76
- # Initialize Select-specific attributes
77
77
  self._with_parts: dict[str, Union[exp.CTE, Select]] = {}
78
78
  self._hints: list[dict[str, object]] = []
79
79
 
@@ -169,3 +169,148 @@ class Select(
169
169
  )
170
170
 
171
171
  return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
172
+
173
+ def _validate_select_expression(self) -> None:
174
+ """Validate that current expression is a valid SELECT statement.
175
+
176
+ Raises:
177
+ SQLBuilderError: If expression is None or not a SELECT statement
178
+ """
179
+ if self._expression is None or not isinstance(self._expression, exp.Select):
180
+ msg = "Locking clauses can only be applied to SELECT statements"
181
+ raise SQLBuilderError(msg)
182
+
183
+ def _validate_lock_parameters(self, skip_locked: bool, nowait: bool) -> None:
184
+ """Validate locking parameters for conflicting options.
185
+
186
+ Args:
187
+ skip_locked: Whether SKIP LOCKED option is enabled
188
+ nowait: Whether NOWAIT option is enabled
189
+
190
+ Raises:
191
+ SQLBuilderError: If both skip_locked and nowait are True
192
+ """
193
+ if skip_locked and nowait:
194
+ msg = "Cannot use both skip_locked and nowait"
195
+ raise SQLBuilderError(msg)
196
+
197
+ def for_update(
198
+ self, *, skip_locked: bool = False, nowait: bool = False, of: "Optional[Union[str, list[str]]]" = None
199
+ ) -> "Self":
200
+ """Add FOR UPDATE clause to SELECT statement for row-level locking.
201
+
202
+ Args:
203
+ skip_locked: Skip rows that are already locked (SKIP LOCKED)
204
+ nowait: Return immediately if row is locked (NOWAIT)
205
+ of: Table names/aliases to lock (FOR UPDATE OF table)
206
+
207
+ Returns:
208
+ Self for method chaining
209
+ """
210
+ self._validate_select_expression()
211
+ self._validate_lock_parameters(skip_locked, nowait)
212
+
213
+ assert self._expression is not None
214
+ select_expr = cast("exp.Select", self._expression)
215
+
216
+ lock_args = {"update": True}
217
+
218
+ if skip_locked:
219
+ lock_args["wait"] = False
220
+ elif nowait:
221
+ lock_args["wait"] = True
222
+
223
+ if of:
224
+ tables = [of] if isinstance(of, str) else of
225
+ lock_args["expressions"] = [exp.table_(t) for t in tables] # type: ignore[assignment]
226
+
227
+ lock = exp.Lock(**lock_args)
228
+
229
+ current_locks = select_expr.args.get("locks", [])
230
+ current_locks.append(lock)
231
+ select_expr.set("locks", current_locks)
232
+
233
+ return self
234
+
235
+ def for_share(
236
+ self, *, skip_locked: bool = False, nowait: bool = False, of: "Optional[Union[str, list[str]]]" = None
237
+ ) -> "Self":
238
+ """Add FOR SHARE clause for shared row-level locking.
239
+
240
+ Args:
241
+ skip_locked: Skip rows that are already locked (SKIP LOCKED)
242
+ nowait: Return immediately if row is locked (NOWAIT)
243
+ of: Table names/aliases to lock (FOR SHARE OF table)
244
+
245
+ Returns:
246
+ Self for method chaining
247
+ """
248
+ self._validate_select_expression()
249
+ self._validate_lock_parameters(skip_locked, nowait)
250
+
251
+ assert self._expression is not None
252
+ select_expr = cast("exp.Select", self._expression)
253
+
254
+ lock_args = {"update": False}
255
+
256
+ if skip_locked:
257
+ lock_args["wait"] = False
258
+ elif nowait:
259
+ lock_args["wait"] = True
260
+
261
+ if of:
262
+ tables = [of] if isinstance(of, str) else of
263
+ lock_args["expressions"] = [exp.table_(t) for t in tables] # type: ignore[assignment]
264
+
265
+ lock = exp.Lock(**lock_args)
266
+
267
+ current_locks = select_expr.args.get("locks", [])
268
+ current_locks.append(lock)
269
+ select_expr.set("locks", current_locks)
270
+
271
+ return self
272
+
273
+ def for_key_share(self) -> "Self":
274
+ """Add FOR KEY SHARE clause (PostgreSQL-specific).
275
+
276
+ FOR KEY SHARE is like FOR SHARE, but the lock is weaker:
277
+ SELECT FOR UPDATE is blocked, but not SELECT FOR NO KEY UPDATE.
278
+
279
+ Returns:
280
+ Self for method chaining
281
+ """
282
+ self._validate_select_expression()
283
+
284
+ assert self._expression is not None
285
+ select_expr = cast("exp.Select", self._expression)
286
+
287
+ lock = exp.Lock(update=False, key=True)
288
+
289
+ current_locks = select_expr.args.get("locks", [])
290
+ current_locks.append(lock)
291
+ select_expr.set("locks", current_locks)
292
+
293
+ return self
294
+
295
+ def for_no_key_update(self) -> "Self":
296
+ """Add FOR NO KEY UPDATE clause (PostgreSQL-specific).
297
+
298
+ FOR NO KEY UPDATE is like FOR UPDATE, but the lock is weaker:
299
+ it does not block SELECT FOR KEY SHARE commands that attempt to
300
+ acquire a share lock on the same rows.
301
+
302
+ Returns:
303
+ Self for method chaining
304
+ """
305
+ self._validate_select_expression()
306
+
307
+ assert self._expression is not None
308
+ select_expr = cast("exp.Select", self._expression)
309
+
310
+ lock = exp.Lock(update=True, key=False)
311
+
312
+ current_locks = select_expr.args.get("locks", [])
313
+ current_locks.append(lock)
314
+ select_expr.set("locks", current_locks)
315
+
316
+ return self
@@ -117,8 +117,7 @@ class CommonTableExpressionMixin:
117
117
  if recursive:
118
118
  existing_with.set("recursive", recursive)
119
119
  else:
120
- # Only SELECT, INSERT, UPDATE support WITH clauses
121
- if hasattr(expression, "with_") and isinstance(expression, (exp.Select, exp.Insert, exp.Update)):
120
+ if isinstance(expression, (exp.Select, exp.Insert, exp.Update)):
122
121
  updated_expression = expression.with_(cte_alias_expr, as_=name, copy=False)
123
122
  builder.set_expression(updated_expression)
124
123
  if recursive:
@@ -82,16 +82,12 @@ class JoinClauseMixin:
82
82
  self, table: Any, alias: Optional[str], builder: "SQLBuilderProtocol"
83
83
  ) -> exp.Expression:
84
84
  """Handle table parameters that are query builders."""
85
- if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
86
- table_expr_value = getattr(table, "_expression", None)
87
- if table_expr_value is not None:
88
- subquery_exp = exp.paren(table_expr_value)
89
- else:
90
- subquery_exp = exp.paren(exp.Anonymous(this=""))
85
+ if hasattr(table, "_expression") and table._expression is not None:
86
+ subquery_exp = exp.paren(table._expression)
91
87
  return exp.alias_(subquery_exp, alias) if alias else subquery_exp
92
88
  subquery = table.build()
93
89
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
94
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
90
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect))
95
91
  return exp.alias_(subquery_exp, alias) if alias else subquery_exp
96
92
 
97
93
  def _parse_on_condition(
@@ -107,28 +103,20 @@ class JoinClauseMixin:
107
103
  return self._handle_sql_object_condition(on, builder)
108
104
  if isinstance(on, exp.Expression):
109
105
  return on
110
- # Last resort - convert to string and parse
111
106
  return exp.condition(str(on))
112
107
 
113
108
  def _handle_sql_object_condition(self, on: Any, builder: "SQLBuilderProtocol") -> exp.Expression:
114
109
  """Handle SQL object conditions with parameter binding."""
115
- expression = getattr(on, "expression", None)
116
- if expression is not None and isinstance(expression, exp.Expression):
117
- # Merge parameters from SQL object into builder
118
- if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
119
- sql_parameters = getattr(on, "parameters", {})
120
- for param_name, param_value in sql_parameters.items():
110
+ if hasattr(on, "expression") and on.expression is not None:
111
+ if hasattr(on, "parameters"):
112
+ for param_name, param_value in on.parameters.items():
121
113
  builder.add_parameter(param_value, name=param_name)
122
- return cast("exp.Expression", expression)
123
- # If expression is None, fall back to parsing the raw SQL
124
- sql_text = getattr(on, "sql", "")
125
- # Merge parameters even when parsing raw SQL
126
- if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
127
- sql_parameters = getattr(on, "parameters", {})
128
- for param_name, param_value in sql_parameters.items():
114
+ return cast("exp.Expression", on.expression)
115
+ if hasattr(on, "parameters"):
116
+ for param_name, param_value in on.parameters.items():
129
117
  builder.add_parameter(param_value, name=param_name)
130
- parsed_expr = exp.maybe_parse(sql_text)
131
- return parsed_expr if parsed_expr is not None else exp.condition(str(sql_text))
118
+ parsed_expr = exp.maybe_parse(on.sql)
119
+ return parsed_expr if parsed_expr is not None else exp.condition(str(on.sql))
132
120
 
133
121
  def _create_join_expression(
134
122
  self, table_expr: exp.Expression, on_expr: Optional[exp.Expression], join_type: str
@@ -195,17 +183,13 @@ class JoinClauseMixin:
195
183
  if isinstance(table, str):
196
184
  table_expr = parse_table_expression(table, alias)
197
185
  elif has_query_builder_parameters(table):
198
- if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
199
- table_expr_value = getattr(table, "_expression", None)
200
- if table_expr_value is not None:
201
- subquery_exp = exp.paren(table_expr_value)
202
- else:
203
- subquery_exp = exp.paren(exp.Anonymous(this=""))
186
+ if hasattr(table, "_expression") and table._expression is not None:
187
+ subquery_exp = exp.paren(table._expression)
204
188
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
205
189
  else:
206
190
  subquery = table.build()
207
191
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
208
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
192
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect))
209
193
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
210
194
  else:
211
195
  table_expr = table