sqlspec 0.16.2__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 +16 -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/_parsing_utils.py +27 -0
- sqlspec/builder/mixins/_join_operations.py +115 -1
- sqlspec/builder/mixins/_select_operations.py +307 -3
- sqlspec/builder/mixins/_where_clause.py +60 -11
- 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.2.dist-info → sqlspec-0.17.0.dist-info}/METADATA +1 -1
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/RECORD +29 -26
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -12,11 +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
16
|
from sqlspec.core.statement import SQL
|
|
17
17
|
from sqlspec.protocols import SelectBuilderProtocol, SQLBuilderProtocol
|
|
18
18
|
|
|
19
|
-
__all__ = ("CaseBuilder", "SelectClauseMixin")
|
|
19
|
+
__all__ = ("Case", "CaseBuilder", "SelectClauseMixin", "SubqueryBuilder", "WindowFunctionBuilder")
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@trait
|
|
@@ -28,7 +28,7 @@ class SelectClauseMixin:
|
|
|
28
28
|
# Type annotation for PyRight - this will be provided by the base class
|
|
29
29
|
_expression: Optional[exp.Expression]
|
|
30
30
|
|
|
31
|
-
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL"]) -> Self:
|
|
31
|
+
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL", "Case"]) -> Self:
|
|
32
32
|
"""Add columns to SELECT clause.
|
|
33
33
|
|
|
34
34
|
Raises:
|
|
@@ -602,3 +602,307 @@ class CaseBuilder:
|
|
|
602
602
|
"""
|
|
603
603
|
select_expr = exp.alias_(self._case_expr, self._alias) if self._alias else self._case_expr
|
|
604
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))
|
|
@@ -214,20 +214,22 @@ class WhereClauseMixin:
|
|
|
214
214
|
condition: Union[
|
|
215
215
|
str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
|
|
216
216
|
],
|
|
217
|
-
|
|
217
|
+
*values: Any,
|
|
218
218
|
operator: Optional[str] = None,
|
|
219
|
+
**kwargs: Any,
|
|
219
220
|
) -> Self:
|
|
220
221
|
"""Add a WHERE clause to the statement.
|
|
221
222
|
|
|
222
223
|
Args:
|
|
223
224
|
condition: The condition for the WHERE clause. Can be:
|
|
224
|
-
- A string condition
|
|
225
|
-
- A string column name (when
|
|
225
|
+
- A string condition with or without parameter placeholders
|
|
226
|
+
- A string column name (when values are provided)
|
|
226
227
|
- A sqlglot Expression or Condition
|
|
227
228
|
- A 2-tuple (column, value) for equality comparison
|
|
228
229
|
- A 3-tuple (column, operator, value) for custom comparison
|
|
229
|
-
|
|
230
|
-
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)
|
|
231
233
|
|
|
232
234
|
Raises:
|
|
233
235
|
SQLBuilderError: If the current expression is not a supported statement type.
|
|
@@ -248,16 +250,63 @@ class WhereClauseMixin:
|
|
|
248
250
|
msg = "WHERE clause requires a table to be set. Use from() to set the table first."
|
|
249
251
|
raise SQLBuilderError(msg)
|
|
250
252
|
|
|
251
|
-
|
|
253
|
+
# Handle string conditions with external parameters
|
|
254
|
+
if values or kwargs:
|
|
252
255
|
if not isinstance(condition, str):
|
|
253
|
-
msg = "When
|
|
256
|
+
msg = "When values are provided, condition must be a string"
|
|
254
257
|
raise SQLBuilderError(msg)
|
|
255
258
|
|
|
256
|
-
if
|
|
257
|
-
|
|
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
|
|
258
304
|
else:
|
|
259
|
-
|
|
260
|
-
|
|
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):
|
|
261
310
|
where_expr = parse_condition_expression(condition)
|
|
262
311
|
elif isinstance(condition, (exp.Expression, exp.Condition)):
|
|
263
312
|
where_expr = condition
|
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 = (
|
sqlspec/loader.py
CHANGED
|
@@ -14,9 +14,13 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Optional, Union
|
|
15
15
|
|
|
16
16
|
from sqlspec.core.cache import CacheKey, get_cache_config, get_default_cache
|
|
17
|
-
from sqlspec.core.
|
|
18
|
-
from sqlspec.
|
|
19
|
-
|
|
17
|
+
from sqlspec.core.statement import SQL
|
|
18
|
+
from sqlspec.exceptions import (
|
|
19
|
+
MissingDependencyError,
|
|
20
|
+
SQLFileNotFoundError,
|
|
21
|
+
SQLFileParseError,
|
|
22
|
+
StorageOperationFailedError,
|
|
23
|
+
)
|
|
20
24
|
from sqlspec.storage import storage_registry
|
|
21
25
|
from sqlspec.storage.registry import StorageRegistry
|
|
22
26
|
from sqlspec.utils.correlation import CorrelationContext
|
|
@@ -29,7 +33,7 @@ logger = get_logger("loader")
|
|
|
29
33
|
# Matches: -- name: query_name (supports hyphens and special suffixes)
|
|
30
34
|
# We capture the name plus any trailing special characters
|
|
31
35
|
QUERY_NAME_PATTERN = re.compile(r"^\s*--\s*name\s*:\s*([\w-]+[^\w\s]*)\s*$", re.MULTILINE | re.IGNORECASE)
|
|
32
|
-
TRIM_SPECIAL_CHARS = re.compile(r"[^\w
|
|
36
|
+
TRIM_SPECIAL_CHARS = re.compile(r"[^\w.-]")
|
|
33
37
|
|
|
34
38
|
# Matches: -- dialect: dialect_name (optional dialect specification)
|
|
35
39
|
DIALECT_PATTERN = re.compile(r"^\s*--\s*dialect\s*:\s*(?P<dialect>[a-zA-Z0-9_]+)\s*$", re.IGNORECASE | re.MULTILINE)
|
|
@@ -304,6 +308,12 @@ class SQLFileLoader:
|
|
|
304
308
|
return backend.read_text(path_str, encoding=self.encoding)
|
|
305
309
|
except KeyError as e:
|
|
306
310
|
raise SQLFileNotFoundError(path_str) from e
|
|
311
|
+
except MissingDependencyError:
|
|
312
|
+
# Fall back to standard file reading when no storage backend is available
|
|
313
|
+
try:
|
|
314
|
+
return path.read_text(encoding=self.encoding) # type: ignore[union-attr]
|
|
315
|
+
except FileNotFoundError as e:
|
|
316
|
+
raise SQLFileNotFoundError(path_str) from e
|
|
307
317
|
except StorageOperationFailedError as e:
|
|
308
318
|
if "not found" in str(e).lower() or "no such file" in str(e).lower():
|
|
309
319
|
raise SQLFileNotFoundError(path_str) from e
|
|
@@ -570,8 +580,11 @@ class SQLFileLoader:
|
|
|
570
580
|
Raises:
|
|
571
581
|
ValueError: If query name already exists.
|
|
572
582
|
"""
|
|
573
|
-
|
|
574
|
-
|
|
583
|
+
# Normalize the name for consistency with file-loaded queries
|
|
584
|
+
normalized_name = _normalize_query_name(name)
|
|
585
|
+
|
|
586
|
+
if normalized_name in self._queries:
|
|
587
|
+
existing_source = self._query_to_file.get(normalized_name, "<directly added>")
|
|
575
588
|
msg = f"Query name '{name}' already exists (source: {existing_source})"
|
|
576
589
|
raise ValueError(msg)
|
|
577
590
|
|
|
@@ -588,21 +601,16 @@ class SQLFileLoader:
|
|
|
588
601
|
else:
|
|
589
602
|
dialect = normalized_dialect
|
|
590
603
|
|
|
591
|
-
statement = NamedStatement(name=
|
|
592
|
-
self._queries[
|
|
593
|
-
self._query_to_file[
|
|
604
|
+
statement = NamedStatement(name=normalized_name, sql=sql.strip(), dialect=dialect, start_line=0)
|
|
605
|
+
self._queries[normalized_name] = statement
|
|
606
|
+
self._query_to_file[normalized_name] = "<directly added>"
|
|
594
607
|
|
|
595
|
-
def get_sql(
|
|
596
|
-
|
|
597
|
-
) -> "SQL":
|
|
598
|
-
"""Get a SQL object by statement name with dialect support.
|
|
608
|
+
def get_sql(self, name: str) -> "SQL":
|
|
609
|
+
"""Get a SQL object by statement name.
|
|
599
610
|
|
|
600
611
|
Args:
|
|
601
612
|
name: Name of the statement (from -- name: in SQL file).
|
|
602
613
|
Hyphens in names are converted to underscores.
|
|
603
|
-
parameters: Parameters for the SQL statement.
|
|
604
|
-
dialect: Optional dialect override.
|
|
605
|
-
**kwargs: Additional parameters to pass to the SQL object.
|
|
606
614
|
|
|
607
615
|
Returns:
|
|
608
616
|
SQL object ready for execution.
|
|
@@ -629,46 +637,11 @@ class SQLFileLoader:
|
|
|
629
637
|
raise SQLFileNotFoundError(name, path=f"Statement '{name}' not found. Available statements: {available}")
|
|
630
638
|
|
|
631
639
|
parsed_statement = self._queries[safe_name]
|
|
632
|
-
|
|
633
|
-
effective_dialect = dialect or parsed_statement.dialect
|
|
634
|
-
|
|
635
|
-
if dialect is not None:
|
|
636
|
-
normalized_dialect = _normalize_dialect(dialect)
|
|
637
|
-
if normalized_dialect not in SUPPORTED_DIALECTS:
|
|
638
|
-
suggestions = _get_dialect_suggestions(normalized_dialect)
|
|
639
|
-
warning_msg = f"Unknown dialect '{dialect}'"
|
|
640
|
-
if suggestions:
|
|
641
|
-
warning_msg += f". Did you mean: {', '.join(suggestions)}?"
|
|
642
|
-
warning_msg += f". Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}. Using dialect as-is."
|
|
643
|
-
logger.warning(warning_msg)
|
|
644
|
-
effective_dialect = dialect.lower()
|
|
645
|
-
else:
|
|
646
|
-
effective_dialect = normalized_dialect
|
|
647
|
-
|
|
648
|
-
sql_kwargs = dict(kwargs)
|
|
649
|
-
if parameters is not None:
|
|
650
|
-
sql_kwargs["parameters"] = parameters
|
|
651
|
-
|
|
652
640
|
sqlglot_dialect = None
|
|
653
|
-
if
|
|
654
|
-
sqlglot_dialect = _normalize_dialect_for_sqlglot(
|
|
655
|
-
|
|
656
|
-
if not effective_dialect and "statement_config" not in sql_kwargs:
|
|
657
|
-
validator = ParameterValidator()
|
|
658
|
-
param_info = validator.extract_parameters(parsed_statement.sql)
|
|
659
|
-
if param_info:
|
|
660
|
-
styles = {p.style for p in param_info}
|
|
661
|
-
if styles:
|
|
662
|
-
detected_style = next(iter(styles))
|
|
663
|
-
sql_kwargs["statement_config"] = StatementConfig(
|
|
664
|
-
parameter_config=ParameterStyleConfig(
|
|
665
|
-
default_parameter_style=detected_style,
|
|
666
|
-
supported_parameter_styles=styles,
|
|
667
|
-
preserve_parameter_format=True,
|
|
668
|
-
)
|
|
669
|
-
)
|
|
641
|
+
if parsed_statement.dialect:
|
|
642
|
+
sqlglot_dialect = _normalize_dialect_for_sqlglot(parsed_statement.dialect)
|
|
670
643
|
|
|
671
|
-
return SQL(parsed_statement.sql, dialect=sqlglot_dialect
|
|
644
|
+
return SQL(parsed_statement.sql, dialect=sqlglot_dialect)
|
|
672
645
|
|
|
673
646
|
def get_file(self, path: Union[str, Path]) -> "Optional[SQLFile]":
|
|
674
647
|
"""Get a loaded SQLFile object by path.
|
sqlspec/storage/registry.py
CHANGED
|
@@ -151,8 +151,8 @@ class StorageRegistry:
|
|
|
151
151
|
return self._create_backend("fsspec", uri, **kwargs)
|
|
152
152
|
except (ValueError, ImportError, NotImplementedError):
|
|
153
153
|
pass
|
|
154
|
-
msg =
|
|
155
|
-
raise MissingDependencyError(msg)
|
|
154
|
+
msg = "obstore"
|
|
155
|
+
raise MissingDependencyError(msg, "fsspec")
|
|
156
156
|
|
|
157
157
|
def _determine_backend_class(self, uri: str) -> type[ObjectStoreProtocol]:
|
|
158
158
|
"""Determine the backend class for a URI based on availability."""
|