sqlspec 0.25.0__py3-none-any.whl → 0.26.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/_serialization.py +223 -21
- sqlspec/_sql.py +12 -50
- sqlspec/_typing.py +9 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +127 -18
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +63 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +6 -0
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/builder/_base.py +82 -42
- sqlspec/builder/_column.py +57 -24
- sqlspec/builder/_ddl.py +84 -34
- sqlspec/builder/_insert.py +30 -52
- sqlspec/builder/_parsing_utils.py +104 -8
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/mixins/_cte_and_set_ops.py +1 -2
- sqlspec/builder/mixins/_join_operations.py +14 -30
- sqlspec/builder/mixins/_merge_operations.py +167 -61
- sqlspec/builder/mixins/_order_limit_operations.py +3 -10
- sqlspec/builder/mixins/_select_operations.py +3 -9
- sqlspec/builder/mixins/_update_operations.py +3 -22
- sqlspec/builder/mixins/_where_clause.py +4 -10
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/cache.py +2 -2
- sqlspec/core/compiler.py +56 -1
- sqlspec/core/parameters.py +7 -3
- sqlspec/core/statement.py +5 -0
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +106 -3
- sqlspec/driver/_common.py +156 -4
- sqlspec/driver/_sync.py +106 -3
- sqlspec/exceptions.py +5 -0
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/typing.py +2 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/builder/_column.py
CHANGED
|
@@ -5,6 +5,7 @@ SQL conditions with parameter binding.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from collections.abc import Iterable
|
|
8
|
+
from datetime import date, datetime
|
|
8
9
|
from typing import Any, Optional, cast
|
|
9
10
|
|
|
10
11
|
from sqlglot import exp
|
|
@@ -14,6 +15,24 @@ from sqlspec.utils.type_guards import has_sql_method
|
|
|
14
15
|
__all__ = ("Column", "ColumnExpression", "FunctionColumn")
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def _convert_value(value: Any) -> exp.Expression:
|
|
19
|
+
"""Convert a Python value to a SQLGlot expression.
|
|
20
|
+
|
|
21
|
+
Special handling for datetime objects to prevent SQLGlot from
|
|
22
|
+
converting them to TIME_STR_TO_TIME function calls. Datetime
|
|
23
|
+
objects should be passed as parameters, not converted to SQL functions.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
value: The value to convert
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A SQLGlot expression representing the value
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(value, (datetime, date)):
|
|
32
|
+
return exp.Literal(this=value, is_string=False)
|
|
33
|
+
return exp.convert(value)
|
|
34
|
+
|
|
35
|
+
|
|
17
36
|
class ColumnExpression:
|
|
18
37
|
"""Base class for column expressions that can be combined with operators."""
|
|
19
38
|
|
|
@@ -67,33 +86,37 @@ class Column:
|
|
|
67
86
|
else:
|
|
68
87
|
self._expression = exp.Column(this=exp.Identifier(this=name))
|
|
69
88
|
|
|
89
|
+
def _convert_value(self, value: Any) -> exp.Expression:
|
|
90
|
+
"""Convert a Python value to a SQLGlot expression."""
|
|
91
|
+
return _convert_value(value)
|
|
92
|
+
|
|
70
93
|
def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
71
94
|
"""Equal to (==)."""
|
|
72
95
|
if other is None:
|
|
73
96
|
return ColumnExpression(exp.Is(this=self._expression, expression=exp.Null()))
|
|
74
|
-
return ColumnExpression(exp.EQ(this=self._expression, expression=
|
|
97
|
+
return ColumnExpression(exp.EQ(this=self._expression, expression=self._convert_value(other)))
|
|
75
98
|
|
|
76
99
|
def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
77
100
|
"""Not equal to (!=)."""
|
|
78
101
|
if other is None:
|
|
79
102
|
return ColumnExpression(exp.Not(this=exp.Is(this=self._expression, expression=exp.Null())))
|
|
80
|
-
return ColumnExpression(exp.NEQ(this=self._expression, expression=
|
|
103
|
+
return ColumnExpression(exp.NEQ(this=self._expression, expression=self._convert_value(other)))
|
|
81
104
|
|
|
82
105
|
def __gt__(self, other: Any) -> ColumnExpression:
|
|
83
106
|
"""Greater than (>)."""
|
|
84
|
-
return ColumnExpression(exp.GT(this=self._expression, expression=
|
|
107
|
+
return ColumnExpression(exp.GT(this=self._expression, expression=self._convert_value(other)))
|
|
85
108
|
|
|
86
109
|
def __ge__(self, other: Any) -> ColumnExpression:
|
|
87
110
|
"""Greater than or equal (>=)."""
|
|
88
|
-
return ColumnExpression(exp.GTE(this=self._expression, expression=
|
|
111
|
+
return ColumnExpression(exp.GTE(this=self._expression, expression=self._convert_value(other)))
|
|
89
112
|
|
|
90
113
|
def __lt__(self, other: Any) -> ColumnExpression:
|
|
91
114
|
"""Less than (<)."""
|
|
92
|
-
return ColumnExpression(exp.LT(this=self._expression, expression=
|
|
115
|
+
return ColumnExpression(exp.LT(this=self._expression, expression=self._convert_value(other)))
|
|
93
116
|
|
|
94
117
|
def __le__(self, other: Any) -> ColumnExpression:
|
|
95
118
|
"""Less than or equal (<=)."""
|
|
96
|
-
return ColumnExpression(exp.LTE(this=self._expression, expression=
|
|
119
|
+
return ColumnExpression(exp.LTE(this=self._expression, expression=self._convert_value(other)))
|
|
97
120
|
|
|
98
121
|
def __invert__(self) -> ColumnExpression:
|
|
99
122
|
"""Apply NOT operator (~)."""
|
|
@@ -102,18 +125,20 @@ class Column:
|
|
|
102
125
|
def like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
|
|
103
126
|
"""SQL LIKE pattern matching."""
|
|
104
127
|
if escape:
|
|
105
|
-
like_expr = exp.Like(
|
|
128
|
+
like_expr = exp.Like(
|
|
129
|
+
this=self._expression, expression=self._convert_value(pattern), escape=self._convert_value(escape)
|
|
130
|
+
)
|
|
106
131
|
else:
|
|
107
|
-
like_expr = exp.Like(this=self._expression, expression=
|
|
132
|
+
like_expr = exp.Like(this=self._expression, expression=self._convert_value(pattern))
|
|
108
133
|
return ColumnExpression(like_expr)
|
|
109
134
|
|
|
110
135
|
def ilike(self, pattern: str) -> ColumnExpression:
|
|
111
136
|
"""Case-insensitive LIKE."""
|
|
112
|
-
return ColumnExpression(exp.ILike(this=self._expression, expression=
|
|
137
|
+
return ColumnExpression(exp.ILike(this=self._expression, expression=self._convert_value(pattern)))
|
|
113
138
|
|
|
114
139
|
def in_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
115
140
|
"""SQL IN clause."""
|
|
116
|
-
converted_values = [
|
|
141
|
+
converted_values = [self._convert_value(v) for v in values]
|
|
117
142
|
return ColumnExpression(exp.In(this=self._expression, expressions=converted_values))
|
|
118
143
|
|
|
119
144
|
def not_in(self, values: Iterable[Any]) -> ColumnExpression:
|
|
@@ -122,7 +147,9 @@ class Column:
|
|
|
122
147
|
|
|
123
148
|
def between(self, start: Any, end: Any) -> ColumnExpression:
|
|
124
149
|
"""SQL BETWEEN clause."""
|
|
125
|
-
return ColumnExpression(
|
|
150
|
+
return ColumnExpression(
|
|
151
|
+
exp.Between(this=self._expression, low=self._convert_value(start), high=self._convert_value(end))
|
|
152
|
+
)
|
|
126
153
|
|
|
127
154
|
def is_null(self) -> ColumnExpression:
|
|
128
155
|
"""SQL IS NULL."""
|
|
@@ -142,12 +169,12 @@ class Column:
|
|
|
142
169
|
|
|
143
170
|
def any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
144
171
|
"""SQL = ANY(...) clause."""
|
|
145
|
-
converted_values = [
|
|
172
|
+
converted_values = [self._convert_value(v) for v in values]
|
|
146
173
|
return ColumnExpression(exp.EQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
|
|
147
174
|
|
|
148
175
|
def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
149
176
|
"""SQL <> ANY(...) clause."""
|
|
150
|
-
converted_values = [
|
|
177
|
+
converted_values = [self._convert_value(v) for v in values]
|
|
151
178
|
return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
|
|
152
179
|
|
|
153
180
|
def lower(self) -> "FunctionColumn":
|
|
@@ -186,14 +213,14 @@ class Column:
|
|
|
186
213
|
|
|
187
214
|
def substring(self, start: int, length: Optional[int] = None) -> "FunctionColumn":
|
|
188
215
|
"""SQL SUBSTRING() function."""
|
|
189
|
-
args = [
|
|
216
|
+
args = [self._convert_value(start)]
|
|
190
217
|
if length is not None:
|
|
191
|
-
args.append(
|
|
218
|
+
args.append(self._convert_value(length))
|
|
192
219
|
return FunctionColumn(exp.Substring(this=self._expression, expressions=args))
|
|
193
220
|
|
|
194
221
|
def coalesce(self, *values: Any) -> "FunctionColumn":
|
|
195
222
|
"""SQL COALESCE() function."""
|
|
196
|
-
expressions = [self._expression] + [
|
|
223
|
+
expressions = [self._expression] + [self._convert_value(v) for v in values]
|
|
197
224
|
return FunctionColumn(exp.Coalesce(expressions=expressions))
|
|
198
225
|
|
|
199
226
|
def cast(self, data_type: str) -> "FunctionColumn":
|
|
@@ -272,22 +299,26 @@ class FunctionColumn:
|
|
|
272
299
|
def __init__(self, expression: exp.Expression) -> None:
|
|
273
300
|
self._expression = expression
|
|
274
301
|
|
|
302
|
+
def _convert_value(self, value: Any) -> exp.Expression:
|
|
303
|
+
"""Convert a Python value to a SQLGlot expression."""
|
|
304
|
+
return _convert_value(value)
|
|
305
|
+
|
|
275
306
|
def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
276
|
-
return ColumnExpression(exp.EQ(this=self._expression, expression=
|
|
307
|
+
return ColumnExpression(exp.EQ(this=self._expression, expression=self._convert_value(other)))
|
|
277
308
|
|
|
278
309
|
def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
279
|
-
return ColumnExpression(exp.NEQ(this=self._expression, expression=
|
|
310
|
+
return ColumnExpression(exp.NEQ(this=self._expression, expression=self._convert_value(other)))
|
|
280
311
|
|
|
281
312
|
def like(self, pattern: str) -> ColumnExpression:
|
|
282
|
-
return ColumnExpression(exp.Like(this=self._expression, expression=
|
|
313
|
+
return ColumnExpression(exp.Like(this=self._expression, expression=self._convert_value(pattern)))
|
|
283
314
|
|
|
284
315
|
def ilike(self, pattern: str) -> ColumnExpression:
|
|
285
316
|
"""Case-insensitive LIKE."""
|
|
286
|
-
return ColumnExpression(exp.ILike(this=self._expression, expression=
|
|
317
|
+
return ColumnExpression(exp.ILike(this=self._expression, expression=self._convert_value(pattern)))
|
|
287
318
|
|
|
288
319
|
def in_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
289
320
|
"""SQL IN clause."""
|
|
290
|
-
converted_values = [
|
|
321
|
+
converted_values = [self._convert_value(v) for v in values]
|
|
291
322
|
return ColumnExpression(exp.In(this=self._expression, expressions=converted_values))
|
|
292
323
|
|
|
293
324
|
def not_in_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
@@ -304,7 +335,9 @@ class FunctionColumn:
|
|
|
304
335
|
|
|
305
336
|
def between(self, start: Any, end: Any) -> ColumnExpression:
|
|
306
337
|
"""SQL BETWEEN clause."""
|
|
307
|
-
return ColumnExpression(
|
|
338
|
+
return ColumnExpression(
|
|
339
|
+
exp.Between(this=self._expression, low=self._convert_value(start), high=self._convert_value(end))
|
|
340
|
+
)
|
|
308
341
|
|
|
309
342
|
def is_null(self) -> ColumnExpression:
|
|
310
343
|
"""SQL IS NULL."""
|
|
@@ -316,12 +349,12 @@ class FunctionColumn:
|
|
|
316
349
|
|
|
317
350
|
def any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
318
351
|
"""SQL = ANY(...) clause."""
|
|
319
|
-
converted_values = [
|
|
352
|
+
converted_values = [self._convert_value(v) for v in values]
|
|
320
353
|
return ColumnExpression(exp.EQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
|
|
321
354
|
|
|
322
355
|
def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
|
|
323
356
|
"""SQL <> ANY(...) clause."""
|
|
324
|
-
converted_values = [
|
|
357
|
+
converted_values = [self._convert_value(v) for v in values]
|
|
325
358
|
return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
|
|
326
359
|
|
|
327
360
|
def alias(self, alias_name: str) -> exp.Expression:
|
sqlspec/builder/_ddl.py
CHANGED
|
@@ -11,12 +11,14 @@ from sqlglot.dialects.dialect import DialectType
|
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
|
|
13
13
|
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
14
|
+
from sqlspec.builder._select import Select
|
|
14
15
|
from sqlspec.core.result import SQLResult
|
|
16
|
+
from sqlspec.core.statement import SQL
|
|
15
17
|
from sqlspec.utils.type_guards import has_sqlglot_expression, has_with_method
|
|
16
18
|
|
|
17
19
|
if TYPE_CHECKING:
|
|
18
20
|
from sqlspec.builder._column import ColumnExpression
|
|
19
|
-
from sqlspec.core.statement import
|
|
21
|
+
from sqlspec.core.statement import StatementConfig
|
|
20
22
|
|
|
21
23
|
__all__ = (
|
|
22
24
|
"AlterOperation",
|
|
@@ -39,6 +41,37 @@ __all__ = (
|
|
|
39
41
|
"Truncate",
|
|
40
42
|
)
|
|
41
43
|
|
|
44
|
+
CONSTRAINT_TYPE_PRIMARY_KEY = "PRIMARY KEY"
|
|
45
|
+
CONSTRAINT_TYPE_FOREIGN_KEY = "FOREIGN KEY"
|
|
46
|
+
CONSTRAINT_TYPE_UNIQUE = "UNIQUE"
|
|
47
|
+
CONSTRAINT_TYPE_CHECK = "CHECK"
|
|
48
|
+
|
|
49
|
+
FOREIGN_KEY_ACTION_CASCADE = "CASCADE"
|
|
50
|
+
FOREIGN_KEY_ACTION_SET_NULL = "SET NULL"
|
|
51
|
+
FOREIGN_KEY_ACTION_SET_DEFAULT = "SET DEFAULT"
|
|
52
|
+
FOREIGN_KEY_ACTION_RESTRICT = "RESTRICT"
|
|
53
|
+
FOREIGN_KEY_ACTION_NO_ACTION = "NO ACTION"
|
|
54
|
+
|
|
55
|
+
VALID_FOREIGN_KEY_ACTIONS = {
|
|
56
|
+
FOREIGN_KEY_ACTION_CASCADE,
|
|
57
|
+
FOREIGN_KEY_ACTION_SET_NULL,
|
|
58
|
+
FOREIGN_KEY_ACTION_SET_DEFAULT,
|
|
59
|
+
FOREIGN_KEY_ACTION_RESTRICT,
|
|
60
|
+
FOREIGN_KEY_ACTION_NO_ACTION,
|
|
61
|
+
None,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
VALID_CONSTRAINT_TYPES = {
|
|
65
|
+
CONSTRAINT_TYPE_PRIMARY_KEY,
|
|
66
|
+
CONSTRAINT_TYPE_FOREIGN_KEY,
|
|
67
|
+
CONSTRAINT_TYPE_UNIQUE,
|
|
68
|
+
CONSTRAINT_TYPE_CHECK,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
CURRENT_TIMESTAMP_KEYWORD = "CURRENT_TIMESTAMP"
|
|
72
|
+
CURRENT_DATE_KEYWORD = "CURRENT_DATE"
|
|
73
|
+
CURRENT_TIME_KEYWORD = "CURRENT_TIME"
|
|
74
|
+
|
|
42
75
|
|
|
43
76
|
def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
44
77
|
"""Build SQLGlot expression for a column definition."""
|
|
@@ -59,11 +92,11 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
59
92
|
default_expr: Optional[exp.Expression] = None
|
|
60
93
|
if isinstance(col.default, str):
|
|
61
94
|
default_upper = col.default.upper()
|
|
62
|
-
if default_upper ==
|
|
95
|
+
if default_upper == CURRENT_TIMESTAMP_KEYWORD:
|
|
63
96
|
default_expr = exp.CurrentTimestamp()
|
|
64
|
-
elif default_upper ==
|
|
97
|
+
elif default_upper == CURRENT_DATE_KEYWORD:
|
|
65
98
|
default_expr = exp.CurrentDate()
|
|
66
|
-
elif default_upper ==
|
|
99
|
+
elif default_upper == CURRENT_TIME_KEYWORD:
|
|
67
100
|
default_expr = exp.CurrentTime()
|
|
68
101
|
elif "(" in col.default:
|
|
69
102
|
default_expr = exp.maybe_parse(col.default)
|
|
@@ -96,14 +129,14 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
96
129
|
|
|
97
130
|
def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
|
|
98
131
|
"""Build SQLGlot expression for a table constraint."""
|
|
99
|
-
if constraint.constraint_type ==
|
|
132
|
+
if constraint.constraint_type == CONSTRAINT_TYPE_PRIMARY_KEY:
|
|
100
133
|
pk_constraint = exp.PrimaryKey(expressions=[exp.to_identifier(col) for col in constraint.columns])
|
|
101
134
|
|
|
102
135
|
if constraint.name:
|
|
103
136
|
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=pk_constraint)
|
|
104
137
|
return pk_constraint
|
|
105
138
|
|
|
106
|
-
if constraint.constraint_type ==
|
|
139
|
+
if constraint.constraint_type == CONSTRAINT_TYPE_FOREIGN_KEY:
|
|
107
140
|
fk_constraint = exp.ForeignKey(
|
|
108
141
|
expressions=[exp.to_identifier(col) for col in constraint.columns],
|
|
109
142
|
reference=exp.Reference(
|
|
@@ -118,14 +151,14 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
|
|
|
118
151
|
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=fk_constraint)
|
|
119
152
|
return fk_constraint
|
|
120
153
|
|
|
121
|
-
if constraint.constraint_type ==
|
|
154
|
+
if constraint.constraint_type == CONSTRAINT_TYPE_UNIQUE:
|
|
122
155
|
unique_constraint = exp.UniqueKeyProperty(expressions=[exp.to_identifier(col) for col in constraint.columns])
|
|
123
156
|
|
|
124
157
|
if constraint.name:
|
|
125
158
|
return exp.Constraint(this=exp.to_identifier(constraint.name), expression=unique_constraint)
|
|
126
159
|
return unique_constraint
|
|
127
160
|
|
|
128
|
-
if constraint.constraint_type ==
|
|
161
|
+
if constraint.constraint_type == CONSTRAINT_TYPE_CHECK:
|
|
129
162
|
check_expr = exp.Check(this=exp.maybe_parse(constraint.condition) if constraint.condition else None)
|
|
130
163
|
|
|
131
164
|
if constraint.name:
|
|
@@ -356,7 +389,7 @@ class CreateTable(DDLBuilder):
|
|
|
356
389
|
|
|
357
390
|
self._columns.append(column_def)
|
|
358
391
|
|
|
359
|
-
if primary_key and not
|
|
392
|
+
if primary_key and not self._has_primary_key_constraint():
|
|
360
393
|
self.primary_key_constraint([name])
|
|
361
394
|
|
|
362
395
|
return self
|
|
@@ -368,13 +401,13 @@ class CreateTable(DDLBuilder):
|
|
|
368
401
|
if not col_list:
|
|
369
402
|
self._raise_sql_builder_error("Primary key must include at least one column")
|
|
370
403
|
|
|
371
|
-
existing_pk =
|
|
404
|
+
existing_pk = self._find_primary_key_constraint()
|
|
372
405
|
if existing_pk:
|
|
373
406
|
for col in col_list:
|
|
374
407
|
if col not in existing_pk.columns:
|
|
375
408
|
existing_pk.columns.append(col)
|
|
376
409
|
else:
|
|
377
|
-
constraint = ConstraintDefinition(constraint_type=
|
|
410
|
+
constraint = ConstraintDefinition(constraint_type=CONSTRAINT_TYPE_PRIMARY_KEY, name=name, columns=col_list)
|
|
378
411
|
self._constraints.append(constraint)
|
|
379
412
|
|
|
380
413
|
return self
|
|
@@ -398,14 +431,11 @@ class CreateTable(DDLBuilder):
|
|
|
398
431
|
if len(col_list) != len(ref_col_list):
|
|
399
432
|
self._raise_sql_builder_error("Foreign key columns and referenced columns must have same length")
|
|
400
433
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
self._raise_sql_builder_error(f"Invalid ON DELETE action: {on_delete}")
|
|
404
|
-
if on_update and on_update.upper() not in valid_actions:
|
|
405
|
-
self._raise_sql_builder_error(f"Invalid ON UPDATE action: {on_update}")
|
|
434
|
+
self._validate_foreign_key_action(on_delete, "ON DELETE")
|
|
435
|
+
self._validate_foreign_key_action(on_update, "ON UPDATE")
|
|
406
436
|
|
|
407
437
|
constraint = ConstraintDefinition(
|
|
408
|
-
constraint_type=
|
|
438
|
+
constraint_type=CONSTRAINT_TYPE_FOREIGN_KEY,
|
|
409
439
|
name=name,
|
|
410
440
|
columns=col_list,
|
|
411
441
|
references_table=references_table,
|
|
@@ -426,7 +456,7 @@ class CreateTable(DDLBuilder):
|
|
|
426
456
|
if not col_list:
|
|
427
457
|
self._raise_sql_builder_error("Unique constraint must include at least one column")
|
|
428
458
|
|
|
429
|
-
constraint = ConstraintDefinition(constraint_type=
|
|
459
|
+
constraint = ConstraintDefinition(constraint_type=CONSTRAINT_TYPE_UNIQUE, name=name, columns=col_list)
|
|
430
460
|
|
|
431
461
|
self._constraints.append(constraint)
|
|
432
462
|
return self
|
|
@@ -443,7 +473,7 @@ class CreateTable(DDLBuilder):
|
|
|
443
473
|
else:
|
|
444
474
|
condition_str = str(condition)
|
|
445
475
|
|
|
446
|
-
constraint = ConstraintDefinition(constraint_type=
|
|
476
|
+
constraint = ConstraintDefinition(constraint_type=CONSTRAINT_TYPE_CHECK, name=name, condition=condition_str)
|
|
447
477
|
|
|
448
478
|
self._constraints.append(constraint)
|
|
449
479
|
return self
|
|
@@ -484,10 +514,8 @@ class CreateTable(DDLBuilder):
|
|
|
484
514
|
column_defs.append(col_expr)
|
|
485
515
|
|
|
486
516
|
for constraint in self._constraints:
|
|
487
|
-
if
|
|
488
|
-
|
|
489
|
-
if any(c.name == col_name and c.primary_key for c in self._columns):
|
|
490
|
-
continue
|
|
517
|
+
if self._is_redundant_single_column_primary_key(constraint):
|
|
518
|
+
continue
|
|
491
519
|
|
|
492
520
|
constraint_expr = build_constraint_expression(constraint)
|
|
493
521
|
if constraint_expr:
|
|
@@ -531,6 +559,27 @@ class CreateTable(DDLBuilder):
|
|
|
531
559
|
like=like_expr,
|
|
532
560
|
)
|
|
533
561
|
|
|
562
|
+
def _has_primary_key_constraint(self) -> bool:
|
|
563
|
+
"""Check if table already has a primary key constraint."""
|
|
564
|
+
return any(c.constraint_type == CONSTRAINT_TYPE_PRIMARY_KEY for c in self._constraints)
|
|
565
|
+
|
|
566
|
+
def _find_primary_key_constraint(self) -> "Optional[ConstraintDefinition]":
|
|
567
|
+
"""Find existing primary key constraint."""
|
|
568
|
+
return next((c for c in self._constraints if c.constraint_type == CONSTRAINT_TYPE_PRIMARY_KEY), None)
|
|
569
|
+
|
|
570
|
+
def _validate_foreign_key_action(self, action: "Optional[str]", action_type: str) -> None:
|
|
571
|
+
"""Validate foreign key action (ON DELETE or ON UPDATE)."""
|
|
572
|
+
if action and action.upper() not in VALID_FOREIGN_KEY_ACTIONS:
|
|
573
|
+
self._raise_sql_builder_error(f"Invalid {action_type} action: {action}")
|
|
574
|
+
|
|
575
|
+
def _is_redundant_single_column_primary_key(self, constraint: "ConstraintDefinition") -> bool:
|
|
576
|
+
"""Check if constraint is a redundant single-column primary key."""
|
|
577
|
+
if constraint.constraint_type != CONSTRAINT_TYPE_PRIMARY_KEY or len(constraint.columns) != 1:
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
col_name = constraint.columns[0]
|
|
581
|
+
return any(c.name == col_name and c.primary_key for c in self._columns)
|
|
582
|
+
|
|
534
583
|
|
|
535
584
|
class DropTable(DDLBuilder):
|
|
536
585
|
"""Builder for DROP TABLE [IF EXISTS] ... [CASCADE|RESTRICT]."""
|
|
@@ -966,8 +1015,6 @@ class CreateTableAsSelect(DDLBuilder):
|
|
|
966
1015
|
|
|
967
1016
|
select_expr = None
|
|
968
1017
|
select_parameters = None
|
|
969
|
-
from sqlspec.builder._select import Select
|
|
970
|
-
from sqlspec.core.statement import SQL
|
|
971
1018
|
|
|
972
1019
|
if isinstance(self._select_query, SQL):
|
|
973
1020
|
select_expr = self._select_query.expression
|
|
@@ -1093,8 +1140,6 @@ class CreateMaterializedView(DDLBuilder):
|
|
|
1093
1140
|
|
|
1094
1141
|
select_expr = None
|
|
1095
1142
|
select_parameters = None
|
|
1096
|
-
from sqlspec.builder._select import Select
|
|
1097
|
-
from sqlspec.core.statement import SQL
|
|
1098
1143
|
|
|
1099
1144
|
if isinstance(self._select_query, SQL):
|
|
1100
1145
|
select_expr = self._select_query.expression
|
|
@@ -1191,8 +1236,6 @@ class CreateView(DDLBuilder):
|
|
|
1191
1236
|
|
|
1192
1237
|
select_expr = None
|
|
1193
1238
|
select_parameters = None
|
|
1194
|
-
from sqlspec.builder._select import Select
|
|
1195
|
-
from sqlspec.core.statement import SQL
|
|
1196
1239
|
|
|
1197
1240
|
if isinstance(self._select_query, SQL):
|
|
1198
1241
|
select_expr = self._select_query.expression
|
|
@@ -1347,8 +1390,7 @@ class AlterTable(DDLBuilder):
|
|
|
1347
1390
|
on_delete: Foreign key ON DELETE action
|
|
1348
1391
|
on_update: Foreign key ON UPDATE action
|
|
1349
1392
|
"""
|
|
1350
|
-
|
|
1351
|
-
if constraint_type.upper() not in valid_types:
|
|
1393
|
+
if constraint_type.upper() not in VALID_CONSTRAINT_TYPES:
|
|
1352
1394
|
self._raise_sql_builder_error(f"Invalid constraint type: {constraint_type}")
|
|
1353
1395
|
|
|
1354
1396
|
col_list = None
|
|
@@ -1361,8 +1403,8 @@ class AlterTable(DDLBuilder):
|
|
|
1361
1403
|
|
|
1362
1404
|
condition_str: Optional[str] = None
|
|
1363
1405
|
if condition is not None:
|
|
1364
|
-
if
|
|
1365
|
-
sqlglot_expr =
|
|
1406
|
+
if has_sqlglot_expression(condition):
|
|
1407
|
+
sqlglot_expr = condition.sqlglot_expression
|
|
1366
1408
|
condition_str = sqlglot_expr.sql(dialect=self.dialect) if sqlglot_expr else str(condition)
|
|
1367
1409
|
else:
|
|
1368
1410
|
condition_str = str(condition)
|
|
@@ -1474,7 +1516,7 @@ class AlterTable(DDLBuilder):
|
|
|
1474
1516
|
default_val = op.column_definition.default
|
|
1475
1517
|
default_expr: Optional[exp.Expression]
|
|
1476
1518
|
if isinstance(default_val, str):
|
|
1477
|
-
if
|
|
1519
|
+
if self._is_sql_function_default(default_val):
|
|
1478
1520
|
default_expr = exp.maybe_parse(default_val)
|
|
1479
1521
|
else:
|
|
1480
1522
|
default_expr = exp.convert(default_val)
|
|
@@ -1494,6 +1536,14 @@ class AlterTable(DDLBuilder):
|
|
|
1494
1536
|
self._raise_sql_builder_error(f"Unknown operation type: {op.operation_type}")
|
|
1495
1537
|
raise AssertionError
|
|
1496
1538
|
|
|
1539
|
+
def _is_sql_function_default(self, default_val: str) -> bool:
|
|
1540
|
+
"""Check if default value is a SQL function or expression."""
|
|
1541
|
+
default_upper = default_val.upper()
|
|
1542
|
+
return (
|
|
1543
|
+
default_upper in {CURRENT_TIMESTAMP_KEYWORD, CURRENT_DATE_KEYWORD, CURRENT_TIME_KEYWORD}
|
|
1544
|
+
or "(" in default_val
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1497
1547
|
|
|
1498
1548
|
class CommentOn(DDLBuilder):
|
|
1499
1549
|
"""Builder for COMMENT ON ... IS ... statements."""
|
sqlspec/builder/_insert.py
CHANGED
|
@@ -10,6 +10,7 @@ from sqlglot import exp
|
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
12
|
from sqlspec.builder._base import QueryBuilder
|
|
13
|
+
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
13
14
|
from sqlspec.builder.mixins import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin, ReturningClauseMixin
|
|
14
15
|
from sqlspec.core.result import SQLResult
|
|
15
16
|
from sqlspec.exceptions import SQLBuilderError
|
|
@@ -46,7 +47,6 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
46
47
|
"""
|
|
47
48
|
super().__init__(**kwargs)
|
|
48
49
|
|
|
49
|
-
# Initialize Insert-specific attributes
|
|
50
50
|
self._table: Optional[str] = None
|
|
51
51
|
self._columns: list[str] = []
|
|
52
52
|
self._values_added_count: int = 0
|
|
@@ -130,7 +130,7 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
130
130
|
|
|
131
131
|
if len(values) == 1:
|
|
132
132
|
values_0 = values[0]
|
|
133
|
-
if
|
|
133
|
+
if isinstance(values_0, dict):
|
|
134
134
|
return self.values_from_dict(values_0)
|
|
135
135
|
|
|
136
136
|
insert_expr = self.get_insert_expression()
|
|
@@ -144,22 +144,8 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
144
144
|
if isinstance(value, exp.Expression):
|
|
145
145
|
value_placeholders.append(value)
|
|
146
146
|
elif has_expression_and_sql(value):
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
150
|
-
# Merge parameters from SQL object into builder
|
|
151
|
-
self._merge_sql_object_parameters(value)
|
|
152
|
-
value_placeholders.append(expression)
|
|
153
|
-
else:
|
|
154
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
155
|
-
sql_text = getattr(value, "sql", "")
|
|
156
|
-
# Merge parameters even when parsing raw SQL
|
|
157
|
-
self._merge_sql_object_parameters(value)
|
|
158
|
-
# Check if sql_text is callable (like Expression.sql method)
|
|
159
|
-
if callable(sql_text):
|
|
160
|
-
sql_text = str(value)
|
|
161
|
-
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
162
|
-
value_placeholders.append(value_expr)
|
|
147
|
+
value_expr = extract_sql_object_expression(value, builder=self)
|
|
148
|
+
value_placeholders.append(value_expr)
|
|
163
149
|
else:
|
|
164
150
|
if self._columns and i < len(self._columns):
|
|
165
151
|
column_str = str(self._columns[i])
|
|
@@ -258,17 +244,14 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
258
244
|
|
|
259
245
|
Example:
|
|
260
246
|
```python
|
|
261
|
-
# ON CONFLICT (id) DO NOTHING
|
|
262
247
|
sql.insert("users").values(id=1, name="John").on_conflict(
|
|
263
248
|
"id"
|
|
264
249
|
).do_nothing()
|
|
265
250
|
|
|
266
|
-
# ON CONFLICT (email, username) DO UPDATE SET updated_at = NOW()
|
|
267
251
|
sql.insert("users").values(...).on_conflict(
|
|
268
252
|
"email", "username"
|
|
269
253
|
).do_update(updated_at=sql.raw("NOW()"))
|
|
270
254
|
|
|
271
|
-
# ON CONFLICT DO NOTHING (catches all conflicts)
|
|
272
255
|
sql.insert("users").values(...).on_conflict().do_nothing()
|
|
273
256
|
```
|
|
274
257
|
"""
|
|
@@ -290,22 +273,41 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
290
273
|
return self.on_conflict(*columns).do_nothing()
|
|
291
274
|
|
|
292
275
|
def on_duplicate_key_update(self, **kwargs: Any) -> "Insert":
|
|
293
|
-
"""Adds
|
|
276
|
+
"""Adds MySQL-style ON DUPLICATE KEY UPDATE clause.
|
|
294
277
|
|
|
295
278
|
Args:
|
|
296
|
-
**kwargs: Column-value pairs to update on
|
|
279
|
+
**kwargs: Column-value pairs to update on duplicate key.
|
|
297
280
|
|
|
298
281
|
Returns:
|
|
299
282
|
The current builder instance for method chaining.
|
|
300
283
|
|
|
301
284
|
Note:
|
|
302
|
-
This method
|
|
303
|
-
|
|
304
|
-
ON DUPLICATE KEY UPDATE, etc.).
|
|
285
|
+
This method creates MySQL-specific ON DUPLICATE KEY UPDATE syntax.
|
|
286
|
+
For PostgreSQL, use on_conflict() instead.
|
|
305
287
|
"""
|
|
306
288
|
if not kwargs:
|
|
307
289
|
return self
|
|
308
|
-
|
|
290
|
+
|
|
291
|
+
insert_expr = self._get_insert_expression()
|
|
292
|
+
|
|
293
|
+
set_expressions = []
|
|
294
|
+
for col, val in kwargs.items():
|
|
295
|
+
if has_expression_and_sql(val):
|
|
296
|
+
value_expr = extract_sql_object_expression(val, builder=self)
|
|
297
|
+
elif isinstance(val, exp.Expression):
|
|
298
|
+
value_expr = val
|
|
299
|
+
else:
|
|
300
|
+
param_name = self.generate_unique_parameter_name(col)
|
|
301
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
302
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
303
|
+
|
|
304
|
+
set_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
305
|
+
|
|
306
|
+
on_conflict = exp.OnConflict(duplicate=True, action=exp.var("UPDATE"), expressions=set_expressions or None)
|
|
307
|
+
|
|
308
|
+
insert_expr.set("conflict", on_conflict)
|
|
309
|
+
|
|
310
|
+
return self
|
|
309
311
|
|
|
310
312
|
|
|
311
313
|
class ConflictBuilder:
|
|
@@ -342,7 +344,6 @@ class ConflictBuilder:
|
|
|
342
344
|
"""
|
|
343
345
|
insert_expr = self._insert_builder.get_insert_expression()
|
|
344
346
|
|
|
345
|
-
# Create ON CONFLICT with proper structure
|
|
346
347
|
conflict_keys = [exp.to_identifier(col) for col in self._columns] if self._columns else None
|
|
347
348
|
on_conflict = exp.OnConflict(conflict_keys=conflict_keys, action=exp.var("DO NOTHING"))
|
|
348
349
|
|
|
@@ -369,42 +370,19 @@ class ConflictBuilder:
|
|
|
369
370
|
"""
|
|
370
371
|
insert_expr = self._insert_builder.get_insert_expression()
|
|
371
372
|
|
|
372
|
-
# Create SET expressions for the UPDATE
|
|
373
373
|
set_expressions = []
|
|
374
374
|
for col, val in kwargs.items():
|
|
375
375
|
if has_expression_and_sql(val):
|
|
376
|
-
|
|
377
|
-
expression = getattr(val, "expression", None)
|
|
378
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
379
|
-
# Merge parameters from SQL object into builder
|
|
380
|
-
if hasattr(val, "parameters"):
|
|
381
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
382
|
-
for param_name, param_value in sql_parameters.items():
|
|
383
|
-
self._insert_builder.add_parameter(param_value, name=param_name)
|
|
384
|
-
value_expr = expression
|
|
385
|
-
else:
|
|
386
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
387
|
-
sql_text = getattr(val, "sql", "")
|
|
388
|
-
# Merge parameters even when parsing raw SQL
|
|
389
|
-
if hasattr(val, "parameters"):
|
|
390
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
391
|
-
for param_name, param_value in sql_parameters.items():
|
|
392
|
-
self._insert_builder.add_parameter(param_value, name=param_name)
|
|
393
|
-
# Check if sql_text is callable (like Expression.sql method)
|
|
394
|
-
if callable(sql_text):
|
|
395
|
-
sql_text = str(val)
|
|
396
|
-
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
376
|
+
value_expr = extract_sql_object_expression(val, builder=self._insert_builder)
|
|
397
377
|
elif isinstance(val, exp.Expression):
|
|
398
378
|
value_expr = val
|
|
399
379
|
else:
|
|
400
|
-
# Create parameter for regular values
|
|
401
380
|
param_name = self._insert_builder.generate_unique_parameter_name(col)
|
|
402
381
|
_, param_name = self._insert_builder.add_parameter(val, name=param_name)
|
|
403
382
|
value_expr = exp.Placeholder(this=param_name)
|
|
404
383
|
|
|
405
384
|
set_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
406
385
|
|
|
407
|
-
# Create ON CONFLICT with proper structure
|
|
408
386
|
conflict_keys = [exp.to_identifier(col) for col in self._columns] if self._columns else None
|
|
409
387
|
on_conflict = exp.OnConflict(
|
|
410
388
|
conflict_keys=conflict_keys, action=exp.var("DO UPDATE"), expressions=set_expressions or None
|