sqlspec 0.13.1__py3-none-any.whl → 0.14.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.

Files changed (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +2 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +2 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +5 -39
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +4 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +2 -55
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +4 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.1.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,18 +1,14 @@
1
1
  """SQL statement builder mixins."""
2
2
 
3
- from sqlspec.statement.builder.mixins._aggregate_functions import AggregateFunctionsMixin
4
- from sqlspec.statement.builder.mixins._case_builder import CaseBuilderMixin
5
- from sqlspec.statement.builder.mixins._common_table_expr import CommonTableExpressionMixin
6
- from sqlspec.statement.builder.mixins._delete_from import DeleteFromClauseMixin
7
- from sqlspec.statement.builder.mixins._from import FromClauseMixin
8
- from sqlspec.statement.builder.mixins._group_by import GroupByClauseMixin
9
- from sqlspec.statement.builder.mixins._having import HavingClauseMixin
10
- from sqlspec.statement.builder.mixins._insert_from_select import InsertFromSelectMixin
11
- from sqlspec.statement.builder.mixins._insert_into import InsertIntoClauseMixin
12
- from sqlspec.statement.builder.mixins._insert_values import InsertValuesMixin
13
- from sqlspec.statement.builder.mixins._join import JoinClauseMixin
14
- from sqlspec.statement.builder.mixins._limit_offset import LimitOffsetClauseMixin
15
- from sqlspec.statement.builder.mixins._merge_clauses import (
3
+ from sqlspec.statement.builder.mixins._cte_and_set_ops import CommonTableExpressionMixin, SetOperationMixin
4
+ from sqlspec.statement.builder.mixins._delete_operations import DeleteFromClauseMixin
5
+ from sqlspec.statement.builder.mixins._insert_operations import (
6
+ InsertFromSelectMixin,
7
+ InsertIntoClauseMixin,
8
+ InsertValuesMixin,
9
+ )
10
+ from sqlspec.statement.builder.mixins._join_operations import JoinClauseMixin
11
+ from sqlspec.statement.builder.mixins._merge_operations import (
16
12
  MergeIntoClauseMixin,
17
13
  MergeMatchedClauseMixin,
18
14
  MergeNotMatchedBySourceClauseMixin,
@@ -20,25 +16,24 @@ from sqlspec.statement.builder.mixins._merge_clauses import (
20
16
  MergeOnClauseMixin,
21
17
  MergeUsingClauseMixin,
22
18
  )
23
- from sqlspec.statement.builder.mixins._order_by import OrderByClauseMixin
24
- from sqlspec.statement.builder.mixins._pivot import PivotClauseMixin
25
- from sqlspec.statement.builder.mixins._returning import ReturningClauseMixin
26
- from sqlspec.statement.builder.mixins._select_columns import SelectColumnsMixin
27
- from sqlspec.statement.builder.mixins._set_ops import SetOperationMixin
28
- from sqlspec.statement.builder.mixins._unpivot import UnpivotClauseMixin
29
- from sqlspec.statement.builder.mixins._update_from import UpdateFromClauseMixin
30
- from sqlspec.statement.builder.mixins._update_set import UpdateSetClauseMixin
31
- from sqlspec.statement.builder.mixins._update_table import UpdateTableClauseMixin
32
- from sqlspec.statement.builder.mixins._where import WhereClauseMixin
33
- from sqlspec.statement.builder.mixins._window_functions import WindowFunctionsMixin
19
+ from sqlspec.statement.builder.mixins._order_limit_operations import (
20
+ LimitOffsetClauseMixin,
21
+ OrderByClauseMixin,
22
+ ReturningClauseMixin,
23
+ )
24
+ from sqlspec.statement.builder.mixins._pivot_operations import PivotClauseMixin, UnpivotClauseMixin
25
+ from sqlspec.statement.builder.mixins._select_operations import CaseBuilder, SelectClauseMixin
26
+ from sqlspec.statement.builder.mixins._update_operations import (
27
+ UpdateFromClauseMixin,
28
+ UpdateSetClauseMixin,
29
+ UpdateTableClauseMixin,
30
+ )
31
+ from sqlspec.statement.builder.mixins._where_clause import HavingClauseMixin, WhereClauseMixin
34
32
 
35
33
  __all__ = (
36
- "AggregateFunctionsMixin",
37
- "CaseBuilderMixin",
34
+ "CaseBuilder",
38
35
  "CommonTableExpressionMixin",
39
36
  "DeleteFromClauseMixin",
40
- "FromClauseMixin",
41
- "GroupByClauseMixin",
42
37
  "HavingClauseMixin",
43
38
  "InsertFromSelectMixin",
44
39
  "InsertIntoClauseMixin",
@@ -54,12 +49,11 @@ __all__ = (
54
49
  "OrderByClauseMixin",
55
50
  "PivotClauseMixin",
56
51
  "ReturningClauseMixin",
57
- "SelectColumnsMixin",
52
+ "SelectClauseMixin",
58
53
  "SetOperationMixin",
59
54
  "UnpivotClauseMixin",
60
55
  "UpdateFromClauseMixin",
61
56
  "UpdateSetClauseMixin",
62
57
  "UpdateTableClauseMixin",
63
58
  "WhereClauseMixin",
64
- "WindowFunctionsMixin",
65
59
  )
@@ -1,11 +1,95 @@
1
- from typing import Any, Optional
1
+ """CTE (Common Table Expression) and Set Operations mixins for SQL builders."""
2
+
3
+ from typing import Any, Optional, Union
2
4
 
3
5
  from sqlglot import exp
4
6
  from typing_extensions import Self
5
7
 
6
8
  from sqlspec.exceptions import SQLBuilderError
7
9
 
8
- __all__ = ("SetOperationMixin",)
10
+ __all__ = ("CommonTableExpressionMixin", "SetOperationMixin")
11
+
12
+
13
+ class CommonTableExpressionMixin:
14
+ """Mixin providing WITH clause (Common Table Expressions) support for SQL builders."""
15
+
16
+ _expression: Optional[exp.Expression] = None
17
+
18
+ def with_(
19
+ self, name: str, query: Union[Any, str], recursive: bool = False, columns: Optional[list[str]] = None
20
+ ) -> Self:
21
+ """Add WITH clause (Common Table Expression).
22
+
23
+ Args:
24
+ name: The name of the CTE.
25
+ query: The query for the CTE (builder instance or SQL string).
26
+ recursive: Whether this is a recursive CTE.
27
+ columns: Optional column names for the CTE.
28
+
29
+ Raises:
30
+ SQLBuilderError: If the query type is unsupported.
31
+
32
+ Returns:
33
+ The current builder instance for method chaining.
34
+ """
35
+ if self._expression is None:
36
+ msg = "Cannot add WITH clause: expression not initialized."
37
+ raise SQLBuilderError(msg)
38
+
39
+ if not hasattr(self._expression, "with_") and not isinstance(
40
+ self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)
41
+ ):
42
+ msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
43
+ raise SQLBuilderError(msg)
44
+
45
+ cte_expr: Optional[exp.Expression] = None
46
+ if hasattr(query, "to_statement"):
47
+ # Query is a builder instance
48
+ built_query = query.to_statement() # pyright: ignore
49
+ cte_sql = built_query.to_sql()
50
+ cte_expr = exp.maybe_parse(cte_sql, dialect=getattr(self, "dialect", None))
51
+
52
+ # Merge parameters
53
+ if hasattr(self, "add_parameter"):
54
+ parameters = getattr(built_query, "parameters", None) or {}
55
+ for param_name, param_value in parameters.items():
56
+ self.add_parameter(param_value, name=param_name) # pyright: ignore
57
+ elif isinstance(query, str):
58
+ cte_expr = exp.maybe_parse(query, dialect=getattr(self, "dialect", None))
59
+ elif isinstance(query, exp.Expression):
60
+ cte_expr = query
61
+
62
+ if not cte_expr:
63
+ msg = f"Could not parse CTE query: {query}"
64
+ raise SQLBuilderError(msg)
65
+
66
+ if columns:
67
+ # CTE with explicit column list: name(col1, col2, ...)
68
+ cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
69
+ else:
70
+ # Simple CTE alias: name
71
+ cte_alias_expr = exp.alias_(cte_expr, name)
72
+
73
+ # Different handling for different expression types
74
+ if hasattr(self._expression, "with_"):
75
+ existing_with = self._expression.args.get("with") # pyright: ignore
76
+ if existing_with:
77
+ existing_with.expressions.append(cte_alias_expr)
78
+ if recursive:
79
+ existing_with.set("recursive", recursive)
80
+ else:
81
+ self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False) # pyright: ignore
82
+ if recursive:
83
+ with_clause = self._expression.find(exp.With)
84
+ if with_clause:
85
+ with_clause.set("recursive", recursive)
86
+ else:
87
+ # Store CTEs for later application during build
88
+ if not hasattr(self, "_with_ctes"):
89
+ setattr(self, "_with_ctes", {})
90
+ self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name)) # type: ignore[attr-defined]
91
+
92
+ return self
9
93
 
10
94
 
11
95
  class SetOperationMixin:
@@ -1,3 +1,5 @@
1
+ """Delete operation mixins for SQL builders."""
2
+
1
3
  from typing import Optional
2
4
 
3
5
  from sqlglot import exp
@@ -1,3 +1,5 @@
1
+ """Insert operation mixins for SQL builders."""
2
+
1
3
  from collections.abc import Sequence
2
4
  from typing import Any, Optional, Union
3
5
 
@@ -6,7 +8,35 @@ from typing_extensions import Self
6
8
 
7
9
  from sqlspec.exceptions import SQLBuilderError
8
10
 
9
- __all__ = ("InsertValuesMixin",)
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
10
40
 
11
41
 
12
42
  class InsertValuesMixin:
@@ -65,3 +95,42 @@ class InsertValuesMixin:
65
95
  The current builder instance for method chaining.
66
96
  """
67
97
  return self.values(*values)
98
+
99
+
100
+ class InsertFromSelectMixin:
101
+ """Mixin providing INSERT ... SELECT support for INSERT builders."""
102
+
103
+ _expression: Optional[exp.Expression] = None
104
+
105
+ def from_select(self, select_builder: Any) -> Self:
106
+ """Sets the INSERT source to a SELECT statement.
107
+
108
+ Args:
109
+ select_builder: A SelectBuilder instance representing the SELECT query.
110
+
111
+ Returns:
112
+ The current builder instance for method chaining.
113
+
114
+ Raises:
115
+ SQLBuilderError: If the table is not set or the select_builder is invalid.
116
+ """
117
+ if not getattr(self, "_table", None):
118
+ msg = "The target table must be set using .into() before adding values."
119
+ raise SQLBuilderError(msg)
120
+ if self._expression is None:
121
+ self._expression = exp.Insert()
122
+ if not isinstance(self._expression, exp.Insert):
123
+ msg = "Cannot set INSERT source on a non-INSERT expression."
124
+ raise SQLBuilderError(msg)
125
+ # Merge parameters from the SELECT builder
126
+ subquery_params = getattr(select_builder, "_parameters", None)
127
+ if subquery_params:
128
+ for p_name, p_value in subquery_params.items():
129
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
130
+ select_expr = getattr(select_builder, "_expression", None)
131
+ if select_expr and isinstance(select_expr, exp.Select):
132
+ self._expression.set("expression", select_expr.copy())
133
+ else:
134
+ msg = "SelectBuilder must have a valid SELECT expression."
135
+ raise SQLBuilderError(msg)
136
+ return self
@@ -1,3 +1,5 @@
1
+ """Merge operation mixins for SQL builders."""
2
+
1
3
  from typing import Any, Optional, Union
2
4
 
3
5
  from sqlglot import exp
@@ -0,0 +1,123 @@
1
+ """Order, Limit, Offset and Returning operations mixins for SQL builders."""
2
+
3
+ from typing import TYPE_CHECKING, Optional, Union, cast
4
+
5
+ from sqlglot import exp
6
+ from typing_extensions import Self
7
+
8
+ from sqlspec.exceptions import SQLBuilderError
9
+ from sqlspec.statement.builder._parsing_utils import parse_order_expression
10
+
11
+ if TYPE_CHECKING:
12
+ from sqlspec.protocols import SQLBuilderProtocol
13
+
14
+ __all__ = ("LimitOffsetClauseMixin", "OrderByClauseMixin", "ReturningClauseMixin")
15
+
16
+
17
+ class OrderByClauseMixin:
18
+ """Mixin providing ORDER BY clause."""
19
+
20
+ _expression: Optional[exp.Expression] = None
21
+
22
+ def order_by(self, *items: Union[str, exp.Ordered], desc: bool = False) -> Self:
23
+ """Add ORDER BY clause.
24
+
25
+ Args:
26
+ *items: Columns to order by. Can be strings (column names) or sqlglot.exp.Ordered instances for specific directions (e.g., exp.column("name").desc()).
27
+ desc: Whether to order in descending order (applies to all items if they are strings).
28
+
29
+ Raises:
30
+ SQLBuilderError: If the current expression is not a SELECT statement or if the item type is unsupported.
31
+
32
+ Returns:
33
+ The current builder instance for method chaining.
34
+ """
35
+ builder = cast("SQLBuilderProtocol", self)
36
+ if not isinstance(builder._expression, exp.Select):
37
+ msg = "ORDER BY is only supported for SELECT statements."
38
+ raise SQLBuilderError(msg)
39
+
40
+ current_expr = builder._expression
41
+ for item in items:
42
+ if isinstance(item, str):
43
+ order_item = parse_order_expression(item)
44
+ if desc:
45
+ order_item = order_item.desc()
46
+ else:
47
+ order_item = item
48
+ current_expr = current_expr.order_by(order_item, copy=False)
49
+ builder._expression = current_expr
50
+ return cast("Self", builder)
51
+
52
+
53
+ class LimitOffsetClauseMixin:
54
+ """Mixin providing LIMIT and OFFSET clauses."""
55
+
56
+ _expression: Optional[exp.Expression] = None
57
+
58
+ def limit(self, value: int) -> Self:
59
+ """Add LIMIT clause.
60
+
61
+ Args:
62
+ value: The maximum number of rows to return.
63
+
64
+ Raises:
65
+ SQLBuilderError: If the current expression is not a SELECT statement.
66
+
67
+ Returns:
68
+ The current builder instance for method chaining.
69
+ """
70
+ builder = cast("SQLBuilderProtocol", self)
71
+ if not isinstance(builder._expression, exp.Select):
72
+ msg = "LIMIT is only supported for SELECT statements."
73
+ raise SQLBuilderError(msg)
74
+ builder._expression = builder._expression.limit(exp.Literal.number(value), copy=False)
75
+ return cast("Self", builder)
76
+
77
+ def offset(self, value: int) -> Self:
78
+ """Add OFFSET clause.
79
+
80
+ Args:
81
+ value: The number of rows to skip before starting to return rows.
82
+
83
+ Raises:
84
+ SQLBuilderError: If the current expression is not a SELECT statement.
85
+
86
+ Returns:
87
+ The current builder instance for method chaining.
88
+ """
89
+ builder = cast("SQLBuilderProtocol", self)
90
+ if not isinstance(builder._expression, exp.Select):
91
+ msg = "OFFSET is only supported for SELECT statements."
92
+ raise SQLBuilderError(msg)
93
+ builder._expression = builder._expression.offset(exp.Literal.number(value), copy=False)
94
+ return cast("Self", builder)
95
+
96
+
97
+ class ReturningClauseMixin:
98
+ """Mixin providing RETURNING clause."""
99
+
100
+ _expression: Optional[exp.Expression] = None
101
+
102
+ def returning(self, *columns: Union[str, exp.Expression]) -> Self:
103
+ """Add RETURNING clause to the statement.
104
+
105
+ Args:
106
+ *columns: Columns to return. Can be strings or sqlglot expressions.
107
+
108
+ Raises:
109
+ SQLBuilderError: If the current expression is not INSERT, UPDATE, or DELETE.
110
+
111
+ Returns:
112
+ The current builder instance for method chaining.
113
+ """
114
+ if self._expression is None:
115
+ msg = "Cannot add RETURNING: expression is not initialized."
116
+ raise SQLBuilderError(msg)
117
+ valid_types = (exp.Insert, exp.Update, exp.Delete)
118
+ if not isinstance(self._expression, valid_types):
119
+ msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
120
+ raise SQLBuilderError(msg)
121
+ returning_exprs = [exp.column(c) if isinstance(c, str) else c for c in columns]
122
+ self._expression.set("returning", exp.Returning(expressions=returning_exprs))
123
+ return self
@@ -1,3 +1,5 @@
1
+ """Pivot and Unpivot operations mixins for SQL builders."""
2
+
1
3
  from typing import TYPE_CHECKING, Optional, Union, cast
2
4
 
3
5
  from sqlglot import exp
@@ -5,9 +7,9 @@ from sqlglot import exp
5
7
  if TYPE_CHECKING:
6
8
  from sqlglot.dialects.dialect import DialectType
7
9
 
8
- from sqlspec.statement.builder.select import Select
10
+ from sqlspec.statement.builder._select import Select
9
11
 
10
- __all__ = ("PivotClauseMixin",)
12
+ __all__ = ("PivotClauseMixin", "UnpivotClauseMixin")
11
13
 
12
14
 
13
15
  class PivotClauseMixin:
@@ -77,3 +79,70 @@ class PivotClauseMixin:
77
79
  table.set("pivots", existing_pivots)
78
80
 
79
81
  return cast("Select", self)
82
+
83
+
84
+ class UnpivotClauseMixin:
85
+ """Mixin class to add UNPIVOT functionality to a Select."""
86
+
87
+ _expression: "Optional[exp.Expression]" = None
88
+ dialect: "DialectType" = None
89
+
90
+ def unpivot(
91
+ self: "UnpivotClauseMixin",
92
+ value_column_name: str,
93
+ name_column_name: str,
94
+ columns_to_unpivot: list[Union[str, exp.Expression]],
95
+ alias: Optional[str] = None,
96
+ ) -> "Select":
97
+ """Adds an UNPIVOT clause to the SELECT statement.
98
+
99
+ Example:
100
+ `query.unpivot(value_column_name="Sales", name_column_name="Quarter", columns_to_unpivot=["Q1Sales", "Q2Sales"], alias="UnpivotTable")`
101
+
102
+ Args:
103
+ value_column_name: The name for the new column that will hold the values from the unpivoted columns.
104
+ name_column_name: The name for the new column that will hold the names of the original unpivoted columns.
105
+ columns_to_unpivot: A list of columns to be unpivoted into rows.
106
+ alias: Optional alias for the unpivoted table/subquery.
107
+
108
+ Raises:
109
+ TypeError: If the current expression is not a Select expression.
110
+
111
+ Returns:
112
+ The Select instance for chaining.
113
+ """
114
+ current_expr = self._expression
115
+ if not isinstance(current_expr, exp.Select):
116
+ # SelectBuilder's __init__ ensures _expression is exp.Select.
117
+ msg = "Unpivot can only be applied to a Select expression managed by Select."
118
+ raise TypeError(msg)
119
+
120
+ value_col_ident = exp.to_identifier(value_column_name)
121
+ name_col_ident = exp.to_identifier(name_column_name)
122
+
123
+ unpivot_cols_exprs: list[exp.Expression] = []
124
+ for col_name_or_expr in columns_to_unpivot:
125
+ if isinstance(col_name_or_expr, exp.Expression):
126
+ unpivot_cols_exprs.append(col_name_or_expr)
127
+ elif isinstance(col_name_or_expr, str):
128
+ unpivot_cols_exprs.append(exp.column(col_name_or_expr))
129
+ else:
130
+ # Fallback for other types, should ideally be an error or more specific handling
131
+ unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
132
+
133
+ in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
134
+
135
+ unpivot_node = exp.Pivot(expressions=[value_col_ident], fields=[in_expr], unpivot=True)
136
+
137
+ if alias:
138
+ unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
139
+
140
+ from_clause = current_expr.args.get("from")
141
+ if from_clause and isinstance(from_clause, exp.From):
142
+ table = from_clause.this
143
+ if isinstance(table, exp.Table):
144
+ existing_pivots = table.args.get("pivots", [])
145
+ existing_pivots.append(unpivot_node)
146
+ table.set("pivots", existing_pivots)
147
+
148
+ return cast("Select", self)