sqlspec 0.16.2__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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 (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1782 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +473 -0
  52. sqlspec/builder/_column.py +320 -0
  53. sqlspec/builder/_ddl.py +1346 -0
  54. sqlspec/builder/_ddl_utils.py +103 -0
  55. sqlspec/builder/_delete.py +76 -0
  56. sqlspec/builder/_insert.py +421 -0
  57. sqlspec/builder/_merge.py +71 -0
  58. sqlspec/builder/_parsing_utils.py +164 -0
  59. sqlspec/builder/_select.py +170 -0
  60. sqlspec/builder/_update.py +188 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  63. sqlspec/builder/mixins/_delete_operations.py +41 -0
  64. sqlspec/builder/mixins/_insert_operations.py +244 -0
  65. sqlspec/builder/mixins/_join_operations.py +149 -0
  66. sqlspec/builder/mixins/_merge_operations.py +562 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  69. sqlspec/builder/mixins/_select_operations.py +604 -0
  70. sqlspec/builder/mixins/_update_operations.py +202 -0
  71. sqlspec/builder/mixins/_where_clause.py +644 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +871 -0
  77. sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +417 -0
  79. sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1237 -0
  85. sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +677 -0
  87. sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +676 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +502 -0
  93. sqlspec/driver/_common.py +631 -0
  94. sqlspec/driver/_sync.py +503 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +193 -0
  97. sqlspec/driver/mixins/_sql_translator.py +86 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +407 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1139 -0
  143. sqlspec-0.16.2.dist-info/METADATA +365 -0
  144. sqlspec-0.16.2.dist-info/RECORD +148 -0
  145. sqlspec-0.16.2.dist-info/WHEEL +7 -0
  146. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.2.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.2.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,164 @@
1
+ """Centralized parsing utilities for SQLSpec builders.
2
+
3
+ This module provides common parsing functions to handle complex SQL expressions
4
+ that users might pass as strings to various builder methods.
5
+ """
6
+
7
+ import contextlib
8
+ from typing import Any, Final, Optional, Union, cast
9
+
10
+ from sqlglot import exp, maybe_parse, parse_one
11
+
12
+ from sqlspec.utils.type_guards import has_expression_attr, has_parameter_builder
13
+
14
+
15
+ def parse_column_expression(
16
+ column_input: Union[str, exp.Expression, Any], builder: Optional[Any] = None
17
+ ) -> exp.Expression:
18
+ """Parse a column input that might be a complex expression.
19
+
20
+ Handles cases like:
21
+ - Simple column names: "name" -> Column(this=name)
22
+ - Qualified names: "users.name" -> Column(table=users, this=name)
23
+ - Aliased columns: "name AS user_name" -> Alias(this=Column(name), alias=user_name)
24
+ - Function calls: "MAX(price)" -> Max(this=Column(price))
25
+ - Complex expressions: "CASE WHEN ... END" -> Case(...)
26
+ - Custom Column objects from our builder
27
+ - SQL objects with raw SQL expressions
28
+
29
+ Args:
30
+ column_input: String, SQLGlot expression, SQL object, or Column object
31
+ builder: Optional builder instance for parameter merging
32
+
33
+ Returns:
34
+ exp.Expression: Parsed SQLGlot expression
35
+ """
36
+ if isinstance(column_input, exp.Expression):
37
+ return column_input
38
+
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
66
+
67
+ return exp.maybe_parse(column_input) or exp.column(str(column_input))
68
+
69
+
70
+ def parse_table_expression(table_input: str, explicit_alias: Optional[str] = None) -> exp.Expression:
71
+ """Parses a table string that can be a name, a name with an alias, or a subquery string."""
72
+ with contextlib.suppress(Exception):
73
+ parsed = parse_one(f"SELECT * FROM {table_input}")
74
+ if isinstance(parsed, exp.Select) and parsed.args.get("from"):
75
+ from_clause = cast("exp.From", parsed.args.get("from"))
76
+ table_expr = from_clause.this
77
+
78
+ if explicit_alias:
79
+ return exp.alias_(table_expr, explicit_alias) # type:ignore[no-any-return]
80
+ return table_expr # type:ignore[no-any-return]
81
+
82
+ return exp.to_table(table_input, alias=explicit_alias)
83
+
84
+
85
+ def parse_order_expression(order_input: Union[str, exp.Expression]) -> exp.Expression:
86
+ """Parse an ORDER BY expression that might include direction.
87
+
88
+ Handles cases like:
89
+ - Simple column: "name" -> Column(this=name)
90
+ - With direction: "name DESC" -> Ordered(this=Column(name), desc=True)
91
+ - Qualified: "users.name ASC" -> Ordered(this=Column(table=users, this=name), desc=False)
92
+ - Function: "COUNT(*) DESC" -> Ordered(this=Count(this=Star), desc=True)
93
+
94
+ Args:
95
+ order_input: String or SQLGlot expression for ORDER BY
96
+
97
+ Returns:
98
+ exp.Expression: Parsed SQLGlot expression (usually Ordered or Column)
99
+ """
100
+ if isinstance(order_input, exp.Expression):
101
+ return order_input
102
+
103
+ with contextlib.suppress(Exception):
104
+ parsed = maybe_parse(str(order_input), into=exp.Ordered)
105
+ if parsed:
106
+ return parsed
107
+
108
+ return parse_column_expression(order_input)
109
+
110
+
111
+ def parse_condition_expression(
112
+ condition_input: Union[str, exp.Expression, tuple[str, Any]], builder: "Any" = None
113
+ ) -> exp.Expression:
114
+ """Parse a condition that might be complex SQL.
115
+
116
+ Handles cases like:
117
+ - Simple conditions: "name = 'John'" -> EQ(Column(name), Literal('John'))
118
+ - Tuple format: ("name", "John") -> EQ(Column(name), Literal('John'))
119
+ - Complex conditions: "age > 18 AND status = 'active'" -> And(GT(...), EQ(...))
120
+ - Function conditions: "LENGTH(name) > 5" -> GT(Length(Column(name)), Literal(5))
121
+
122
+ Args:
123
+ condition_input: String, tuple, or SQLGlot expression for condition
124
+ builder: Optional builder instance for parameter binding
125
+
126
+ Returns:
127
+ exp.Expression: Parsed SQLGlot expression (usually a comparison or logical op)
128
+ """
129
+ if isinstance(condition_input, exp.Expression):
130
+ return condition_input
131
+
132
+ tuple_condition_parts: Final[int] = 2
133
+ if isinstance(condition_input, tuple) and len(condition_input) == tuple_condition_parts:
134
+ column, value = condition_input
135
+ column_expr = parse_column_expression(column)
136
+ if value is None:
137
+ return exp.Is(this=column_expr, expression=exp.null())
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)
144
+ return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
145
+ if isinstance(value, str):
146
+ return exp.EQ(this=column_expr, expression=exp.convert(value))
147
+ if isinstance(value, (int, float)):
148
+ return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
149
+ return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
150
+
151
+ if not isinstance(condition_input, str):
152
+ condition_input = str(condition_input)
153
+
154
+ try:
155
+ return exp.condition(condition_input)
156
+ except Exception:
157
+ try:
158
+ parsed = exp.maybe_parse(condition_input) # type: ignore[var-annotated]
159
+ return parsed or exp.condition(condition_input)
160
+ except Exception:
161
+ return exp.condition(condition_input)
162
+
163
+
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)
@@ -0,0 +1,188 @@
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
+ from typing import TYPE_CHECKING, Any, Optional, Union
8
+
9
+ from sqlglot import exp
10
+ from typing_extensions import Self
11
+
12
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
13
+ from sqlspec.builder.mixins import (
14
+ ReturningClauseMixin,
15
+ UpdateFromClauseMixin,
16
+ UpdateSetClauseMixin,
17
+ UpdateTableClauseMixin,
18
+ WhereClauseMixin,
19
+ )
20
+ from sqlspec.core.result import SQLResult
21
+ from sqlspec.exceptions import SQLBuilderError
22
+
23
+ if TYPE_CHECKING:
24
+ from sqlspec.builder._select import Select
25
+
26
+ __all__ = ("Update",)
27
+
28
+
29
+ class Update(
30
+ QueryBuilder,
31
+ WhereClauseMixin,
32
+ ReturningClauseMixin,
33
+ UpdateSetClauseMixin,
34
+ UpdateFromClauseMixin,
35
+ UpdateTableClauseMixin,
36
+ ):
37
+ """Builder for UPDATE statements.
38
+
39
+ This builder provides a fluent interface for constructing SQL UPDATE statements
40
+ with automatic parameter binding and validation.
41
+
42
+ Example:
43
+ ```python
44
+ update_query = (
45
+ Update()
46
+ .table("users")
47
+ .set(name="John Doe")
48
+ .set(email="john@example.com")
49
+ .where("id = 1")
50
+ )
51
+
52
+ update_query = (
53
+ Update("users").set(name="John Doe").where("id = 1")
54
+ )
55
+
56
+ update_query = (
57
+ Update()
58
+ .table("users")
59
+ .set(status="active")
60
+ .where_eq("id", 123)
61
+ )
62
+
63
+ update_query = (
64
+ Update()
65
+ .table("users", "u")
66
+ .set(name="Updated Name")
67
+ .from_("profiles", "p")
68
+ .where("u.id = p.user_id AND p.is_verified = true")
69
+ )
70
+ ```
71
+ """
72
+
73
+ __slots__ = ("_table",)
74
+ _expression: Optional[exp.Expression]
75
+
76
+ def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
77
+ """Initialize UPDATE with optional table.
78
+
79
+ Args:
80
+ table: Target table name
81
+ **kwargs: Additional QueryBuilder arguments
82
+ """
83
+ super().__init__(**kwargs)
84
+ self._initialize_expression()
85
+
86
+ if table:
87
+ self.table(table)
88
+
89
+ @property
90
+ def _expected_result_type(self) -> "type[SQLResult]":
91
+ """Return the expected result type for this builder."""
92
+ return SQLResult
93
+
94
+ def _create_base_expression(self) -> exp.Update:
95
+ """Create a base UPDATE expression.
96
+
97
+ Returns:
98
+ A new sqlglot Update expression with empty clauses.
99
+ """
100
+ return exp.Update(this=None, expressions=[], joins=[])
101
+
102
+ def join(
103
+ self,
104
+ table: "Union[str, exp.Expression, Select]",
105
+ on: "Union[str, exp.Expression]",
106
+ alias: "Optional[str]" = None,
107
+ join_type: str = "INNER",
108
+ ) -> "Self":
109
+ """Add JOIN clause to the UPDATE statement.
110
+
111
+ Args:
112
+ table: The table name, expression, or subquery to join.
113
+ on: The JOIN condition.
114
+ alias: Optional alias for the joined table.
115
+ join_type: Type of join (INNER, LEFT, RIGHT, FULL).
116
+
117
+ Returns:
118
+ The current builder instance for method chaining.
119
+
120
+ Raises:
121
+ SQLBuilderError: If the current expression is not an UPDATE statement.
122
+ """
123
+ if self._expression is None or not isinstance(self._expression, exp.Update):
124
+ msg = "Cannot add JOIN clause to non-UPDATE expression."
125
+ raise SQLBuilderError(msg)
126
+
127
+ table_expr: exp.Expression
128
+ if isinstance(table, str):
129
+ table_expr = exp.table_(table, alias=alias)
130
+ elif isinstance(table, QueryBuilder):
131
+ subquery = table.build()
132
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
133
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
134
+
135
+ subquery_parameters = table._parameters
136
+ if subquery_parameters:
137
+ for p_name, p_value in subquery_parameters.items():
138
+ self.add_parameter(p_value, name=p_name)
139
+ else:
140
+ table_expr = table
141
+
142
+ on_expr: exp.Expression = exp.condition(on) if isinstance(on, str) else on
143
+
144
+ join_type_upper = join_type.upper()
145
+ if join_type_upper == "INNER":
146
+ join_expr = exp.Join(this=table_expr, on=on_expr)
147
+ elif join_type_upper == "LEFT":
148
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
149
+ elif join_type_upper == "RIGHT":
150
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
151
+ elif join_type_upper == "FULL":
152
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
153
+ else:
154
+ msg = f"Unsupported join type: {join_type}"
155
+ raise SQLBuilderError(msg)
156
+
157
+ if not self._expression.args.get("joins"):
158
+ self._expression.set("joins", [])
159
+ self._expression.args["joins"].append(join_expr)
160
+
161
+ return self
162
+
163
+ def build(self) -> "SafeQuery":
164
+ """Build the UPDATE query with validation.
165
+
166
+ Returns:
167
+ SafeQuery: The built query with SQL and parameters.
168
+
169
+ Raises:
170
+ SQLBuilderError: If no table is set or expression is not an UPDATE.
171
+ """
172
+ if self._expression is None:
173
+ msg = "UPDATE expression not initialized."
174
+ raise SQLBuilderError(msg)
175
+
176
+ if not isinstance(self._expression, exp.Update):
177
+ msg = "No UPDATE expression to build or expression is of the wrong type."
178
+ raise SQLBuilderError(msg)
179
+
180
+ if getattr(self._expression, "this", None) is None:
181
+ msg = "No table specified for UPDATE statement."
182
+ raise SQLBuilderError(msg)
183
+
184
+ if not self._expression.args.get("expressions"):
185
+ msg = "At least one SET clause must be specified for UPDATE statement."
186
+ raise SQLBuilderError(msg)
187
+
188
+ return super().build()
@@ -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
+ )