sqlspec 0.16.2__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1782 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +164 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +149 -0
- sqlspec/builder/mixins/_merge_operations.py +562 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.2.dist-info/METADATA +365 -0
- sqlspec-0.16.2.dist-info/RECORD +148 -0
- sqlspec-0.16.2.dist-info/WHEEL +7 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.2.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.2.dist-info/licenses/NOTICE +29 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""DDL builder utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sqlspec.builder._ddl import ColumnDefinition, ConstraintDefinition
|
|
9
|
+
|
|
10
|
+
__all__ = ("build_column_expression", "build_constraint_expression")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
14
|
+
"""Build SQLGlot expression for a column definition."""
|
|
15
|
+
col_def = exp.ColumnDef(this=exp.to_identifier(col.name), kind=exp.DataType.build(col.dtype))
|
|
16
|
+
|
|
17
|
+
constraints: list[exp.ColumnConstraint] = []
|
|
18
|
+
|
|
19
|
+
if col.not_null:
|
|
20
|
+
constraints.append(exp.ColumnConstraint(kind=exp.NotNullColumnConstraint()))
|
|
21
|
+
|
|
22
|
+
if col.primary_key:
|
|
23
|
+
constraints.append(exp.ColumnConstraint(kind=exp.PrimaryKeyColumnConstraint()))
|
|
24
|
+
|
|
25
|
+
if col.unique:
|
|
26
|
+
constraints.append(exp.ColumnConstraint(kind=exp.UniqueColumnConstraint()))
|
|
27
|
+
|
|
28
|
+
if col.default is not None:
|
|
29
|
+
default_expr: Optional[exp.Expression] = None
|
|
30
|
+
if isinstance(col.default, str):
|
|
31
|
+
if col.default.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in col.default:
|
|
32
|
+
default_expr = exp.maybe_parse(col.default)
|
|
33
|
+
else:
|
|
34
|
+
default_expr = exp.convert(col.default)
|
|
35
|
+
else:
|
|
36
|
+
default_expr = exp.convert(col.default)
|
|
37
|
+
|
|
38
|
+
constraints.append(exp.ColumnConstraint(kind=default_expr))
|
|
39
|
+
|
|
40
|
+
if col.check:
|
|
41
|
+
check_expr = exp.Check(this=exp.maybe_parse(col.check))
|
|
42
|
+
constraints.append(exp.ColumnConstraint(kind=check_expr))
|
|
43
|
+
|
|
44
|
+
if col.comment:
|
|
45
|
+
constraints.append(exp.ColumnConstraint(kind=exp.CommentColumnConstraint(this=exp.convert(col.comment))))
|
|
46
|
+
|
|
47
|
+
if col.generated:
|
|
48
|
+
generated_expr = exp.GeneratedAsIdentityColumnConstraint(this=exp.maybe_parse(col.generated))
|
|
49
|
+
constraints.append(exp.ColumnConstraint(kind=generated_expr))
|
|
50
|
+
|
|
51
|
+
if col.collate:
|
|
52
|
+
constraints.append(exp.ColumnConstraint(kind=exp.CollateColumnConstraint(this=exp.to_identifier(col.collate))))
|
|
53
|
+
|
|
54
|
+
if constraints:
|
|
55
|
+
col_def.set("constraints", constraints)
|
|
56
|
+
|
|
57
|
+
return col_def
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
|
|
61
|
+
"""Build SQLGlot expression for a table constraint."""
|
|
62
|
+
if constraint.constraint_type == "PRIMARY KEY":
|
|
63
|
+
pk_cols = [exp.to_identifier(col) for col in constraint.columns]
|
|
64
|
+
pk_constraint = exp.PrimaryKey(expressions=pk_cols)
|
|
65
|
+
|
|
66
|
+
if constraint.name:
|
|
67
|
+
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=pk_constraint)
|
|
68
|
+
return pk_constraint
|
|
69
|
+
|
|
70
|
+
if constraint.constraint_type == "FOREIGN KEY":
|
|
71
|
+
fk_cols = [exp.to_identifier(col) for col in constraint.columns]
|
|
72
|
+
ref_cols = [exp.to_identifier(col) for col in constraint.references_columns]
|
|
73
|
+
|
|
74
|
+
fk_constraint = exp.ForeignKey(
|
|
75
|
+
expressions=fk_cols,
|
|
76
|
+
reference=exp.Reference(
|
|
77
|
+
this=exp.to_table(constraint.references_table) if constraint.references_table else None,
|
|
78
|
+
expressions=ref_cols,
|
|
79
|
+
on_delete=constraint.on_delete,
|
|
80
|
+
on_update=constraint.on_update,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if constraint.name:
|
|
85
|
+
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=fk_constraint)
|
|
86
|
+
return fk_constraint
|
|
87
|
+
|
|
88
|
+
if constraint.constraint_type == "UNIQUE":
|
|
89
|
+
unique_cols = [exp.to_identifier(col) for col in constraint.columns]
|
|
90
|
+
unique_constraint = exp.UniqueKeyProperty(expressions=unique_cols)
|
|
91
|
+
|
|
92
|
+
if constraint.name:
|
|
93
|
+
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=unique_constraint)
|
|
94
|
+
return unique_constraint
|
|
95
|
+
|
|
96
|
+
if constraint.constraint_type == "CHECK":
|
|
97
|
+
check_expr = exp.Check(this=exp.maybe_parse(constraint.condition) if constraint.condition else None)
|
|
98
|
+
|
|
99
|
+
if constraint.name:
|
|
100
|
+
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=check_expr)
|
|
101
|
+
return check_expr
|
|
102
|
+
|
|
103
|
+
return None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Safe SQL query builder with validation and parameter binding.
|
|
2
|
+
|
|
3
|
+
This module provides a fluent interface for building SQL queries safely,
|
|
4
|
+
with automatic parameter binding and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
|
|
11
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
12
|
+
from sqlspec.builder.mixins import DeleteFromClauseMixin, ReturningClauseMixin, WhereClauseMixin
|
|
13
|
+
from sqlspec.core.result import SQLResult
|
|
14
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
15
|
+
|
|
16
|
+
__all__ = ("Delete",)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromClauseMixin):
|
|
20
|
+
"""Builder for DELETE statements.
|
|
21
|
+
|
|
22
|
+
This builder provides a fluent interface for constructing SQL DELETE statements
|
|
23
|
+
with automatic parameter binding and validation. It does not support JOIN
|
|
24
|
+
operations to maintain cross-dialect compatibility and safety.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ("_table",)
|
|
28
|
+
_expression: Optional[exp.Expression]
|
|
29
|
+
|
|
30
|
+
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
31
|
+
"""Initialize DELETE with optional table.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
table: Target table name
|
|
35
|
+
**kwargs: Additional QueryBuilder arguments
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(**kwargs)
|
|
38
|
+
self._initialize_expression()
|
|
39
|
+
|
|
40
|
+
self._table = None
|
|
41
|
+
|
|
42
|
+
if table:
|
|
43
|
+
self.from_(table)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
47
|
+
"""Get the expected result type for DELETE operations.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The ExecuteResult type for DELETE statements.
|
|
51
|
+
"""
|
|
52
|
+
return SQLResult
|
|
53
|
+
|
|
54
|
+
def _create_base_expression(self) -> "exp.Delete":
|
|
55
|
+
"""Create a new sqlglot Delete expression.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A new sqlglot Delete expression.
|
|
59
|
+
"""
|
|
60
|
+
return exp.Delete()
|
|
61
|
+
|
|
62
|
+
def build(self) -> "SafeQuery":
|
|
63
|
+
"""Build the DELETE query with validation.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
SafeQuery: The built query with SQL and parameters.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
SQLBuilderError: If the table is not specified.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if not self._table:
|
|
73
|
+
msg = "DELETE requires a table to be specified. Use from() to set the table."
|
|
74
|
+
raise SQLBuilderError(msg)
|
|
75
|
+
|
|
76
|
+
return super().build()
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Safe SQL query builder with validation and parameter binding.
|
|
2
|
+
|
|
3
|
+
This module provides a fluent interface for building SQL queries safely,
|
|
4
|
+
with automatic parameter binding and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Final, Optional
|
|
8
|
+
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
from sqlspec.builder._base import QueryBuilder
|
|
13
|
+
from sqlspec.builder.mixins import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin, ReturningClauseMixin
|
|
14
|
+
from sqlspec.core.result import SQLResult
|
|
15
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Mapping, Sequence
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ("Insert",)
|
|
22
|
+
|
|
23
|
+
ERR_MSG_TABLE_NOT_SET: Final[str] = "The target table must be set using .into() before adding values."
|
|
24
|
+
ERR_MSG_VALUES_COLUMNS_MISMATCH: Final[str] = (
|
|
25
|
+
"Number of values ({values_len}) does not match the number of specified columns ({columns_len})."
|
|
26
|
+
)
|
|
27
|
+
ERR_MSG_INTERNAL_EXPRESSION_TYPE: Final[str] = "Internal error: expression is not an Insert instance as expected."
|
|
28
|
+
ERR_MSG_EXPRESSION_NOT_INITIALIZED: Final[str] = "Internal error: base expression not initialized."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSelectMixin, InsertIntoClauseMixin):
|
|
32
|
+
"""Builder for INSERT statements.
|
|
33
|
+
|
|
34
|
+
This builder facilitates the construction of SQL INSERT queries
|
|
35
|
+
in a safe and dialect-agnostic manner with automatic parameter binding.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("_columns", "_table", "_values_added_count")
|
|
39
|
+
|
|
40
|
+
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
41
|
+
"""Initialize INSERT with optional table.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
table: Target table name
|
|
45
|
+
**kwargs: Additional QueryBuilder arguments
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(**kwargs)
|
|
48
|
+
|
|
49
|
+
# Initialize Insert-specific attributes
|
|
50
|
+
self._table: Optional[str] = None
|
|
51
|
+
self._columns: list[str] = []
|
|
52
|
+
self._values_added_count: int = 0
|
|
53
|
+
|
|
54
|
+
self._initialize_expression()
|
|
55
|
+
|
|
56
|
+
if table:
|
|
57
|
+
self.into(table)
|
|
58
|
+
|
|
59
|
+
def _create_base_expression(self) -> exp.Insert:
|
|
60
|
+
"""Create a base INSERT expression.
|
|
61
|
+
|
|
62
|
+
This method is called by the base QueryBuilder during initialization.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A new sqlglot Insert expression.
|
|
66
|
+
"""
|
|
67
|
+
return exp.Insert()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
71
|
+
"""Specifies the expected result type for an INSERT query.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The type of result expected for INSERT operations.
|
|
75
|
+
"""
|
|
76
|
+
return SQLResult
|
|
77
|
+
|
|
78
|
+
def _get_insert_expression(self) -> exp.Insert:
|
|
79
|
+
"""Safely gets and casts the internal expression to exp.Insert.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The internal expression as exp.Insert.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
SQLBuilderError: If the expression is not initialized or is not an Insert.
|
|
86
|
+
"""
|
|
87
|
+
if self._expression is None:
|
|
88
|
+
raise SQLBuilderError(ERR_MSG_EXPRESSION_NOT_INITIALIZED)
|
|
89
|
+
if not isinstance(self._expression, exp.Insert):
|
|
90
|
+
raise SQLBuilderError(ERR_MSG_INTERNAL_EXPRESSION_TYPE)
|
|
91
|
+
return self._expression
|
|
92
|
+
|
|
93
|
+
def values(self, *values: Any, **kwargs: Any) -> "Self":
|
|
94
|
+
"""Adds a row of values to the INSERT statement.
|
|
95
|
+
|
|
96
|
+
This method can be called multiple times to insert multiple rows,
|
|
97
|
+
resulting in a multi-row INSERT statement like `VALUES (...), (...)`.
|
|
98
|
+
|
|
99
|
+
Supports:
|
|
100
|
+
- values(val1, val2, val3)
|
|
101
|
+
- values(col1=val1, col2=val2)
|
|
102
|
+
- values(mapping)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
*values: The values for the row to be inserted. The number of values
|
|
106
|
+
must match the number of columns set by `columns()`, if `columns()` was called
|
|
107
|
+
and specified any non-empty list of columns.
|
|
108
|
+
**kwargs: Column-value pairs for named values.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The current builder instance for method chaining.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
SQLBuilderError: If `into()` has not been called to set the table,
|
|
115
|
+
or if `columns()` was called with a non-empty list of columns
|
|
116
|
+
and the number of values does not match the number of specified columns.
|
|
117
|
+
"""
|
|
118
|
+
if not self._table:
|
|
119
|
+
raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
|
|
120
|
+
|
|
121
|
+
if kwargs:
|
|
122
|
+
if values:
|
|
123
|
+
msg = "Cannot mix positional values with keyword values."
|
|
124
|
+
raise SQLBuilderError(msg)
|
|
125
|
+
return self.values_from_dict(kwargs)
|
|
126
|
+
|
|
127
|
+
if len(values) == 1:
|
|
128
|
+
try:
|
|
129
|
+
values_0 = values[0]
|
|
130
|
+
if hasattr(values_0, "items"):
|
|
131
|
+
return self.values_from_dict(values_0)
|
|
132
|
+
except (AttributeError, TypeError):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
insert_expr = self._get_insert_expression()
|
|
136
|
+
|
|
137
|
+
if self._columns and len(values) != len(self._columns):
|
|
138
|
+
msg = ERR_MSG_VALUES_COLUMNS_MISMATCH.format(values_len=len(values), columns_len=len(self._columns))
|
|
139
|
+
raise SQLBuilderError(msg)
|
|
140
|
+
|
|
141
|
+
value_placeholders: list[exp.Expression] = []
|
|
142
|
+
for i, value in enumerate(values):
|
|
143
|
+
if isinstance(value, exp.Expression):
|
|
144
|
+
value_placeholders.append(value)
|
|
145
|
+
elif hasattr(value, "expression") and hasattr(value, "sql"):
|
|
146
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
147
|
+
expression = getattr(value, "expression", None)
|
|
148
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
149
|
+
# Merge parameters from SQL object into builder
|
|
150
|
+
if hasattr(value, "parameters"):
|
|
151
|
+
sql_parameters = getattr(value, "parameters", {})
|
|
152
|
+
for param_name, param_value in sql_parameters.items():
|
|
153
|
+
self.add_parameter(param_value, name=param_name)
|
|
154
|
+
value_placeholders.append(expression)
|
|
155
|
+
else:
|
|
156
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
157
|
+
sql_text = getattr(value, "sql", "")
|
|
158
|
+
# Merge parameters even when parsing raw SQL
|
|
159
|
+
if hasattr(value, "parameters"):
|
|
160
|
+
sql_parameters = getattr(value, "parameters", {})
|
|
161
|
+
for param_name, param_value in sql_parameters.items():
|
|
162
|
+
self.add_parameter(param_value, name=param_name)
|
|
163
|
+
# Check if sql_text is callable (like Expression.sql method)
|
|
164
|
+
if callable(sql_text):
|
|
165
|
+
sql_text = str(value)
|
|
166
|
+
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
167
|
+
value_placeholders.append(value_expr)
|
|
168
|
+
else:
|
|
169
|
+
if self._columns and i < len(self._columns):
|
|
170
|
+
column_str = str(self._columns[i])
|
|
171
|
+
column_name = column_str.rsplit(".", maxsplit=1)[-1] if "." in column_str else column_str
|
|
172
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
173
|
+
else:
|
|
174
|
+
param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
|
|
175
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
176
|
+
value_placeholders.append(exp.var(param_name))
|
|
177
|
+
|
|
178
|
+
tuple_expr = exp.Tuple(expressions=value_placeholders)
|
|
179
|
+
if self._values_added_count == 0:
|
|
180
|
+
insert_expr.set("expression", exp.Values(expressions=[tuple_expr]))
|
|
181
|
+
else:
|
|
182
|
+
current_values = insert_expr.args.get("expression")
|
|
183
|
+
if isinstance(current_values, exp.Values):
|
|
184
|
+
current_values.expressions.append(tuple_expr)
|
|
185
|
+
else:
|
|
186
|
+
insert_expr.set("expression", exp.Values(expressions=[tuple_expr]))
|
|
187
|
+
|
|
188
|
+
self._values_added_count += 1
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def values_from_dict(self, data: "Mapping[str, Any]") -> "Self":
|
|
192
|
+
"""Adds a row of values from a dictionary.
|
|
193
|
+
|
|
194
|
+
This is a convenience method that automatically sets columns based on
|
|
195
|
+
the dictionary keys and values based on the dictionary values.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
data: A mapping of column names to values.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The current builder instance for method chaining.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
SQLBuilderError: If `into()` has not been called to set the table.
|
|
205
|
+
"""
|
|
206
|
+
if not self._table:
|
|
207
|
+
raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
|
|
208
|
+
|
|
209
|
+
data_keys = list(data.keys())
|
|
210
|
+
if not self._columns:
|
|
211
|
+
self.columns(*data_keys)
|
|
212
|
+
elif set(self._columns) != set(data_keys):
|
|
213
|
+
msg = f"Dictionary keys {set(data_keys)} do not match existing columns {set(self._columns)}."
|
|
214
|
+
raise SQLBuilderError(msg)
|
|
215
|
+
|
|
216
|
+
return self.values(*[data[col] for col in self._columns])
|
|
217
|
+
|
|
218
|
+
def values_from_dicts(self, data: "Sequence[Mapping[str, Any]]") -> "Self":
|
|
219
|
+
"""Adds multiple rows of values from a sequence of dictionaries.
|
|
220
|
+
|
|
221
|
+
This is a convenience method for bulk inserts from structured data.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
data: A sequence of mappings, each representing a row of data.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The current builder instance for method chaining.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
SQLBuilderError: If `into()` has not been called to set the table,
|
|
231
|
+
or if dictionaries have inconsistent keys.
|
|
232
|
+
"""
|
|
233
|
+
if not data:
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
first_dict = data[0]
|
|
237
|
+
if not self._columns:
|
|
238
|
+
self.columns(*first_dict.keys())
|
|
239
|
+
|
|
240
|
+
expected_keys = set(self._columns)
|
|
241
|
+
for i, row_dict in enumerate(data):
|
|
242
|
+
if set(row_dict.keys()) != expected_keys:
|
|
243
|
+
msg = (
|
|
244
|
+
f"Dictionary at index {i} has keys {set(row_dict.keys())} "
|
|
245
|
+
f"which do not match expected keys {expected_keys}."
|
|
246
|
+
)
|
|
247
|
+
raise SQLBuilderError(msg)
|
|
248
|
+
|
|
249
|
+
for row_dict in data:
|
|
250
|
+
self.values(*[row_dict[col] for col in self._columns])
|
|
251
|
+
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def on_conflict(self, *columns: str) -> "ConflictBuilder":
|
|
255
|
+
"""Adds an ON CONFLICT clause with specified columns.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
*columns: Column names that define the conflict. If no columns provided,
|
|
259
|
+
creates an ON CONFLICT without specific columns (catches all conflicts).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
A ConflictBuilder instance for chaining conflict resolution methods.
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
```python
|
|
266
|
+
# ON CONFLICT (id) DO NOTHING
|
|
267
|
+
sql.insert("users").values(id=1, name="John").on_conflict(
|
|
268
|
+
"id"
|
|
269
|
+
).do_nothing()
|
|
270
|
+
|
|
271
|
+
# ON CONFLICT (email, username) DO UPDATE SET updated_at = NOW()
|
|
272
|
+
sql.insert("users").values(...).on_conflict(
|
|
273
|
+
"email", "username"
|
|
274
|
+
).do_update(updated_at=sql.raw("NOW()"))
|
|
275
|
+
|
|
276
|
+
# ON CONFLICT DO NOTHING (catches all conflicts)
|
|
277
|
+
sql.insert("users").values(...).on_conflict().do_nothing()
|
|
278
|
+
```
|
|
279
|
+
"""
|
|
280
|
+
return ConflictBuilder(self, columns)
|
|
281
|
+
|
|
282
|
+
def on_conflict_do_nothing(self, *columns: str) -> "Insert":
|
|
283
|
+
"""Adds an ON CONFLICT DO NOTHING clause (convenience method).
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
*columns: Column names that define the conflict. If no columns provided,
|
|
287
|
+
creates an ON CONFLICT without specific columns.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The current builder instance for method chaining.
|
|
291
|
+
|
|
292
|
+
Note:
|
|
293
|
+
This is a convenience method. For more control, use on_conflict().do_nothing().
|
|
294
|
+
"""
|
|
295
|
+
return self.on_conflict(*columns).do_nothing()
|
|
296
|
+
|
|
297
|
+
def on_duplicate_key_update(self, **kwargs: Any) -> "Insert":
|
|
298
|
+
"""Adds conflict resolution using the ON CONFLICT syntax (cross-database compatible).
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
**kwargs: Column-value pairs to update on conflict.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
The current builder instance for method chaining.
|
|
305
|
+
|
|
306
|
+
Note:
|
|
307
|
+
This method uses PostgreSQL-style ON CONFLICT syntax but SQLGlot will
|
|
308
|
+
transpile it to the appropriate syntax for each database (MySQL's
|
|
309
|
+
ON DUPLICATE KEY UPDATE, etc.).
|
|
310
|
+
"""
|
|
311
|
+
if not kwargs:
|
|
312
|
+
return self
|
|
313
|
+
return self.on_conflict().do_update(**kwargs)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ConflictBuilder:
|
|
317
|
+
"""Builder for ON CONFLICT clauses in INSERT statements.
|
|
318
|
+
|
|
319
|
+
This builder provides a fluent interface for constructing conflict resolution
|
|
320
|
+
clauses using PostgreSQL-style syntax, which SQLGlot can transpile to other dialects.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
__slots__ = ("_columns", "_insert_builder")
|
|
324
|
+
|
|
325
|
+
def __init__(self, insert_builder: "Insert", columns: tuple[str, ...]) -> None:
|
|
326
|
+
"""Initialize ConflictBuilder.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
insert_builder: The parent Insert builder
|
|
330
|
+
columns: Column names that define the conflict
|
|
331
|
+
"""
|
|
332
|
+
self._insert_builder = insert_builder
|
|
333
|
+
self._columns = columns
|
|
334
|
+
|
|
335
|
+
def do_nothing(self) -> "Insert":
|
|
336
|
+
"""Add DO NOTHING conflict resolution.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The parent Insert builder for method chaining.
|
|
340
|
+
|
|
341
|
+
Example:
|
|
342
|
+
```python
|
|
343
|
+
sql.insert("users").values(id=1, name="John").on_conflict(
|
|
344
|
+
"id"
|
|
345
|
+
).do_nothing()
|
|
346
|
+
```
|
|
347
|
+
"""
|
|
348
|
+
insert_expr = self._insert_builder._get_insert_expression()
|
|
349
|
+
|
|
350
|
+
# Create ON CONFLICT with proper structure
|
|
351
|
+
conflict_keys = [exp.to_identifier(col) for col in self._columns] if self._columns else None
|
|
352
|
+
on_conflict = exp.OnConflict(conflict_keys=conflict_keys, action=exp.var("DO NOTHING"))
|
|
353
|
+
|
|
354
|
+
insert_expr.set("conflict", on_conflict)
|
|
355
|
+
return self._insert_builder
|
|
356
|
+
|
|
357
|
+
def do_update(self, **kwargs: Any) -> "Insert":
|
|
358
|
+
"""Add DO UPDATE conflict resolution with SET clauses.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
**kwargs: Column-value pairs to update on conflict.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The parent Insert builder for method chaining.
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
```python
|
|
368
|
+
sql.insert("users").values(id=1, name="John").on_conflict(
|
|
369
|
+
"id"
|
|
370
|
+
).do_update(
|
|
371
|
+
name="Updated Name", updated_at=sql.raw("NOW()")
|
|
372
|
+
)
|
|
373
|
+
```
|
|
374
|
+
"""
|
|
375
|
+
insert_expr = self._insert_builder._get_insert_expression()
|
|
376
|
+
|
|
377
|
+
# Create SET expressions for the UPDATE
|
|
378
|
+
set_expressions = []
|
|
379
|
+
for col, val in kwargs.items():
|
|
380
|
+
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
381
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
382
|
+
expression = getattr(val, "expression", None)
|
|
383
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
384
|
+
# Merge parameters from SQL object into builder
|
|
385
|
+
if hasattr(val, "parameters"):
|
|
386
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
387
|
+
for param_name, param_value in sql_parameters.items():
|
|
388
|
+
self._insert_builder.add_parameter(param_value, name=param_name)
|
|
389
|
+
value_expr = expression
|
|
390
|
+
else:
|
|
391
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
392
|
+
sql_text = getattr(val, "sql", "")
|
|
393
|
+
# Merge parameters even when parsing raw SQL
|
|
394
|
+
if hasattr(val, "parameters"):
|
|
395
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
396
|
+
for param_name, param_value in sql_parameters.items():
|
|
397
|
+
self._insert_builder.add_parameter(param_value, name=param_name)
|
|
398
|
+
# Check if sql_text is callable (like Expression.sql method)
|
|
399
|
+
if callable(sql_text):
|
|
400
|
+
sql_text = str(val)
|
|
401
|
+
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
402
|
+
elif isinstance(val, exp.Expression):
|
|
403
|
+
value_expr = val
|
|
404
|
+
else:
|
|
405
|
+
# Create parameter for regular values
|
|
406
|
+
param_name = self._insert_builder._generate_unique_parameter_name(col)
|
|
407
|
+
_, param_name = self._insert_builder.add_parameter(val, name=param_name)
|
|
408
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
409
|
+
|
|
410
|
+
set_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
411
|
+
|
|
412
|
+
# Create ON CONFLICT with proper structure
|
|
413
|
+
conflict_keys = [exp.to_identifier(col) for col in self._columns] if self._columns else None
|
|
414
|
+
on_conflict = exp.OnConflict(
|
|
415
|
+
conflict_keys=conflict_keys,
|
|
416
|
+
action=exp.var("DO UPDATE"),
|
|
417
|
+
expressions=set_expressions if set_expressions else None,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
insert_expr.set("conflict", on_conflict)
|
|
421
|
+
return self._insert_builder
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Safe SQL query builder with validation and parameter binding.
|
|
2
|
+
|
|
3
|
+
This module provides a fluent interface for building SQL queries safely,
|
|
4
|
+
with automatic parameter binding and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
|
|
11
|
+
from sqlspec.builder._base import QueryBuilder
|
|
12
|
+
from sqlspec.builder.mixins import (
|
|
13
|
+
MergeIntoClauseMixin,
|
|
14
|
+
MergeMatchedClauseMixin,
|
|
15
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
16
|
+
MergeNotMatchedClauseMixin,
|
|
17
|
+
MergeOnClauseMixin,
|
|
18
|
+
MergeUsingClauseMixin,
|
|
19
|
+
)
|
|
20
|
+
from sqlspec.core.result import SQLResult
|
|
21
|
+
|
|
22
|
+
__all__ = ("Merge",)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Merge(
|
|
26
|
+
QueryBuilder,
|
|
27
|
+
MergeUsingClauseMixin,
|
|
28
|
+
MergeOnClauseMixin,
|
|
29
|
+
MergeMatchedClauseMixin,
|
|
30
|
+
MergeNotMatchedClauseMixin,
|
|
31
|
+
MergeIntoClauseMixin,
|
|
32
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
33
|
+
):
|
|
34
|
+
"""Builder for MERGE statements.
|
|
35
|
+
|
|
36
|
+
This builder provides a fluent interface for constructing SQL MERGE statements
|
|
37
|
+
(also known as UPSERT in some databases) with automatic parameter binding and validation.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__slots__ = ()
|
|
41
|
+
_expression: Optional[exp.Expression]
|
|
42
|
+
|
|
43
|
+
def __init__(self, target_table: Optional[str] = None, **kwargs: Any) -> None:
|
|
44
|
+
"""Initialize MERGE with optional target table.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
target_table: Target table name
|
|
48
|
+
**kwargs: Additional QueryBuilder arguments
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(**kwargs)
|
|
51
|
+
self._initialize_expression()
|
|
52
|
+
|
|
53
|
+
if target_table:
|
|
54
|
+
self.into(target_table)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
58
|
+
"""Return the expected result type for this builder.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The SQLResult type for MERGE statements.
|
|
62
|
+
"""
|
|
63
|
+
return SQLResult
|
|
64
|
+
|
|
65
|
+
def _create_base_expression(self) -> "exp.Merge":
|
|
66
|
+
"""Create a base MERGE expression.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A new sqlglot Merge expression with empty clauses.
|
|
70
|
+
"""
|
|
71
|
+
return exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|