Skip to content

Commit c1ca3f9

Browse files
committed
improve style of tests and get passing
1 parent 9782568 commit c1ca3f9

2 files changed

Lines changed: 121 additions & 79 deletions

File tree

duckdb/template.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
if 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
4451
class 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+
166177
def _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:
215230
def _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

350386
T = TypeVar("T")

0 commit comments

Comments
 (0)