sqlspec 0.16.1__py3-none-any.whl → 0.17.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 +11 -1
- sqlspec/_sql.py +18 -412
- sqlspec/adapters/aiosqlite/__init__.py +11 -1
- sqlspec/adapters/aiosqlite/config.py +137 -165
- sqlspec/adapters/aiosqlite/driver.py +21 -10
- sqlspec/adapters/aiosqlite/pool.py +492 -0
- sqlspec/adapters/duckdb/__init__.py +2 -0
- sqlspec/adapters/duckdb/config.py +11 -235
- sqlspec/adapters/duckdb/pool.py +243 -0
- sqlspec/adapters/sqlite/__init__.py +2 -0
- sqlspec/adapters/sqlite/config.py +4 -115
- sqlspec/adapters/sqlite/pool.py +140 -0
- sqlspec/base.py +147 -26
- sqlspec/builder/__init__.py +6 -0
- sqlspec/builder/_insert.py +177 -12
- sqlspec/builder/_parsing_utils.py +53 -2
- sqlspec/builder/mixins/_join_operations.py +148 -7
- sqlspec/builder/mixins/_merge_operations.py +102 -16
- sqlspec/builder/mixins/_select_operations.py +311 -6
- sqlspec/builder/mixins/_update_operations.py +49 -34
- sqlspec/builder/mixins/_where_clause.py +85 -13
- sqlspec/core/compiler.py +7 -5
- sqlspec/driver/_common.py +9 -1
- sqlspec/loader.py +27 -54
- sqlspec/storage/registry.py +2 -2
- sqlspec/typing.py +53 -99
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/METADATA +1 -1
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/RECORD +32 -29
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -12,10 +12,11 @@ from sqlspec.exceptions import SQLBuilderError
|
|
|
12
12
|
from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from sqlspec.builder._column import Column, FunctionColumn
|
|
15
|
+
from sqlspec.builder._column import Column, ColumnExpression, FunctionColumn
|
|
16
|
+
from sqlspec.core.statement import SQL
|
|
16
17
|
from sqlspec.protocols import SelectBuilderProtocol, SQLBuilderProtocol
|
|
17
18
|
|
|
18
|
-
__all__ = ("CaseBuilder", "SelectClauseMixin")
|
|
19
|
+
__all__ = ("Case", "CaseBuilder", "SelectClauseMixin", "SubqueryBuilder", "WindowFunctionBuilder")
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@trait
|
|
@@ -27,7 +28,7 @@ class SelectClauseMixin:
|
|
|
27
28
|
# Type annotation for PyRight - this will be provided by the base class
|
|
28
29
|
_expression: Optional[exp.Expression]
|
|
29
30
|
|
|
30
|
-
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
|
|
31
|
+
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL", "Case"]) -> Self:
|
|
31
32
|
"""Add columns to SELECT clause.
|
|
32
33
|
|
|
33
34
|
Raises:
|
|
@@ -43,10 +44,10 @@ class SelectClauseMixin:
|
|
|
43
44
|
msg = "Cannot add select columns to a non-SELECT expression."
|
|
44
45
|
raise SQLBuilderError(msg)
|
|
45
46
|
for column in columns:
|
|
46
|
-
builder._expression = builder._expression.select(parse_column_expression(column), copy=False)
|
|
47
|
+
builder._expression = builder._expression.select(parse_column_expression(column, builder), copy=False)
|
|
47
48
|
return cast("Self", builder)
|
|
48
49
|
|
|
49
|
-
def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
|
|
50
|
+
def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL"]) -> Self:
|
|
50
51
|
"""Add DISTINCT clause to SELECT.
|
|
51
52
|
|
|
52
53
|
Args:
|
|
@@ -67,7 +68,7 @@ class SelectClauseMixin:
|
|
|
67
68
|
if not columns:
|
|
68
69
|
builder._expression.set("distinct", exp.Distinct())
|
|
69
70
|
else:
|
|
70
|
-
distinct_columns = [parse_column_expression(column) for column in columns]
|
|
71
|
+
distinct_columns = [parse_column_expression(column, builder) for column in columns]
|
|
71
72
|
builder._expression.set("distinct", exp.Distinct(expressions=distinct_columns))
|
|
72
73
|
return cast("Self", builder)
|
|
73
74
|
|
|
@@ -601,3 +602,307 @@ class CaseBuilder:
|
|
|
601
602
|
"""
|
|
602
603
|
select_expr = exp.alias_(self._case_expr, self._alias) if self._alias else self._case_expr
|
|
603
604
|
return self._parent.select(select_expr)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@trait
|
|
608
|
+
class WindowFunctionBuilder:
|
|
609
|
+
"""Builder for window functions with fluent syntax.
|
|
610
|
+
|
|
611
|
+
Example:
|
|
612
|
+
```python
|
|
613
|
+
from sqlspec import sql
|
|
614
|
+
|
|
615
|
+
# sql.row_number_.partition_by("department").order_by("salary")
|
|
616
|
+
window_func = (
|
|
617
|
+
sql.row_number_.partition_by("department")
|
|
618
|
+
.order_by("salary")
|
|
619
|
+
.as_("row_num")
|
|
620
|
+
)
|
|
621
|
+
```
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
def __init__(self, function_name: str) -> None:
|
|
625
|
+
"""Initialize the window function builder.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
function_name: Name of the window function (row_number, rank, etc.)
|
|
629
|
+
"""
|
|
630
|
+
self._function_name = function_name
|
|
631
|
+
self._partition_by_cols: list[exp.Expression] = []
|
|
632
|
+
self._order_by_cols: list[exp.Expression] = []
|
|
633
|
+
self._alias: Optional[str] = None
|
|
634
|
+
|
|
635
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
636
|
+
"""Equal to (==) - convert to expression then compare."""
|
|
637
|
+
from sqlspec.builder._column import ColumnExpression
|
|
638
|
+
|
|
639
|
+
window_expr = self._build_expression()
|
|
640
|
+
if other is None:
|
|
641
|
+
return ColumnExpression(exp.Is(this=window_expr, expression=exp.Null()))
|
|
642
|
+
return ColumnExpression(exp.EQ(this=window_expr, expression=exp.convert(other)))
|
|
643
|
+
|
|
644
|
+
def __hash__(self) -> int:
|
|
645
|
+
"""Make WindowFunctionBuilder hashable."""
|
|
646
|
+
return hash(id(self))
|
|
647
|
+
|
|
648
|
+
def partition_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
|
|
649
|
+
"""Add PARTITION BY clause.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
*columns: Columns to partition by.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Self for method chaining.
|
|
656
|
+
"""
|
|
657
|
+
for col in columns:
|
|
658
|
+
col_expr = exp.column(col) if isinstance(col, str) else col
|
|
659
|
+
self._partition_by_cols.append(col_expr)
|
|
660
|
+
return self
|
|
661
|
+
|
|
662
|
+
def order_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
|
|
663
|
+
"""Add ORDER BY clause.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
*columns: Columns to order by.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Self for method chaining.
|
|
670
|
+
"""
|
|
671
|
+
for col in columns:
|
|
672
|
+
if isinstance(col, str):
|
|
673
|
+
col_expr = exp.column(col).asc()
|
|
674
|
+
self._order_by_cols.append(col_expr)
|
|
675
|
+
else:
|
|
676
|
+
# Convert to ordered expression
|
|
677
|
+
self._order_by_cols.append(exp.Ordered(this=col, desc=False))
|
|
678
|
+
return self
|
|
679
|
+
|
|
680
|
+
def as_(self, alias: str) -> exp.Alias:
|
|
681
|
+
"""Complete the window function with an alias.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
alias: Alias name for the window function.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
Aliased window function expression.
|
|
688
|
+
"""
|
|
689
|
+
window_expr = self._build_expression()
|
|
690
|
+
return cast("exp.Alias", exp.alias_(window_expr, alias))
|
|
691
|
+
|
|
692
|
+
def build(self) -> exp.Expression:
|
|
693
|
+
"""Complete the window function without an alias.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
Window function expression.
|
|
697
|
+
"""
|
|
698
|
+
return self._build_expression()
|
|
699
|
+
|
|
700
|
+
def _build_expression(self) -> exp.Expression:
|
|
701
|
+
"""Build the complete window function expression."""
|
|
702
|
+
# Create the function expression
|
|
703
|
+
func_expr = exp.Anonymous(this=self._function_name.upper(), expressions=[])
|
|
704
|
+
|
|
705
|
+
# Build the OVER clause arguments
|
|
706
|
+
over_args: dict[str, Any] = {}
|
|
707
|
+
|
|
708
|
+
if self._partition_by_cols:
|
|
709
|
+
over_args["partition_by"] = self._partition_by_cols
|
|
710
|
+
|
|
711
|
+
if self._order_by_cols:
|
|
712
|
+
over_args["order"] = exp.Order(expressions=self._order_by_cols)
|
|
713
|
+
|
|
714
|
+
return exp.Window(this=func_expr, **over_args)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
@trait
|
|
718
|
+
class SubqueryBuilder:
|
|
719
|
+
"""Builder for subquery operations with fluent syntax.
|
|
720
|
+
|
|
721
|
+
Example:
|
|
722
|
+
```python
|
|
723
|
+
from sqlspec import sql
|
|
724
|
+
|
|
725
|
+
# sql.exists_(subquery)
|
|
726
|
+
exists_check = sql.exists_(
|
|
727
|
+
sql.select("1")
|
|
728
|
+
.from_("orders")
|
|
729
|
+
.where_eq("user_id", sql.users.id)
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# sql.in_(subquery)
|
|
733
|
+
in_check = sql.in_(
|
|
734
|
+
sql.select("category_id")
|
|
735
|
+
.from_("categories")
|
|
736
|
+
.where_eq("active", True)
|
|
737
|
+
)
|
|
738
|
+
```
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
def __init__(self, operation: str) -> None:
|
|
742
|
+
"""Initialize the subquery builder.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
operation: Type of subquery operation (exists, in, any, all)
|
|
746
|
+
"""
|
|
747
|
+
self._operation = operation
|
|
748
|
+
|
|
749
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
750
|
+
"""Equal to (==) - not typically used but needed for type consistency."""
|
|
751
|
+
from sqlspec.builder._column import ColumnExpression
|
|
752
|
+
|
|
753
|
+
# SubqueryBuilder doesn't have a direct expression, so this is a placeholder
|
|
754
|
+
# In practice, this shouldn't be called as subqueries are used differently
|
|
755
|
+
placeholder_expr = exp.Literal.string(f"subquery_{self._operation}")
|
|
756
|
+
if other is None:
|
|
757
|
+
return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
|
|
758
|
+
return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
|
|
759
|
+
|
|
760
|
+
def __hash__(self) -> int:
|
|
761
|
+
"""Make SubqueryBuilder hashable."""
|
|
762
|
+
return hash(id(self))
|
|
763
|
+
|
|
764
|
+
def __call__(self, subquery: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
765
|
+
"""Build the subquery expression.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
subquery: The subquery - can be a SQL string, SelectBuilder, or expression
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
The subquery expression (EXISTS, IN, ANY, ALL, etc.)
|
|
772
|
+
"""
|
|
773
|
+
subquery_expr: exp.Expression
|
|
774
|
+
if isinstance(subquery, str):
|
|
775
|
+
# Parse as SQL
|
|
776
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(subquery)
|
|
777
|
+
if not parsed:
|
|
778
|
+
msg = f"Could not parse subquery SQL: {subquery}"
|
|
779
|
+
raise SQLBuilderError(msg)
|
|
780
|
+
subquery_expr = parsed
|
|
781
|
+
elif hasattr(subquery, "build") and callable(getattr(subquery, "build", None)):
|
|
782
|
+
# It's a query builder - build it to get the SQL and parse
|
|
783
|
+
built_query = subquery.build() # pyright: ignore[reportAttributeAccessIssue]
|
|
784
|
+
subquery_expr = exp.maybe_parse(built_query.sql)
|
|
785
|
+
if not subquery_expr:
|
|
786
|
+
msg = f"Could not parse built query: {built_query.sql}"
|
|
787
|
+
raise SQLBuilderError(msg)
|
|
788
|
+
elif isinstance(subquery, exp.Expression):
|
|
789
|
+
subquery_expr = subquery
|
|
790
|
+
else:
|
|
791
|
+
# Try to convert to expression
|
|
792
|
+
parsed = exp.maybe_parse(str(subquery))
|
|
793
|
+
if not parsed:
|
|
794
|
+
msg = f"Could not convert subquery to expression: {subquery}"
|
|
795
|
+
raise SQLBuilderError(msg)
|
|
796
|
+
subquery_expr = parsed
|
|
797
|
+
|
|
798
|
+
# Build the appropriate expression based on operation
|
|
799
|
+
if self._operation == "exists":
|
|
800
|
+
return exp.Exists(this=subquery_expr)
|
|
801
|
+
if self._operation == "in":
|
|
802
|
+
# For IN, we create a subquery that can be used with WHERE column IN (subquery)
|
|
803
|
+
return exp.In(expressions=[subquery_expr])
|
|
804
|
+
if self._operation == "any":
|
|
805
|
+
return exp.Any(this=subquery_expr)
|
|
806
|
+
if self._operation == "all":
|
|
807
|
+
return exp.All(this=subquery_expr)
|
|
808
|
+
msg = f"Unknown subquery operation: {self._operation}"
|
|
809
|
+
raise SQLBuilderError(msg)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
@trait
|
|
813
|
+
class Case:
|
|
814
|
+
"""Builder for CASE expressions using the SQL factory.
|
|
815
|
+
|
|
816
|
+
Example:
|
|
817
|
+
```python
|
|
818
|
+
from sqlspec import sql
|
|
819
|
+
|
|
820
|
+
case_expr = (
|
|
821
|
+
sql.case()
|
|
822
|
+
.when(sql.age < 18, "Minor")
|
|
823
|
+
.when(sql.age < 65, "Adult")
|
|
824
|
+
.else_("Senior")
|
|
825
|
+
.end()
|
|
826
|
+
)
|
|
827
|
+
```
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
def __init__(self) -> None:
|
|
831
|
+
"""Initialize the CASE expression builder."""
|
|
832
|
+
self._conditions: list[exp.If] = []
|
|
833
|
+
self._default: Optional[exp.Expression] = None
|
|
834
|
+
|
|
835
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
836
|
+
"""Equal to (==) - convert to expression then compare."""
|
|
837
|
+
from sqlspec.builder._column import ColumnExpression
|
|
838
|
+
|
|
839
|
+
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
840
|
+
if other is None:
|
|
841
|
+
return ColumnExpression(exp.Is(this=case_expr, expression=exp.Null()))
|
|
842
|
+
return ColumnExpression(exp.EQ(this=case_expr, expression=exp.convert(other)))
|
|
843
|
+
|
|
844
|
+
def __hash__(self) -> int:
|
|
845
|
+
"""Make Case hashable."""
|
|
846
|
+
return hash(id(self))
|
|
847
|
+
|
|
848
|
+
def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> Self:
|
|
849
|
+
"""Add a WHEN clause.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
condition: Condition to test.
|
|
853
|
+
value: Value to return if condition is true.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
Self for method chaining.
|
|
857
|
+
"""
|
|
858
|
+
from sqlspec._sql import SQLFactory
|
|
859
|
+
|
|
860
|
+
cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
|
|
861
|
+
val_expr = SQLFactory._to_literal(value)
|
|
862
|
+
|
|
863
|
+
# SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
|
|
864
|
+
when_clause = exp.If(this=cond_expr, true=val_expr)
|
|
865
|
+
self._conditions.append(when_clause)
|
|
866
|
+
return self
|
|
867
|
+
|
|
868
|
+
def else_(self, value: Union[str, exp.Expression, Any]) -> Self:
|
|
869
|
+
"""Add an ELSE clause.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
value: Default value to return.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
Self for method chaining.
|
|
876
|
+
"""
|
|
877
|
+
from sqlspec._sql import SQLFactory
|
|
878
|
+
|
|
879
|
+
self._default = SQLFactory._to_literal(value)
|
|
880
|
+
return self
|
|
881
|
+
|
|
882
|
+
def end(self) -> Self:
|
|
883
|
+
"""Complete the CASE expression.
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
Complete CASE expression.
|
|
887
|
+
"""
|
|
888
|
+
return self
|
|
889
|
+
|
|
890
|
+
@property
|
|
891
|
+
def _expression(self) -> exp.Case:
|
|
892
|
+
"""Get the sqlglot expression for this case builder.
|
|
893
|
+
|
|
894
|
+
This allows the CaseBuilder to be used wherever expressions are expected.
|
|
895
|
+
"""
|
|
896
|
+
return exp.Case(ifs=self._conditions, default=self._default)
|
|
897
|
+
|
|
898
|
+
def as_(self, alias: str) -> exp.Alias:
|
|
899
|
+
"""Complete the CASE expression with an alias.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
alias: Alias name for the CASE expression.
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
Aliased CASE expression.
|
|
906
|
+
"""
|
|
907
|
+
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
908
|
+
return cast("exp.Alias", exp.alias_(case_expr, alias))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Update operation mixins for SQL builders."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
|
-
from typing import Any, Optional, Union
|
|
4
|
+
from typing import Any, Optional, Union, cast
|
|
5
5
|
|
|
6
6
|
from mypy_extensions import trait
|
|
7
7
|
from sqlglot import exp
|
|
@@ -61,6 +61,52 @@ class UpdateSetClauseMixin:
|
|
|
61
61
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
62
62
|
raise NotImplementedError(msg)
|
|
63
63
|
|
|
64
|
+
def _process_update_value(self, val: Any, col: Any) -> exp.Expression:
|
|
65
|
+
"""Process a value for UPDATE assignment, handling SQL objects and parameters.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
val: The value to process
|
|
69
|
+
col: The column name for parameter naming
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The processed expression for the value
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(val, exp.Expression):
|
|
75
|
+
return val
|
|
76
|
+
if has_query_builder_parameters(val):
|
|
77
|
+
subquery = val.build()
|
|
78
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
79
|
+
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
80
|
+
if has_query_builder_parameters(val):
|
|
81
|
+
for p_name, p_value in val.parameters.items():
|
|
82
|
+
self.add_parameter(p_value, name=p_name)
|
|
83
|
+
return value_expr
|
|
84
|
+
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
85
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
86
|
+
expression = getattr(val, "expression", None)
|
|
87
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
88
|
+
# Merge parameters from SQL object into builder
|
|
89
|
+
if hasattr(val, "parameters"):
|
|
90
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
91
|
+
for param_name, param_value in sql_parameters.items():
|
|
92
|
+
self.add_parameter(param_value, name=param_name)
|
|
93
|
+
return cast("exp.Expression", expression)
|
|
94
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
95
|
+
sql_text = getattr(val, "sql", "")
|
|
96
|
+
# Merge parameters even when parsing raw SQL
|
|
97
|
+
if hasattr(val, "parameters"):
|
|
98
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
99
|
+
for param_name, param_value in sql_parameters.items():
|
|
100
|
+
self.add_parameter(param_value, name=param_name)
|
|
101
|
+
parsed_expr = exp.maybe_parse(sql_text)
|
|
102
|
+
return parsed_expr if parsed_expr is not None else exp.convert(str(sql_text))
|
|
103
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
104
|
+
if "." in column_name:
|
|
105
|
+
column_name = column_name.split(".")[-1]
|
|
106
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
107
|
+
param_name = self.add_parameter(val, name=param_name)[1]
|
|
108
|
+
return exp.Placeholder(this=param_name)
|
|
109
|
+
|
|
64
110
|
def set(self, *args: Any, **kwargs: Any) -> Self:
|
|
65
111
|
"""Set columns and values for the UPDATE statement.
|
|
66
112
|
|
|
@@ -80,7 +126,6 @@ class UpdateSetClauseMixin:
|
|
|
80
126
|
Returns:
|
|
81
127
|
The current builder instance for method chaining.
|
|
82
128
|
"""
|
|
83
|
-
|
|
84
129
|
if self._expression is None:
|
|
85
130
|
self._expression = exp.Update()
|
|
86
131
|
if not isinstance(self._expression, exp.Update):
|
|
@@ -90,42 +135,12 @@ class UpdateSetClauseMixin:
|
|
|
90
135
|
if len(args) == MIN_SET_ARGS and not kwargs:
|
|
91
136
|
col, val = args
|
|
92
137
|
col_expr = col if isinstance(col, exp.Column) else exp.column(col)
|
|
93
|
-
|
|
94
|
-
value_expr = val
|
|
95
|
-
elif has_query_builder_parameters(val):
|
|
96
|
-
subquery = val.build()
|
|
97
|
-
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
98
|
-
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
99
|
-
if has_query_builder_parameters(val):
|
|
100
|
-
for p_name, p_value in val.parameters.items():
|
|
101
|
-
self.add_parameter(p_value, name=p_name)
|
|
102
|
-
else:
|
|
103
|
-
column_name = col if isinstance(col, str) else str(col)
|
|
104
|
-
if "." in column_name:
|
|
105
|
-
column_name = column_name.split(".")[-1]
|
|
106
|
-
param_name = self._generate_unique_parameter_name(column_name)
|
|
107
|
-
param_name = self.add_parameter(val, name=param_name)[1]
|
|
108
|
-
value_expr = exp.Placeholder(this=param_name)
|
|
138
|
+
value_expr = self._process_update_value(val, col)
|
|
109
139
|
assignments.append(exp.EQ(this=col_expr, expression=value_expr))
|
|
110
140
|
elif (len(args) == 1 and isinstance(args[0], Mapping)) or kwargs:
|
|
111
141
|
all_values = dict(args[0] if args else {}, **kwargs)
|
|
112
142
|
for col, val in all_values.items():
|
|
113
|
-
|
|
114
|
-
value_expr = val
|
|
115
|
-
elif has_query_builder_parameters(val):
|
|
116
|
-
subquery = val.build()
|
|
117
|
-
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
118
|
-
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
119
|
-
if has_query_builder_parameters(val):
|
|
120
|
-
for p_name, p_value in val.parameters.items():
|
|
121
|
-
self.add_parameter(p_value, name=p_name)
|
|
122
|
-
else:
|
|
123
|
-
column_name = col if isinstance(col, str) else str(col)
|
|
124
|
-
if "." in column_name:
|
|
125
|
-
column_name = column_name.split(".")[-1]
|
|
126
|
-
param_name = self._generate_unique_parameter_name(column_name)
|
|
127
|
-
param_name = self.add_parameter(val, name=param_name)[1]
|
|
128
|
-
value_expr = exp.Placeholder(this=param_name)
|
|
143
|
+
value_expr = self._process_update_value(val, col)
|
|
129
144
|
assignments.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
130
145
|
else:
|
|
131
146
|
msg = "Invalid arguments for set(): use (column, value), mapping, or kwargs."
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
5
5
|
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from sqlspec.core.statement import SQL
|
|
8
|
+
|
|
6
9
|
from mypy_extensions import trait
|
|
7
10
|
from sqlglot import exp
|
|
8
11
|
from typing_extensions import Self
|
|
@@ -208,21 +211,25 @@ class WhereClauseMixin:
|
|
|
208
211
|
|
|
209
212
|
def where(
|
|
210
213
|
self,
|
|
211
|
-
condition: Union[
|
|
212
|
-
|
|
214
|
+
condition: Union[
|
|
215
|
+
str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
|
|
216
|
+
],
|
|
217
|
+
*values: Any,
|
|
213
218
|
operator: Optional[str] = None,
|
|
219
|
+
**kwargs: Any,
|
|
214
220
|
) -> Self:
|
|
215
221
|
"""Add a WHERE clause to the statement.
|
|
216
222
|
|
|
217
223
|
Args:
|
|
218
224
|
condition: The condition for the WHERE clause. Can be:
|
|
219
|
-
- A string condition
|
|
220
|
-
- A string column name (when
|
|
225
|
+
- A string condition with or without parameter placeholders
|
|
226
|
+
- A string column name (when values are provided)
|
|
221
227
|
- A sqlglot Expression or Condition
|
|
222
228
|
- A 2-tuple (column, value) for equality comparison
|
|
223
229
|
- A 3-tuple (column, operator, value) for custom comparison
|
|
224
|
-
|
|
225
|
-
operator: Operator for comparison (when
|
|
230
|
+
*values: Positional values for parameter binding (when condition contains placeholders or is a column name)
|
|
231
|
+
operator: Operator for comparison (when condition is a column name)
|
|
232
|
+
**kwargs: Named parameters for parameter binding (when condition contains named placeholders)
|
|
226
233
|
|
|
227
234
|
Raises:
|
|
228
235
|
SQLBuilderError: If the current expression is not a supported statement type.
|
|
@@ -243,16 +250,63 @@ class WhereClauseMixin:
|
|
|
243
250
|
msg = "WHERE clause requires a table to be set. Use from() to set the table first."
|
|
244
251
|
raise SQLBuilderError(msg)
|
|
245
252
|
|
|
246
|
-
|
|
253
|
+
# Handle string conditions with external parameters
|
|
254
|
+
if values or kwargs:
|
|
247
255
|
if not isinstance(condition, str):
|
|
248
|
-
msg = "When
|
|
256
|
+
msg = "When values are provided, condition must be a string"
|
|
249
257
|
raise SQLBuilderError(msg)
|
|
250
258
|
|
|
251
|
-
if
|
|
252
|
-
|
|
259
|
+
# Check if condition contains parameter placeholders
|
|
260
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterValidator
|
|
261
|
+
|
|
262
|
+
validator = ParameterValidator()
|
|
263
|
+
param_info = validator.extract_parameters(condition)
|
|
264
|
+
|
|
265
|
+
if param_info:
|
|
266
|
+
# String condition with placeholders - create SQL object with parameters
|
|
267
|
+
from sqlspec import sql as sql_factory
|
|
268
|
+
|
|
269
|
+
# Create parameter mapping based on the detected parameter info
|
|
270
|
+
param_dict = dict(kwargs) # Start with named parameters
|
|
271
|
+
|
|
272
|
+
# Handle positional parameters - these are ordinal-based ($1, $2, :1, :2, ?)
|
|
273
|
+
positional_params = [
|
|
274
|
+
param
|
|
275
|
+
for param in param_info
|
|
276
|
+
if param.style in {ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK}
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
# Map positional values to positional parameters
|
|
280
|
+
if len(values) != len(positional_params):
|
|
281
|
+
msg = f"Parameter count mismatch: condition has {len(positional_params)} positional placeholders, got {len(values)} values"
|
|
282
|
+
raise SQLBuilderError(msg)
|
|
283
|
+
|
|
284
|
+
for i, value in enumerate(values):
|
|
285
|
+
param_dict[f"param_{i}"] = value
|
|
286
|
+
|
|
287
|
+
# Create SQL object with parameters that will be processed correctly
|
|
288
|
+
condition = sql_factory.raw(condition, **param_dict)
|
|
289
|
+
# Fall through to existing SQL object handling logic
|
|
290
|
+
|
|
291
|
+
elif len(values) == 1 and not kwargs:
|
|
292
|
+
# Single value - treat as column = value
|
|
293
|
+
if operator is not None:
|
|
294
|
+
where_expr = self._process_tuple_condition((condition, operator, values[0]))
|
|
295
|
+
else:
|
|
296
|
+
where_expr = self._process_tuple_condition((condition, values[0]))
|
|
297
|
+
# Process this condition and skip the rest
|
|
298
|
+
if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
|
|
299
|
+
builder._expression = builder._expression.where(where_expr, copy=False)
|
|
300
|
+
else:
|
|
301
|
+
msg = f"WHERE clause not supported for {type(builder._expression).__name__}"
|
|
302
|
+
raise SQLBuilderError(msg)
|
|
303
|
+
return self
|
|
253
304
|
else:
|
|
254
|
-
|
|
255
|
-
|
|
305
|
+
msg = f"Cannot bind parameters to condition without placeholders: {condition}"
|
|
306
|
+
raise SQLBuilderError(msg)
|
|
307
|
+
|
|
308
|
+
# Handle all condition types (including SQL objects created above)
|
|
309
|
+
if isinstance(condition, str):
|
|
256
310
|
where_expr = parse_condition_expression(condition)
|
|
257
311
|
elif isinstance(condition, (exp.Expression, exp.Condition)):
|
|
258
312
|
where_expr = condition
|
|
@@ -267,6 +321,25 @@ class WhereClauseMixin:
|
|
|
267
321
|
where_expr = builder._parameterize_expression(raw_expr)
|
|
268
322
|
else:
|
|
269
323
|
where_expr = parse_condition_expression(str(condition))
|
|
324
|
+
elif hasattr(condition, "expression") and hasattr(condition, "sql"):
|
|
325
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
326
|
+
expression = getattr(condition, "expression", None)
|
|
327
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
328
|
+
# Merge parameters from SQL object into builder
|
|
329
|
+
if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
|
|
330
|
+
sql_parameters = getattr(condition, "parameters", {})
|
|
331
|
+
for param_name, param_value in sql_parameters.items():
|
|
332
|
+
builder.add_parameter(param_value, name=param_name)
|
|
333
|
+
where_expr = expression
|
|
334
|
+
else:
|
|
335
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
336
|
+
sql_text = getattr(condition, "sql", "")
|
|
337
|
+
# Merge parameters even when parsing raw SQL
|
|
338
|
+
if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
|
|
339
|
+
sql_parameters = getattr(condition, "parameters", {})
|
|
340
|
+
for param_name, param_value in sql_parameters.items():
|
|
341
|
+
builder.add_parameter(param_value, name=param_name)
|
|
342
|
+
where_expr = parse_condition_expression(sql_text)
|
|
270
343
|
else:
|
|
271
344
|
msg = f"Unsupported condition type: {type(condition).__name__}"
|
|
272
345
|
raise SQLBuilderError(msg)
|
|
@@ -596,7 +669,6 @@ class HavingClauseMixin:
|
|
|
596
669
|
|
|
597
670
|
__slots__ = ()
|
|
598
671
|
|
|
599
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
600
672
|
_expression: Optional[exp.Expression]
|
|
601
673
|
|
|
602
674
|
def having(self, condition: Union[str, exp.Expression]) -> Self:
|
sqlspec/core/compiler.py
CHANGED
|
@@ -277,11 +277,13 @@ class SQLProcessor:
|
|
|
277
277
|
if self._config.parameter_config.needs_static_script_compilation and processed_params is None:
|
|
278
278
|
final_sql, final_params = processed_sql, processed_params
|
|
279
279
|
elif ast_was_transformed and expression is not None:
|
|
280
|
-
final_sql =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
final_sql, final_params = self._parameter_processor.process(
|
|
281
|
+
sql=expression.sql(dialect=dialect_str),
|
|
282
|
+
parameters=final_parameters,
|
|
283
|
+
config=self._config.parameter_config,
|
|
284
|
+
dialect=dialect_str,
|
|
285
|
+
is_many=is_many,
|
|
286
|
+
)
|
|
285
287
|
output_transformer = self._config.output_transformer
|
|
286
288
|
if output_transformer:
|
|
287
289
|
final_sql, final_params = output_transformer(final_sql, final_params)
|
sqlspec/driver/_common.py
CHANGED
|
@@ -275,7 +275,15 @@ class CommonDriverAttributesMixin:
|
|
|
275
275
|
kwargs = kwargs or {}
|
|
276
276
|
|
|
277
277
|
if isinstance(statement, QueryBuilder):
|
|
278
|
-
|
|
278
|
+
sql_statement = statement.to_statement(statement_config)
|
|
279
|
+
if parameters or kwargs:
|
|
280
|
+
merged_parameters = (
|
|
281
|
+
(*sql_statement._positional_parameters, *parameters)
|
|
282
|
+
if parameters
|
|
283
|
+
else sql_statement._positional_parameters
|
|
284
|
+
)
|
|
285
|
+
return SQL(sql_statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
|
|
286
|
+
return sql_statement
|
|
279
287
|
if isinstance(statement, SQL):
|
|
280
288
|
if parameters or kwargs:
|
|
281
289
|
merged_parameters = (
|