1010if TYPE_CHECKING :
1111 from collections .abc import Iterator
1212
13+ from typing_extensions import TypeIs
14+
1315__all__ = [
1416 "CompiledSql" ,
1517 "IntoInterpolation" ,
@@ -39,6 +41,11 @@ class CompiledSql:
3941 sql : str
4042 params : dict [str , object ] = dataclasses .field (default_factory = dict )
4143
44+ def __str__ (self ) -> NoReturn :
45+ """Disallow accidentally converting to a string, since that would lose the parameters."""
46+ msg = "CompiledSql cannot be directly converted to a string, since it may contain parameters. Please use the .sql attribute for the SQL string, and the .params attribute for the parameters." # noqa: E501
47+ raise NotImplementedError (msg )
48+
4249
4350@runtime_checkable
4451class SupportsDuckdbTemplate (Protocol ):
@@ -163,6 +170,10 @@ def compile(*part: str | IntoInterpolation | Param | SupportsDuckdbTemplate | ob
163170 return t .compile ()
164171
165172
173+ def _is_iterable_nonstring (value : object ) -> TypeIs [Iterable ]:
174+ return isinstance (value , Iterable ) and not isinstance (value , str | bytes )
175+
176+
166177def _expand_part (part : object ) -> Iterable [str | IntoInterpolation ]:
167178 if isinstance (part , SupportsDuckdbTemplate ):
168179 raw = part .__duckdb_template__ ()
@@ -172,8 +183,9 @@ def _expand_part(part: object) -> Iterable[str | IntoInterpolation]:
172183 yield raw
173184 elif isinstance (raw , Param ):
174185 yield ParamInterpolation (raw )
175- elif isinstance (raw , Iterable ):
176- yield from _expand_part (raw )
186+ elif _is_iterable_nonstring (raw ):
187+ for item in raw :
188+ yield from _expand_part (item )
177189 else :
178190 p = param (value = raw )
179191 yield ParamInterpolation (p )
@@ -183,6 +195,9 @@ def _expand_part(part: object) -> Iterable[str | IntoInterpolation]:
183195 yield part
184196 elif isinstance (part , Param ):
185197 yield ParamInterpolation (part )
198+ elif _is_iterable_nonstring (part ):
199+ for item in part :
200+ yield from _expand_part (item )
186201 else :
187202 p = param (value = part )
188203 yield ParamInterpolation (p )
@@ -215,8 +230,12 @@ def _resolve(parts: Iterable[str | IntoInterpolation]) -> ResolvedSqlTemplate:
215230def _resolve_interpolation (interp : IntoInterpolation ) -> Iterable [str | Param ]:
216231 value = interp .value
217232 if isinstance (value , Param ):
218- # If it's already a Param, we can skip the template resolution and just return it as a param.
219- return (value ,)
233+ # Preserve direct ParamInterpolation behavior while allowing nested named Params to keep exact names.
234+ if isinstance (interp , ParamInterpolation ):
235+ return (value ,)
236+ if value .name is None :
237+ return (param (value .value ),)
238+ return (param (value .value , name = value .name , exact = True ),)
220239
221240 # if conversion specified (!s, !r, !a), treat as raw sql, eg
222241 # name = "Alice"
@@ -293,6 +312,10 @@ class SqlTemplate:
293312 """A sequence of strings and Interpolations."""
294313
295314 def __init__ (self , * parts : str | IntoInterpolation ) -> None :
315+ for part in parts :
316+ if not isinstance (part , str | IntoInterpolation ):
317+ msg = f"Unexpected part type in SqlTemplate: { type (part ).__name__ } . Expected str or IntoInterpolation."
318+ raise TypeError (msg )
296319 self .strings , self .interpolations = parse_parts (parts )
297320
298321 def __iter__ (self ) -> Iterator [str | IntoInterpolation ]:
@@ -344,7 +367,20 @@ def __repr__(self) -> str:
344367 return f"ResolvedSqlTemplate({ ', ' .join (part_strings )} )"
345368
346369 def __iter__ (self ) -> Iterator [str | Param ]:
347- yield from self .parts
370+ start = 0
371+ end = len (self .parts )
372+ while start < end and self .parts [start ] == "" :
373+ start += 1
374+ while end > start and self .parts [end - 1 ] == "" :
375+ end -= 1
376+ yield from self .parts [start :end ]
377+
378+ def __eq__ (self , other : object ) -> bool :
379+ if isinstance (other , CompiledSql ):
380+ return self .compile () == other
381+ if isinstance (other , ResolvedSqlTemplate ):
382+ return self .parts == other .parts
383+ return False
348384
349385
350386T = TypeVar ("T" )
0 commit comments