sqlspec 0.11.0__py3-none-any.whl → 0.12.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/__init__.py +16 -3
- sqlspec/_serialization.py +3 -10
- sqlspec/_sql.py +1147 -0
- sqlspec/_typing.py +343 -41
- sqlspec/adapters/adbc/__init__.py +2 -6
- sqlspec/adapters/adbc/config.py +474 -149
- sqlspec/adapters/adbc/driver.py +330 -644
- sqlspec/adapters/aiosqlite/__init__.py +2 -6
- sqlspec/adapters/aiosqlite/config.py +143 -57
- sqlspec/adapters/aiosqlite/driver.py +269 -462
- sqlspec/adapters/asyncmy/__init__.py +3 -8
- sqlspec/adapters/asyncmy/config.py +247 -202
- sqlspec/adapters/asyncmy/driver.py +217 -451
- sqlspec/adapters/asyncpg/__init__.py +4 -7
- sqlspec/adapters/asyncpg/config.py +329 -176
- sqlspec/adapters/asyncpg/driver.py +418 -498
- sqlspec/adapters/bigquery/__init__.py +2 -2
- sqlspec/adapters/bigquery/config.py +407 -0
- sqlspec/adapters/bigquery/driver.py +592 -634
- sqlspec/adapters/duckdb/__init__.py +4 -1
- sqlspec/adapters/duckdb/config.py +432 -321
- sqlspec/adapters/duckdb/driver.py +393 -436
- sqlspec/adapters/oracledb/__init__.py +3 -8
- sqlspec/adapters/oracledb/config.py +625 -0
- sqlspec/adapters/oracledb/driver.py +549 -942
- sqlspec/adapters/psqlpy/__init__.py +4 -7
- sqlspec/adapters/psqlpy/config.py +372 -203
- sqlspec/adapters/psqlpy/driver.py +197 -550
- sqlspec/adapters/psycopg/__init__.py +3 -8
- sqlspec/adapters/psycopg/config.py +741 -0
- sqlspec/adapters/psycopg/driver.py +732 -733
- sqlspec/adapters/sqlite/__init__.py +2 -6
- sqlspec/adapters/sqlite/config.py +146 -81
- sqlspec/adapters/sqlite/driver.py +243 -426
- sqlspec/base.py +220 -825
- sqlspec/config.py +354 -0
- sqlspec/driver/__init__.py +22 -0
- sqlspec/driver/_async.py +252 -0
- sqlspec/driver/_common.py +338 -0
- sqlspec/driver/_sync.py +261 -0
- sqlspec/driver/mixins/__init__.py +17 -0
- sqlspec/driver/mixins/_pipeline.py +523 -0
- sqlspec/driver/mixins/_result_utils.py +122 -0
- sqlspec/driver/mixins/_sql_translator.py +35 -0
- sqlspec/driver/mixins/_storage.py +993 -0
- sqlspec/driver/mixins/_type_coercion.py +131 -0
- sqlspec/exceptions.py +299 -7
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +474 -0
- sqlspec/extensions/litestar/__init__.py +1 -6
- sqlspec/extensions/litestar/_utils.py +1 -5
- sqlspec/extensions/litestar/config.py +5 -6
- sqlspec/extensions/litestar/handlers.py +13 -12
- sqlspec/extensions/litestar/plugin.py +22 -24
- sqlspec/extensions/litestar/providers.py +37 -55
- sqlspec/loader.py +528 -0
- sqlspec/service/__init__.py +3 -0
- sqlspec/service/base.py +24 -0
- sqlspec/service/pagination.py +26 -0
- sqlspec/statement/__init__.py +21 -0
- sqlspec/statement/builder/__init__.py +54 -0
- sqlspec/statement/builder/_ddl_utils.py +119 -0
- sqlspec/statement/builder/_parsing_utils.py +135 -0
- sqlspec/statement/builder/base.py +328 -0
- sqlspec/statement/builder/ddl.py +1379 -0
- sqlspec/statement/builder/delete.py +80 -0
- sqlspec/statement/builder/insert.py +274 -0
- sqlspec/statement/builder/merge.py +95 -0
- sqlspec/statement/builder/mixins/__init__.py +65 -0
- sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
- sqlspec/statement/builder/mixins/_case_builder.py +91 -0
- sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
- sqlspec/statement/builder/mixins/_delete_from.py +34 -0
- sqlspec/statement/builder/mixins/_from.py +61 -0
- sqlspec/statement/builder/mixins/_group_by.py +119 -0
- sqlspec/statement/builder/mixins/_having.py +35 -0
- sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
- sqlspec/statement/builder/mixins/_insert_into.py +36 -0
- sqlspec/statement/builder/mixins/_insert_values.py +69 -0
- sqlspec/statement/builder/mixins/_join.py +110 -0
- sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
- sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
- sqlspec/statement/builder/mixins/_order_by.py +46 -0
- sqlspec/statement/builder/mixins/_pivot.py +82 -0
- sqlspec/statement/builder/mixins/_returning.py +37 -0
- sqlspec/statement/builder/mixins/_select_columns.py +60 -0
- sqlspec/statement/builder/mixins/_set_ops.py +122 -0
- sqlspec/statement/builder/mixins/_unpivot.py +80 -0
- sqlspec/statement/builder/mixins/_update_from.py +54 -0
- sqlspec/statement/builder/mixins/_update_set.py +91 -0
- sqlspec/statement/builder/mixins/_update_table.py +29 -0
- sqlspec/statement/builder/mixins/_where.py +374 -0
- sqlspec/statement/builder/mixins/_window_functions.py +86 -0
- sqlspec/statement/builder/protocols.py +20 -0
- sqlspec/statement/builder/select.py +206 -0
- sqlspec/statement/builder/update.py +178 -0
- sqlspec/statement/filters.py +571 -0
- sqlspec/statement/parameters.py +736 -0
- sqlspec/statement/pipelines/__init__.py +67 -0
- sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
- sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
- sqlspec/statement/pipelines/base.py +315 -0
- sqlspec/statement/pipelines/context.py +119 -0
- sqlspec/statement/pipelines/result_types.py +41 -0
- sqlspec/statement/pipelines/transformers/__init__.py +8 -0
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
- sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
- sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
- sqlspec/statement/pipelines/validators/__init__.py +23 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
- sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
- sqlspec/statement/pipelines/validators/_performance.py +703 -0
- sqlspec/statement/pipelines/validators/_security.py +990 -0
- sqlspec/statement/pipelines/validators/base.py +67 -0
- sqlspec/statement/result.py +527 -0
- sqlspec/statement/splitter.py +701 -0
- sqlspec/statement/sql.py +1198 -0
- sqlspec/storage/__init__.py +15 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +166 -0
- sqlspec/storage/backends/fsspec.py +315 -0
- sqlspec/storage/backends/obstore.py +464 -0
- sqlspec/storage/protocol.py +170 -0
- sqlspec/storage/registry.py +315 -0
- sqlspec/typing.py +157 -36
- sqlspec/utils/correlation.py +155 -0
- sqlspec/utils/deprecation.py +3 -6
- sqlspec/utils/fixtures.py +6 -11
- sqlspec/utils/logging.py +135 -0
- sqlspec/utils/module_loader.py +45 -43
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +6 -8
- sqlspec/utils/sync_tools.py +15 -27
- sqlspec/utils/text.py +58 -26
- {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/METADATA +100 -26
- sqlspec-0.12.0.dist-info/RECORD +145 -0
- sqlspec/adapters/bigquery/config/__init__.py +0 -3
- sqlspec/adapters/bigquery/config/_common.py +0 -40
- sqlspec/adapters/bigquery/config/_sync.py +0 -87
- sqlspec/adapters/oracledb/config/__init__.py +0 -9
- sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
- sqlspec/adapters/oracledb/config/_common.py +0 -131
- sqlspec/adapters/oracledb/config/_sync.py +0 -186
- sqlspec/adapters/psycopg/config/__init__.py +0 -19
- sqlspec/adapters/psycopg/config/_async.py +0 -169
- sqlspec/adapters/psycopg/config/_common.py +0 -56
- sqlspec/adapters/psycopg/config/_sync.py +0 -168
- sqlspec/filters.py +0 -330
- sqlspec/mixins.py +0 -306
- sqlspec/statement.py +0 -378
- sqlspec-0.11.0.dist-info/RECORD +0 -69
- {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
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 dataclasses import dataclass, field
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from sqlglot import exp
|
|
11
|
+
|
|
12
|
+
from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
|
|
13
|
+
from sqlspec.statement.builder.mixins import DeleteFromClauseMixin, ReturningClauseMixin, WhereClauseMixin
|
|
14
|
+
from sqlspec.statement.result import SQLResult
|
|
15
|
+
from sqlspec.typing import RowT
|
|
16
|
+
|
|
17
|
+
__all__ = ("DeleteBuilder",)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(unsafe_hash=True)
|
|
21
|
+
class DeleteBuilder(QueryBuilder[RowT], WhereClauseMixin, ReturningClauseMixin, DeleteFromClauseMixin):
|
|
22
|
+
"""Builder for DELETE statements.
|
|
23
|
+
|
|
24
|
+
This builder provides a fluent interface for constructing SQL DELETE statements
|
|
25
|
+
with automatic parameter binding and validation. It does not support JOIN
|
|
26
|
+
operations to maintain cross-dialect compatibility and safety.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
```python
|
|
30
|
+
# Basic DELETE
|
|
31
|
+
delete_query = (
|
|
32
|
+
DeleteBuilder().from_("users").where("age < 18")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# DELETE with parameterized conditions
|
|
36
|
+
delete_query = (
|
|
37
|
+
DeleteBuilder()
|
|
38
|
+
.from_("users")
|
|
39
|
+
.where_eq("status", "inactive")
|
|
40
|
+
.where_in("category", ["test", "demo"])
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_table: "Optional[str]" = field(default=None, init=False)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
49
|
+
"""Get the expected result type for DELETE operations.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The ExecuteResult type for DELETE statements.
|
|
53
|
+
"""
|
|
54
|
+
return SQLResult[RowT]
|
|
55
|
+
|
|
56
|
+
def _create_base_expression(self) -> "exp.Delete":
|
|
57
|
+
"""Create a new sqlglot Delete expression.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A new sqlglot Delete expression.
|
|
61
|
+
"""
|
|
62
|
+
return exp.Delete()
|
|
63
|
+
|
|
64
|
+
def build(self) -> "SafeQuery":
|
|
65
|
+
"""Build the DELETE query with validation.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
SafeQuery: The built query with SQL and parameters.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
SQLBuilderError: If the table is not specified.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if not self._table:
|
|
75
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
76
|
+
|
|
77
|
+
msg = "DELETE requires a table to be specified. Use from() to set the table."
|
|
78
|
+
raise SQLBuilderError(msg)
|
|
79
|
+
|
|
80
|
+
return super().build()
|
|
@@ -0,0 +1,274 @@
|
|
|
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 dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
9
|
+
|
|
10
|
+
from sqlglot import exp
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
14
|
+
from sqlspec.statement.builder.base import QueryBuilder
|
|
15
|
+
from sqlspec.statement.builder.mixins import (
|
|
16
|
+
InsertFromSelectMixin,
|
|
17
|
+
InsertIntoClauseMixin,
|
|
18
|
+
InsertValuesMixin,
|
|
19
|
+
ReturningClauseMixin,
|
|
20
|
+
)
|
|
21
|
+
from sqlspec.statement.result import SQLResult
|
|
22
|
+
from sqlspec.typing import RowT
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Mapping, Sequence
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ("InsertBuilder",)
|
|
29
|
+
|
|
30
|
+
ERR_MSG_TABLE_NOT_SET = "The target table must be set using .into() before adding values."
|
|
31
|
+
ERR_MSG_VALUES_COLUMNS_MISMATCH = (
|
|
32
|
+
"Number of values ({values_len}) does not match the number of specified columns ({columns_len})."
|
|
33
|
+
)
|
|
34
|
+
ERR_MSG_INTERNAL_EXPRESSION_TYPE = "Internal error: expression is not an Insert instance as expected."
|
|
35
|
+
ERR_MSG_EXPRESSION_NOT_INITIALIZED = "Internal error: base expression not initialized."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(unsafe_hash=True)
|
|
39
|
+
class InsertBuilder(
|
|
40
|
+
QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, InsertFromSelectMixin, InsertIntoClauseMixin
|
|
41
|
+
):
|
|
42
|
+
"""Builder for INSERT statements.
|
|
43
|
+
|
|
44
|
+
This builder facilitates the construction of SQL INSERT queries
|
|
45
|
+
in a safe and dialect-agnostic manner with automatic parameter binding.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
```python
|
|
49
|
+
# Basic INSERT with values
|
|
50
|
+
insert_query = (
|
|
51
|
+
InsertBuilder()
|
|
52
|
+
.into("users")
|
|
53
|
+
.columns("name", "email", "age")
|
|
54
|
+
.values("John Doe", "john@example.com", 30)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Multi-row INSERT
|
|
58
|
+
insert_query = (
|
|
59
|
+
InsertBuilder()
|
|
60
|
+
.into("users")
|
|
61
|
+
.columns("name", "email")
|
|
62
|
+
.values("John", "john@example.com")
|
|
63
|
+
.values("Jane", "jane@example.com")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# INSERT from dictionary
|
|
67
|
+
insert_query = (
|
|
68
|
+
InsertBuilder()
|
|
69
|
+
.into("users")
|
|
70
|
+
.values_from_dict(
|
|
71
|
+
{"name": "John", "email": "john@example.com"}
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# INSERT from SELECT
|
|
76
|
+
insert_query = (
|
|
77
|
+
InsertBuilder()
|
|
78
|
+
.into("users_backup")
|
|
79
|
+
.from_select(
|
|
80
|
+
SelectBuilder()
|
|
81
|
+
.select("name", "email")
|
|
82
|
+
.from_("users")
|
|
83
|
+
.where("active = true")
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
_table: "Optional[str]" = field(default=None, init=False)
|
|
90
|
+
_columns: list[str] = field(default_factory=list, init=False)
|
|
91
|
+
_values_added_count: int = field(default=0, init=False)
|
|
92
|
+
|
|
93
|
+
def _create_base_expression(self) -> exp.Insert:
|
|
94
|
+
"""Create a base INSERT expression.
|
|
95
|
+
|
|
96
|
+
This method is called by the base QueryBuilder during initialization.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A new sqlglot Insert expression.
|
|
100
|
+
"""
|
|
101
|
+
return exp.Insert()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
105
|
+
"""Specifies the expected result type for an INSERT query.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The type of result expected for INSERT operations.
|
|
109
|
+
"""
|
|
110
|
+
return SQLResult[RowT]
|
|
111
|
+
|
|
112
|
+
def _get_insert_expression(self) -> exp.Insert:
|
|
113
|
+
"""Safely gets and casts the internal expression to exp.Insert.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The internal expression as exp.Insert.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
SQLBuilderError: If the expression is not initialized or is not an Insert.
|
|
120
|
+
"""
|
|
121
|
+
if self._expression is None:
|
|
122
|
+
raise SQLBuilderError(ERR_MSG_EXPRESSION_NOT_INITIALIZED)
|
|
123
|
+
if not isinstance(self._expression, exp.Insert):
|
|
124
|
+
raise SQLBuilderError(ERR_MSG_INTERNAL_EXPRESSION_TYPE)
|
|
125
|
+
return self._expression
|
|
126
|
+
|
|
127
|
+
def values(self, *values: Any) -> "Self":
|
|
128
|
+
"""Adds a row of values to the INSERT statement.
|
|
129
|
+
|
|
130
|
+
This method can be called multiple times to insert multiple rows,
|
|
131
|
+
resulting in a multi-row INSERT statement like `VALUES (...), (...)`.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
*values: The values for the row to be inserted. The number of values
|
|
135
|
+
must match the number of columns set by `columns()`, if `columns()` was called
|
|
136
|
+
and specified any non-empty list of columns.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The current builder instance for method chaining.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
SQLBuilderError: If `into()` has not been called to set the table,
|
|
143
|
+
or if `columns()` was called with a non-empty list of columns
|
|
144
|
+
and the number of values does not match the number of specified columns.
|
|
145
|
+
"""
|
|
146
|
+
if not self._table:
|
|
147
|
+
raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
|
|
148
|
+
|
|
149
|
+
insert_expr = self._get_insert_expression()
|
|
150
|
+
|
|
151
|
+
if self._columns and len(values) != len(self._columns):
|
|
152
|
+
msg = ERR_MSG_VALUES_COLUMNS_MISMATCH.format(values_len=len(values), columns_len=len(self._columns))
|
|
153
|
+
raise SQLBuilderError(msg)
|
|
154
|
+
|
|
155
|
+
param_names = [self._add_parameter(value) for value in values]
|
|
156
|
+
value_placeholders = tuple(exp.var(name) for name in param_names)
|
|
157
|
+
|
|
158
|
+
current_values_expression = insert_expr.args.get("expression")
|
|
159
|
+
|
|
160
|
+
if self._values_added_count == 0:
|
|
161
|
+
new_values_node = exp.Values(expressions=[exp.Tuple(expressions=list(value_placeholders))])
|
|
162
|
+
insert_expr.set("expression", new_values_node)
|
|
163
|
+
elif isinstance(current_values_expression, exp.Values):
|
|
164
|
+
current_values_expression.expressions.append(exp.Tuple(expressions=list(value_placeholders)))
|
|
165
|
+
else:
|
|
166
|
+
# This case should ideally not be reached if logic is correct:
|
|
167
|
+
# means _values_added_count > 0 but expression is not exp.Values.
|
|
168
|
+
# Fallback to creating a new Values node, though this might indicate an issue.
|
|
169
|
+
new_values_node = exp.Values(expressions=[exp.Tuple(expressions=list(value_placeholders))])
|
|
170
|
+
insert_expr.set("expression", new_values_node)
|
|
171
|
+
|
|
172
|
+
self._values_added_count += 1
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def values_from_dict(self, data: "Mapping[str, Any]") -> "Self":
|
|
176
|
+
"""Adds a row of values from a dictionary.
|
|
177
|
+
|
|
178
|
+
This is a convenience method that automatically sets columns based on
|
|
179
|
+
the dictionary keys and values based on the dictionary values.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
data: A mapping of column names to values.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The current builder instance for method chaining.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
SQLBuilderError: If `into()` has not been called to set the table.
|
|
189
|
+
"""
|
|
190
|
+
if not self._table:
|
|
191
|
+
raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
|
|
192
|
+
|
|
193
|
+
if not self._columns:
|
|
194
|
+
# Set columns from dictionary keys if not already set
|
|
195
|
+
self.columns(*data.keys())
|
|
196
|
+
elif set(self._columns) != set(data.keys()):
|
|
197
|
+
# Verify that dictionary keys match existing columns
|
|
198
|
+
msg = f"Dictionary keys {set(data.keys())} do not match existing columns {set(self._columns)}."
|
|
199
|
+
raise SQLBuilderError(msg)
|
|
200
|
+
|
|
201
|
+
# Add values in the same order as columns
|
|
202
|
+
return self.values(*[data[col] for col in self._columns])
|
|
203
|
+
|
|
204
|
+
def values_from_dicts(self, data: "Sequence[Mapping[str, Any]]") -> "Self":
|
|
205
|
+
"""Adds multiple rows of values from a sequence of dictionaries.
|
|
206
|
+
|
|
207
|
+
This is a convenience method for bulk inserts from structured data.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
data: A sequence of mappings, each representing a row of data.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The current builder instance for method chaining.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
SQLBuilderError: If `into()` has not been called to set the table,
|
|
217
|
+
or if dictionaries have inconsistent keys.
|
|
218
|
+
"""
|
|
219
|
+
if not data:
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
# Use the first dictionary to establish columns
|
|
223
|
+
first_dict = data[0]
|
|
224
|
+
if not self._columns:
|
|
225
|
+
self.columns(*first_dict.keys())
|
|
226
|
+
|
|
227
|
+
# Validate that all dictionaries have the same keys
|
|
228
|
+
expected_keys = set(self._columns)
|
|
229
|
+
for i, row_dict in enumerate(data):
|
|
230
|
+
if set(row_dict.keys()) != expected_keys:
|
|
231
|
+
msg = (
|
|
232
|
+
f"Dictionary at index {i} has keys {set(row_dict.keys())} "
|
|
233
|
+
f"which do not match expected keys {expected_keys}."
|
|
234
|
+
)
|
|
235
|
+
raise SQLBuilderError(msg)
|
|
236
|
+
|
|
237
|
+
# Add each row
|
|
238
|
+
for row_dict in data:
|
|
239
|
+
self.values(*[row_dict[col] for col in self._columns])
|
|
240
|
+
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def on_conflict_do_nothing(self) -> "Self":
|
|
244
|
+
"""Adds an ON CONFLICT DO NOTHING clause (PostgreSQL syntax).
|
|
245
|
+
|
|
246
|
+
This is used to ignore rows that would cause a conflict.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
The current builder instance for method chaining.
|
|
250
|
+
|
|
251
|
+
Note:
|
|
252
|
+
This is PostgreSQL-specific syntax. Different databases have different syntax.
|
|
253
|
+
For a more general solution, you might need dialect-specific handling.
|
|
254
|
+
"""
|
|
255
|
+
insert_expr = self._get_insert_expression()
|
|
256
|
+
# Using sqlglot's OnConflict expression if available
|
|
257
|
+
try:
|
|
258
|
+
on_conflict = exp.OnConflict(this=None, expressions=[])
|
|
259
|
+
insert_expr.set("on", on_conflict)
|
|
260
|
+
except AttributeError:
|
|
261
|
+
# Fallback for older sqlglot versions
|
|
262
|
+
pass
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def on_duplicate_key_update(self, **set_values: Any) -> "Self":
|
|
266
|
+
"""Adds an ON DUPLICATE KEY UPDATE clause (MySQL syntax).
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
**set_values: Column-value pairs to update on duplicate key.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The current builder instance for method chaining.
|
|
273
|
+
"""
|
|
274
|
+
return self
|
|
@@ -0,0 +1,95 @@
|
|
|
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 dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
|
|
11
|
+
from sqlspec.statement.builder.base import QueryBuilder
|
|
12
|
+
from sqlspec.statement.builder.mixins import (
|
|
13
|
+
MergeIntoClauseMixin,
|
|
14
|
+
MergeMatchedClauseMixin,
|
|
15
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
16
|
+
MergeNotMatchedClauseMixin,
|
|
17
|
+
MergeOnClauseMixin,
|
|
18
|
+
MergeUsingClauseMixin,
|
|
19
|
+
)
|
|
20
|
+
from sqlspec.statement.result import SQLResult
|
|
21
|
+
from sqlspec.typing import RowT
|
|
22
|
+
|
|
23
|
+
__all__ = ("MergeBuilder",)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(unsafe_hash=True)
|
|
27
|
+
class MergeBuilder(
|
|
28
|
+
QueryBuilder[RowT],
|
|
29
|
+
MergeUsingClauseMixin,
|
|
30
|
+
MergeOnClauseMixin,
|
|
31
|
+
MergeMatchedClauseMixin,
|
|
32
|
+
MergeNotMatchedClauseMixin,
|
|
33
|
+
MergeIntoClauseMixin,
|
|
34
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
35
|
+
):
|
|
36
|
+
"""Builder for MERGE statements.
|
|
37
|
+
|
|
38
|
+
This builder provides a fluent interface for constructing SQL MERGE statements
|
|
39
|
+
(also known as UPSERT in some databases) with automatic parameter binding and validation.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
# Basic MERGE statement
|
|
44
|
+
merge_query = (
|
|
45
|
+
MergeBuilder()
|
|
46
|
+
.into("target_table")
|
|
47
|
+
.using("source_table", "src")
|
|
48
|
+
.on("target_table.id = src.id")
|
|
49
|
+
.when_matched_then_update(
|
|
50
|
+
{"name": "src.name", "updated_at": "NOW()"}
|
|
51
|
+
)
|
|
52
|
+
.when_not_matched_then_insert(
|
|
53
|
+
columns=["id", "name", "created_at"],
|
|
54
|
+
values=["src.id", "src.name", "NOW()"],
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# MERGE with subquery source
|
|
59
|
+
source_query = (
|
|
60
|
+
SelectBuilder()
|
|
61
|
+
.select("id", "name", "email")
|
|
62
|
+
.from_("temp_users")
|
|
63
|
+
.where("status = 'pending'")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
merge_query = (
|
|
67
|
+
MergeBuilder()
|
|
68
|
+
.into("users")
|
|
69
|
+
.using(source_query, "src")
|
|
70
|
+
.on("users.email = src.email")
|
|
71
|
+
.when_matched_then_update({"name": "src.name"})
|
|
72
|
+
.when_not_matched_then_insert(
|
|
73
|
+
columns=["id", "name", "email"],
|
|
74
|
+
values=["src.id", "src.name", "src.email"],
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
82
|
+
"""Return the expected result type for this builder.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The SQLResult type for MERGE statements.
|
|
86
|
+
"""
|
|
87
|
+
return SQLResult[RowT]
|
|
88
|
+
|
|
89
|
+
def _create_base_expression(self) -> "exp.Merge":
|
|
90
|
+
"""Create a base MERGE expression.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A new sqlglot Merge expression with empty clauses.
|
|
94
|
+
"""
|
|
95
|
+
return exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""SQL statement builder mixins."""
|
|
2
|
+
|
|
3
|
+
from sqlspec.statement.builder.mixins._aggregate_functions import AggregateFunctionsMixin
|
|
4
|
+
from sqlspec.statement.builder.mixins._case_builder import CaseBuilderMixin
|
|
5
|
+
from sqlspec.statement.builder.mixins._common_table_expr import CommonTableExpressionMixin
|
|
6
|
+
from sqlspec.statement.builder.mixins._delete_from import DeleteFromClauseMixin
|
|
7
|
+
from sqlspec.statement.builder.mixins._from import FromClauseMixin
|
|
8
|
+
from sqlspec.statement.builder.mixins._group_by import GroupByClauseMixin
|
|
9
|
+
from sqlspec.statement.builder.mixins._having import HavingClauseMixin
|
|
10
|
+
from sqlspec.statement.builder.mixins._insert_from_select import InsertFromSelectMixin
|
|
11
|
+
from sqlspec.statement.builder.mixins._insert_into import InsertIntoClauseMixin
|
|
12
|
+
from sqlspec.statement.builder.mixins._insert_values import InsertValuesMixin
|
|
13
|
+
from sqlspec.statement.builder.mixins._join import JoinClauseMixin
|
|
14
|
+
from sqlspec.statement.builder.mixins._limit_offset import LimitOffsetClauseMixin
|
|
15
|
+
from sqlspec.statement.builder.mixins._merge_clauses import (
|
|
16
|
+
MergeIntoClauseMixin,
|
|
17
|
+
MergeMatchedClauseMixin,
|
|
18
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
19
|
+
MergeNotMatchedClauseMixin,
|
|
20
|
+
MergeOnClauseMixin,
|
|
21
|
+
MergeUsingClauseMixin,
|
|
22
|
+
)
|
|
23
|
+
from sqlspec.statement.builder.mixins._order_by import OrderByClauseMixin
|
|
24
|
+
from sqlspec.statement.builder.mixins._pivot import PivotClauseMixin
|
|
25
|
+
from sqlspec.statement.builder.mixins._returning import ReturningClauseMixin
|
|
26
|
+
from sqlspec.statement.builder.mixins._select_columns import SelectColumnsMixin
|
|
27
|
+
from sqlspec.statement.builder.mixins._set_ops import SetOperationMixin
|
|
28
|
+
from sqlspec.statement.builder.mixins._unpivot import UnpivotClauseMixin
|
|
29
|
+
from sqlspec.statement.builder.mixins._update_from import UpdateFromClauseMixin
|
|
30
|
+
from sqlspec.statement.builder.mixins._update_set import UpdateSetClauseMixin
|
|
31
|
+
from sqlspec.statement.builder.mixins._update_table import UpdateTableClauseMixin
|
|
32
|
+
from sqlspec.statement.builder.mixins._where import WhereClauseMixin
|
|
33
|
+
from sqlspec.statement.builder.mixins._window_functions import WindowFunctionsMixin
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
"AggregateFunctionsMixin",
|
|
37
|
+
"CaseBuilderMixin",
|
|
38
|
+
"CommonTableExpressionMixin",
|
|
39
|
+
"DeleteFromClauseMixin",
|
|
40
|
+
"FromClauseMixin",
|
|
41
|
+
"GroupByClauseMixin",
|
|
42
|
+
"HavingClauseMixin",
|
|
43
|
+
"InsertFromSelectMixin",
|
|
44
|
+
"InsertIntoClauseMixin",
|
|
45
|
+
"InsertValuesMixin",
|
|
46
|
+
"JoinClauseMixin",
|
|
47
|
+
"LimitOffsetClauseMixin",
|
|
48
|
+
"MergeIntoClauseMixin",
|
|
49
|
+
"MergeMatchedClauseMixin",
|
|
50
|
+
"MergeNotMatchedBySourceClauseMixin",
|
|
51
|
+
"MergeNotMatchedClauseMixin",
|
|
52
|
+
"MergeOnClauseMixin",
|
|
53
|
+
"MergeUsingClauseMixin",
|
|
54
|
+
"OrderByClauseMixin",
|
|
55
|
+
"PivotClauseMixin",
|
|
56
|
+
"ReturningClauseMixin",
|
|
57
|
+
"SelectColumnsMixin",
|
|
58
|
+
"SetOperationMixin",
|
|
59
|
+
"UnpivotClauseMixin",
|
|
60
|
+
"UpdateFromClauseMixin",
|
|
61
|
+
"UpdateSetClauseMixin",
|
|
62
|
+
"UpdateTableClauseMixin",
|
|
63
|
+
"WhereClauseMixin",
|
|
64
|
+
"WindowFunctionsMixin",
|
|
65
|
+
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional, Union, cast
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from sqlspec.statement.builder.protocols import SelectBuilderProtocol
|
|
8
|
+
|
|
9
|
+
__all__ = ("AggregateFunctionsMixin",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AggregateFunctionsMixin:
|
|
13
|
+
"""Mixin providing aggregate function methods for SQL builders."""
|
|
14
|
+
|
|
15
|
+
def count_(self, column: "Union[str, exp.Expression]" = "*", alias: Optional[str] = None) -> Self:
|
|
16
|
+
"""Add COUNT function to SELECT clause.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
column: The column to count (default is "*").
|
|
20
|
+
alias: Optional alias for the count.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The current builder instance for method chaining.
|
|
24
|
+
"""
|
|
25
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
26
|
+
if column == "*":
|
|
27
|
+
count_expr = exp.Count(this=exp.Star())
|
|
28
|
+
else:
|
|
29
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
30
|
+
count_expr = exp.Count(this=col_expr)
|
|
31
|
+
|
|
32
|
+
select_expr = exp.alias_(count_expr, alias) if alias else count_expr
|
|
33
|
+
return cast("Self", builder.select(select_expr))
|
|
34
|
+
|
|
35
|
+
def sum_(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
36
|
+
"""Add SUM function to SELECT clause.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
column: The column to sum.
|
|
40
|
+
alias: Optional alias for the sum.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The current builder instance for method chaining.
|
|
44
|
+
"""
|
|
45
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
46
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
47
|
+
sum_expr = exp.Sum(this=col_expr)
|
|
48
|
+
select_expr = exp.alias_(sum_expr, alias) if alias else sum_expr
|
|
49
|
+
return cast("Self", builder.select(select_expr))
|
|
50
|
+
|
|
51
|
+
def avg_(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
52
|
+
"""Add AVG function to SELECT clause.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
column: The column to average.
|
|
56
|
+
alias: Optional alias for the average.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The current builder instance for method chaining.
|
|
60
|
+
"""
|
|
61
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
62
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
63
|
+
avg_expr = exp.Avg(this=col_expr)
|
|
64
|
+
select_expr = exp.alias_(avg_expr, alias) if alias else avg_expr
|
|
65
|
+
return cast("Self", builder.select(select_expr))
|
|
66
|
+
|
|
67
|
+
def max_(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
68
|
+
"""Add MAX function to SELECT clause.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
column: The column to find the maximum of.
|
|
72
|
+
alias: Optional alias for the maximum.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The current builder instance for method chaining.
|
|
76
|
+
"""
|
|
77
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
78
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
79
|
+
max_expr = exp.Max(this=col_expr)
|
|
80
|
+
select_expr = exp.alias_(max_expr, alias) if alias else max_expr
|
|
81
|
+
return cast("Self", builder.select(select_expr))
|
|
82
|
+
|
|
83
|
+
def min_(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
84
|
+
"""Add MIN function to SELECT clause.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
column: The column to find the minimum of.
|
|
88
|
+
alias: Optional alias for the minimum.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The current builder instance for method chaining.
|
|
92
|
+
"""
|
|
93
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
94
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
95
|
+
min_expr = exp.Min(this=col_expr)
|
|
96
|
+
select_expr = exp.alias_(min_expr, alias) if alias else min_expr
|
|
97
|
+
return cast("Self", builder.select(select_expr))
|
|
98
|
+
|
|
99
|
+
def array_agg(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
100
|
+
"""Add ARRAY_AGG aggregate function to SELECT clause.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
column: The column to aggregate into an array.
|
|
104
|
+
alias: Optional alias for the result.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The current builder instance for method chaining.
|
|
108
|
+
"""
|
|
109
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
110
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
111
|
+
array_agg_expr = exp.ArrayAgg(this=col_expr)
|
|
112
|
+
select_expr = exp.alias_(array_agg_expr, alias) if alias else array_agg_expr
|
|
113
|
+
return cast("Self", builder.select(select_expr))
|
|
114
|
+
|
|
115
|
+
def bool_and(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
116
|
+
"""Add BOOL_AND aggregate function to SELECT clause (PostgreSQL, DuckDB, etc).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
column: The boolean column to aggregate.
|
|
120
|
+
alias: Optional alias for the result.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The current builder instance for method chaining.
|
|
124
|
+
|
|
125
|
+
Note:
|
|
126
|
+
Uses exp.Anonymous for BOOL_AND. Not all dialects support this function.
|
|
127
|
+
"""
|
|
128
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
129
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
130
|
+
bool_and_expr = exp.Anonymous(this="BOOL_AND", expressions=[col_expr])
|
|
131
|
+
select_expr = exp.alias_(bool_and_expr, alias) if alias else bool_and_expr
|
|
132
|
+
return cast("Self", builder.select(select_expr))
|
|
133
|
+
|
|
134
|
+
def bool_or(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
135
|
+
"""Add BOOL_OR aggregate function to SELECT clause (PostgreSQL, DuckDB, etc).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
column: The boolean column to aggregate.
|
|
139
|
+
alias: Optional alias for the result.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The current builder instance for method chaining.
|
|
143
|
+
|
|
144
|
+
Note:
|
|
145
|
+
Uses exp.Anonymous for BOOL_OR. Not all dialects support this function.
|
|
146
|
+
"""
|
|
147
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
148
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
149
|
+
bool_or_expr = exp.Anonymous(this="BOOL_OR", expressions=[col_expr])
|
|
150
|
+
select_expr = exp.alias_(bool_or_expr, alias) if alias else bool_or_expr
|
|
151
|
+
return cast("Self", builder.select(select_expr))
|