sqlspec 0.11.0__py3-none-any.whl → 0.12.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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -644
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -462
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +217 -451
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +418 -498
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +592 -634
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +393 -436
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +549 -942
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -550
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +741 -0
  31. sqlspec/adapters/psycopg/driver.py +732 -733
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +243 -426
  35. sqlspec/base.py +220 -825
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/METADATA +100 -26
  137. sqlspec-0.12.0.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -330
  150. sqlspec/mixins.py +0 -306
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.0.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,91 @@
1
+ # mypy: disable-error-code="valid-type,type-var"
2
+ from dataclasses import dataclass
3
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
4
+
5
+ from sqlglot import exp
6
+
7
+ if TYPE_CHECKING:
8
+ from sqlspec.statement.builder.base import QueryBuilder
9
+ from sqlspec.typing import RowT
10
+
11
+ __all__ = ("CaseBuilder", "CaseBuilderMixin")
12
+
13
+
14
+ class CaseBuilderMixin:
15
+ """Mixin providing CASE expression functionality for SQL builders."""
16
+
17
+ def case_(self, alias: "Optional[str]" = None) -> "CaseBuilder":
18
+ """Create a CASE expression for the SELECT clause.
19
+
20
+ Args:
21
+ alias: Optional alias for the CASE expression.
22
+
23
+ Returns:
24
+ CaseBuilder: A CaseBuilder instance for building the CASE expression.
25
+ """
26
+ builder = cast("QueryBuilder[RowT]", self) # pyright: ignore
27
+ return CaseBuilder(builder, alias)
28
+
29
+
30
+ @dataclass
31
+ class CaseBuilder:
32
+ """Builder for CASE expressions."""
33
+
34
+ _parent: "QueryBuilder[RowT]" # pyright: ignore
35
+ _alias: Optional[str]
36
+ _case_expr: exp.Case
37
+
38
+ def __init__(self, parent: "QueryBuilder[RowT]", alias: "Optional[str]" = None) -> None:
39
+ """Initialize CaseBuilder.
40
+
41
+ Args:
42
+ parent: The parent builder.
43
+ alias: Optional alias for the CASE expression.
44
+ """
45
+ self._parent = parent
46
+ self._alias = alias
47
+ self._case_expr = exp.Case()
48
+
49
+ def when(self, condition: "Union[str, exp.Expression]", value: "Any") -> "CaseBuilder":
50
+ """Add WHEN clause to CASE expression.
51
+
52
+ Args:
53
+ condition: The condition to test.
54
+ value: The value to return if condition is true.
55
+
56
+ Returns:
57
+ CaseBuilder: The current builder instance for method chaining.
58
+ """
59
+ cond_expr = exp.condition(condition) if isinstance(condition, str) else condition
60
+ param_name = self._parent.add_parameter(value)[1]
61
+ value_expr = exp.Placeholder(this=param_name)
62
+
63
+ when_clause = exp.When(this=cond_expr, then=value_expr)
64
+
65
+ if not self._case_expr.args.get("ifs"):
66
+ self._case_expr.set("ifs", [])
67
+ self._case_expr.args["ifs"].append(when_clause)
68
+ return self
69
+
70
+ def else_(self, value: "Any") -> "CaseBuilder":
71
+ """Add ELSE clause to CASE expression.
72
+
73
+ Args:
74
+ value: The value to return if no conditions match.
75
+
76
+ Returns:
77
+ CaseBuilder: The current builder instance for method chaining.
78
+ """
79
+ param_name = self._parent.add_parameter(value)[1]
80
+ value_expr = exp.Placeholder(this=param_name)
81
+ self._case_expr.set("default", value_expr)
82
+ return self
83
+
84
+ def end(self) -> "QueryBuilder[RowT]":
85
+ """Finalize the CASE expression and add it to the SELECT clause.
86
+
87
+ Returns:
88
+ The parent builder instance.
89
+ """
90
+ select_expr = exp.alias_(self._case_expr, self._alias) if self._alias else self._case_expr
91
+ return cast("QueryBuilder[RowT]", self._parent.select(select_expr)) # type: ignore[attr-defined]
@@ -0,0 +1,91 @@
1
+ from typing import Any, Optional, Union
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = ("CommonTableExpressionMixin",)
9
+
10
+
11
+ class CommonTableExpressionMixin:
12
+ """Mixin providing WITH clause (Common Table Expressions) support for SQL builders."""
13
+
14
+ _expression: Optional[exp.Expression] = None
15
+
16
+ def with_(
17
+ self, name: str, query: Union[Any, str], recursive: bool = False, columns: Optional[list[str]] = None
18
+ ) -> Self:
19
+ """Add WITH clause (Common Table Expression).
20
+
21
+ Args:
22
+ name: The name of the CTE.
23
+ query: The query for the CTE (builder instance or SQL string).
24
+ recursive: Whether this is a recursive CTE.
25
+ columns: Optional column names for the CTE.
26
+
27
+ Raises:
28
+ SQLBuilderError: If the query type is unsupported.
29
+
30
+ Returns:
31
+ The current builder instance for method chaining.
32
+ """
33
+ if self._expression is None:
34
+ msg = "Cannot add WITH clause: expression not initialized."
35
+ raise SQLBuilderError(msg)
36
+
37
+ if not hasattr(self._expression, "with_") and not isinstance(
38
+ self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)
39
+ ):
40
+ msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
41
+ raise SQLBuilderError(msg)
42
+
43
+ cte_expr: Optional[exp.Expression] = None
44
+ if hasattr(query, "to_statement"):
45
+ # Query is a builder instance
46
+ built_query = query.to_statement() # pyright: ignore
47
+ cte_sql = built_query.to_sql()
48
+ cte_expr = exp.maybe_parse(cte_sql, dialect=getattr(self, "dialect", None))
49
+
50
+ # Merge parameters
51
+ if hasattr(self, "add_parameter"):
52
+ parameters = getattr(built_query, "parameters", None) or {}
53
+ for param_name, param_value in parameters.items():
54
+ self.add_parameter(param_value, name=param_name) # pyright: ignore
55
+ elif isinstance(query, str):
56
+ cte_expr = exp.maybe_parse(query, dialect=getattr(self, "dialect", None))
57
+ elif isinstance(query, exp.Expression):
58
+ cte_expr = query
59
+
60
+ if not cte_expr:
61
+ msg = f"Could not parse CTE query: {query}"
62
+ raise SQLBuilderError(msg)
63
+
64
+ # Create a proper CTE with table alias
65
+ if columns:
66
+ # CTE with explicit column list: name(col1, col2, ...)
67
+ cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
68
+ else:
69
+ # Simple CTE alias: name
70
+ cte_alias_expr = exp.alias_(cte_expr, name)
71
+
72
+ # Different handling for different expression types
73
+ if hasattr(self._expression, "with_"):
74
+ existing_with = self._expression.args.get("with") # pyright: ignore
75
+ if existing_with:
76
+ existing_with.expressions.append(cte_alias_expr)
77
+ if recursive:
78
+ existing_with.set("recursive", recursive)
79
+ else:
80
+ self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False) # pyright: ignore
81
+ if recursive:
82
+ with_clause = self._expression.find(exp.With)
83
+ if with_clause:
84
+ with_clause.set("recursive", recursive)
85
+ else:
86
+ # Store CTEs for later application during build
87
+ if not hasattr(self, "_with_ctes"):
88
+ setattr(self, "_with_ctes", {})
89
+ self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name)) # type: ignore[attr-defined]
90
+
91
+ return self
@@ -0,0 +1,34 @@
1
+ from typing import Optional
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = ("DeleteFromClauseMixin",)
9
+
10
+
11
+ class DeleteFromClauseMixin:
12
+ """Mixin providing FROM clause for DELETE builders."""
13
+
14
+ _expression: Optional[exp.Expression] = None
15
+
16
+ def from_(self, table: str) -> Self:
17
+ """Set the target table for the DELETE statement.
18
+
19
+ Args:
20
+ table: The table name to delete from.
21
+
22
+ Returns:
23
+ The current builder instance for method chaining.
24
+ """
25
+ if self._expression is None:
26
+ self._expression = exp.Delete()
27
+ if not isinstance(self._expression, exp.Delete):
28
+ current_expr_type = type(self._expression).__name__
29
+ msg = f"Base expression for DeleteBuilder is {current_expr_type}, expected Delete."
30
+ raise SQLBuilderError(msg)
31
+
32
+ setattr(self, "_table", table)
33
+ self._expression.set("this", exp.to_table(table))
34
+ return self
@@ -0,0 +1,61 @@
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.exceptions import SQLBuilderError
7
+ from sqlspec.statement.builder._parsing_utils import parse_table_expression
8
+ from sqlspec.typing import is_expression
9
+
10
+ if TYPE_CHECKING:
11
+ from sqlspec.statement.builder.protocols import BuilderProtocol
12
+
13
+ __all__ = ("FromClauseMixin",)
14
+
15
+
16
+ class FromClauseMixin:
17
+ """Mixin providing FROM clause for SELECT builders."""
18
+
19
+ def from_(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
20
+ """Add FROM clause.
21
+
22
+ Args:
23
+ table: The table name, expression, or subquery to select from.
24
+ alias: Optional alias for the table.
25
+
26
+ Raises:
27
+ SQLBuilderError: If the current expression is not a SELECT statement or if the table type is unsupported.
28
+
29
+ Returns:
30
+ The current builder instance for method chaining.
31
+ """
32
+ builder = cast("BuilderProtocol", self)
33
+ if builder._expression is None:
34
+ builder._expression = exp.Select()
35
+ if not isinstance(builder._expression, exp.Select):
36
+ msg = "FROM clause is only supported for SELECT statements."
37
+ raise SQLBuilderError(msg)
38
+ from_expr: exp.Expression
39
+ if isinstance(table, str):
40
+ from_expr = parse_table_expression(table, alias)
41
+ elif is_expression(table):
42
+ # Direct sqlglot expression - use as is
43
+ from_expr = exp.alias_(table, alias) if alias else table
44
+ elif hasattr(table, "build"):
45
+ # Query builder with build() method
46
+ subquery = table.build() # pyright: ignore
47
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(builder, "dialect", None)))
48
+ from_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
49
+ current_params = getattr(builder, "_parameters", None)
50
+ merged_params = getattr(type(builder), "ParameterConverter", None)
51
+ if merged_params:
52
+ merged_params = merged_params.merge_parameters(
53
+ parameters=subquery.parameters,
54
+ args=current_params if isinstance(current_params, list) else None,
55
+ kwargs=current_params if isinstance(current_params, dict) else {},
56
+ )
57
+ setattr(builder, "_parameters", merged_params)
58
+ else:
59
+ from_expr = table
60
+ builder._expression = builder._expression.from_(from_expr, copy=False)
61
+ return cast("Self", builder)
@@ -0,0 +1,119 @@
1
+ from typing import Optional, Union
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ __all__ = ("GroupByClauseMixin",)
7
+
8
+
9
+ class GroupByClauseMixin:
10
+ """Mixin providing GROUP BY clause functionality for SQL builders."""
11
+
12
+ _expression: Optional[exp.Expression] = None
13
+
14
+ def group_by(self, *columns: Union[str, exp.Expression]) -> Self:
15
+ """Add GROUP BY clause.
16
+
17
+ Args:
18
+ *columns: Columns to group by. Can be column names, expressions,
19
+ or special grouping expressions like ROLLUP, CUBE, etc.
20
+
21
+ Returns:
22
+ The current builder instance for method chaining.
23
+ """
24
+ if self._expression is None or not isinstance(self._expression, exp.Select):
25
+ return self
26
+
27
+ for column in columns:
28
+ self._expression = self._expression.group_by(
29
+ exp.column(column) if isinstance(column, str) else column, copy=False
30
+ )
31
+ return self
32
+
33
+ def group_by_rollup(self, *columns: Union[str, exp.Expression]) -> Self:
34
+ """Add GROUP BY ROLLUP clause.
35
+
36
+ ROLLUP generates subtotals and grand totals for a hierarchical set of columns.
37
+
38
+ Args:
39
+ *columns: Columns to include in the rollup hierarchy.
40
+
41
+ Returns:
42
+ The current builder instance for method chaining.
43
+
44
+ Example:
45
+ ```python
46
+ # GROUP BY ROLLUP(product, region)
47
+ query = (
48
+ sql.select("product", "region", sql.sum("sales"))
49
+ .from_("sales_data")
50
+ .group_by_rollup("product", "region")
51
+ )
52
+ ```
53
+ """
54
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
55
+ rollup_expr = exp.Rollup(expressions=column_exprs)
56
+ return self.group_by(rollup_expr)
57
+
58
+ def group_by_cube(self, *columns: Union[str, exp.Expression]) -> Self:
59
+ """Add GROUP BY CUBE clause.
60
+
61
+ CUBE generates subtotals for all possible combinations of the specified columns.
62
+
63
+ Args:
64
+ *columns: Columns to include in the cube.
65
+
66
+ Returns:
67
+ The current builder instance for method chaining.
68
+
69
+ Example:
70
+ ```python
71
+ # GROUP BY CUBE(product, region)
72
+ query = (
73
+ sql.select("product", "region", sql.sum("sales"))
74
+ .from_("sales_data")
75
+ .group_by_cube("product", "region")
76
+ )
77
+ ```
78
+ """
79
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
80
+ cube_expr = exp.Cube(expressions=column_exprs)
81
+ return self.group_by(cube_expr)
82
+
83
+ def group_by_grouping_sets(self, *column_sets: Union[tuple[str, ...], list[str]]) -> Self:
84
+ """Add GROUP BY GROUPING SETS clause.
85
+
86
+ GROUPING SETS allows you to specify multiple grouping sets in a single query.
87
+
88
+ Args:
89
+ *column_sets: Sets of columns to group by. Each set can be a tuple or list.
90
+ Empty tuple/list creates a grand total grouping.
91
+
92
+ Returns:
93
+ The current builder instance for method chaining.
94
+
95
+ Example:
96
+ ```python
97
+ # GROUP BY GROUPING SETS ((product), (region), ())
98
+ query = (
99
+ sql.select("product", "region", sql.sum("sales"))
100
+ .from_("sales_data")
101
+ .group_by_grouping_sets(("product",), ("region",), ())
102
+ )
103
+ ```
104
+ """
105
+ set_expressions = []
106
+ for column_set in column_sets:
107
+ if isinstance(column_set, (tuple, list)):
108
+ if len(column_set) == 0:
109
+ # Empty set for grand total
110
+ set_expressions.append(exp.Tuple(expressions=[]))
111
+ else:
112
+ columns = [exp.column(col) for col in column_set]
113
+ set_expressions.append(exp.Tuple(expressions=columns))
114
+ else:
115
+ # Single column
116
+ set_expressions.append(exp.column(column_set))
117
+
118
+ grouping_sets_expr = exp.GroupingSets(expressions=set_expressions)
119
+ return self.group_by(grouping_sets_expr)
@@ -0,0 +1,35 @@
1
+ from typing import Optional, Union
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = ("HavingClauseMixin",)
9
+
10
+
11
+ class HavingClauseMixin:
12
+ """Mixin providing HAVING clause for SELECT builders."""
13
+
14
+ _expression: Optional[exp.Expression] = None
15
+
16
+ def having(self, condition: Union[str, exp.Expression]) -> Self:
17
+ """Add HAVING clause.
18
+
19
+ Args:
20
+ condition: The condition for the HAVING clause.
21
+
22
+ Raises:
23
+ SQLBuilderError: If the current expression is not a SELECT statement.
24
+
25
+ Returns:
26
+ The current builder instance for method chaining.
27
+ """
28
+ if self._expression is None:
29
+ self._expression = exp.Select()
30
+ if not isinstance(self._expression, exp.Select):
31
+ msg = "Cannot add HAVING to a non-SELECT expression."
32
+ raise SQLBuilderError(msg)
33
+ having_expr = exp.condition(condition) if isinstance(condition, str) else condition
34
+ self._expression = self._expression.having(having_expr, copy=False)
35
+ return self
@@ -0,0 +1,48 @@
1
+ from typing import Any, Optional
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = ("InsertFromSelectMixin",)
9
+
10
+
11
+ class InsertFromSelectMixin:
12
+ """Mixin providing INSERT ... SELECT support for INSERT builders."""
13
+
14
+ _expression: Optional[exp.Expression] = None
15
+
16
+ def from_select(self, select_builder: Any) -> Self:
17
+ """Sets the INSERT source to a SELECT statement.
18
+
19
+ Args:
20
+ select_builder: A SelectBuilder instance representing the SELECT query.
21
+
22
+ Returns:
23
+ The current builder instance for method chaining.
24
+
25
+ Raises:
26
+ SQLBuilderError: If the table is not set or the select_builder is invalid.
27
+ """
28
+ if not getattr(self, "_table", None):
29
+ msg = "The target table must be set using .into() before adding values."
30
+ raise SQLBuilderError(msg)
31
+ if self._expression is None:
32
+ self._expression = exp.Insert()
33
+ if not isinstance(self._expression, exp.Insert):
34
+ msg = "Cannot set INSERT source on a non-INSERT expression."
35
+ raise SQLBuilderError(msg)
36
+ # Merge parameters from the SELECT builder
37
+ subquery_params = getattr(select_builder, "_parameters", None)
38
+ if subquery_params:
39
+ for p_name, p_value in subquery_params.items():
40
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
41
+ # Set the SELECT expression as the source
42
+ select_expr = getattr(select_builder, "_expression", None)
43
+ if select_expr and isinstance(select_expr, exp.Select):
44
+ self._expression.set("expression", select_expr.copy())
45
+ else:
46
+ msg = "SelectBuilder must have a valid SELECT expression."
47
+ raise SQLBuilderError(msg)
48
+ return self
@@ -0,0 +1,36 @@
1
+ from typing import Optional
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = ("InsertIntoClauseMixin",)
9
+
10
+
11
+ class InsertIntoClauseMixin:
12
+ """Mixin providing INTO clause for INSERT builders."""
13
+
14
+ _expression: Optional[exp.Expression] = None
15
+
16
+ def into(self, table: str) -> Self:
17
+ """Set the target table for the INSERT statement.
18
+
19
+ Args:
20
+ table: The name of the table to insert data into.
21
+
22
+ Raises:
23
+ SQLBuilderError: If the current expression is not an INSERT statement.
24
+
25
+ Returns:
26
+ The current builder instance for method chaining.
27
+ """
28
+ if self._expression is None:
29
+ self._expression = exp.Insert()
30
+ if not isinstance(self._expression, exp.Insert):
31
+ msg = "Cannot set target table on a non-INSERT expression."
32
+ raise SQLBuilderError(msg)
33
+
34
+ setattr(self, "_table", table)
35
+ self._expression.set("this", exp.to_table(table))
36
+ return self
@@ -0,0 +1,69 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any, Optional, Union
3
+
4
+ from sqlglot import exp
5
+ from typing_extensions import Self
6
+
7
+ from sqlspec.exceptions import SQLBuilderError
8
+
9
+ __all__ = ("InsertValuesMixin",)
10
+
11
+
12
+ class InsertValuesMixin:
13
+ """Mixin providing VALUES and columns methods for INSERT builders."""
14
+
15
+ _expression: Optional[exp.Expression] = None
16
+
17
+ def columns(self, *columns: Union[str, exp.Expression]) -> Self:
18
+ """Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
19
+ if self._expression is None:
20
+ self._expression = exp.Insert()
21
+ if not isinstance(self._expression, exp.Insert):
22
+ msg = "Cannot set columns on a non-INSERT expression."
23
+ raise SQLBuilderError(msg)
24
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
25
+ self._expression.set("columns", column_exprs)
26
+ # Synchronize the _columns attribute on the builder (if present)
27
+ if hasattr(self, "_columns"):
28
+ # If no columns, clear the list
29
+ if not columns:
30
+ self._columns.clear() # pyright: ignore
31
+ else:
32
+ self._columns[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns] # pyright: ignore
33
+ return self
34
+
35
+ def values(self, *values: Any) -> Self:
36
+ """Add a row of values to the INSERT statement, validating against _columns if set."""
37
+ if self._expression is None:
38
+ self._expression = exp.Insert()
39
+ if not isinstance(self._expression, exp.Insert):
40
+ msg = "Cannot add values to a non-INSERT expression."
41
+ raise SQLBuilderError(msg)
42
+ # Validate value count if _columns is present and non-empty
43
+ if (
44
+ hasattr(self, "_columns") and getattr(self, "_columns", []) and len(values) != len(self._columns) # pyright: ignore
45
+ ):
46
+ msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(self._columns)})." # pyright: ignore
47
+ raise SQLBuilderError(msg)
48
+ row_exprs = []
49
+ for v in values:
50
+ if isinstance(v, exp.Expression):
51
+ row_exprs.append(v)
52
+ else:
53
+ # Add as parameter
54
+ _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
55
+ row_exprs.append(exp.var(param_name))
56
+ values_expr = exp.Values(expressions=[row_exprs])
57
+ self._expression.set("expression", values_expr)
58
+ return self
59
+
60
+ def add_values(self, values: Sequence[Any]) -> Self:
61
+ """Add a row of values to the INSERT statement (alternative signature).
62
+
63
+ Args:
64
+ values: Sequence of values for the row.
65
+
66
+ Returns:
67
+ The current builder instance for method chaining.
68
+ """
69
+ return self.values(*values)