sqlspec 0.16.0__cp310-cp310-macosx_13_0_x86_64.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.

Files changed (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-310-darwin.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1347 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +440 -0
  52. sqlspec/builder/_column.py +324 -0
  53. sqlspec/builder/_ddl.py +1383 -0
  54. sqlspec/builder/_ddl_utils.py +104 -0
  55. sqlspec/builder/_delete.py +77 -0
  56. sqlspec/builder/_insert.py +241 -0
  57. sqlspec/builder/_merge.py +56 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +174 -0
  60. sqlspec/builder/_update.py +186 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +195 -0
  63. sqlspec/builder/mixins/_delete_operations.py +36 -0
  64. sqlspec/builder/mixins/_insert_operations.py +152 -0
  65. sqlspec/builder/mixins/_join_operations.py +115 -0
  66. sqlspec/builder/mixins/_merge_operations.py +416 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +123 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +144 -0
  69. sqlspec/builder/mixins/_select_operations.py +599 -0
  70. sqlspec/builder/mixins/_update_operations.py +164 -0
  71. sqlspec/builder/mixins/_where_clause.py +609 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-310-darwin.so +0 -0
  76. sqlspec/core/cache.py +873 -0
  77. sqlspec/core/compiler.cpython-310-darwin.so +0 -0
  78. sqlspec/core/compiler.py +396 -0
  79. sqlspec/core/filters.cpython-310-darwin.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-310-darwin.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-310-darwin.so +0 -0
  84. sqlspec/core/parameters.py +1209 -0
  85. sqlspec/core/result.cpython-310-darwin.so +0 -0
  86. sqlspec/core/result.py +664 -0
  87. sqlspec/core/splitter.cpython-310-darwin.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-310-darwin.so +0 -0
  90. sqlspec/core/statement.py +666 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +472 -0
  93. sqlspec/driver/_common.py +612 -0
  94. sqlspec/driver/_sync.py +473 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +164 -0
  97. sqlspec/driver/mixins/_sql_translator.py +36 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-310-darwin.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +400 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-310-darwin.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-310-darwin.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-310-darwin.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-310-darwin.so +0 -0
  142. sqlspec/utils/type_guards.py +1135 -0
  143. sqlspec-0.16.0.dist-info/METADATA +365 -0
  144. sqlspec-0.16.0.dist-info/RECORD +148 -0
  145. sqlspec-0.16.0.dist-info/WHEEL +4 -0
  146. sqlspec-0.16.0.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.0.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.0.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,152 @@
1
+ """Insert operation mixins for SQL builders."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any, Optional, Union
5
+
6
+ from sqlglot import exp
7
+ from typing_extensions import Self
8
+
9
+ from sqlspec.exceptions import SQLBuilderError
10
+
11
+ __all__ = ("InsertFromSelectMixin", "InsertIntoClauseMixin", "InsertValuesMixin")
12
+
13
+
14
+ class InsertIntoClauseMixin:
15
+ """Mixin providing INTO clause for INSERT builders."""
16
+
17
+ _expression: Optional[exp.Expression] = None
18
+
19
+ def into(self, table: str) -> Self:
20
+ """Set the target table for the INSERT statement.
21
+
22
+ Args:
23
+ table: The name of the table to insert data into.
24
+
25
+ Raises:
26
+ SQLBuilderError: If the current expression is not an INSERT statement.
27
+
28
+ Returns:
29
+ The current builder instance for method chaining.
30
+ """
31
+ if self._expression is None:
32
+ self._expression = exp.Insert()
33
+ if not isinstance(self._expression, exp.Insert):
34
+ msg = "Cannot set target table on a non-INSERT expression."
35
+ raise SQLBuilderError(msg)
36
+
37
+ setattr(self, "_table", table)
38
+ self._expression.set("this", exp.to_table(table))
39
+ return self
40
+
41
+
42
+ class InsertValuesMixin:
43
+ """Mixin providing VALUES and columns methods for INSERT builders."""
44
+
45
+ _expression: Optional[exp.Expression] = None
46
+
47
+ def columns(self, *columns: Union[str, exp.Expression]) -> Self:
48
+ """Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
49
+ if self._expression is None:
50
+ self._expression = exp.Insert()
51
+ if not isinstance(self._expression, exp.Insert):
52
+ msg = "Cannot set columns on a non-INSERT expression."
53
+ raise SQLBuilderError(msg)
54
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
55
+ self._expression.set("columns", column_exprs)
56
+ try:
57
+ cols = self._columns # type: ignore[attr-defined]
58
+ if not columns:
59
+ cols.clear()
60
+ else:
61
+ cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
62
+ except AttributeError:
63
+ pass
64
+ return self
65
+
66
+ def values(self, *values: Any) -> Self:
67
+ """Add a row of values to the INSERT statement, validating against _columns if set."""
68
+ if self._expression is None:
69
+ self._expression = exp.Insert()
70
+ if not isinstance(self._expression, exp.Insert):
71
+ msg = "Cannot add values to a non-INSERT expression."
72
+ raise SQLBuilderError(msg)
73
+ try:
74
+ _columns = self._columns # type: ignore[attr-defined]
75
+ if _columns and len(values) != len(_columns):
76
+ msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
77
+ raise SQLBuilderError(msg)
78
+ except AttributeError:
79
+ pass
80
+ row_exprs = []
81
+ for i, v in enumerate(values):
82
+ if isinstance(v, exp.Expression):
83
+ row_exprs.append(v)
84
+ else:
85
+ # Try to use column name if available, otherwise use position-based name
86
+ try:
87
+ _columns = self._columns # type: ignore[attr-defined]
88
+ if _columns and i < len(_columns):
89
+ column_name = str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
90
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
91
+ else:
92
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
93
+ except AttributeError:
94
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
95
+ _, param_name = self.add_parameter(v, name=param_name) # type: ignore[attr-defined]
96
+ row_exprs.append(exp.var(param_name))
97
+ values_expr = exp.Values(expressions=[row_exprs])
98
+ self._expression.set("expression", values_expr)
99
+ return self
100
+
101
+ def add_values(self, values: Sequence[Any]) -> Self:
102
+ """Add a row of values to the INSERT statement (alternative signature).
103
+
104
+ Args:
105
+ values: Sequence of values for the row.
106
+
107
+ Returns:
108
+ The current builder instance for method chaining.
109
+ """
110
+ return self.values(*values)
111
+
112
+
113
+ class InsertFromSelectMixin:
114
+ """Mixin providing INSERT ... SELECT support for INSERT builders."""
115
+
116
+ _expression: Optional[exp.Expression] = None
117
+
118
+ def from_select(self, select_builder: Any) -> Self:
119
+ """Sets the INSERT source to a SELECT statement.
120
+
121
+ Args:
122
+ select_builder: A SelectBuilder instance representing the SELECT query.
123
+
124
+ Returns:
125
+ The current builder instance for method chaining.
126
+
127
+ Raises:
128
+ SQLBuilderError: If the table is not set or the select_builder is invalid.
129
+ """
130
+ try:
131
+ if not self._table: # type: ignore[attr-defined]
132
+ msg = "The target table must be set using .into() before adding values."
133
+ raise SQLBuilderError(msg)
134
+ except AttributeError:
135
+ msg = "The target table must be set using .into() before adding values."
136
+ raise SQLBuilderError(msg)
137
+ if self._expression is None:
138
+ self._expression = exp.Insert()
139
+ if not isinstance(self._expression, exp.Insert):
140
+ msg = "Cannot set INSERT source on a non-INSERT expression."
141
+ raise SQLBuilderError(msg)
142
+ subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
143
+ if subquery_parameters:
144
+ for p_name, p_value in subquery_parameters.items():
145
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
146
+ select_expr = select_builder._expression # pyright: ignore[attr-defined]
147
+ if select_expr and isinstance(select_expr, exp.Select):
148
+ self._expression.set("expression", select_expr.copy())
149
+ else:
150
+ msg = "SelectBuilder must have a valid SELECT expression."
151
+ raise SQLBuilderError(msg)
152
+ return self
@@ -0,0 +1,115 @@
1
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.builder._parsing_utils import parse_table_expression
7
+ from sqlspec.exceptions import SQLBuilderError
8
+ from sqlspec.utils.type_guards import has_query_builder_parameters
9
+
10
+ if TYPE_CHECKING:
11
+ from sqlspec.protocols import SQLBuilderProtocol
12
+
13
+ __all__ = ("JoinClauseMixin",)
14
+
15
+
16
+ class JoinClauseMixin:
17
+ """Mixin providing JOIN clause methods for SELECT builders."""
18
+
19
+ def join(
20
+ self,
21
+ table: Union[str, exp.Expression, Any],
22
+ on: Optional[Union[str, exp.Expression]] = None,
23
+ alias: Optional[str] = None,
24
+ join_type: str = "INNER",
25
+ ) -> Self:
26
+ builder = cast("SQLBuilderProtocol", self)
27
+ if builder._expression is None:
28
+ builder._expression = exp.Select()
29
+ if not isinstance(builder._expression, exp.Select):
30
+ msg = "JOIN clause is only supported for SELECT statements."
31
+ raise SQLBuilderError(msg)
32
+ table_expr: exp.Expression
33
+ if isinstance(table, str):
34
+ table_expr = parse_table_expression(table, alias)
35
+ elif has_query_builder_parameters(table):
36
+ if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
37
+ table_expr_value = getattr(table, "_expression", None)
38
+ if table_expr_value is not None:
39
+ subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
40
+ else:
41
+ subquery_exp = exp.paren(exp.Anonymous(this=""))
42
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
43
+ else:
44
+ subquery = table.build() # pyright: ignore
45
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
46
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
47
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
48
+ else:
49
+ table_expr = table
50
+ on_expr: Optional[exp.Expression] = None
51
+ if on is not None:
52
+ on_expr = exp.condition(on) if isinstance(on, str) else on
53
+ join_type_upper = join_type.upper()
54
+ if join_type_upper == "INNER":
55
+ join_expr = exp.Join(this=table_expr, on=on_expr)
56
+ elif join_type_upper == "LEFT":
57
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
58
+ elif join_type_upper == "RIGHT":
59
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
60
+ elif join_type_upper == "FULL":
61
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
62
+ else:
63
+ msg = f"Unsupported join type: {join_type}"
64
+ raise SQLBuilderError(msg)
65
+ builder._expression = builder._expression.join(join_expr, copy=False)
66
+ return cast("Self", builder)
67
+
68
+ def inner_join(
69
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
70
+ ) -> Self:
71
+ return self.join(table, on, alias, "INNER")
72
+
73
+ def left_join(
74
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
75
+ ) -> Self:
76
+ return self.join(table, on, alias, "LEFT")
77
+
78
+ def right_join(
79
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
80
+ ) -> Self:
81
+ return self.join(table, on, alias, "RIGHT")
82
+
83
+ def full_join(
84
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
85
+ ) -> Self:
86
+ return self.join(table, on, alias, "FULL")
87
+
88
+ def cross_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
89
+ builder = cast("SQLBuilderProtocol", self)
90
+ if builder._expression is None:
91
+ builder._expression = exp.Select()
92
+ if not isinstance(builder._expression, exp.Select):
93
+ msg = "Cannot add cross join to a non-SELECT expression."
94
+ raise SQLBuilderError(msg)
95
+ table_expr: exp.Expression
96
+ if isinstance(table, str):
97
+ table_expr = parse_table_expression(table, alias)
98
+ elif has_query_builder_parameters(table):
99
+ if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
100
+ table_expr_value = getattr(table, "_expression", None)
101
+ if table_expr_value is not None:
102
+ subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
103
+ else:
104
+ subquery_exp = exp.paren(exp.Anonymous(this=""))
105
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
106
+ else:
107
+ subquery = table.build() # pyright: ignore
108
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
109
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
110
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
111
+ else:
112
+ table_expr = table
113
+ join_expr = exp.Join(this=table_expr, kind="CROSS")
114
+ builder._expression = builder._expression.join(join_expr, copy=False)
115
+ return cast("Self", builder)
@@ -0,0 +1,416 @@
1
+ """Merge operation mixins for SQL builders."""
2
+
3
+ from typing import Any, Optional, Union
4
+
5
+ from sqlglot import exp
6
+ from typing_extensions import Self
7
+
8
+ from sqlspec.exceptions import SQLBuilderError
9
+ from sqlspec.utils.type_guards import has_query_builder_parameters
10
+
11
+ __all__ = (
12
+ "MergeIntoClauseMixin",
13
+ "MergeMatchedClauseMixin",
14
+ "MergeNotMatchedBySourceClauseMixin",
15
+ "MergeNotMatchedClauseMixin",
16
+ "MergeOnClauseMixin",
17
+ "MergeUsingClauseMixin",
18
+ )
19
+
20
+
21
+ class MergeIntoClauseMixin:
22
+ """Mixin providing INTO clause for MERGE builders."""
23
+
24
+ _expression: Optional[exp.Expression] = None
25
+
26
+ def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
27
+ """Set the target table for the MERGE operation (INTO clause).
28
+
29
+ Args:
30
+ table: The target table name or expression for the MERGE operation.
31
+ Can be a string (table name) or an sqlglot Expression.
32
+ alias: Optional alias for the target table.
33
+
34
+ Returns:
35
+ The current builder instance for method chaining.
36
+ """
37
+ if self._expression is None:
38
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # pyright: ignore
39
+ if not isinstance(self._expression, exp.Merge): # pyright: ignore
40
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # pyright: ignore
41
+ self._expression.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
42
+ return self
43
+
44
+
45
+ class MergeUsingClauseMixin:
46
+ """Mixin providing USING clause for MERGE builders."""
47
+
48
+ _expression: Optional[exp.Expression] = None
49
+
50
+ def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
51
+ """Set the source data for the MERGE operation (USING clause).
52
+
53
+ Args:
54
+ source: The source data for the MERGE operation.
55
+ Can be a string (table name), an sqlglot Expression, or a SelectBuilder instance.
56
+ alias: Optional alias for the source table.
57
+
58
+ Returns:
59
+ The current builder instance for method chaining.
60
+
61
+ Raises:
62
+ SQLBuilderError: If the current expression is not a MERGE statement or if the source type is unsupported.
63
+ """
64
+ if self._expression is None:
65
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
66
+ if not isinstance(self._expression, exp.Merge):
67
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
68
+
69
+ source_expr: exp.Expression
70
+ if isinstance(source, str):
71
+ source_expr = exp.to_table(source, alias=alias)
72
+ elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
73
+ subquery_builder_parameters = source.parameters
74
+ if subquery_builder_parameters:
75
+ for p_name, p_value in subquery_builder_parameters.items():
76
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
77
+
78
+ subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
79
+ source_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
80
+ elif isinstance(source, exp.Expression):
81
+ source_expr = source
82
+ if alias:
83
+ source_expr = exp.alias_(source_expr, alias)
84
+ else:
85
+ msg = f"Unsupported source type for USING clause: {type(source)}"
86
+ raise SQLBuilderError(msg)
87
+
88
+ self._expression.set("using", source_expr)
89
+ return self
90
+
91
+
92
+ class MergeOnClauseMixin:
93
+ """Mixin providing ON clause for MERGE builders."""
94
+
95
+ _expression: Optional[exp.Expression] = None
96
+
97
+ def on(self, condition: Union[str, exp.Expression]) -> Self:
98
+ """Set the join condition for the MERGE operation (ON clause).
99
+
100
+ Args:
101
+ condition: The join condition for the MERGE operation.
102
+ Can be a string (SQL condition) or an sqlglot Expression.
103
+
104
+ Returns:
105
+ The current builder instance for method chaining.
106
+
107
+ Raises:
108
+ SQLBuilderError: If the current expression is not a MERGE statement or if the condition type is unsupported.
109
+ """
110
+ if self._expression is None:
111
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
112
+ if not isinstance(self._expression, exp.Merge):
113
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
114
+
115
+ condition_expr: exp.Expression
116
+ if isinstance(condition, str):
117
+ parsed_condition: Optional[exp.Expression] = exp.maybe_parse(
118
+ condition, dialect=getattr(self, "dialect", None)
119
+ )
120
+ if not parsed_condition:
121
+ msg = f"Could not parse ON condition: {condition}"
122
+ raise SQLBuilderError(msg)
123
+ condition_expr = parsed_condition
124
+ elif isinstance(condition, exp.Expression):
125
+ condition_expr = condition
126
+ else:
127
+ msg = f"Unsupported condition type for ON clause: {type(condition)}"
128
+ raise SQLBuilderError(msg)
129
+
130
+ self._expression.set("on", condition_expr)
131
+ return self
132
+
133
+
134
+ class MergeMatchedClauseMixin:
135
+ """Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
136
+
137
+ _expression: Optional[exp.Expression] = None
138
+
139
+ def _add_when_clause(self, when_clause: exp.When) -> None:
140
+ """Helper to add a WHEN clause to the MERGE statement.
141
+
142
+ Args:
143
+ when_clause: The WHEN clause to add.
144
+ """
145
+ if self._expression is None:
146
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
147
+ if not isinstance(self._expression, exp.Merge):
148
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
149
+
150
+ whens = self._expression.args.get("whens")
151
+ if not whens:
152
+ whens = exp.Whens(expressions=[])
153
+ self._expression.set("whens", whens)
154
+
155
+ whens.append("expressions", when_clause)
156
+
157
+ def when_matched_then_update(
158
+ self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
159
+ ) -> Self:
160
+ """Define the UPDATE action for matched rows.
161
+
162
+ Args:
163
+ set_values: A dictionary of column names and their new values to set.
164
+ The values will be parameterized.
165
+ condition: An optional additional condition for this specific action.
166
+
167
+ Raises:
168
+ SQLBuilderError: If the condition type is unsupported.
169
+
170
+ Returns:
171
+ The current builder instance for method chaining.
172
+ """
173
+ update_expressions: list[exp.EQ] = []
174
+ for col, val in set_values.items():
175
+ column_name = col if isinstance(col, str) else str(col)
176
+ if "." in column_name:
177
+ column_name = column_name.split(".")[-1]
178
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
179
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
180
+ update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
181
+
182
+ when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
183
+
184
+ if condition:
185
+ condition_expr: exp.Expression
186
+ if isinstance(condition, str):
187
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
188
+ condition, dialect=getattr(self, "dialect", None)
189
+ )
190
+ if not parsed_cond:
191
+ msg = f"Could not parse WHEN clause condition: {condition}"
192
+ raise SQLBuilderError(msg)
193
+ condition_expr = parsed_cond
194
+ elif isinstance(condition, exp.Expression):
195
+ condition_expr = condition
196
+ else:
197
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
198
+ raise SQLBuilderError(msg)
199
+ when_args["this"] = condition_expr
200
+
201
+ when_clause = exp.When(**when_args)
202
+ self._add_when_clause(when_clause)
203
+ return self
204
+
205
+ def when_matched_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
206
+ """Define the DELETE action for matched rows.
207
+
208
+ Args:
209
+ condition: An optional additional condition for this specific action.
210
+
211
+ Raises:
212
+ SQLBuilderError: If the condition type is unsupported.
213
+
214
+ Returns:
215
+ The current builder instance for method chaining.
216
+ """
217
+ when_args: dict[str, Any] = {"matched": True, "then": exp.Delete()}
218
+
219
+ if condition:
220
+ condition_expr: exp.Expression
221
+ if isinstance(condition, str):
222
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
223
+ condition, dialect=getattr(self, "dialect", None)
224
+ )
225
+ if not parsed_cond:
226
+ msg = f"Could not parse WHEN clause condition: {condition}"
227
+ raise SQLBuilderError(msg)
228
+ condition_expr = parsed_cond
229
+ elif isinstance(condition, exp.Expression):
230
+ condition_expr = condition
231
+ else:
232
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
233
+ raise SQLBuilderError(msg)
234
+ when_args["this"] = condition_expr
235
+
236
+ when_clause = exp.When(**when_args)
237
+ self._add_when_clause(when_clause)
238
+ return self
239
+
240
+
241
+ class MergeNotMatchedClauseMixin:
242
+ """Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
243
+
244
+ _expression: Optional[exp.Expression] = None
245
+
246
+ def when_not_matched_then_insert(
247
+ self,
248
+ columns: Optional[list[str]] = None,
249
+ values: Optional[list[Any]] = None,
250
+ condition: Optional[Union[str, exp.Expression]] = None,
251
+ by_target: bool = True,
252
+ ) -> Self:
253
+ """Define the INSERT action for rows not matched.
254
+
255
+ Args:
256
+ columns: A list of column names to insert into. If None, implies INSERT DEFAULT VALUES or matching source columns.
257
+ values: A list of values corresponding to the columns.
258
+ These values will be parameterized. If None, implies INSERT DEFAULT VALUES or subquery source.
259
+ condition: An optional additional condition for this specific action.
260
+ by_target: If True (default), condition is "WHEN NOT MATCHED [BY TARGET]".
261
+ If False, condition is "WHEN NOT MATCHED BY SOURCE".
262
+
263
+ Returns:
264
+ The current builder instance for method chaining.
265
+
266
+ Raises:
267
+ SQLBuilderError: If columns and values are provided but do not match in length,
268
+ or if columns are provided without values.
269
+ """
270
+ insert_args: dict[str, Any] = {}
271
+ if columns and values:
272
+ if len(columns) != len(values):
273
+ msg = "Number of columns must match number of values for INSERT."
274
+ raise SQLBuilderError(msg)
275
+
276
+ parameterized_values: list[exp.Expression] = []
277
+ for i, val in enumerate(values):
278
+ column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
279
+ if "." in column_name:
280
+ column_name = column_name.split(".")[-1]
281
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
282
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
283
+ parameterized_values.append(exp.var(param_name))
284
+
285
+ insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
286
+ insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
287
+ elif columns and not values:
288
+ msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
289
+ raise SQLBuilderError(msg)
290
+ elif not columns and not values:
291
+ pass
292
+ else:
293
+ msg = "Cannot specify values without columns for INSERT action."
294
+ raise SQLBuilderError(msg)
295
+
296
+ when_args: dict[str, Any] = {"matched": False, "then": exp.Insert(**insert_args)}
297
+
298
+ if not by_target:
299
+ when_args["source"] = True
300
+
301
+ if condition:
302
+ condition_expr: exp.Expression
303
+ if isinstance(condition, str):
304
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
305
+ condition, dialect=getattr(self, "dialect", None)
306
+ )
307
+ if not parsed_cond:
308
+ msg = f"Could not parse WHEN clause condition: {condition}"
309
+ raise SQLBuilderError(msg)
310
+ condition_expr = parsed_cond
311
+ elif isinstance(condition, exp.Expression):
312
+ condition_expr = condition
313
+ else:
314
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
315
+ raise SQLBuilderError(msg)
316
+ when_args["this"] = condition_expr
317
+
318
+ when_clause = exp.When(**when_args)
319
+ self._add_when_clause(when_clause) # type: ignore[attr-defined]
320
+ return self
321
+
322
+
323
+ class MergeNotMatchedBySourceClauseMixin:
324
+ """Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
325
+
326
+ _expression: Optional[exp.Expression] = None
327
+
328
+ def when_not_matched_by_source_then_update(
329
+ self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
330
+ ) -> Self:
331
+ """Define the UPDATE action for rows not matched by source.
332
+
333
+ This is useful for handling rows that exist in the target but not in the source.
334
+
335
+ Args:
336
+ set_values: A dictionary of column names and their new values to set.
337
+ condition: An optional additional condition for this specific action.
338
+
339
+ Raises:
340
+ SQLBuilderError: If the condition type is unsupported.
341
+
342
+ Returns:
343
+ The current builder instance for method chaining.
344
+ """
345
+ update_expressions: list[exp.EQ] = []
346
+ for col, val in set_values.items():
347
+ column_name = col if isinstance(col, str) else str(col)
348
+ if "." in column_name:
349
+ column_name = column_name.split(".")[-1]
350
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
351
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
352
+ update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
353
+
354
+ when_args: dict[str, Any] = {
355
+ "matched": False,
356
+ "source": True,
357
+ "then": exp.Update(expressions=update_expressions),
358
+ }
359
+
360
+ if condition:
361
+ condition_expr: exp.Expression
362
+ if isinstance(condition, str):
363
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
364
+ condition, dialect=getattr(self, "dialect", None)
365
+ )
366
+ if not parsed_cond:
367
+ msg = f"Could not parse WHEN clause condition: {condition}"
368
+ raise SQLBuilderError(msg)
369
+ condition_expr = parsed_cond
370
+ elif isinstance(condition, exp.Expression):
371
+ condition_expr = condition
372
+ else:
373
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
374
+ raise SQLBuilderError(msg)
375
+ when_args["this"] = condition_expr
376
+
377
+ when_clause = exp.When(**when_args)
378
+ self._add_when_clause(when_clause) # type: ignore[attr-defined]
379
+ return self
380
+
381
+ def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
382
+ """Define the DELETE action for rows not matched by source.
383
+
384
+ This is useful for cleaning up rows that exist in the target but not in the source.
385
+
386
+ Args:
387
+ condition: An optional additional condition for this specific action.
388
+
389
+ Raises:
390
+ SQLBuilderError: If the condition type is unsupported.
391
+
392
+ Returns:
393
+ The current builder instance for method chaining.
394
+ """
395
+ when_args: dict[str, Any] = {"matched": False, "source": True, "then": exp.Delete()}
396
+
397
+ if condition:
398
+ condition_expr: exp.Expression
399
+ if isinstance(condition, str):
400
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
401
+ condition, dialect=getattr(self, "dialect", None)
402
+ )
403
+ if not parsed_cond:
404
+ msg = f"Could not parse WHEN clause condition: {condition}"
405
+ raise SQLBuilderError(msg)
406
+ condition_expr = parsed_cond
407
+ elif isinstance(condition, exp.Expression):
408
+ condition_expr = condition
409
+ else:
410
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
411
+ raise SQLBuilderError(msg)
412
+ when_args["this"] = condition_expr
413
+
414
+ when_clause = exp.When(**when_args)
415
+ self._add_when_clause(when_clause) # type: ignore[attr-defined]
416
+ return self