sqlspec 0.12.1__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/_sql.py +21 -180
- sqlspec/adapters/adbc/config.py +10 -12
- sqlspec/adapters/adbc/driver.py +120 -118
- sqlspec/adapters/aiosqlite/config.py +3 -3
- sqlspec/adapters/aiosqlite/driver.py +116 -141
- sqlspec/adapters/asyncmy/config.py +3 -4
- sqlspec/adapters/asyncmy/driver.py +123 -135
- sqlspec/adapters/asyncpg/config.py +3 -7
- sqlspec/adapters/asyncpg/driver.py +98 -140
- sqlspec/adapters/bigquery/config.py +4 -5
- sqlspec/adapters/bigquery/driver.py +231 -181
- sqlspec/adapters/duckdb/config.py +3 -6
- sqlspec/adapters/duckdb/driver.py +132 -124
- sqlspec/adapters/oracledb/config.py +6 -5
- sqlspec/adapters/oracledb/driver.py +242 -259
- sqlspec/adapters/psqlpy/config.py +3 -7
- sqlspec/adapters/psqlpy/driver.py +118 -93
- sqlspec/adapters/psycopg/config.py +34 -30
- sqlspec/adapters/psycopg/driver.py +342 -214
- sqlspec/adapters/sqlite/config.py +3 -3
- sqlspec/adapters/sqlite/driver.py +150 -104
- sqlspec/config.py +0 -4
- sqlspec/driver/_async.py +89 -98
- sqlspec/driver/_common.py +52 -17
- sqlspec/driver/_sync.py +81 -105
- sqlspec/driver/connection.py +207 -0
- sqlspec/driver/mixins/_csv_writer.py +91 -0
- sqlspec/driver/mixins/_pipeline.py +38 -49
- sqlspec/driver/mixins/_result_utils.py +27 -9
- sqlspec/driver/mixins/_storage.py +149 -216
- sqlspec/driver/mixins/_type_coercion.py +3 -4
- sqlspec/driver/parameters.py +138 -0
- sqlspec/exceptions.py +10 -2
- sqlspec/extensions/aiosql/adapter.py +0 -10
- sqlspec/extensions/litestar/handlers.py +0 -1
- sqlspec/extensions/litestar/plugin.py +0 -3
- sqlspec/extensions/litestar/providers.py +0 -14
- sqlspec/loader.py +31 -118
- sqlspec/protocols.py +542 -0
- sqlspec/service/__init__.py +3 -2
- sqlspec/service/_util.py +147 -0
- sqlspec/service/base.py +1116 -9
- sqlspec/statement/builder/__init__.py +42 -32
- sqlspec/statement/builder/_ddl_utils.py +0 -10
- sqlspec/statement/builder/_parsing_utils.py +10 -4
- sqlspec/statement/builder/base.py +70 -23
- sqlspec/statement/builder/column.py +283 -0
- sqlspec/statement/builder/ddl.py +102 -65
- sqlspec/statement/builder/delete.py +23 -7
- sqlspec/statement/builder/insert.py +29 -15
- sqlspec/statement/builder/merge.py +4 -4
- sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
- sqlspec/statement/builder/mixins/_delete_from.py +1 -1
- sqlspec/statement/builder/mixins/_from.py +10 -8
- sqlspec/statement/builder/mixins/_group_by.py +0 -1
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
- sqlspec/statement/builder/mixins/_insert_values.py +0 -2
- sqlspec/statement/builder/mixins/_join.py +20 -13
- sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
- sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
- sqlspec/statement/builder/mixins/_order_by.py +2 -2
- sqlspec/statement/builder/mixins/_pivot.py +4 -7
- sqlspec/statement/builder/mixins/_select_columns.py +6 -5
- sqlspec/statement/builder/mixins/_unpivot.py +6 -9
- sqlspec/statement/builder/mixins/_update_from.py +2 -1
- sqlspec/statement/builder/mixins/_update_set.py +11 -8
- sqlspec/statement/builder/mixins/_where.py +61 -34
- sqlspec/statement/builder/select.py +32 -17
- sqlspec/statement/builder/update.py +25 -11
- sqlspec/statement/filters.py +39 -14
- sqlspec/statement/parameter_manager.py +220 -0
- sqlspec/statement/parameters.py +210 -79
- sqlspec/statement/pipelines/__init__.py +166 -23
- sqlspec/statement/pipelines/analyzers/_analyzer.py +22 -25
- sqlspec/statement/pipelines/context.py +35 -39
- sqlspec/statement/pipelines/transformers/__init__.py +2 -3
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +667 -43
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
- sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
- sqlspec/statement/pipelines/validators/_performance.py +38 -23
- sqlspec/statement/pipelines/validators/_security.py +39 -62
- sqlspec/statement/result.py +37 -129
- sqlspec/statement/splitter.py +0 -12
- sqlspec/statement/sql.py +885 -379
- sqlspec/statement/sql_compiler.py +140 -0
- sqlspec/storage/__init__.py +10 -2
- sqlspec/storage/backends/fsspec.py +82 -35
- sqlspec/storage/backends/obstore.py +66 -49
- sqlspec/storage/capabilities.py +101 -0
- sqlspec/storage/registry.py +56 -83
- sqlspec/typing.py +6 -434
- sqlspec/utils/cached_property.py +25 -0
- sqlspec/utils/correlation.py +0 -2
- sqlspec/utils/logging.py +0 -6
- sqlspec/utils/sync_tools.py +0 -4
- sqlspec/utils/text.py +0 -5
- sqlspec/utils/type_guards.py +892 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
- sqlspec-0.13.0.dist-info/RECORD +150 -0
- sqlspec/statement/builder/protocols.py +0 -20
- sqlspec/statement/pipelines/base.py +0 -315
- sqlspec/statement/pipelines/result_types.py +0 -41
- sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
- sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
- sqlspec/statement/pipelines/validators/base.py +0 -67
- sqlspec/storage/protocol.py +0 -170
- sqlspec-0.12.1.dist-info/RECORD +0 -145
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -7,7 +7,8 @@ from sqlspec.exceptions import SQLBuilderError
|
|
|
7
7
|
from sqlspec.statement.builder._parsing_utils import parse_column_expression
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from sqlspec.
|
|
10
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
11
|
+
from sqlspec.statement.builder.column import Column, FunctionColumn
|
|
11
12
|
|
|
12
13
|
__all__ = ("SelectColumnsMixin",)
|
|
13
14
|
|
|
@@ -15,7 +16,7 @@ __all__ = ("SelectColumnsMixin",)
|
|
|
15
16
|
class SelectColumnsMixin:
|
|
16
17
|
"""Mixin providing SELECT column and DISTINCT clauses for SELECT builders."""
|
|
17
18
|
|
|
18
|
-
def select(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
19
|
+
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
|
|
19
20
|
"""Add columns to SELECT clause.
|
|
20
21
|
|
|
21
22
|
Raises:
|
|
@@ -24,7 +25,7 @@ class SelectColumnsMixin:
|
|
|
24
25
|
Returns:
|
|
25
26
|
The current builder instance for method chaining.
|
|
26
27
|
"""
|
|
27
|
-
builder = cast("
|
|
28
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
28
29
|
if builder._expression is None:
|
|
29
30
|
builder._expression = exp.Select()
|
|
30
31
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -34,7 +35,7 @@ class SelectColumnsMixin:
|
|
|
34
35
|
builder._expression = builder._expression.select(parse_column_expression(column), copy=False)
|
|
35
36
|
return cast("Self", builder)
|
|
36
37
|
|
|
37
|
-
def distinct(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
38
|
+
def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
|
|
38
39
|
"""Add DISTINCT clause to SELECT.
|
|
39
40
|
|
|
40
41
|
Args:
|
|
@@ -46,7 +47,7 @@ class SelectColumnsMixin:
|
|
|
46
47
|
Returns:
|
|
47
48
|
The current builder instance for method chaining.
|
|
48
49
|
"""
|
|
49
|
-
builder = cast("
|
|
50
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
50
51
|
if builder._expression is None:
|
|
51
52
|
builder._expression = exp.Select()
|
|
52
53
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -5,13 +5,13 @@ from sqlglot import exp
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from sqlglot.dialects.dialect import DialectType
|
|
7
7
|
|
|
8
|
-
from sqlspec.statement.builder.select import
|
|
8
|
+
from sqlspec.statement.builder.select import Select
|
|
9
9
|
|
|
10
10
|
__all__ = ("UnpivotClauseMixin",)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class UnpivotClauseMixin:
|
|
14
|
-
"""Mixin class to add UNPIVOT functionality to a
|
|
14
|
+
"""Mixin class to add UNPIVOT functionality to a Select."""
|
|
15
15
|
|
|
16
16
|
_expression: "Optional[exp.Expression]" = None
|
|
17
17
|
dialect: "DialectType" = None
|
|
@@ -22,7 +22,7 @@ class UnpivotClauseMixin:
|
|
|
22
22
|
name_column_name: str,
|
|
23
23
|
columns_to_unpivot: list[Union[str, exp.Expression]],
|
|
24
24
|
alias: Optional[str] = None,
|
|
25
|
-
) -> "
|
|
25
|
+
) -> "Select":
|
|
26
26
|
"""Adds an UNPIVOT clause to the SELECT statement.
|
|
27
27
|
|
|
28
28
|
Example:
|
|
@@ -38,12 +38,12 @@ class UnpivotClauseMixin:
|
|
|
38
38
|
TypeError: If the current expression is not a Select expression.
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
|
-
The
|
|
41
|
+
The Select instance for chaining.
|
|
42
42
|
"""
|
|
43
43
|
current_expr = self._expression
|
|
44
44
|
if not isinstance(current_expr, exp.Select):
|
|
45
45
|
# SelectBuilder's __init__ ensures _expression is exp.Select.
|
|
46
|
-
msg = "Unpivot can only be applied to a Select expression managed by
|
|
46
|
+
msg = "Unpivot can only be applied to a Select expression managed by Select."
|
|
47
47
|
raise TypeError(msg)
|
|
48
48
|
|
|
49
49
|
value_col_ident = exp.to_identifier(value_column_name)
|
|
@@ -59,7 +59,6 @@ class UnpivotClauseMixin:
|
|
|
59
59
|
# Fallback for other types, should ideally be an error or more specific handling
|
|
60
60
|
unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
|
|
61
61
|
|
|
62
|
-
# Create the unpivot expression (stored as Pivot with unpivot=True)
|
|
63
62
|
in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
|
|
64
63
|
|
|
65
64
|
unpivot_node = exp.Pivot(expressions=[value_col_ident], fields=[in_expr], unpivot=True)
|
|
@@ -67,14 +66,12 @@ class UnpivotClauseMixin:
|
|
|
67
66
|
if alias:
|
|
68
67
|
unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
69
68
|
|
|
70
|
-
# Add unpivot to the table in the FROM clause
|
|
71
69
|
from_clause = current_expr.args.get("from")
|
|
72
70
|
if from_clause and isinstance(from_clause, exp.From):
|
|
73
71
|
table = from_clause.this
|
|
74
72
|
if isinstance(table, exp.Table):
|
|
75
|
-
# Add to pivots array
|
|
76
73
|
existing_pivots = table.args.get("pivots", [])
|
|
77
74
|
existing_pivots.append(unpivot_node)
|
|
78
75
|
table.set("pivots", existing_pivots)
|
|
79
76
|
|
|
80
|
-
return cast("
|
|
77
|
+
return cast("Select", self)
|
|
@@ -4,6 +4,7 @@ from sqlglot import exp
|
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
6
|
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
7
8
|
|
|
8
9
|
__all__ = ("UpdateFromClauseMixin",)
|
|
9
10
|
|
|
@@ -30,7 +31,7 @@ class UpdateFromClauseMixin:
|
|
|
30
31
|
table_expr: exp.Expression
|
|
31
32
|
if isinstance(table, str):
|
|
32
33
|
table_expr = exp.to_table(table, alias=alias)
|
|
33
|
-
elif
|
|
34
|
+
elif has_query_builder_parameters(table):
|
|
34
35
|
subquery_builder_params = getattr(table, "_parameters", None)
|
|
35
36
|
if subquery_builder_params:
|
|
36
37
|
for p_name, p_value in subquery_builder_params.items():
|
|
@@ -5,6 +5,7 @@ from sqlglot import exp
|
|
|
5
5
|
from typing_extensions import Self
|
|
6
6
|
|
|
7
7
|
from sqlspec.exceptions import SQLBuilderError
|
|
8
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
8
9
|
|
|
9
10
|
__all__ = ("UpdateSetClauseMixin",)
|
|
10
11
|
|
|
@@ -49,14 +50,15 @@ class UpdateSetClauseMixin:
|
|
|
49
50
|
# If value is an expression, use it directly
|
|
50
51
|
if isinstance(val, exp.Expression):
|
|
51
52
|
value_expr = val
|
|
52
|
-
elif
|
|
53
|
+
elif has_query_builder_parameters(val):
|
|
53
54
|
# It's a builder (like SelectBuilder), convert to subquery
|
|
54
55
|
subquery = val.build()
|
|
55
56
|
# Parse the SQL and use as expression
|
|
56
|
-
|
|
57
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
58
|
+
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
57
59
|
# Merge parameters from subquery
|
|
58
|
-
if
|
|
59
|
-
for p_name, p_value in val.
|
|
60
|
+
if has_query_builder_parameters(val):
|
|
61
|
+
for p_name, p_value in val.parameters.items():
|
|
60
62
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
61
63
|
else:
|
|
62
64
|
param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
|
|
@@ -69,14 +71,15 @@ class UpdateSetClauseMixin:
|
|
|
69
71
|
# If value is an expression, use it directly
|
|
70
72
|
if isinstance(val, exp.Expression):
|
|
71
73
|
value_expr = val
|
|
72
|
-
elif
|
|
74
|
+
elif has_query_builder_parameters(val):
|
|
73
75
|
# It's a builder (like SelectBuilder), convert to subquery
|
|
74
76
|
subquery = val.build()
|
|
75
77
|
# Parse the SQL and use as expression
|
|
76
|
-
|
|
78
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
79
|
+
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
77
80
|
# Merge parameters from subquery
|
|
78
|
-
if
|
|
79
|
-
for p_name, p_value in val.
|
|
81
|
+
if has_query_builder_parameters(val):
|
|
82
|
+
for p_name, p_value in val.parameters.items():
|
|
80
83
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
81
84
|
else:
|
|
82
85
|
param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
|
|
@@ -6,9 +6,11 @@ from typing_extensions import Self
|
|
|
6
6
|
|
|
7
7
|
from sqlspec.exceptions import SQLBuilderError
|
|
8
8
|
from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
|
|
9
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters, is_iterable_parameters
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
|
-
from sqlspec.
|
|
12
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
13
|
+
from sqlspec.statement.builder.column import ColumnExpression
|
|
12
14
|
|
|
13
15
|
__all__ = ("WhereClauseMixin",)
|
|
14
16
|
|
|
@@ -17,7 +19,8 @@ class WhereClauseMixin:
|
|
|
17
19
|
"""Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
|
|
18
20
|
|
|
19
21
|
def where(
|
|
20
|
-
self,
|
|
22
|
+
self,
|
|
23
|
+
condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
|
|
21
24
|
) -> Self:
|
|
22
25
|
"""Add a WHERE clause to the statement.
|
|
23
26
|
|
|
@@ -34,14 +37,14 @@ class WhereClauseMixin:
|
|
|
34
37
|
Returns:
|
|
35
38
|
The current builder instance for method chaining.
|
|
36
39
|
"""
|
|
37
|
-
# Special case: if this is an
|
|
40
|
+
# Special case: if this is an Update and _expression is not exp.Update, raise the expected error for test coverage
|
|
38
41
|
|
|
39
|
-
if self.__class__.__name__ == "
|
|
42
|
+
if self.__class__.__name__ == "Update" and not (
|
|
40
43
|
hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
|
|
41
44
|
):
|
|
42
45
|
msg = "Cannot add WHERE clause to non-UPDATE expression"
|
|
43
46
|
raise SQLBuilderError(msg)
|
|
44
|
-
builder = cast("
|
|
47
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
45
48
|
if builder._expression is None:
|
|
46
49
|
msg = "Cannot add WHERE clause: expression is not initialized."
|
|
47
50
|
raise SQLBuilderError(msg)
|
|
@@ -50,7 +53,6 @@ class WhereClauseMixin:
|
|
|
50
53
|
msg = f"Cannot add WHERE clause to unsupported expression type: {type(builder._expression).__name__}."
|
|
51
54
|
raise SQLBuilderError(msg)
|
|
52
55
|
|
|
53
|
-
# Check if table is set for DELETE queries
|
|
54
56
|
if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
|
|
55
57
|
msg = "WHERE clause requires a table to be set. Use from() to set the table first."
|
|
56
58
|
raise SQLBuilderError(msg)
|
|
@@ -58,7 +60,6 @@ class WhereClauseMixin:
|
|
|
58
60
|
# Normalize the condition using enhanced parsing
|
|
59
61
|
condition_expr: exp.Expression
|
|
60
62
|
if isinstance(condition, tuple):
|
|
61
|
-
# Handle tuple format with proper parameter binding
|
|
62
63
|
if len(condition) == 2:
|
|
63
64
|
# 2-tuple: (column, value) -> column = value
|
|
64
65
|
param_name = builder.add_parameter(condition[1])[1]
|
|
@@ -87,7 +88,6 @@ class WhereClauseMixin:
|
|
|
87
88
|
"any": exp.Any,
|
|
88
89
|
}
|
|
89
90
|
operator = operator.lower()
|
|
90
|
-
# Handle special cases for NOT operators
|
|
91
91
|
if operator == "not like":
|
|
92
92
|
condition_expr = exp.Not(this=exp.Like(this=col_expr, expression=placeholder_expr))
|
|
93
93
|
elif operator == "not in":
|
|
@@ -104,7 +104,20 @@ class WhereClauseMixin:
|
|
|
104
104
|
else:
|
|
105
105
|
msg = f"WHERE tuple must have 2 or 3 elements, got {len(condition)}"
|
|
106
106
|
raise SQLBuilderError(msg)
|
|
107
|
+
# Handle ColumnExpression objects
|
|
108
|
+
elif hasattr(condition, "sqlglot_expression"):
|
|
109
|
+
# This is a ColumnExpression from our new Column syntax
|
|
110
|
+
raw_expr = getattr(condition, "sqlglot_expression", None)
|
|
111
|
+
if raw_expr is not None:
|
|
112
|
+
condition_expr = builder._parameterize_expression(raw_expr)
|
|
113
|
+
else:
|
|
114
|
+
# Fallback if attribute exists but is None
|
|
115
|
+
condition_expr = parse_condition_expression(str(condition))
|
|
107
116
|
else:
|
|
117
|
+
# Existing logic for strings and raw SQLGlot expressions
|
|
118
|
+
# Convert to string if it's not a recognized type
|
|
119
|
+
if not isinstance(condition, (str, exp.Expression, tuple)):
|
|
120
|
+
condition = str(condition)
|
|
108
121
|
condition_expr = parse_condition_expression(condition)
|
|
109
122
|
|
|
110
123
|
# Use dialect if available for Delete
|
|
@@ -194,13 +207,16 @@ class WhereClauseMixin:
|
|
|
194
207
|
|
|
195
208
|
def where_exists(self, subquery: "Union[str, Any]") -> "Self":
|
|
196
209
|
sub_expr: exp.Expression
|
|
197
|
-
if
|
|
198
|
-
subquery_builder_params: dict[str, Any] = subquery.
|
|
210
|
+
if has_query_builder_parameters(subquery):
|
|
211
|
+
subquery_builder_params: dict[str, Any] = subquery.parameters
|
|
199
212
|
if subquery_builder_params:
|
|
200
213
|
for p_name, p_value in subquery_builder_params.items():
|
|
201
214
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
202
215
|
sub_sql_obj = subquery.build() # pyright: ignore
|
|
203
|
-
|
|
216
|
+
sql_str = (
|
|
217
|
+
sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
|
|
218
|
+
)
|
|
219
|
+
sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
|
|
204
220
|
else:
|
|
205
221
|
sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
|
|
206
222
|
|
|
@@ -213,13 +229,16 @@ class WhereClauseMixin:
|
|
|
213
229
|
|
|
214
230
|
def where_not_exists(self, subquery: "Union[str, Any]") -> "Self":
|
|
215
231
|
sub_expr: exp.Expression
|
|
216
|
-
if
|
|
217
|
-
subquery_builder_params: dict[str, Any] = subquery.
|
|
232
|
+
if has_query_builder_parameters(subquery):
|
|
233
|
+
subquery_builder_params: dict[str, Any] = subquery.parameters
|
|
218
234
|
if subquery_builder_params:
|
|
219
235
|
for p_name, p_value in subquery_builder_params.items():
|
|
220
236
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
221
237
|
sub_sql_obj = subquery.build() # pyright: ignore
|
|
222
|
-
|
|
238
|
+
sql_str = (
|
|
239
|
+
sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
|
|
240
|
+
)
|
|
241
|
+
sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
|
|
223
242
|
else:
|
|
224
243
|
sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
|
|
225
244
|
|
|
@@ -238,16 +257,18 @@ class WhereClauseMixin:
|
|
|
238
257
|
"""Add a WHERE ... IN (...) clause. Supports subqueries and iterables."""
|
|
239
258
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
240
259
|
# Subquery support
|
|
241
|
-
if
|
|
242
|
-
|
|
260
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
261
|
+
subquery_exp: exp.Expression
|
|
262
|
+
if has_query_builder_parameters(values):
|
|
243
263
|
subquery = values.build() # pyright: ignore
|
|
244
|
-
|
|
264
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
265
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
245
266
|
else:
|
|
246
|
-
subquery_exp = values
|
|
267
|
+
subquery_exp = values # type: ignore[assignment]
|
|
247
268
|
condition = col_expr.isin(subquery_exp)
|
|
248
269
|
return self.where(condition)
|
|
249
270
|
# Iterable of values
|
|
250
|
-
if not
|
|
271
|
+
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
251
272
|
msg = "Unsupported type for 'values' in WHERE IN"
|
|
252
273
|
raise SQLBuilderError(msg)
|
|
253
274
|
params = []
|
|
@@ -260,15 +281,17 @@ class WhereClauseMixin:
|
|
|
260
281
|
def where_not_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
|
|
261
282
|
"""Add a WHERE ... NOT IN (...) clause. Supports subqueries and iterables."""
|
|
262
283
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
263
|
-
if
|
|
264
|
-
|
|
284
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
285
|
+
subquery_exp: exp.Expression
|
|
286
|
+
if has_query_builder_parameters(values):
|
|
265
287
|
subquery = values.build() # pyright: ignore
|
|
266
|
-
|
|
288
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
289
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
267
290
|
else:
|
|
268
|
-
subquery_exp = values
|
|
291
|
+
subquery_exp = values # type: ignore[assignment]
|
|
269
292
|
condition = exp.Not(this=col_expr.isin(subquery_exp))
|
|
270
293
|
return self.where(condition)
|
|
271
|
-
if not
|
|
294
|
+
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
272
295
|
msg = "Values for where_not_in must be a non-string iterable or subquery."
|
|
273
296
|
raise SQLBuilderError(msg)
|
|
274
297
|
params = []
|
|
@@ -294,12 +317,14 @@ class WhereClauseMixin:
|
|
|
294
317
|
The current builder instance for method chaining.
|
|
295
318
|
"""
|
|
296
319
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
297
|
-
if
|
|
298
|
-
|
|
320
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
321
|
+
subquery_exp: exp.Expression
|
|
322
|
+
if has_query_builder_parameters(values):
|
|
299
323
|
subquery = values.build() # pyright: ignore
|
|
300
|
-
|
|
324
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
325
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
301
326
|
else:
|
|
302
|
-
subquery_exp = values
|
|
327
|
+
subquery_exp = values # type: ignore[assignment]
|
|
303
328
|
condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
304
329
|
return self.where(condition)
|
|
305
330
|
if isinstance(values, str):
|
|
@@ -317,7 +342,7 @@ class WhereClauseMixin:
|
|
|
317
342
|
# If parsing fails, fall through to error
|
|
318
343
|
msg = "Unsupported type for 'values' in WHERE ANY"
|
|
319
344
|
raise SQLBuilderError(msg)
|
|
320
|
-
if not
|
|
345
|
+
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
321
346
|
msg = "Unsupported type for 'values' in WHERE ANY"
|
|
322
347
|
raise SQLBuilderError(msg)
|
|
323
348
|
params = []
|
|
@@ -339,12 +364,14 @@ class WhereClauseMixin:
|
|
|
339
364
|
The current builder instance for method chaining.
|
|
340
365
|
"""
|
|
341
366
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
342
|
-
if
|
|
343
|
-
|
|
367
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
368
|
+
subquery_exp: exp.Expression
|
|
369
|
+
if has_query_builder_parameters(values):
|
|
344
370
|
subquery = values.build() # pyright: ignore
|
|
345
|
-
|
|
371
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
372
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
346
373
|
else:
|
|
347
|
-
subquery_exp = values
|
|
374
|
+
subquery_exp = values # type: ignore[assignment]
|
|
348
375
|
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
349
376
|
return self.where(condition)
|
|
350
377
|
if isinstance(values, str):
|
|
@@ -362,7 +389,7 @@ class WhereClauseMixin:
|
|
|
362
389
|
# If parsing fails, fall through to error
|
|
363
390
|
msg = "Unsupported type for 'values' in WHERE NOT ANY"
|
|
364
391
|
raise SQLBuilderError(msg)
|
|
365
|
-
if not
|
|
392
|
+
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
366
393
|
msg = "Unsupported type for 'values' in WHERE NOT ANY"
|
|
367
394
|
raise SQLBuilderError(msg)
|
|
368
395
|
params = []
|
|
@@ -6,7 +6,7 @@ with automatic parameter binding and validation.
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Optional, Union, cast
|
|
9
|
+
from typing import Any, Optional, Union, cast
|
|
10
10
|
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from typing_extensions import Self
|
|
@@ -32,11 +32,11 @@ from sqlspec.statement.builder.mixins import (
|
|
|
32
32
|
from sqlspec.statement.result import SQLResult
|
|
33
33
|
from sqlspec.typing import RowT
|
|
34
34
|
|
|
35
|
-
__all__ = ("
|
|
35
|
+
__all__ = ("Select",)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@dataclass
|
|
39
|
-
class
|
|
39
|
+
class Select(
|
|
40
40
|
QueryBuilder[RowT],
|
|
41
41
|
WhereClauseMixin,
|
|
42
42
|
OrderByClauseMixin,
|
|
@@ -77,16 +77,37 @@ class SelectBuilder(
|
|
|
77
77
|
_schema: The schema/model class for row typing, if set via as_schema().
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
-
_with_parts: "dict[str, Union[exp.CTE,
|
|
80
|
+
_with_parts: "dict[str, Union[exp.CTE, Select]]" = field(default_factory=dict, init=False)
|
|
81
81
|
_expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
|
|
82
82
|
_schema: Optional[type[RowT]] = None
|
|
83
83
|
_hints: "list[dict[str, object]]" = field(default_factory=list, init=False, repr=False)
|
|
84
84
|
|
|
85
|
-
def
|
|
86
|
-
|
|
85
|
+
def __init__(self, *columns: str, **kwargs: Any) -> None:
|
|
86
|
+
"""Initialize SELECT with optional columns.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
*columns: Column names to select (e.g., "id", "name", "u.email")
|
|
90
|
+
**kwargs: Additional QueryBuilder arguments (dialect, schema, etc.)
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
Select("id", "name") # Shorthand for Select().select("id", "name")
|
|
94
|
+
Select() # Same as SelectBuilder() - start empty
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(**kwargs)
|
|
97
|
+
|
|
98
|
+
# Initialize fields from dataclass
|
|
99
|
+
self._with_parts = {}
|
|
100
|
+
self._expression = None
|
|
101
|
+
self._schema = None
|
|
102
|
+
self._hints = []
|
|
103
|
+
|
|
87
104
|
if self._expression is None:
|
|
88
105
|
self._create_base_expression()
|
|
89
106
|
|
|
107
|
+
# Add columns if provided - just a shorthand for .select()
|
|
108
|
+
if columns:
|
|
109
|
+
self.select(*columns)
|
|
110
|
+
|
|
90
111
|
@property
|
|
91
112
|
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
92
113
|
"""Get the expected result type for SELECT operations.
|
|
@@ -102,8 +123,8 @@ class SelectBuilder(
|
|
|
102
123
|
# At this point, self._expression is exp.Select
|
|
103
124
|
return self._expression
|
|
104
125
|
|
|
105
|
-
def as_schema(self, schema: "type[RowT]") -> "
|
|
106
|
-
"""Return a new
|
|
126
|
+
def as_schema(self, schema: "type[RowT]") -> "Select[RowT]":
|
|
127
|
+
"""Return a new Select instance parameterized with the given schema/model type.
|
|
107
128
|
|
|
108
129
|
This enables type-safe result mapping: the returned builder will carry the schema type
|
|
109
130
|
for static analysis and IDE autocompletion. The schema should be a class such as a Pydantic
|
|
@@ -113,15 +134,15 @@ class SelectBuilder(
|
|
|
113
134
|
schema: The schema/model class to use for row typing (e.g., a Pydantic model, dataclass, or msgspec.Struct).
|
|
114
135
|
|
|
115
136
|
Returns:
|
|
116
|
-
|
|
137
|
+
Select[RowT]: A new Select instance with RowT set to the provided schema/model type.
|
|
117
138
|
"""
|
|
118
|
-
new_builder =
|
|
139
|
+
new_builder = Select()
|
|
119
140
|
new_builder._expression = self._expression.copy() if self._expression is not None else None
|
|
120
141
|
new_builder._parameters = self._parameters.copy()
|
|
121
142
|
new_builder._parameter_counter = self._parameter_counter
|
|
122
143
|
new_builder.dialect = self.dialect
|
|
123
144
|
new_builder._schema = schema # type: ignore[assignment]
|
|
124
|
-
return cast("
|
|
145
|
+
return cast("Select[RowT]", new_builder)
|
|
125
146
|
|
|
126
147
|
def with_hint(
|
|
127
148
|
self,
|
|
@@ -151,15 +172,12 @@ class SelectBuilder(
|
|
|
151
172
|
Returns:
|
|
152
173
|
SafeQuery: A dataclass containing the SQL string and parameters.
|
|
153
174
|
"""
|
|
154
|
-
# Call parent build method which handles CTEs and optimization
|
|
155
175
|
safe_query = super().build()
|
|
156
176
|
|
|
157
|
-
# Apply hints using SQLGlot's proper hint support (more robust than regex)
|
|
158
177
|
if hasattr(self, "_hints") and self._hints:
|
|
159
178
|
modified_expr = self._expression.copy() if self._expression else None
|
|
160
179
|
|
|
161
180
|
if modified_expr and isinstance(modified_expr, exp.Select):
|
|
162
|
-
# Apply statement-level hints using SQLGlot's Hint expression
|
|
163
181
|
statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
|
|
164
182
|
if statement_hints:
|
|
165
183
|
# Parse each hint and create proper hint expressions
|
|
@@ -172,12 +190,10 @@ class SelectBuilder(
|
|
|
172
190
|
if hint_expr:
|
|
173
191
|
hint_expressions.append(hint_expr)
|
|
174
192
|
else:
|
|
175
|
-
# Create a raw identifier for unparsable hints
|
|
176
193
|
hint_expressions.append(exp.Anonymous(this=hint_str))
|
|
177
194
|
except Exception: # noqa: PERF203
|
|
178
195
|
hint_expressions.append(exp.Anonymous(this=str(hint)))
|
|
179
196
|
|
|
180
|
-
# Create a Hint node and attach to SELECT
|
|
181
197
|
if hint_expressions:
|
|
182
198
|
hint_node = exp.Hint(expressions=hint_expressions)
|
|
183
199
|
modified_expr.set("hint", hint_node)
|
|
@@ -186,7 +202,6 @@ class SelectBuilder(
|
|
|
186
202
|
# since SQLGlot doesn't have a standard way to attach hints to individual tables
|
|
187
203
|
modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
|
|
188
204
|
|
|
189
|
-
# Apply table-level hints via string manipulation (as fallback)
|
|
190
205
|
table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
|
|
191
206
|
if table_hints:
|
|
192
207
|
for th in table_hints:
|
|
@@ -5,7 +5,7 @@ with automatic parameter binding and validation.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
9
9
|
|
|
10
10
|
from sqlglot import exp
|
|
11
11
|
from typing_extensions import Self
|
|
@@ -23,13 +23,13 @@ from sqlspec.statement.result import SQLResult
|
|
|
23
23
|
from sqlspec.typing import RowT
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from sqlspec.statement.builder.select import
|
|
26
|
+
from sqlspec.statement.builder.select import Select
|
|
27
27
|
|
|
28
|
-
__all__ = ("
|
|
28
|
+
__all__ = ("Update",)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass(unsafe_hash=True)
|
|
32
|
-
class
|
|
32
|
+
class Update(
|
|
33
33
|
QueryBuilder[RowT],
|
|
34
34
|
WhereClauseMixin,
|
|
35
35
|
ReturningClauseMixin,
|
|
@@ -46,16 +46,21 @@ class UpdateBuilder(
|
|
|
46
46
|
```python
|
|
47
47
|
# Basic UPDATE
|
|
48
48
|
update_query = (
|
|
49
|
-
|
|
49
|
+
Update()
|
|
50
50
|
.table("users")
|
|
51
51
|
.set(name="John Doe")
|
|
52
52
|
.set(email="john@example.com")
|
|
53
53
|
.where("id = 1")
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
+
# Even more concise with constructor
|
|
57
|
+
update_query = (
|
|
58
|
+
Update("users").set(name="John Doe").where("id = 1")
|
|
59
|
+
)
|
|
60
|
+
|
|
56
61
|
# UPDATE with parameterized conditions
|
|
57
62
|
update_query = (
|
|
58
|
-
|
|
63
|
+
Update()
|
|
59
64
|
.table("users")
|
|
60
65
|
.set(status="active")
|
|
61
66
|
.where_eq("id", 123)
|
|
@@ -63,7 +68,7 @@ class UpdateBuilder(
|
|
|
63
68
|
|
|
64
69
|
# UPDATE with FROM clause (PostgreSQL style)
|
|
65
70
|
update_query = (
|
|
66
|
-
|
|
71
|
+
Update()
|
|
67
72
|
.table("users", "u")
|
|
68
73
|
.set(name="Updated Name")
|
|
69
74
|
.from_("profiles", "p")
|
|
@@ -72,6 +77,18 @@ class UpdateBuilder(
|
|
|
72
77
|
```
|
|
73
78
|
"""
|
|
74
79
|
|
|
80
|
+
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
81
|
+
"""Initialize UPDATE with optional table.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
table: Target table name
|
|
85
|
+
**kwargs: Additional QueryBuilder arguments
|
|
86
|
+
"""
|
|
87
|
+
super().__init__(**kwargs)
|
|
88
|
+
|
|
89
|
+
if table:
|
|
90
|
+
self.table(table)
|
|
91
|
+
|
|
75
92
|
@property
|
|
76
93
|
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
77
94
|
"""Return the expected result type for this builder."""
|
|
@@ -87,7 +104,7 @@ class UpdateBuilder(
|
|
|
87
104
|
|
|
88
105
|
def join(
|
|
89
106
|
self,
|
|
90
|
-
table: "Union[str, exp.Expression,
|
|
107
|
+
table: "Union[str, exp.Expression, Select[RowT]]",
|
|
91
108
|
on: "Union[str, exp.Expression]",
|
|
92
109
|
alias: "Optional[str]" = None,
|
|
93
110
|
join_type: str = "INNER",
|
|
@@ -141,7 +158,6 @@ class UpdateBuilder(
|
|
|
141
158
|
msg = f"Unsupported join type: {join_type}"
|
|
142
159
|
raise SQLBuilderError(msg)
|
|
143
160
|
|
|
144
|
-
# Add join to the UPDATE expression
|
|
145
161
|
if not self._expression.args.get("joins"):
|
|
146
162
|
self._expression.set("joins", [])
|
|
147
163
|
self._expression.args["joins"].append(join_expr)
|
|
@@ -165,12 +181,10 @@ class UpdateBuilder(
|
|
|
165
181
|
msg = "No UPDATE expression to build or expression is of the wrong type."
|
|
166
182
|
raise SQLBuilderError(msg)
|
|
167
183
|
|
|
168
|
-
# Check that the table is set
|
|
169
184
|
if getattr(self._expression, "this", None) is None:
|
|
170
185
|
msg = "No table specified for UPDATE statement."
|
|
171
186
|
raise SQLBuilderError(msg)
|
|
172
187
|
|
|
173
|
-
# Check that at least one SET expression exists
|
|
174
188
|
if not self._expression.args.get("expressions"):
|
|
175
189
|
msg = "At least one SET clause must be specified for UPDATE statement."
|
|
176
190
|
raise SQLBuilderError(msg)
|