Skip to content

Commit 50aa534

Browse files
committed
try a mock API
1 parent 25fb0d9 commit 50aa534

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

tstrings.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from collections.abc import Iterable
2+
from typing import Literal, Protocol, TypedDict, runtime_checkable
3+
4+
from typing_extensions import Self
5+
6+
7+
# from string.templatelib import Interpolation, Template
8+
class PInterpolation(Protocol):
9+
value: object
10+
conversion: Literal["s", "r", "a"] | None
11+
12+
13+
@runtime_checkable
14+
class PTemplate(Protocol):
15+
strings: list[str]
16+
interpolations: list[PInterpolation]
17+
18+
19+
class Param(TypedDict):
20+
name: str
21+
value: object
22+
23+
24+
class ResolvedQuery(TypedDict):
25+
sql: str
26+
params: list[Param]
27+
28+
29+
class PQueryContext(Protocol):
30+
def current_params(self) -> list[Param]:
31+
"""Return the list of parameters currently in the context."""
32+
33+
def resolve_next(
34+
self,
35+
thing: object,
36+
/,
37+
) -> str:
38+
"""Resolve the given thing as the next step in resolving a query."""
39+
40+
def add_param(self, value: object, /, name: str | None = None) -> Param:
41+
"""Create a new parameter with the given value and an optional name. Does NOT add it to the context."""
42+
43+
44+
class ResolveOneFunc(Protocol):
45+
def __call__(self, thing: object, ctx: PQueryContext, /, **ignored_kwargs) -> str: ...
46+
47+
48+
class QueryContext:
49+
def __init__(self, params: Iterable[Param] | None = None, resolver: ResolveOneFunc | None = None):
50+
self._params: list[Param] = list(params) if params is not None else []
51+
self._resolver: ResolveOneFunc = resolver if resolver is not None else resolve_one
52+
53+
def current_params(self) -> list[Param]:
54+
# Full deep copy so the user can't mess up the internal state
55+
return [{"name": p["name"], "value": p["value"]} for p in self._params]
56+
57+
def resolve_next(self, thing: object, /) -> str:
58+
return self._resolver(thing, self)
59+
60+
def add_param(self, value: object, /, name: str | None = None) -> Param:
61+
param_name = (
62+
name
63+
if name is not None
64+
else generate_unique_param_name((p["name"] for p in self._params), template="param_{idx}")
65+
)
66+
p = Param(name=param_name, value=value)
67+
self._params.append(p)
68+
return p
69+
70+
71+
def resolve_query(thing: object) -> ResolvedQuery:
72+
ctx = QueryContext()
73+
sql = resolve_one(thing, ctx)
74+
return {"sql": sql, "params": ctx.current_params()}
75+
76+
77+
def resolve_one(thing: object, ctx: PQueryContext, /, **ignored_kwargs) -> str:
78+
if isinstance(thing, SupportsDuckdbResolve):
79+
return thing.__duckdb_resolve__(ctx, **ignored_kwargs)
80+
if isinstance(thing, PTemplate):
81+
return resolve_template(thing, ctx)
82+
param = ctx.add_param(thing)
83+
return f"${param['name']}"
84+
85+
86+
def resolve_template(template: PTemplate, ctx: PQueryContext) -> str:
87+
"""Resolve a Template, recursively resolving any interpolations and applying any specified conversions."""
88+
sql_parts = []
89+
90+
for i, static_part in enumerate(template.strings):
91+
sql_parts.append(static_part)
92+
if i < len(template.interpolations):
93+
interp = template.interpolations[i]
94+
value = interp.value
95+
96+
# Apply conversion if specified (!s, !r, !a)
97+
if interp.conversion == "s":
98+
value = str(value)
99+
elif interp.conversion == "r":
100+
value = repr(value)
101+
elif interp.conversion == "a":
102+
value = ascii(value)
103+
104+
sql_parts.append(ctx.resolve_next(value))
105+
106+
return "".join(sql_parts)
107+
108+
109+
def generate_unique_param_name(existing_names: Iterable[str], *, template: str | None = None) -> str:
110+
"""Generate a unique parameter name that does not conflict with existing_names.
111+
112+
If template is provided, it should be a string with a single {idx} placeholder
113+
that will be replaced with an integer index to generate candidate names.
114+
If template is None, a default template of "param_{idx}" will be used.
115+
"""
116+
if template is None:
117+
template = "param_{idx}"
118+
existing_names = set(existing_names)
119+
idx = 1
120+
while True:
121+
candidate = template.format(idx=idx)
122+
if candidate not in existing_names:
123+
return candidate
124+
idx += 1
125+
126+
127+
@runtime_checkable
128+
class SupportsDuckdbResolve(Protocol):
129+
def __duckdb_resolve__(self, ctx: PQueryContext, /, **future_kwargs) -> str: ...
130+
131+
132+
class DuckdDbPyRelation:
133+
def __duckdb_resolve__(self, ctx: PQueryContext, /, **future_kwargs) -> str:
134+
# this would just return the existing SQL.
135+
return "SELECT * FROM some_table"

0 commit comments

Comments
 (0)