sqlspec 0.13.1__py3-none-any.whl → 0.16.2__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 (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
@@ -5,12 +5,16 @@ that users might pass as strings to various builder methods.
5
5
  """
6
6
 
7
7
  import contextlib
8
- from typing import Any, Optional, Union, cast
8
+ 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.utils.type_guards import has_expression_attr, has_parameter_builder
12
13
 
13
- def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> exp.Expression:
14
+
15
+ def parse_column_expression(
16
+ column_input: Union[str, exp.Expression, Any], builder: Optional[Any] = None
17
+ ) -> exp.Expression:
14
18
  """Parse a column input that might be a complex expression.
15
19
 
16
20
  Handles cases like:
@@ -20,9 +24,11 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
20
24
  - Function calls: "MAX(price)" -> Max(this=Column(price))
21
25
  - Complex expressions: "CASE WHEN ... END" -> Case(...)
22
26
  - Custom Column objects from our builder
27
+ - SQL objects with raw SQL expressions
23
28
 
24
29
  Args:
25
- column_input: String, SQLGlot expression, or Column object
30
+ column_input: String, SQLGlot expression, SQL object, or Column object
31
+ builder: Optional builder instance for parameter merging
26
32
 
27
33
  Returns:
28
34
  exp.Expression: Parsed SQLGlot expression
@@ -30,11 +36,33 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
30
36
  if isinstance(column_input, exp.Expression):
31
37
  return column_input
32
38
 
33
- # Handle our custom Column objects
34
- if hasattr(column_input, "_expr"):
35
- attr_value = getattr(column_input, "_expr", None)
36
- if isinstance(attr_value, exp.Expression):
37
- return attr_value
39
+ # Handle SQL objects (from sql.raw with parameters)
40
+ if hasattr(column_input, "expression") and hasattr(column_input, "sql"):
41
+ # This is likely a SQL object
42
+ expression = getattr(column_input, "expression", None)
43
+ if expression is not None and isinstance(expression, exp.Expression):
44
+ # Merge parameters from SQL object into builder if available
45
+ if builder and hasattr(column_input, "parameters") and hasattr(builder, "add_parameter"):
46
+ sql_parameters = getattr(column_input, "parameters", {})
47
+ for param_name, param_value in sql_parameters.items():
48
+ builder.add_parameter(param_value, name=param_name)
49
+ return cast("exp.Expression", expression)
50
+ # If expression is None, fall back to parsing the raw SQL
51
+ sql_text = getattr(column_input, "sql", "")
52
+ # Merge parameters even when parsing raw SQL
53
+ if builder and hasattr(column_input, "parameters") and hasattr(builder, "add_parameter"):
54
+ sql_parameters = getattr(column_input, "parameters", {})
55
+ for param_name, param_value in sql_parameters.items():
56
+ builder.add_parameter(param_value, name=param_name)
57
+ return exp.maybe_parse(sql_text) or exp.column(str(sql_text))
58
+
59
+ if has_expression_attr(column_input):
60
+ try:
61
+ attr_value = column_input._expression
62
+ if isinstance(attr_value, exp.Expression):
63
+ return attr_value
64
+ except AttributeError:
65
+ pass
38
66
 
39
67
  return exp.maybe_parse(column_input) or exp.column(str(column_input))
40
68
 
@@ -42,7 +70,6 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
42
70
  def parse_table_expression(table_input: str, explicit_alias: Optional[str] = None) -> exp.Expression:
43
71
  """Parses a table string that can be a name, a name with an alias, or a subquery string."""
44
72
  with contextlib.suppress(Exception):
45
- # Wrapping in a SELECT statement is a robust way to parse various table-like syntaxes
46
73
  parsed = parse_one(f"SELECT * FROM {table_input}")
47
74
  if isinstance(parsed, exp.Select) and parsed.args.get("from"):
48
75
  from_clause = cast("exp.From", parsed.args.get("from"))
@@ -102,40 +129,36 @@ def parse_condition_expression(
102
129
  if isinstance(condition_input, exp.Expression):
103
130
  return condition_input
104
131
 
105
- tuple_condition_parts = 2
132
+ tuple_condition_parts: Final[int] = 2
106
133
  if isinstance(condition_input, tuple) and len(condition_input) == tuple_condition_parts:
107
134
  column, value = condition_input
108
135
  column_expr = parse_column_expression(column)
109
136
  if value is None:
110
137
  return exp.Is(this=column_expr, expression=exp.null())
111
- # Use builder's parameter system if available
112
- if builder and hasattr(builder, "add_parameter"):
113
- _, param_name = builder.add_parameter(value)
138
+ if builder and has_parameter_builder(builder):
139
+ from sqlspec.builder.mixins._where_clause import _extract_column_name
140
+
141
+ column_name = _extract_column_name(column)
142
+ param_name = builder._generate_unique_parameter_name(column_name)
143
+ _, param_name = builder.add_parameter(value, name=param_name)
114
144
  return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
115
145
  if isinstance(value, str):
116
- return exp.EQ(this=column_expr, expression=exp.Literal.string(value))
146
+ return exp.EQ(this=column_expr, expression=exp.convert(value))
117
147
  if isinstance(value, (int, float)):
118
- return exp.EQ(this=column_expr, expression=exp.Literal.number(str(value)))
119
- return exp.EQ(this=column_expr, expression=exp.Literal.string(str(value)))
148
+ return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
149
+ return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
120
150
 
121
151
  if not isinstance(condition_input, str):
122
152
  condition_input = str(condition_input)
123
153
 
124
154
  try:
125
- # Parse as condition using SQLGlot's condition parser
126
155
  return exp.condition(condition_input)
127
156
  except Exception:
128
- # If that fails, try parsing as a general expression
129
157
  try:
130
158
  parsed = exp.maybe_parse(condition_input) # type: ignore[var-annotated]
131
- if parsed:
132
- return parsed # type:ignore[no-any-return]
133
- except Exception: # noqa: S110
134
- # SQLGlot condition parsing failed, will use raw condition
135
- pass
136
-
137
- # Ultimate fallback: treat as raw condition string
138
- return exp.condition(condition_input)
159
+ return parsed or exp.condition(condition_input)
160
+ except Exception:
161
+ return exp.condition(condition_input)
139
162
 
140
163
 
141
164
  __all__ = ("parse_column_expression", "parse_condition_expression", "parse_order_expression", "parse_table_expression")
@@ -0,0 +1,170 @@
1
+ """Safe SQL query builder with validation and parameter binding.
2
+
3
+ This module provides a fluent interface for building SQL queries safely,
4
+ with automatic parameter binding and validation.
5
+ """
6
+
7
+ import re
8
+ from typing import Any, Callable, Final, Optional, Union
9
+
10
+ from sqlglot import exp
11
+ from typing_extensions import Self
12
+
13
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
14
+ from sqlspec.builder.mixins import (
15
+ CommonTableExpressionMixin,
16
+ HavingClauseMixin,
17
+ JoinClauseMixin,
18
+ LimitOffsetClauseMixin,
19
+ OrderByClauseMixin,
20
+ PivotClauseMixin,
21
+ SelectClauseMixin,
22
+ SetOperationMixin,
23
+ UnpivotClauseMixin,
24
+ WhereClauseMixin,
25
+ )
26
+ from sqlspec.core.result import SQLResult
27
+
28
+ __all__ = ("Select",)
29
+
30
+
31
+ TABLE_HINT_PATTERN: Final[str] = r"\b{}\b(\s+AS\s+\w+)?"
32
+
33
+
34
+ class Select(
35
+ QueryBuilder,
36
+ WhereClauseMixin,
37
+ OrderByClauseMixin,
38
+ LimitOffsetClauseMixin,
39
+ SelectClauseMixin,
40
+ JoinClauseMixin,
41
+ HavingClauseMixin,
42
+ SetOperationMixin,
43
+ CommonTableExpressionMixin,
44
+ PivotClauseMixin,
45
+ UnpivotClauseMixin,
46
+ ):
47
+ """Type-safe builder for SELECT queries with schema/model integration.
48
+
49
+ This builder provides a fluent, safe interface for constructing SQL SELECT statements.
50
+
51
+ Example:
52
+ >>> class User(BaseModel):
53
+ ... id: int
54
+ ... name: str
55
+ >>> builder = Select("id", "name").from_("users")
56
+ >>> result = driver.execute(builder)
57
+ """
58
+
59
+ __slots__ = ("_hints", "_with_parts")
60
+ _expression: Optional[exp.Expression]
61
+
62
+ def __init__(self, *columns: str, **kwargs: Any) -> None:
63
+ """Initialize SELECT with optional columns.
64
+
65
+ Args:
66
+ *columns: Column names to select (e.g., "id", "name", "u.email")
67
+ **kwargs: Additional QueryBuilder arguments (dialect, schema, etc.)
68
+
69
+ Examples:
70
+ Select("id", "name") # Shorthand for Select().select("id", "name")
71
+ Select() # Same as Select() - start empty
72
+ """
73
+ super().__init__(**kwargs)
74
+
75
+ # Initialize Select-specific attributes
76
+ self._with_parts: dict[str, Union[exp.CTE, Select]] = {}
77
+ self._hints: list[dict[str, object]] = []
78
+
79
+ self._initialize_expression()
80
+
81
+ if columns:
82
+ self.select(*columns)
83
+
84
+ @property
85
+ def _expected_result_type(self) -> "type[SQLResult]":
86
+ """Get the expected result type for SELECT operations.
87
+
88
+ Returns:
89
+ type: The SelectResult type.
90
+ """
91
+ return SQLResult
92
+
93
+ def _create_base_expression(self) -> exp.Select:
94
+ """Create base SELECT expression."""
95
+ if self._expression is None or not isinstance(self._expression, exp.Select):
96
+ self._expression = exp.Select()
97
+ return self._expression
98
+
99
+ def with_hint(
100
+ self,
101
+ hint: "str",
102
+ *,
103
+ location: "str" = "statement",
104
+ table: "Optional[str]" = None,
105
+ dialect: "Optional[str]" = None,
106
+ ) -> "Self":
107
+ """Attach an optimizer or dialect-specific hint to the query.
108
+
109
+ Args:
110
+ hint: The raw hint string (e.g., 'INDEX(users idx_users_name)').
111
+ location: Where to apply the hint ('statement', 'table').
112
+ table: Table name if the hint is for a specific table.
113
+ dialect: Restrict the hint to a specific dialect (optional).
114
+
115
+ Returns:
116
+ The current builder instance for method chaining.
117
+ """
118
+ self._hints.append({"hint": hint, "location": location, "table": table, "dialect": dialect})
119
+ return self
120
+
121
+ def build(self) -> "SafeQuery":
122
+ """Builds the SQL query string and parameters with hint injection.
123
+
124
+ Returns:
125
+ SafeQuery: A dataclass containing the SQL string and parameters.
126
+ """
127
+ safe_query = super().build()
128
+
129
+ if not self._hints:
130
+ return safe_query
131
+
132
+ modified_expr = self._expression or self._create_base_expression()
133
+
134
+ if isinstance(modified_expr, exp.Select):
135
+ statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
136
+ if statement_hints:
137
+
138
+ def parse_hint_safely(hint: Any) -> exp.Expression:
139
+ try:
140
+ hint_str = str(hint)
141
+ hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
142
+ return hint_expr or exp.Anonymous(this=hint_str)
143
+ except Exception:
144
+ return exp.Anonymous(this=str(hint))
145
+
146
+ hint_expressions: list[exp.Expression] = [parse_hint_safely(hint) for hint in statement_hints]
147
+
148
+ if hint_expressions:
149
+ modified_expr.set("hint", exp.Hint(expressions=hint_expressions))
150
+
151
+ modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
152
+
153
+ for hint_dict in self._hints:
154
+ if hint_dict.get("location") == "table" and hint_dict.get("table"):
155
+ table = str(hint_dict["table"])
156
+ hint = str(hint_dict["hint"])
157
+ pattern = TABLE_HINT_PATTERN.format(re.escape(table))
158
+
159
+ def make_replacement(hint_val: str, table_val: str) -> "Callable[[re.Match[str]], str]":
160
+ def replacement_func(match: re.Match[str]) -> str:
161
+ alias_part = match.group(1) or ""
162
+ return f"/*+ {hint_val} */ {table_val}{alias_part}"
163
+
164
+ return replacement_func
165
+
166
+ modified_sql = re.sub(
167
+ pattern, make_replacement(hint, table), modified_sql, count=1, flags=re.IGNORECASE
168
+ )
169
+
170
+ return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
@@ -4,33 +4,30 @@ This module provides a fluent interface for building SQL queries safely,
4
4
  with automatic parameter binding and validation.
5
5
  """
6
6
 
7
- from dataclasses import dataclass
8
7
  from typing import TYPE_CHECKING, Any, Optional, Union
9
8
 
10
9
  from sqlglot import exp
11
10
  from typing_extensions import Self
12
11
 
13
- from sqlspec.exceptions import SQLBuilderError
14
- from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
15
- from sqlspec.statement.builder.mixins import (
12
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
13
+ from sqlspec.builder.mixins import (
16
14
  ReturningClauseMixin,
17
15
  UpdateFromClauseMixin,
18
16
  UpdateSetClauseMixin,
19
17
  UpdateTableClauseMixin,
20
18
  WhereClauseMixin,
21
19
  )
22
- from sqlspec.statement.result import SQLResult
23
- from sqlspec.typing import RowT
20
+ from sqlspec.core.result import SQLResult
21
+ from sqlspec.exceptions import SQLBuilderError
24
22
 
25
23
  if TYPE_CHECKING:
26
- from sqlspec.statement.builder.select import Select
24
+ from sqlspec.builder._select import Select
27
25
 
28
26
  __all__ = ("Update",)
29
27
 
30
28
 
31
- @dataclass(unsafe_hash=True)
32
29
  class Update(
33
- QueryBuilder[RowT],
30
+ QueryBuilder,
34
31
  WhereClauseMixin,
35
32
  ReturningClauseMixin,
36
33
  UpdateSetClauseMixin,
@@ -44,7 +41,6 @@ class Update(
44
41
 
45
42
  Example:
46
43
  ```python
47
- # Basic UPDATE
48
44
  update_query = (
49
45
  Update()
50
46
  .table("users")
@@ -53,12 +49,10 @@ class Update(
53
49
  .where("id = 1")
54
50
  )
55
51
 
56
- # Even more concise with constructor
57
52
  update_query = (
58
53
  Update("users").set(name="John Doe").where("id = 1")
59
54
  )
60
55
 
61
- # UPDATE with parameterized conditions
62
56
  update_query = (
63
57
  Update()
64
58
  .table("users")
@@ -66,7 +60,6 @@ class Update(
66
60
  .where_eq("id", 123)
67
61
  )
68
62
 
69
- # UPDATE with FROM clause (PostgreSQL style)
70
63
  update_query = (
71
64
  Update()
72
65
  .table("users", "u")
@@ -77,6 +70,9 @@ class Update(
77
70
  ```
78
71
  """
79
72
 
73
+ __slots__ = ("_table",)
74
+ _expression: Optional[exp.Expression]
75
+
80
76
  def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
81
77
  """Initialize UPDATE with optional table.
82
78
 
@@ -85,14 +81,15 @@ class Update(
85
81
  **kwargs: Additional QueryBuilder arguments
86
82
  """
87
83
  super().__init__(**kwargs)
84
+ self._initialize_expression()
88
85
 
89
86
  if table:
90
87
  self.table(table)
91
88
 
92
89
  @property
93
- def _expected_result_type(self) -> "type[SQLResult[RowT]]":
90
+ def _expected_result_type(self) -> "type[SQLResult]":
94
91
  """Return the expected result type for this builder."""
95
- return SQLResult[RowT]
92
+ return SQLResult
96
93
 
97
94
  def _create_base_expression(self) -> exp.Update:
98
95
  """Create a base UPDATE expression.
@@ -104,7 +101,7 @@ class Update(
104
101
 
105
102
  def join(
106
103
  self,
107
- table: "Union[str, exp.Expression, Select[RowT]]",
104
+ table: "Union[str, exp.Expression, Select]",
108
105
  on: "Union[str, exp.Expression]",
109
106
  alias: "Optional[str]" = None,
110
107
  join_type: str = "INNER",
@@ -135,10 +132,9 @@ class Update(
135
132
  subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
136
133
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
137
134
 
138
- # Merge parameters
139
- subquery_params = table._parameters
140
- if subquery_params:
141
- for p_name, p_value in subquery_params.items():
135
+ subquery_parameters = table._parameters
136
+ if subquery_parameters:
137
+ for p_name, p_value in subquery_parameters.items():
142
138
  self.add_parameter(p_value, name=p_name)
143
139
  else:
144
140
  table_expr = table
@@ -0,0 +1,55 @@
1
+ """SQL statement builder mixins."""
2
+
3
+ from sqlspec.builder.mixins._cte_and_set_ops import CommonTableExpressionMixin, SetOperationMixin
4
+ from sqlspec.builder.mixins._delete_operations import DeleteFromClauseMixin
5
+ from sqlspec.builder.mixins._insert_operations import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin
6
+ from sqlspec.builder.mixins._join_operations import JoinClauseMixin
7
+ from sqlspec.builder.mixins._merge_operations import (
8
+ MergeIntoClauseMixin,
9
+ MergeMatchedClauseMixin,
10
+ MergeNotMatchedBySourceClauseMixin,
11
+ MergeNotMatchedClauseMixin,
12
+ MergeOnClauseMixin,
13
+ MergeUsingClauseMixin,
14
+ )
15
+ from sqlspec.builder.mixins._order_limit_operations import (
16
+ LimitOffsetClauseMixin,
17
+ OrderByClauseMixin,
18
+ ReturningClauseMixin,
19
+ )
20
+ from sqlspec.builder.mixins._pivot_operations import PivotClauseMixin, UnpivotClauseMixin
21
+ from sqlspec.builder.mixins._select_operations import CaseBuilder, SelectClauseMixin
22
+ from sqlspec.builder.mixins._update_operations import (
23
+ UpdateFromClauseMixin,
24
+ UpdateSetClauseMixin,
25
+ UpdateTableClauseMixin,
26
+ )
27
+ from sqlspec.builder.mixins._where_clause import HavingClauseMixin, WhereClauseMixin
28
+
29
+ __all__ = (
30
+ "CaseBuilder",
31
+ "CommonTableExpressionMixin",
32
+ "DeleteFromClauseMixin",
33
+ "HavingClauseMixin",
34
+ "InsertFromSelectMixin",
35
+ "InsertIntoClauseMixin",
36
+ "InsertValuesMixin",
37
+ "JoinClauseMixin",
38
+ "LimitOffsetClauseMixin",
39
+ "MergeIntoClauseMixin",
40
+ "MergeMatchedClauseMixin",
41
+ "MergeNotMatchedBySourceClauseMixin",
42
+ "MergeNotMatchedClauseMixin",
43
+ "MergeOnClauseMixin",
44
+ "MergeUsingClauseMixin",
45
+ "OrderByClauseMixin",
46
+ "PivotClauseMixin",
47
+ "ReturningClauseMixin",
48
+ "SelectClauseMixin",
49
+ "SetOperationMixin",
50
+ "UnpivotClauseMixin",
51
+ "UpdateFromClauseMixin",
52
+ "UpdateSetClauseMixin",
53
+ "UpdateTableClauseMixin",
54
+ "WhereClauseMixin",
55
+ )