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
|
@@ -8,47 +8,57 @@ parameter binding and validation.
|
|
|
8
8
|
|
|
9
9
|
from sqlspec.exceptions import SQLBuilderError
|
|
10
10
|
from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
|
|
11
|
+
from sqlspec.statement.builder.column import Column, ColumnExpression, FunctionColumn
|
|
11
12
|
from sqlspec.statement.builder.ddl import (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
AlterTable,
|
|
14
|
+
CommentOn,
|
|
15
|
+
CreateIndex,
|
|
16
|
+
CreateMaterializedView,
|
|
17
|
+
CreateSchema,
|
|
18
|
+
CreateTable,
|
|
19
|
+
CreateTableAsSelect,
|
|
20
|
+
CreateView,
|
|
18
21
|
DDLBuilder,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
DropIndex,
|
|
23
|
+
DropSchema,
|
|
24
|
+
DropTable,
|
|
25
|
+
DropView,
|
|
26
|
+
RenameTable,
|
|
27
|
+
TruncateTable,
|
|
24
28
|
)
|
|
25
|
-
from sqlspec.statement.builder.delete import
|
|
26
|
-
from sqlspec.statement.builder.insert import
|
|
27
|
-
from sqlspec.statement.builder.merge import
|
|
29
|
+
from sqlspec.statement.builder.delete import Delete
|
|
30
|
+
from sqlspec.statement.builder.insert import Insert
|
|
31
|
+
from sqlspec.statement.builder.merge import Merge
|
|
28
32
|
from sqlspec.statement.builder.mixins import WhereClauseMixin
|
|
29
|
-
from sqlspec.statement.builder.select import
|
|
30
|
-
from sqlspec.statement.builder.update import
|
|
33
|
+
from sqlspec.statement.builder.select import Select
|
|
34
|
+
from sqlspec.statement.builder.update import Update
|
|
31
35
|
|
|
32
36
|
__all__ = (
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"AlterTable",
|
|
38
|
+
"Column",
|
|
39
|
+
"ColumnExpression",
|
|
40
|
+
"CommentOn",
|
|
41
|
+
"CreateIndex",
|
|
42
|
+
"CreateMaterializedView",
|
|
43
|
+
"CreateSchema",
|
|
44
|
+
"CreateTable",
|
|
45
|
+
"CreateTableAsSelect",
|
|
46
|
+
"CreateView",
|
|
39
47
|
"DDLBuilder",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
48
|
+
"Delete",
|
|
49
|
+
"DropIndex",
|
|
50
|
+
"DropSchema",
|
|
51
|
+
"DropTable",
|
|
52
|
+
"DropView",
|
|
53
|
+
"FunctionColumn",
|
|
54
|
+
"Insert",
|
|
55
|
+
"Merge",
|
|
47
56
|
"QueryBuilder",
|
|
57
|
+
"RenameTable",
|
|
48
58
|
"SQLBuilderError",
|
|
49
59
|
"SafeQuery",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
60
|
+
"Select",
|
|
61
|
+
"TruncateTable",
|
|
62
|
+
"Update",
|
|
53
63
|
"WhereClauseMixin",
|
|
54
64
|
)
|
|
@@ -12,10 +12,8 @@ __all__ = ("build_column_expression", "build_constraint_expression")
|
|
|
12
12
|
|
|
13
13
|
def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
14
14
|
"""Build SQLGlot expression for a column definition."""
|
|
15
|
-
# Start with column name and type
|
|
16
15
|
col_def = exp.ColumnDef(this=exp.to_identifier(col.name), kind=exp.DataType.build(col.dtype))
|
|
17
16
|
|
|
18
|
-
# Add constraints
|
|
19
17
|
constraints: list[exp.ColumnConstraint] = []
|
|
20
18
|
|
|
21
19
|
if col.not_null:
|
|
@@ -28,10 +26,8 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
28
26
|
constraints.append(exp.ColumnConstraint(kind=exp.UniqueColumnConstraint()))
|
|
29
27
|
|
|
30
28
|
if col.default is not None:
|
|
31
|
-
# Handle different default value types
|
|
32
29
|
default_expr: Optional[exp.Expression] = None
|
|
33
30
|
if isinstance(col.default, str):
|
|
34
|
-
# Check if it's a function/expression or a literal string
|
|
35
31
|
if col.default.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in col.default:
|
|
36
32
|
default_expr = exp.maybe_parse(col.default)
|
|
37
33
|
else:
|
|
@@ -55,14 +51,12 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
55
51
|
constraints.append(exp.ColumnConstraint(kind=exp.CommentColumnConstraint(this=exp.Literal.string(col.comment))))
|
|
56
52
|
|
|
57
53
|
if col.generated:
|
|
58
|
-
# Handle generated columns (computed columns)
|
|
59
54
|
generated_expr = exp.GeneratedAsIdentityColumnConstraint(this=exp.maybe_parse(col.generated))
|
|
60
55
|
constraints.append(exp.ColumnConstraint(kind=generated_expr))
|
|
61
56
|
|
|
62
57
|
if col.collate:
|
|
63
58
|
constraints.append(exp.ColumnConstraint(kind=exp.CollateColumnConstraint(this=exp.to_identifier(col.collate))))
|
|
64
59
|
|
|
65
|
-
# Set constraints on column definition
|
|
66
60
|
if constraints:
|
|
67
61
|
col_def.set("constraints", constraints)
|
|
68
62
|
|
|
@@ -72,7 +66,6 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
72
66
|
def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
|
|
73
67
|
"""Build SQLGlot expression for a table constraint."""
|
|
74
68
|
if constraint.constraint_type == "PRIMARY KEY":
|
|
75
|
-
# Build primary key constraint
|
|
76
69
|
pk_cols = [exp.to_identifier(col) for col in constraint.columns]
|
|
77
70
|
pk_constraint = exp.PrimaryKey(expressions=pk_cols)
|
|
78
71
|
|
|
@@ -81,7 +74,6 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
|
|
|
81
74
|
return pk_constraint
|
|
82
75
|
|
|
83
76
|
if constraint.constraint_type == "FOREIGN KEY":
|
|
84
|
-
# Build foreign key constraint
|
|
85
77
|
fk_cols = [exp.to_identifier(col) for col in constraint.columns]
|
|
86
78
|
ref_cols = [exp.to_identifier(col) for col in constraint.references_columns]
|
|
87
79
|
|
|
@@ -100,7 +92,6 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
|
|
|
100
92
|
return fk_constraint
|
|
101
93
|
|
|
102
94
|
if constraint.constraint_type == "UNIQUE":
|
|
103
|
-
# Build unique constraint
|
|
104
95
|
unique_cols = [exp.to_identifier(col) for col in constraint.columns]
|
|
105
96
|
unique_constraint = exp.UniqueKeyProperty(expressions=unique_cols)
|
|
106
97
|
|
|
@@ -109,7 +100,6 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
|
|
|
109
100
|
return unique_constraint
|
|
110
101
|
|
|
111
102
|
if constraint.constraint_type == "CHECK":
|
|
112
|
-
# Build check constraint
|
|
113
103
|
check_expr = exp.Check(this=exp.maybe_parse(constraint.condition) if constraint.condition else None)
|
|
114
104
|
|
|
115
105
|
if constraint.name:
|
|
@@ -10,7 +10,7 @@ from typing import Any, Optional, Union, cast
|
|
|
10
10
|
from sqlglot import exp, maybe_parse, parse_one
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def parse_column_expression(column_input: Union[str, exp.Expression]) -> exp.Expression:
|
|
13
|
+
def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
14
14
|
"""Parse a column input that might be a complex expression.
|
|
15
15
|
|
|
16
16
|
Handles cases like:
|
|
@@ -19,15 +19,23 @@ def parse_column_expression(column_input: Union[str, exp.Expression]) -> exp.Exp
|
|
|
19
19
|
- Aliased columns: "name AS user_name" -> Alias(this=Column(name), alias=user_name)
|
|
20
20
|
- Function calls: "MAX(price)" -> Max(this=Column(price))
|
|
21
21
|
- Complex expressions: "CASE WHEN ... END" -> Case(...)
|
|
22
|
+
- Custom Column objects from our builder
|
|
22
23
|
|
|
23
24
|
Args:
|
|
24
|
-
column_input: String
|
|
25
|
+
column_input: String, SQLGlot expression, or Column object
|
|
25
26
|
|
|
26
27
|
Returns:
|
|
27
28
|
exp.Expression: Parsed SQLGlot expression
|
|
28
29
|
"""
|
|
29
30
|
if isinstance(column_input, exp.Expression):
|
|
30
31
|
return column_input
|
|
32
|
+
|
|
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
|
|
38
|
+
|
|
31
39
|
return exp.maybe_parse(column_input) or exp.column(str(column_input))
|
|
32
40
|
|
|
33
41
|
|
|
@@ -96,7 +104,6 @@ def parse_condition_expression(
|
|
|
96
104
|
|
|
97
105
|
tuple_condition_parts = 2
|
|
98
106
|
if isinstance(condition_input, tuple) and len(condition_input) == tuple_condition_parts:
|
|
99
|
-
# Handle (column, value) tuple format with proper parameter binding
|
|
100
107
|
column, value = condition_input
|
|
101
108
|
column_expr = parse_column_expression(column)
|
|
102
109
|
if value is None:
|
|
@@ -105,7 +112,6 @@ def parse_condition_expression(
|
|
|
105
112
|
if builder and hasattr(builder, "add_parameter"):
|
|
106
113
|
_, param_name = builder.add_parameter(value)
|
|
107
114
|
return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
|
|
108
|
-
# Fallback to literal value
|
|
109
115
|
if isinstance(value, str):
|
|
110
116
|
return exp.EQ(this=column_expr, expression=exp.Literal.string(value))
|
|
111
117
|
if isinstance(value, (int, float)):
|
|
@@ -7,7 +7,7 @@ advanced builder patterns and optimization capabilities.
|
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union, cast
|
|
11
11
|
|
|
12
12
|
import sqlglot
|
|
13
13
|
from sqlglot import Dialect, exp
|
|
@@ -20,6 +20,7 @@ from sqlspec.exceptions import SQLBuilderError
|
|
|
20
20
|
from sqlspec.statement.sql import SQL, SQLConfig
|
|
21
21
|
from sqlspec.typing import RowT
|
|
22
22
|
from sqlspec.utils.logging import get_logger
|
|
23
|
+
from sqlspec.utils.type_guards import has_sql_method, has_with_method
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from sqlspec.statement.result import SQLResult
|
|
@@ -124,6 +125,32 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
124
125
|
self._parameters[param_name] = value
|
|
125
126
|
return param_name
|
|
126
127
|
|
|
128
|
+
def _parameterize_expression(self, expression: exp.Expression) -> exp.Expression:
|
|
129
|
+
"""Replace literal values in an expression with bound parameters.
|
|
130
|
+
|
|
131
|
+
This method traverses a SQLGlot expression tree and replaces literal
|
|
132
|
+
values with parameter placeholders, adding the values to the builder's
|
|
133
|
+
parameter collection.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
expression: The SQLGlot expression to parameterize
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A new expression with literals replaced by parameter placeholders
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def replacer(node: exp.Expression) -> exp.Expression:
|
|
143
|
+
if isinstance(node, exp.Literal):
|
|
144
|
+
# Skip boolean literals (TRUE/FALSE) and NULL
|
|
145
|
+
if node.this in (True, False, None):
|
|
146
|
+
return node
|
|
147
|
+
# Convert other literals to parameters
|
|
148
|
+
param_name = self._add_parameter(node.this, context="where")
|
|
149
|
+
return exp.Placeholder(this=param_name)
|
|
150
|
+
return node
|
|
151
|
+
|
|
152
|
+
return expression.transform(replacer, copy=True)
|
|
153
|
+
|
|
127
154
|
def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
|
|
128
155
|
"""Explicitly adds a parameter to the query.
|
|
129
156
|
|
|
@@ -191,8 +218,10 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
191
218
|
msg = f"CTE query builder expression must be a Select, got {type(query._expression).__name__}."
|
|
192
219
|
self._raise_sql_builder_error(msg)
|
|
193
220
|
cte_select_expression = query._expression.copy()
|
|
194
|
-
for p_name, p_value in query.
|
|
195
|
-
|
|
221
|
+
for p_name, p_value in query.parameters.items():
|
|
222
|
+
# Try to preserve original parameter name, only rename if collision
|
|
223
|
+
unique_name = self._generate_unique_parameter_name(p_name)
|
|
224
|
+
self.add_parameter(p_value, unique_name)
|
|
196
225
|
|
|
197
226
|
elif isinstance(query, str):
|
|
198
227
|
try:
|
|
@@ -229,23 +258,21 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
229
258
|
final_expression = self._expression.copy()
|
|
230
259
|
|
|
231
260
|
if self._with_ctes:
|
|
232
|
-
if
|
|
261
|
+
if has_with_method(final_expression):
|
|
262
|
+
# Type checker now knows final_expression has with_ method
|
|
233
263
|
for alias, cte_node in self._with_ctes.items():
|
|
234
|
-
final_expression = final_expression.with_(
|
|
235
|
-
|
|
236
|
-
)
|
|
237
|
-
elif (
|
|
238
|
-
isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union))
|
|
239
|
-
and self._with_ctes
|
|
240
|
-
):
|
|
264
|
+
final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
|
|
265
|
+
elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
|
|
241
266
|
final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
|
|
242
267
|
|
|
243
|
-
|
|
244
|
-
if self.enable_optimization:
|
|
268
|
+
if self.enable_optimization and isinstance(final_expression, exp.Expression):
|
|
245
269
|
final_expression = self._optimize_expression(final_expression)
|
|
246
270
|
|
|
247
271
|
try:
|
|
248
|
-
|
|
272
|
+
if has_sql_method(final_expression):
|
|
273
|
+
sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True) # pyright: ignore[reportAttributeAccessIssue]
|
|
274
|
+
else:
|
|
275
|
+
sql_string = str(final_expression)
|
|
249
276
|
except Exception as e:
|
|
250
277
|
err_msg = f"Error generating SQL from expression: {e!s}"
|
|
251
278
|
logger.exception("SQL generation failed")
|
|
@@ -292,13 +319,30 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
292
319
|
"""
|
|
293
320
|
safe_query = self.build()
|
|
294
321
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
parameters=
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
322
|
+
if isinstance(safe_query.parameters, dict):
|
|
323
|
+
kwargs = safe_query.parameters
|
|
324
|
+
parameters = None
|
|
325
|
+
else:
|
|
326
|
+
kwargs = None
|
|
327
|
+
parameters = (
|
|
328
|
+
safe_query.parameters
|
|
329
|
+
if isinstance(safe_query.parameters, tuple)
|
|
330
|
+
else tuple(safe_query.parameters)
|
|
331
|
+
if safe_query.parameters
|
|
332
|
+
else None
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if config is None:
|
|
336
|
+
from sqlspec.statement.sql import SQLConfig
|
|
337
|
+
|
|
338
|
+
config = SQLConfig(dialect=safe_query.dialect)
|
|
339
|
+
|
|
340
|
+
# SQL expects parameters as variadic args, not as a keyword
|
|
341
|
+
if kwargs:
|
|
342
|
+
return SQL(safe_query.sql, config=config, **kwargs)
|
|
343
|
+
if parameters:
|
|
344
|
+
return SQL(safe_query.sql, *parameters, config=config)
|
|
345
|
+
return SQL(safe_query.sql, config=config)
|
|
302
346
|
|
|
303
347
|
def __str__(self) -> str:
|
|
304
348
|
"""Return the SQL string representation of the query.
|
|
@@ -309,7 +353,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
309
353
|
try:
|
|
310
354
|
return self.build().sql
|
|
311
355
|
except Exception:
|
|
312
|
-
# Fallback to default representation if build fails
|
|
313
356
|
return super().__str__()
|
|
314
357
|
|
|
315
358
|
@property
|
|
@@ -322,7 +365,11 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
322
365
|
return self.dialect.__name__.lower()
|
|
323
366
|
if isinstance(self.dialect, Dialect):
|
|
324
367
|
return type(self.dialect).__name__.lower()
|
|
325
|
-
# Handle case where dialect might have a __name__ attribute
|
|
326
368
|
if hasattr(self.dialect, "__name__"):
|
|
327
369
|
return self.dialect.__name__.lower()
|
|
328
370
|
return None
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def parameters(self) -> dict[str, Any]:
|
|
374
|
+
"""Public access to query parameters."""
|
|
375
|
+
return self._parameters
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Pythonic column expressions for query building.
|
|
2
|
+
|
|
3
|
+
This module provides Column objects that support native Python operators
|
|
4
|
+
for building SQL conditions with type safety and parameter binding.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from sqlglot import exp
|
|
11
|
+
|
|
12
|
+
from sqlspec.utils.type_guards import has_sql_method
|
|
13
|
+
|
|
14
|
+
__all__ = ("Column", "ColumnExpression", "FunctionColumn")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ColumnExpression:
|
|
18
|
+
"""Base class for column expressions that can be combined with operators."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, expression: exp.Expression) -> None:
|
|
21
|
+
self._expr = expression
|
|
22
|
+
|
|
23
|
+
def __and__(self, other: "ColumnExpression") -> "ColumnExpression":
|
|
24
|
+
"""Combine with AND operator (&)."""
|
|
25
|
+
if not isinstance(other, ColumnExpression):
|
|
26
|
+
return NotImplemented
|
|
27
|
+
return ColumnExpression(exp.And(this=self._expr, expression=other._expr))
|
|
28
|
+
|
|
29
|
+
def __or__(self, other: "ColumnExpression") -> "ColumnExpression":
|
|
30
|
+
"""Combine with OR operator (|)."""
|
|
31
|
+
if not isinstance(other, ColumnExpression):
|
|
32
|
+
return NotImplemented
|
|
33
|
+
return ColumnExpression(exp.Or(this=self._expr, expression=other._expr))
|
|
34
|
+
|
|
35
|
+
def __invert__(self) -> "ColumnExpression":
|
|
36
|
+
"""Apply NOT operator (~)."""
|
|
37
|
+
return ColumnExpression(exp.Not(this=self._expr))
|
|
38
|
+
|
|
39
|
+
def __bool__(self) -> bool:
|
|
40
|
+
"""Prevent accidental use of 'and'/'or' keywords."""
|
|
41
|
+
msg = (
|
|
42
|
+
"Cannot use 'and'/'or' operators on ColumnExpression. "
|
|
43
|
+
"Use '&'/'|' operators instead. "
|
|
44
|
+
f"Expression: {self._expr.sql()}"
|
|
45
|
+
)
|
|
46
|
+
raise TypeError(msg)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def sqlglot_expression(self) -> exp.Expression:
|
|
50
|
+
"""Get the underlying SQLGlot expression."""
|
|
51
|
+
return self._expr
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Column:
|
|
55
|
+
"""Represents a database column with Python operator support."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, name: str, table: Optional[str] = None) -> None:
|
|
58
|
+
self.name = name
|
|
59
|
+
self.table = table
|
|
60
|
+
|
|
61
|
+
# Create SQLGlot column expression
|
|
62
|
+
if table:
|
|
63
|
+
self._expr = exp.Column(this=exp.Identifier(this=name), table=exp.Identifier(this=table))
|
|
64
|
+
else:
|
|
65
|
+
self._expr = exp.Column(this=exp.Identifier(this=name))
|
|
66
|
+
|
|
67
|
+
# Comparison operators
|
|
68
|
+
def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
69
|
+
"""Equal to (==)."""
|
|
70
|
+
if other is None:
|
|
71
|
+
return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
|
|
72
|
+
return ColumnExpression(exp.EQ(this=self._expr, expression=exp.convert(other)))
|
|
73
|
+
|
|
74
|
+
def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
75
|
+
"""Not equal to (!=)."""
|
|
76
|
+
if other is None:
|
|
77
|
+
return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
|
|
78
|
+
return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.convert(other)))
|
|
79
|
+
|
|
80
|
+
def __gt__(self, other: Any) -> ColumnExpression:
|
|
81
|
+
"""Greater than (>)."""
|
|
82
|
+
return ColumnExpression(exp.GT(this=self._expr, expression=exp.convert(other)))
|
|
83
|
+
|
|
84
|
+
def __ge__(self, other: Any) -> ColumnExpression:
|
|
85
|
+
"""Greater than or equal (>=)."""
|
|
86
|
+
return ColumnExpression(exp.GTE(this=self._expr, expression=exp.convert(other)))
|
|
87
|
+
|
|
88
|
+
def __lt__(self, other: Any) -> ColumnExpression:
|
|
89
|
+
"""Less than (<)."""
|
|
90
|
+
return ColumnExpression(exp.LT(this=self._expr, expression=exp.convert(other)))
|
|
91
|
+
|
|
92
|
+
def __le__(self, other: Any) -> ColumnExpression:
|
|
93
|
+
"""Less than or equal (<=)."""
|
|
94
|
+
return ColumnExpression(exp.LTE(this=self._expr, expression=exp.convert(other)))
|
|
95
|
+
|
|
96
|
+
def __invert__(self) -> ColumnExpression:
|
|
97
|
+
"""Apply NOT operator (~)."""
|
|
98
|
+
return ColumnExpression(exp.Not(this=self._expr))
|
|
99
|
+
|
|
100
|
+
# SQL-specific methods
|
|
101
|
+
def like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
|
|
102
|
+
"""SQL LIKE pattern matching."""
|
|
103
|
+
if escape:
|
|
104
|
+
like_expr = exp.Like(this=self._expr, expression=exp.convert(pattern), escape=exp.convert(escape))
|
|
105
|
+
else:
|
|
106
|
+
like_expr = exp.Like(this=self._expr, expression=exp.convert(pattern))
|
|
107
|
+
return ColumnExpression(like_expr)
|
|
108
|
+
|
|
109
|
+
def ilike(self, pattern: str) -> ColumnExpression:
|
|
110
|
+
"""Case-insensitive LIKE."""
|
|
111
|
+
return ColumnExpression(exp.ILike(this=self._expr, expression=exp.convert(pattern)))
|
|
112
|
+
|
|
113
|
+
def in_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
114
|
+
"""SQL IN clause."""
|
|
115
|
+
converted_values = [exp.convert(v) for v in values]
|
|
116
|
+
return ColumnExpression(exp.In(this=self._expr, expressions=converted_values))
|
|
117
|
+
|
|
118
|
+
def not_in(self, values: Iterable[Any]) -> ColumnExpression:
|
|
119
|
+
"""SQL NOT IN clause."""
|
|
120
|
+
return ~self.in_(values)
|
|
121
|
+
|
|
122
|
+
def between(self, start: Any, end: Any) -> ColumnExpression:
|
|
123
|
+
"""SQL BETWEEN clause."""
|
|
124
|
+
return ColumnExpression(exp.Between(this=self._expr, low=exp.convert(start), high=exp.convert(end)))
|
|
125
|
+
|
|
126
|
+
def is_null(self) -> ColumnExpression:
|
|
127
|
+
"""SQL IS NULL."""
|
|
128
|
+
return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
|
|
129
|
+
|
|
130
|
+
def is_not_null(self) -> ColumnExpression:
|
|
131
|
+
"""SQL IS NOT NULL."""
|
|
132
|
+
return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
|
|
133
|
+
|
|
134
|
+
def not_like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
|
|
135
|
+
"""SQL NOT LIKE pattern matching."""
|
|
136
|
+
return ~self.like(pattern, escape)
|
|
137
|
+
|
|
138
|
+
def not_ilike(self, pattern: str) -> ColumnExpression:
|
|
139
|
+
"""Case-insensitive NOT LIKE."""
|
|
140
|
+
return ~self.ilike(pattern)
|
|
141
|
+
|
|
142
|
+
def any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
143
|
+
"""SQL = ANY(...) clause."""
|
|
144
|
+
converted_values = [exp.convert(v) for v in values]
|
|
145
|
+
return ColumnExpression(exp.EQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
|
|
146
|
+
|
|
147
|
+
def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
148
|
+
"""SQL <> ANY(...) clause."""
|
|
149
|
+
converted_values = [exp.convert(v) for v in values]
|
|
150
|
+
return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
|
|
151
|
+
|
|
152
|
+
# SQL Functions
|
|
153
|
+
def lower(self) -> "FunctionColumn":
|
|
154
|
+
"""SQL LOWER() function."""
|
|
155
|
+
return FunctionColumn(exp.Lower(this=self._expr))
|
|
156
|
+
|
|
157
|
+
def upper(self) -> "FunctionColumn":
|
|
158
|
+
"""SQL UPPER() function."""
|
|
159
|
+
return FunctionColumn(exp.Upper(this=self._expr))
|
|
160
|
+
|
|
161
|
+
def length(self) -> "FunctionColumn":
|
|
162
|
+
"""SQL LENGTH() function."""
|
|
163
|
+
return FunctionColumn(exp.Length(this=self._expr))
|
|
164
|
+
|
|
165
|
+
def trim(self) -> "FunctionColumn":
|
|
166
|
+
"""SQL TRIM() function."""
|
|
167
|
+
return FunctionColumn(exp.Trim(this=self._expr))
|
|
168
|
+
|
|
169
|
+
def abs(self) -> "FunctionColumn":
|
|
170
|
+
"""SQL ABS() function."""
|
|
171
|
+
return FunctionColumn(exp.Abs(this=self._expr))
|
|
172
|
+
|
|
173
|
+
def round(self, decimals: int = 0) -> "FunctionColumn":
|
|
174
|
+
"""SQL ROUND() function."""
|
|
175
|
+
if decimals == 0:
|
|
176
|
+
return FunctionColumn(exp.Round(this=self._expr))
|
|
177
|
+
return FunctionColumn(exp.Round(this=self._expr, expression=exp.Literal.number(decimals)))
|
|
178
|
+
|
|
179
|
+
def floor(self) -> "FunctionColumn":
|
|
180
|
+
"""SQL FLOOR() function."""
|
|
181
|
+
return FunctionColumn(exp.Floor(this=self._expr))
|
|
182
|
+
|
|
183
|
+
def ceil(self) -> "FunctionColumn":
|
|
184
|
+
"""SQL CEIL() function."""
|
|
185
|
+
return FunctionColumn(exp.Ceil(this=self._expr))
|
|
186
|
+
|
|
187
|
+
def substring(self, start: int, length: Optional[int] = None) -> "FunctionColumn":
|
|
188
|
+
"""SQL SUBSTRING() function."""
|
|
189
|
+
args = [exp.Literal.number(start)]
|
|
190
|
+
if length is not None:
|
|
191
|
+
args.append(exp.Literal.number(length))
|
|
192
|
+
return FunctionColumn(exp.Substring(this=self._expr, expressions=args))
|
|
193
|
+
|
|
194
|
+
def coalesce(self, *values: Any) -> "FunctionColumn":
|
|
195
|
+
"""SQL COALESCE() function."""
|
|
196
|
+
expressions = [self._expr] + [exp.convert(v) for v in values]
|
|
197
|
+
return FunctionColumn(exp.Coalesce(expressions=expressions))
|
|
198
|
+
|
|
199
|
+
def cast(self, data_type: str) -> "FunctionColumn":
|
|
200
|
+
"""SQL CAST() function."""
|
|
201
|
+
return FunctionColumn(exp.Cast(this=self._expr, to=exp.DataType.build(data_type)))
|
|
202
|
+
|
|
203
|
+
def alias(self, alias_name: str) -> exp.Expression:
|
|
204
|
+
"""Create an aliased column expression."""
|
|
205
|
+
return exp.Alias(this=self._expr, alias=alias_name)
|
|
206
|
+
|
|
207
|
+
def __repr__(self) -> str:
|
|
208
|
+
if self.table:
|
|
209
|
+
return f"Column<{self.table}.{self.name}>"
|
|
210
|
+
return f"Column<{self.name}>"
|
|
211
|
+
|
|
212
|
+
def __hash__(self) -> int:
|
|
213
|
+
"""Hash based on table and column name."""
|
|
214
|
+
return hash((self.table, self.name))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class FunctionColumn:
|
|
218
|
+
"""Represents the result of a SQL function call on a column."""
|
|
219
|
+
|
|
220
|
+
def __init__(self, expression: exp.Expression) -> None:
|
|
221
|
+
self._expr = expression
|
|
222
|
+
|
|
223
|
+
def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
224
|
+
return ColumnExpression(exp.EQ(this=self._expr, expression=exp.convert(other)))
|
|
225
|
+
|
|
226
|
+
def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
227
|
+
return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.convert(other)))
|
|
228
|
+
|
|
229
|
+
def like(self, pattern: str) -> ColumnExpression:
|
|
230
|
+
return ColumnExpression(exp.Like(this=self._expr, expression=exp.convert(pattern)))
|
|
231
|
+
|
|
232
|
+
def ilike(self, pattern: str) -> ColumnExpression:
|
|
233
|
+
"""Case-insensitive LIKE."""
|
|
234
|
+
return ColumnExpression(exp.ILike(this=self._expr, expression=exp.convert(pattern)))
|
|
235
|
+
|
|
236
|
+
def in_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
237
|
+
"""SQL IN clause."""
|
|
238
|
+
converted_values = [exp.convert(v) for v in values]
|
|
239
|
+
return ColumnExpression(exp.In(this=self._expr, expressions=converted_values))
|
|
240
|
+
|
|
241
|
+
def not_in_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
242
|
+
"""SQL NOT IN clause."""
|
|
243
|
+
return ~self.in_(values)
|
|
244
|
+
|
|
245
|
+
def not_like(self, pattern: str) -> ColumnExpression:
|
|
246
|
+
"""SQL NOT LIKE."""
|
|
247
|
+
return ~self.like(pattern)
|
|
248
|
+
|
|
249
|
+
def not_ilike(self, pattern: str) -> ColumnExpression:
|
|
250
|
+
"""Case-insensitive NOT LIKE."""
|
|
251
|
+
return ~self.ilike(pattern)
|
|
252
|
+
|
|
253
|
+
def between(self, start: Any, end: Any) -> ColumnExpression:
|
|
254
|
+
"""SQL BETWEEN clause."""
|
|
255
|
+
return ColumnExpression(exp.Between(this=self._expr, low=exp.convert(start), high=exp.convert(end)))
|
|
256
|
+
|
|
257
|
+
def is_null(self) -> ColumnExpression:
|
|
258
|
+
"""SQL IS NULL."""
|
|
259
|
+
return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
|
|
260
|
+
|
|
261
|
+
def is_not_null(self) -> ColumnExpression:
|
|
262
|
+
"""SQL IS NOT NULL."""
|
|
263
|
+
return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
|
|
264
|
+
|
|
265
|
+
def any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
266
|
+
"""SQL = ANY(...) clause."""
|
|
267
|
+
converted_values = [exp.convert(v) for v in values]
|
|
268
|
+
return ColumnExpression(exp.EQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
|
|
269
|
+
|
|
270
|
+
def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
271
|
+
"""SQL <> ANY(...) clause."""
|
|
272
|
+
converted_values = [exp.convert(v) for v in values]
|
|
273
|
+
return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
|
|
274
|
+
|
|
275
|
+
def alias(self, alias_name: str) -> exp.Expression:
|
|
276
|
+
"""Create an aliased function expression."""
|
|
277
|
+
return exp.Alias(this=self._expr, alias=alias_name)
|
|
278
|
+
|
|
279
|
+
# Add other operators as needed...
|
|
280
|
+
|
|
281
|
+
def __hash__(self) -> int:
|
|
282
|
+
"""Hash based on the SQL expression."""
|
|
283
|
+
return hash(self._expr.sql() if has_sql_method(self._expr) else str(self._expr))
|