sqlspec 0.14.1__py3-none-any.whl → 0.16.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 +50 -25
- sqlspec/__main__.py +1 -1
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +480 -121
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +115 -260
- sqlspec/adapters/adbc/driver.py +462 -367
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +199 -129
- sqlspec/adapters/aiosqlite/driver.py +230 -269
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -168
- sqlspec/adapters/asyncmy/driver.py +260 -225
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +82 -181
- sqlspec/adapters/asyncpg/driver.py +285 -383
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -258
- sqlspec/adapters/bigquery/driver.py +474 -646
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +415 -351
- sqlspec/adapters/duckdb/driver.py +343 -413
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -379
- sqlspec/adapters/oracledb/driver.py +507 -560
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -254
- sqlspec/adapters/psqlpy/driver.py +505 -234
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -403
- sqlspec/adapters/psycopg/driver.py +706 -872
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +202 -118
- sqlspec/adapters/sqlite/driver.py +264 -303
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder → builder}/_base.py +120 -55
- sqlspec/{statement/builder → builder}/_column.py +17 -6
- sqlspec/{statement/builder → builder}/_ddl.py +46 -79
- sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
- sqlspec/{statement/builder → builder}/_delete.py +6 -25
- sqlspec/{statement/builder → builder}/_insert.py +18 -65
- sqlspec/builder/_merge.py +56 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
- sqlspec/{statement/builder → builder}/_select.py +11 -56
- sqlspec/{statement/builder → builder}/_update.py +12 -18
- sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
- sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
- sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +34 -18
- sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
- sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
- sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
- sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
- sqlspec/{statement/builder → builder}/mixins/_select_operations.py +25 -38
- sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
- sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
- sqlspec/cli.py +4 -5
- sqlspec/config.py +180 -133
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +873 -0
- sqlspec/core/compiler.py +396 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1209 -0
- sqlspec/core/result.py +664 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +666 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +387 -176
- sqlspec/driver/_common.py +527 -289
- sqlspec/driver/_sync.py +390 -172
- sqlspec/driver/mixins/__init__.py +2 -19
- sqlspec/driver/mixins/_result_tools.py +164 -0
- sqlspec/driver/mixins/_sql_translator.py +6 -3
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +18 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +424 -105
- sqlspec/migrations/__init__.py +12 -0
- sqlspec/migrations/base.py +92 -68
- sqlspec/migrations/commands.py +24 -106
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +49 -51
- sqlspec/migrations/tracker.py +31 -44
- sqlspec/migrations/utils.py +64 -24
- sqlspec/protocols.py +7 -183
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -3
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +443 -232
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
- sqlspec-0.16.0.dist-info/RECORD +134 -0
- sqlspec/adapters/adbc/transformers.py +0 -108
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_cache.py +0 -114
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -508
- sqlspec/driver/mixins/_query_tools.py +0 -796
- sqlspec/driver/mixins/_result_utils.py +0 -138
- sqlspec/driver/mixins/_storage.py +0 -912
- sqlspec/driver/mixins/_type_coercion.py +0 -128
- sqlspec/driver/parameters.py +0 -138
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/_merge.py +0 -95
- sqlspec/statement/cache.py +0 -50
- sqlspec/statement/filters.py +0 -625
- sqlspec/statement/parameters.py +0 -956
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -109
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -714
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1774
- sqlspec/utils/cached_property.py +0 -25
- sqlspec/utils/statement_hashing.py +0 -203
- sqlspec-0.14.1.dist-info/RECORD +0 -145
- /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""DDL builders for SQLSpec: DROP, CREATE INDEX, TRUNCATE, etc."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
@@ -7,13 +7,13 @@ from sqlglot import exp
|
|
|
7
7
|
from sqlglot.dialects.dialect import DialectType
|
|
8
8
|
from typing_extensions import Self
|
|
9
9
|
|
|
10
|
-
from sqlspec.
|
|
11
|
-
from sqlspec.
|
|
12
|
-
from sqlspec.
|
|
10
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
11
|
+
from sqlspec.builder._ddl_utils import build_column_expression, build_constraint_expression
|
|
12
|
+
from sqlspec.core.result import SQLResult
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from sqlspec.
|
|
16
|
-
from sqlspec.statement
|
|
15
|
+
from sqlspec.builder._column import ColumnExpression
|
|
16
|
+
from sqlspec.core.statement import SQL, StatementConfig
|
|
17
17
|
|
|
18
18
|
__all__ = (
|
|
19
19
|
"AlterOperation",
|
|
@@ -33,19 +33,18 @@ __all__ = (
|
|
|
33
33
|
"DropTable",
|
|
34
34
|
"DropView",
|
|
35
35
|
"RenameTable",
|
|
36
|
-
"
|
|
36
|
+
"Truncate",
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@dataclass
|
|
41
|
-
class DDLBuilder(QueryBuilder
|
|
41
|
+
class DDLBuilder(QueryBuilder):
|
|
42
42
|
"""Base class for DDL builders (CREATE, DROP, ALTER, etc)."""
|
|
43
43
|
|
|
44
44
|
dialect: DialectType = None
|
|
45
45
|
_expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
|
|
46
46
|
|
|
47
47
|
def __post_init__(self) -> None:
|
|
48
|
-
# Override to prevent QueryBuilder from calling _create_base_expression prematurely
|
|
49
48
|
pass
|
|
50
49
|
|
|
51
50
|
def _create_base_expression(self) -> exp.Expression:
|
|
@@ -53,8 +52,7 @@ class DDLBuilder(QueryBuilder[Any]):
|
|
|
53
52
|
raise NotImplementedError(msg)
|
|
54
53
|
|
|
55
54
|
@property
|
|
56
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
57
|
-
# DDL typically returns no rows; use object for now.
|
|
55
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
58
56
|
return SQLResult
|
|
59
57
|
|
|
60
58
|
def build(self) -> "SafeQuery":
|
|
@@ -62,7 +60,7 @@ class DDLBuilder(QueryBuilder[Any]):
|
|
|
62
60
|
self._expression = self._create_base_expression()
|
|
63
61
|
return super().build()
|
|
64
62
|
|
|
65
|
-
def to_statement(self, config: "Optional[
|
|
63
|
+
def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
66
64
|
return super().to_statement(config=config)
|
|
67
65
|
|
|
68
66
|
|
|
@@ -209,10 +207,8 @@ class CreateTable(DDLBuilder):
|
|
|
209
207
|
|
|
210
208
|
def primary_key_constraint(self, columns: "Union[str, list[str]]", name: "Optional[str]" = None) -> "Self":
|
|
211
209
|
"""Add a primary key constraint."""
|
|
212
|
-
# Normalize column list
|
|
213
210
|
col_list = [columns] if isinstance(columns, str) else list(columns)
|
|
214
211
|
|
|
215
|
-
# Validation
|
|
216
212
|
if not col_list:
|
|
217
213
|
self._raise_sql_builder_error("Primary key must include at least one column")
|
|
218
214
|
|
|
@@ -362,16 +358,11 @@ class CreateTable(DDLBuilder):
|
|
|
362
358
|
if self._tablespace:
|
|
363
359
|
props.append(exp.Property(this=exp.to_identifier("TABLESPACE"), value=exp.to_identifier(self._tablespace)))
|
|
364
360
|
if self._partition_by:
|
|
365
|
-
props.append(
|
|
366
|
-
exp.Property(this=exp.to_identifier("PARTITION BY"), value=exp.Literal.string(self._partition_by))
|
|
367
|
-
)
|
|
361
|
+
props.append(exp.Property(this=exp.to_identifier("PARTITION BY"), value=exp.convert(self._partition_by)))
|
|
368
362
|
|
|
369
363
|
for key, value in self._table_options.items():
|
|
370
364
|
if key != "engine": # Skip already handled options
|
|
371
|
-
|
|
372
|
-
props.append(exp.Property(this=exp.to_identifier(key.upper()), value=exp.Literal.string(value)))
|
|
373
|
-
else:
|
|
374
|
-
props.append(exp.Property(this=exp.to_identifier(key.upper()), value=exp.Literal.number(value)))
|
|
365
|
+
props.append(exp.Property(this=exp.to_identifier(key.upper()), value=exp.convert(value)))
|
|
375
366
|
|
|
376
367
|
properties_node = exp.Properties(expressions=props) if props else None
|
|
377
368
|
|
|
@@ -651,7 +642,7 @@ class CreateIndex(DDLBuilder):
|
|
|
651
642
|
|
|
652
643
|
# --- TRUNCATE TABLE ---
|
|
653
644
|
@dataclass
|
|
654
|
-
class
|
|
645
|
+
class Truncate(DDLBuilder):
|
|
655
646
|
"""Builder for TRUNCATE TABLE ... [CASCADE|RESTRICT] [RESTART IDENTITY|CONTINUE IDENTITY]."""
|
|
656
647
|
|
|
657
648
|
_table_name: Optional[str] = None
|
|
@@ -791,16 +782,16 @@ class CreateTableAsSelect(DDLBuilder):
|
|
|
791
782
|
self._raise_sql_builder_error("SELECT query must be set for CREATE TABLE AS SELECT.")
|
|
792
783
|
|
|
793
784
|
select_expr = None
|
|
794
|
-
|
|
795
|
-
from sqlspec.
|
|
796
|
-
from sqlspec.statement
|
|
785
|
+
select_parameters = None
|
|
786
|
+
from sqlspec.builder._select import Select
|
|
787
|
+
from sqlspec.core.statement import SQL
|
|
797
788
|
|
|
798
789
|
if isinstance(self._select_query, SQL):
|
|
799
790
|
select_expr = self._select_query.expression
|
|
800
|
-
|
|
791
|
+
select_parameters = getattr(self._select_query, "parameters", None)
|
|
801
792
|
elif isinstance(self._select_query, Select):
|
|
802
793
|
select_expr = getattr(self._select_query, "_expression", None)
|
|
803
|
-
|
|
794
|
+
select_parameters = getattr(self._select_query, "_parameters", None)
|
|
804
795
|
|
|
805
796
|
with_ctes = getattr(self._select_query, "_with_ctes", {})
|
|
806
797
|
if with_ctes and select_expr and isinstance(select_expr, exp.Select):
|
|
@@ -813,15 +804,15 @@ class CreateTableAsSelect(DDLBuilder):
|
|
|
813
804
|
)
|
|
814
805
|
elif isinstance(self._select_query, str):
|
|
815
806
|
select_expr = exp.maybe_parse(self._select_query)
|
|
816
|
-
|
|
807
|
+
select_parameters = None
|
|
817
808
|
else:
|
|
818
809
|
self._raise_sql_builder_error("Unsupported type for SELECT query in CTAS.")
|
|
819
810
|
if select_expr is None:
|
|
820
811
|
self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
|
|
821
812
|
|
|
822
813
|
# Merge parameters from SELECT if present
|
|
823
|
-
if
|
|
824
|
-
for p_name, p_value in
|
|
814
|
+
if select_parameters:
|
|
815
|
+
for p_name, p_value in select_parameters.items():
|
|
825
816
|
# Always preserve the original parameter name
|
|
826
817
|
# The SELECT query already has unique parameter names
|
|
827
818
|
self._parameters[p_name] = p_value
|
|
@@ -908,27 +899,27 @@ class CreateMaterializedView(DDLBuilder):
|
|
|
908
899
|
self._raise_sql_builder_error("SELECT query must be set for CREATE MATERIALIZED VIEW.")
|
|
909
900
|
|
|
910
901
|
select_expr = None
|
|
911
|
-
|
|
912
|
-
from sqlspec.
|
|
913
|
-
from sqlspec.statement
|
|
902
|
+
select_parameters = None
|
|
903
|
+
from sqlspec.builder._select import Select
|
|
904
|
+
from sqlspec.core.statement import SQL
|
|
914
905
|
|
|
915
906
|
if isinstance(self._select_query, SQL):
|
|
916
907
|
select_expr = self._select_query.expression
|
|
917
|
-
|
|
908
|
+
select_parameters = getattr(self._select_query, "parameters", None)
|
|
918
909
|
elif isinstance(self._select_query, Select):
|
|
919
910
|
select_expr = getattr(self._select_query, "_expression", None)
|
|
920
|
-
|
|
911
|
+
select_parameters = getattr(self._select_query, "_parameters", None)
|
|
921
912
|
elif isinstance(self._select_query, str):
|
|
922
913
|
select_expr = exp.maybe_parse(self._select_query)
|
|
923
|
-
|
|
914
|
+
select_parameters = None
|
|
924
915
|
else:
|
|
925
916
|
self._raise_sql_builder_error("Unsupported type for SELECT query in materialized view.")
|
|
926
917
|
if select_expr is None or not isinstance(select_expr, exp.Select):
|
|
927
918
|
self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
|
|
928
919
|
|
|
929
920
|
# Merge parameters from SELECT if present
|
|
930
|
-
if
|
|
931
|
-
for p_name, p_value in
|
|
921
|
+
if select_parameters:
|
|
922
|
+
for p_name, p_value in select_parameters.items():
|
|
932
923
|
# Always preserve the original parameter name
|
|
933
924
|
# The SELECT query already has unique parameter names
|
|
934
925
|
self._parameters[p_name] = p_value
|
|
@@ -939,9 +930,7 @@ class CreateMaterializedView(DDLBuilder):
|
|
|
939
930
|
|
|
940
931
|
props: list[exp.Property] = []
|
|
941
932
|
if self._refresh_mode:
|
|
942
|
-
props.append(
|
|
943
|
-
exp.Property(this=exp.to_identifier("REFRESH_MODE"), value=exp.Literal.string(self._refresh_mode))
|
|
944
|
-
)
|
|
933
|
+
props.append(exp.Property(this=exp.to_identifier("REFRESH_MODE"), value=exp.convert(self._refresh_mode)))
|
|
945
934
|
if self._tablespace:
|
|
946
935
|
props.append(exp.Property(this=exp.to_identifier("TABLESPACE"), value=exp.to_identifier(self._tablespace)))
|
|
947
936
|
if self._using_index:
|
|
@@ -949,12 +938,10 @@ class CreateMaterializedView(DDLBuilder):
|
|
|
949
938
|
exp.Property(this=exp.to_identifier("USING_INDEX"), value=exp.to_identifier(self._using_index))
|
|
950
939
|
)
|
|
951
940
|
for k, v in self._storage_parameters.items():
|
|
952
|
-
props.append(exp.Property(this=exp.to_identifier(k), value=exp.
|
|
941
|
+
props.append(exp.Property(this=exp.to_identifier(k), value=exp.convert(str(v))))
|
|
953
942
|
if self._with_data is not None:
|
|
954
943
|
props.append(exp.Property(this=exp.to_identifier("WITH_DATA" if self._with_data else "NO_DATA")))
|
|
955
|
-
props.extend(
|
|
956
|
-
exp.Property(this=exp.to_identifier("HINT"), value=exp.Literal.string(hint)) for hint in self._hints
|
|
957
|
-
)
|
|
944
|
+
props.extend(exp.Property(this=exp.to_identifier("HINT"), value=exp.convert(hint)) for hint in self._hints)
|
|
958
945
|
properties_node = exp.Properties(expressions=props) if props else None
|
|
959
946
|
|
|
960
947
|
return exp.Create(
|
|
@@ -1007,27 +994,27 @@ class CreateView(DDLBuilder):
|
|
|
1007
994
|
self._raise_sql_builder_error("SELECT query must be set for CREATE VIEW.")
|
|
1008
995
|
|
|
1009
996
|
select_expr = None
|
|
1010
|
-
|
|
1011
|
-
from sqlspec.
|
|
1012
|
-
from sqlspec.statement
|
|
997
|
+
select_parameters = None
|
|
998
|
+
from sqlspec.builder._select import Select
|
|
999
|
+
from sqlspec.core.statement import SQL
|
|
1013
1000
|
|
|
1014
1001
|
if isinstance(self._select_query, SQL):
|
|
1015
1002
|
select_expr = self._select_query.expression
|
|
1016
|
-
|
|
1003
|
+
select_parameters = getattr(self._select_query, "parameters", None)
|
|
1017
1004
|
elif isinstance(self._select_query, Select):
|
|
1018
1005
|
select_expr = getattr(self._select_query, "_expression", None)
|
|
1019
|
-
|
|
1006
|
+
select_parameters = getattr(self._select_query, "_parameters", None)
|
|
1020
1007
|
elif isinstance(self._select_query, str):
|
|
1021
1008
|
select_expr = exp.maybe_parse(self._select_query)
|
|
1022
|
-
|
|
1009
|
+
select_parameters = None
|
|
1023
1010
|
else:
|
|
1024
1011
|
self._raise_sql_builder_error("Unsupported type for SELECT query in view.")
|
|
1025
1012
|
if select_expr is None or not isinstance(select_expr, exp.Select):
|
|
1026
1013
|
self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
|
|
1027
1014
|
|
|
1028
1015
|
# Merge parameters from SELECT if present
|
|
1029
|
-
if
|
|
1030
|
-
for p_name, p_value in
|
|
1016
|
+
if select_parameters:
|
|
1017
|
+
for p_name, p_value in select_parameters.items():
|
|
1031
1018
|
# Always preserve the original parameter name
|
|
1032
1019
|
# The SELECT query already has unique parameter names
|
|
1033
1020
|
self._parameters[p_name] = p_value
|
|
@@ -1037,7 +1024,7 @@ class CreateView(DDLBuilder):
|
|
|
1037
1024
|
schema_expr = exp.Schema(expressions=[exp.column(c) for c in self._columns])
|
|
1038
1025
|
|
|
1039
1026
|
props: list[exp.Property] = [
|
|
1040
|
-
exp.Property(this=exp.to_identifier("HINT"), value=exp.
|
|
1027
|
+
exp.Property(this=exp.to_identifier("HINT"), value=exp.convert(h)) for h in self._hints
|
|
1041
1028
|
]
|
|
1042
1029
|
properties_node = exp.Properties(expressions=props) if props else None
|
|
1043
1030
|
|
|
@@ -1238,24 +1225,6 @@ class AlterTable(DDLBuilder):
|
|
|
1238
1225
|
self._operations.append(operation)
|
|
1239
1226
|
return self
|
|
1240
1227
|
|
|
1241
|
-
def set_default(self, column: str, default: "Any") -> "Self":
|
|
1242
|
-
"""Set default value for a column."""
|
|
1243
|
-
operation = AlterOperation(
|
|
1244
|
-
operation_type="ALTER COLUMN SET DEFAULT",
|
|
1245
|
-
column_name=column,
|
|
1246
|
-
column_definition=ColumnDefinition(name=column, dtype="", default=default),
|
|
1247
|
-
)
|
|
1248
|
-
|
|
1249
|
-
self._operations.append(operation)
|
|
1250
|
-
return self
|
|
1251
|
-
|
|
1252
|
-
def drop_default(self, column: str) -> "Self":
|
|
1253
|
-
"""Remove default value from a column."""
|
|
1254
|
-
operation = AlterOperation(operation_type="ALTER COLUMN DROP DEFAULT", column_name=column)
|
|
1255
|
-
|
|
1256
|
-
self._operations.append(operation)
|
|
1257
|
-
return self
|
|
1258
|
-
|
|
1259
1228
|
def _create_base_expression(self) -> "exp.Expression":
|
|
1260
1229
|
"""Create the SQLGlot expression for ALTER TABLE."""
|
|
1261
1230
|
if not self._operations:
|
|
@@ -1327,15 +1296,15 @@ class AlterTable(DDLBuilder):
|
|
|
1327
1296
|
if default_val.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in default_val:
|
|
1328
1297
|
default_expr = exp.maybe_parse(default_val)
|
|
1329
1298
|
else:
|
|
1330
|
-
default_expr = exp.
|
|
1299
|
+
default_expr = exp.convert(default_val)
|
|
1331
1300
|
elif isinstance(default_val, (int, float)):
|
|
1332
|
-
default_expr = exp.
|
|
1301
|
+
default_expr = exp.convert(default_val)
|
|
1333
1302
|
elif default_val is True:
|
|
1334
1303
|
default_expr = exp.true()
|
|
1335
1304
|
elif default_val is False:
|
|
1336
1305
|
default_expr = exp.false()
|
|
1337
1306
|
else:
|
|
1338
|
-
default_expr = exp.
|
|
1307
|
+
default_expr = exp.convert(str(default_val))
|
|
1339
1308
|
return exp.AlterColumn(this=exp.to_identifier(op.column_name), default=default_expr)
|
|
1340
1309
|
|
|
1341
1310
|
if op_type == "ALTER COLUMN DROP DEFAULT":
|
|
@@ -1375,14 +1344,12 @@ class CommentOn(DDLBuilder):
|
|
|
1375
1344
|
|
|
1376
1345
|
def _create_base_expression(self) -> exp.Expression:
|
|
1377
1346
|
if self._target_type == "TABLE" and self._table and self._comment is not None:
|
|
1378
|
-
return exp.Comment(
|
|
1379
|
-
this=exp.to_table(self._table), kind="TABLE", expression=exp.Literal.string(self._comment)
|
|
1380
|
-
)
|
|
1347
|
+
return exp.Comment(this=exp.to_table(self._table), kind="TABLE", expression=exp.convert(self._comment))
|
|
1381
1348
|
if self._target_type == "COLUMN" and self._table and self._column and self._comment is not None:
|
|
1382
1349
|
return exp.Comment(
|
|
1383
1350
|
this=exp.Column(table=self._table, this=self._column),
|
|
1384
1351
|
kind="COLUMN",
|
|
1385
|
-
expression=exp.
|
|
1352
|
+
expression=exp.convert(self._comment),
|
|
1386
1353
|
)
|
|
1387
1354
|
self._raise_sql_builder_error("Must specify target and comment for COMMENT ON statement.")
|
|
1388
1355
|
raise AssertionError # This line is unreachable but satisfies the linter
|
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional
|
|
|
5
5
|
from sqlglot import exp
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
from sqlspec.
|
|
8
|
+
from sqlspec.builder._ddl import ColumnDefinition, ConstraintDefinition
|
|
9
9
|
|
|
10
10
|
__all__ = ("build_column_expression", "build_constraint_expression")
|
|
11
11
|
|
|
@@ -31,15 +31,10 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
31
31
|
if col.default.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in col.default:
|
|
32
32
|
default_expr = exp.maybe_parse(col.default)
|
|
33
33
|
else:
|
|
34
|
-
default_expr = exp.
|
|
35
|
-
elif isinstance(col.default, (int, float)):
|
|
36
|
-
default_expr = exp.Literal.number(col.default)
|
|
37
|
-
elif col.default is True:
|
|
38
|
-
default_expr = exp.true()
|
|
39
|
-
elif col.default is False:
|
|
40
|
-
default_expr = exp.false()
|
|
34
|
+
default_expr = exp.convert(col.default)
|
|
41
35
|
else:
|
|
42
|
-
|
|
36
|
+
# Use exp.convert for all other types (int, float, bool, None, etc.)
|
|
37
|
+
default_expr = exp.convert(col.default)
|
|
43
38
|
|
|
44
39
|
constraints.append(exp.ColumnConstraint(kind=default_expr))
|
|
45
40
|
|
|
@@ -48,7 +43,7 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
48
43
|
constraints.append(exp.ColumnConstraint(kind=check_expr))
|
|
49
44
|
|
|
50
45
|
if col.comment:
|
|
51
|
-
constraints.append(exp.ColumnConstraint(kind=exp.CommentColumnConstraint(this=exp.
|
|
46
|
+
constraints.append(exp.ColumnConstraint(kind=exp.CommentColumnConstraint(this=exp.convert(col.comment))))
|
|
52
47
|
|
|
53
48
|
if col.generated:
|
|
54
49
|
generated_expr = exp.GeneratedAsIdentityColumnConstraint(this=exp.maybe_parse(col.generated))
|
|
@@ -9,38 +9,20 @@ from typing import Any, Optional
|
|
|
9
9
|
|
|
10
10
|
from sqlglot import exp
|
|
11
11
|
|
|
12
|
-
from sqlspec.
|
|
13
|
-
from sqlspec.
|
|
14
|
-
from sqlspec.
|
|
15
|
-
from sqlspec.typing import RowT
|
|
12
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
13
|
+
from sqlspec.builder.mixins import DeleteFromClauseMixin, ReturningClauseMixin, WhereClauseMixin
|
|
14
|
+
from sqlspec.core.result import SQLResult
|
|
16
15
|
|
|
17
16
|
__all__ = ("Delete",)
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
@dataclass(unsafe_hash=True)
|
|
21
|
-
class Delete(QueryBuilder
|
|
20
|
+
class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromClauseMixin):
|
|
22
21
|
"""Builder for DELETE statements.
|
|
23
22
|
|
|
24
23
|
This builder provides a fluent interface for constructing SQL DELETE statements
|
|
25
24
|
with automatic parameter binding and validation. It does not support JOIN
|
|
26
25
|
operations to maintain cross-dialect compatibility and safety.
|
|
27
|
-
|
|
28
|
-
Example:
|
|
29
|
-
```python
|
|
30
|
-
# Basic DELETE
|
|
31
|
-
delete_query = Delete().from_("users").where("age < 18")
|
|
32
|
-
|
|
33
|
-
# Even more concise with constructor
|
|
34
|
-
delete_query = Delete("users").where("age < 18")
|
|
35
|
-
|
|
36
|
-
# DELETE with parameterized conditions
|
|
37
|
-
delete_query = (
|
|
38
|
-
Delete()
|
|
39
|
-
.from_("users")
|
|
40
|
-
.where_eq("status", "inactive")
|
|
41
|
-
.where_in("category", ["test", "demo"])
|
|
42
|
-
)
|
|
43
|
-
```
|
|
44
26
|
"""
|
|
45
27
|
|
|
46
28
|
_table: "Optional[str]" = field(default=None, init=False)
|
|
@@ -54,20 +36,19 @@ class Delete(QueryBuilder[RowT], WhereClauseMixin, ReturningClauseMixin, DeleteF
|
|
|
54
36
|
"""
|
|
55
37
|
super().__init__(**kwargs)
|
|
56
38
|
|
|
57
|
-
# Initialize fields from dataclass
|
|
58
39
|
self._table = None
|
|
59
40
|
|
|
60
41
|
if table:
|
|
61
42
|
self.from_(table)
|
|
62
43
|
|
|
63
44
|
@property
|
|
64
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
45
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
65
46
|
"""Get the expected result type for DELETE operations.
|
|
66
47
|
|
|
67
48
|
Returns:
|
|
68
49
|
The ExecuteResult type for DELETE statements.
|
|
69
50
|
"""
|
|
70
|
-
return SQLResult
|
|
51
|
+
return SQLResult
|
|
71
52
|
|
|
72
53
|
def _create_base_expression(self) -> "exp.Delete":
|
|
73
54
|
"""Create a new sqlglot Delete expression.
|
|
@@ -10,16 +10,10 @@ from typing import TYPE_CHECKING, Any, Optional
|
|
|
10
10
|
from sqlglot import exp
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
|
|
13
|
+
from sqlspec.builder._base import QueryBuilder
|
|
14
|
+
from sqlspec.builder.mixins import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin, ReturningClauseMixin
|
|
15
|
+
from sqlspec.core.result import SQLResult
|
|
13
16
|
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
17
|
|
|
24
18
|
if TYPE_CHECKING:
|
|
25
19
|
from collections.abc import Mapping, Sequence
|
|
@@ -36,57 +30,11 @@ ERR_MSG_EXPRESSION_NOT_INITIALIZED = "Internal error: base expression not initia
|
|
|
36
30
|
|
|
37
31
|
|
|
38
32
|
@dataclass(unsafe_hash=True)
|
|
39
|
-
class Insert(QueryBuilder
|
|
33
|
+
class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSelectMixin, InsertIntoClauseMixin):
|
|
40
34
|
"""Builder for INSERT statements.
|
|
41
35
|
|
|
42
36
|
This builder facilitates the construction of SQL INSERT queries
|
|
43
37
|
in a safe and dialect-agnostic manner with automatic parameter binding.
|
|
44
|
-
|
|
45
|
-
Example:
|
|
46
|
-
```python
|
|
47
|
-
# Basic INSERT with values
|
|
48
|
-
insert_query = (
|
|
49
|
-
Insert()
|
|
50
|
-
.into("users")
|
|
51
|
-
.columns("name", "email", "age")
|
|
52
|
-
.values("John Doe", "john@example.com", 30)
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
# Even more concise with constructor
|
|
56
|
-
insert_query = Insert("users").values(
|
|
57
|
-
{"name": "John", "age": 30}
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
# Multi-row INSERT
|
|
61
|
-
insert_query = (
|
|
62
|
-
Insert()
|
|
63
|
-
.into("users")
|
|
64
|
-
.columns("name", "email")
|
|
65
|
-
.values("John", "john@example.com")
|
|
66
|
-
.values("Jane", "jane@example.com")
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# INSERT from dictionary
|
|
70
|
-
insert_query = (
|
|
71
|
-
Insert()
|
|
72
|
-
.into("users")
|
|
73
|
-
.values_from_dict(
|
|
74
|
-
{"name": "John", "email": "john@example.com"}
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
# INSERT from SELECT
|
|
79
|
-
insert_query = (
|
|
80
|
-
Insert()
|
|
81
|
-
.into("users_backup")
|
|
82
|
-
.from_select(
|
|
83
|
-
Select()
|
|
84
|
-
.select("name", "email")
|
|
85
|
-
.from_("users")
|
|
86
|
-
.where("active = true")
|
|
87
|
-
)
|
|
88
|
-
)
|
|
89
|
-
```
|
|
90
38
|
"""
|
|
91
39
|
|
|
92
40
|
_table: "Optional[str]" = field(default=None, init=False)
|
|
@@ -102,7 +50,6 @@ class Insert(QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, Insert
|
|
|
102
50
|
"""
|
|
103
51
|
super().__init__(**kwargs)
|
|
104
52
|
|
|
105
|
-
# Initialize fields from dataclass
|
|
106
53
|
self._table = None
|
|
107
54
|
self._columns = []
|
|
108
55
|
self._values_added_count = 0
|
|
@@ -121,13 +68,13 @@ class Insert(QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, Insert
|
|
|
121
68
|
return exp.Insert()
|
|
122
69
|
|
|
123
70
|
@property
|
|
124
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
71
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
125
72
|
"""Specifies the expected result type for an INSERT query.
|
|
126
73
|
|
|
127
74
|
Returns:
|
|
128
75
|
The type of result expected for INSERT operations.
|
|
129
76
|
"""
|
|
130
|
-
return SQLResult
|
|
77
|
+
return SQLResult
|
|
131
78
|
|
|
132
79
|
def _get_insert_expression(self) -> exp.Insert:
|
|
133
80
|
"""Safely gets and casts the internal expression to exp.Insert.
|
|
@@ -172,7 +119,18 @@ class Insert(QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, Insert
|
|
|
172
119
|
msg = ERR_MSG_VALUES_COLUMNS_MISMATCH.format(values_len=len(values), columns_len=len(self._columns))
|
|
173
120
|
raise SQLBuilderError(msg)
|
|
174
121
|
|
|
175
|
-
param_names = [
|
|
122
|
+
param_names = []
|
|
123
|
+
for i, value in enumerate(values):
|
|
124
|
+
# Try to use column name if available, otherwise use position-based name
|
|
125
|
+
if self._columns and i < len(self._columns):
|
|
126
|
+
column_name = (
|
|
127
|
+
str(self._columns[i]).split(".")[-1] if "." in str(self._columns[i]) else str(self._columns[i])
|
|
128
|
+
)
|
|
129
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
130
|
+
else:
|
|
131
|
+
param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
|
|
132
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
133
|
+
param_names.append(param_name)
|
|
176
134
|
value_placeholders = tuple(exp.var(name) for name in param_names)
|
|
177
135
|
|
|
178
136
|
current_values_expression = insert_expr.args.get("expression")
|
|
@@ -183,8 +141,6 @@ class Insert(QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, Insert
|
|
|
183
141
|
elif isinstance(current_values_expression, exp.Values):
|
|
184
142
|
current_values_expression.expressions.append(exp.Tuple(expressions=list(value_placeholders)))
|
|
185
143
|
else:
|
|
186
|
-
# This case should ideally not be reached if logic is correct:
|
|
187
|
-
# means _values_added_count > 0 but expression is not exp.Values.
|
|
188
144
|
new_values_node = exp.Values(expressions=[exp.Tuple(expressions=list(value_placeholders))])
|
|
189
145
|
insert_expr.set("expression", new_values_node)
|
|
190
146
|
|
|
@@ -212,7 +168,6 @@ class Insert(QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, Insert
|
|
|
212
168
|
if not self._columns:
|
|
213
169
|
self.columns(*data.keys())
|
|
214
170
|
elif set(self._columns) != set(data.keys()):
|
|
215
|
-
# Verify that dictionary keys match existing columns
|
|
216
171
|
msg = f"Dictionary keys {set(data.keys())} do not match existing columns {set(self._columns)}."
|
|
217
172
|
raise SQLBuilderError(msg)
|
|
218
173
|
|
|
@@ -267,12 +222,10 @@ class Insert(QueryBuilder[RowT], ReturningClauseMixin, InsertValuesMixin, Insert
|
|
|
267
222
|
For a more general solution, you might need dialect-specific handling.
|
|
268
223
|
"""
|
|
269
224
|
insert_expr = self._get_insert_expression()
|
|
270
|
-
# Using sqlglot's OnConflict expression if available
|
|
271
225
|
try:
|
|
272
226
|
on_conflict = exp.OnConflict(this=None, expressions=[])
|
|
273
227
|
insert_expr.set("on", on_conflict)
|
|
274
228
|
except AttributeError:
|
|
275
|
-
# Fallback for older sqlglot versions
|
|
276
229
|
pass
|
|
277
230
|
return self
|
|
278
231
|
|
|
@@ -0,0 +1,56 @@
|
|
|
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.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
|
+
@dataclass(unsafe_hash=True)
|
|
26
|
+
class Merge(
|
|
27
|
+
QueryBuilder,
|
|
28
|
+
MergeUsingClauseMixin,
|
|
29
|
+
MergeOnClauseMixin,
|
|
30
|
+
MergeMatchedClauseMixin,
|
|
31
|
+
MergeNotMatchedClauseMixin,
|
|
32
|
+
MergeIntoClauseMixin,
|
|
33
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
34
|
+
):
|
|
35
|
+
"""Builder for MERGE statements.
|
|
36
|
+
|
|
37
|
+
This builder provides a fluent interface for constructing SQL MERGE statements
|
|
38
|
+
(also known as UPSERT in some databases) with automatic parameter binding and validation.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
43
|
+
"""Return the expected result type for this builder.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The SQLResult type for MERGE statements.
|
|
47
|
+
"""
|
|
48
|
+
return SQLResult
|
|
49
|
+
|
|
50
|
+
def _create_base_expression(self) -> "exp.Merge":
|
|
51
|
+
"""Create a base MERGE expression.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A new sqlglot Merge expression with empty clauses.
|
|
55
|
+
"""
|
|
56
|
+
return exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|