Skip to content

Commit 8a3edf5

Browse files
committed
claude19
1 parent f6be6c2 commit 8a3edf5

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

sql_athame/base.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323

2424

2525
def auto_numbered(field_name):
26+
"""Check if a field name should be auto-numbered.
27+
28+
Args:
29+
field_name: The field name to check
30+
31+
Returns:
32+
True if the field name should be auto-numbered (doesn't start with alphanumeric)
33+
"""
2634
return not auto_numbered_re.match(field_name)
2735

2836

@@ -31,6 +39,16 @@ def process_slot_value(
3139
value: Any,
3240
placeholders: dict[str, Placeholder],
3341
) -> Union["Fragment", Placeholder]:
42+
"""Process a slot value during fragment compilation.
43+
44+
Args:
45+
name: The slot name
46+
value: The value to process
47+
placeholders: Dictionary of existing placeholders
48+
49+
Returns:
50+
Either a Fragment if value is a Fragment, or a Placeholder
51+
"""
3452
if isinstance(value, Fragment):
3553
return value
3654
else:
@@ -41,17 +59,64 @@ def process_slot_value(
4159

4260
@dataclasses.dataclass
4361
class Fragment:
62+
"""Core SQL fragment class representing a piece of SQL with placeholders.
63+
64+
A Fragment contains SQL text and placeholders that can be combined with other
65+
fragments to build complex queries. Fragments automatically handle parameter
66+
binding and can be rendered to parameterized queries suitable for database drivers.
67+
68+
Attributes:
69+
parts: List of SQL parts (strings, placeholders, slots, or other fragments)
70+
71+
Example:
72+
>>> from sql_athame import sql
73+
>>> frag = sql("SELECT * FROM users WHERE id = {}", 42)
74+
>>> query, params = frag.query()
75+
>>> query
76+
'SELECT * FROM users WHERE id = $1'
77+
>>> params
78+
[42]
79+
80+
>>> # Fragments can be combined
81+
>>> where_clause = sql("active = {}", True)
82+
>>> full_query = sql("SELECT * FROM users WHERE id = {} AND {}", 42, where_clause)
83+
>>> list(full_query)
84+
['SELECT * FROM users WHERE id = $1 AND active = $2', 42, True]
85+
"""
4486
__slots__ = ["parts"]
4587
parts: list[Part]
4688

4789
def flatten_into(self, parts: list[FlatPart]) -> None:
90+
"""Recursively flatten this fragment into a list of flat parts.
91+
92+
This method traverses nested fragments and adds all parts to the provided list.
93+
String parts are not combined here - that's done in flatten().
94+
95+
Args:
96+
parts: List to append flattened parts to
97+
"""
4898
for part in self.parts:
4999
if isinstance(part, Fragment):
50100
part.flatten_into(parts)
51101
else:
52102
parts.append(part)
53103

54104
def compile(self) -> Callable[..., "Fragment"]:
105+
"""Create an optimized function for filling slots in this fragment.
106+
107+
Returns a compiled function that when called with **kwargs will create a new
108+
Fragment equivalent to calling self.fill(**kwargs). The compilation process
109+
does as much work as possible up front, making repeated slot filling much faster.
110+
111+
Returns:
112+
Function that takes **kwargs and returns a Fragment with slots filled
113+
114+
Example:
115+
>>> template = sql("SELECT * FROM users WHERE name = {name} AND age > {age}")
116+
>>> compiled_template = template.compile()
117+
>>> query1 = compiled_template(name="Alice", age=25)
118+
>>> query2 = compiled_template(name="Bob", age=30)
119+
"""
55120
flattened = self.flatten()
56121
env = dict(
57122
process_slot_value=process_slot_value,
@@ -77,6 +142,14 @@ def compile(self) -> Callable[..., "Fragment"]:
77142
return env["compiled"] # type: ignore
78143

79144
def flatten(self) -> "Fragment":
145+
"""Create a flattened version of this fragment.
146+
147+
Recursively flattens all nested fragments and combines adjacent string parts
148+
into single strings for efficiency.
149+
150+
Returns:
151+
New Fragment with no nested fragments and adjacent strings combined
152+
"""
80153
parts: list[FlatPart] = []
81154
self.flatten_into(parts)
82155
out_parts: list[Part] = []
@@ -88,6 +161,24 @@ def flatten(self) -> "Fragment":
88161
return Fragment(out_parts)
89162

90163
def fill(self, **kwargs: Any) -> "Fragment":
164+
"""Create a new fragment by filling any empty slots with provided values.
165+
166+
Searches for Slot objects in this fragment and replaces them with the
167+
corresponding values from kwargs. If a value is a Fragment, it's substituted
168+
in-place; otherwise it becomes a placeholder.
169+
170+
Args:
171+
**kwargs: Named values to fill into slots
172+
173+
Returns:
174+
New Fragment with slots filled
175+
176+
Example:
177+
>>> template = sql("SELECT * FROM {table} WHERE id = {id}")
178+
>>> query = template.fill(table=sql.identifier("users"), id=42)
179+
>>> list(query)
180+
['SELECT * FROM "users" WHERE id = $1', 42]
181+
"""
91182
parts: list[Part] = []
92183
self.flatten_into(cast(list[FlatPart], parts))
93184
placeholders: dict[str, Placeholder] = {}
@@ -109,6 +200,20 @@ def prep_query(
109200
) -> tuple[str, list[Placeholder]]: ... # pragma: no cover
110201

111202
def prep_query(self, allow_slots: bool = False) -> tuple[str, list[Any]]:
203+
"""Prepare the fragment for query execution.
204+
205+
Flattens the fragment and converts placeholders to numbered parameters ($1, $2, etc.)
206+
suitable for database drivers like asyncpg.
207+
208+
Args:
209+
allow_slots: If True, allows unfilled slots; if False, raises ValueError for unfilled slots
210+
211+
Returns:
212+
Tuple of (query_string, parameter_objects)
213+
214+
Raises:
215+
ValueError: If allow_slots is False and there are unfilled slots
216+
"""
112217
parts: list[FlatPart] = []
113218
self.flatten_into(parts)
114219
args: list[Union[Placeholder, Slot]] = []
@@ -134,14 +239,63 @@ def prep_query(self, allow_slots: bool = False) -> tuple[str, list[Any]]:
134239
return "".join(out_parts).strip(), args
135240

136241
def query(self) -> tuple[str, list[Any]]:
242+
"""Render the fragment into a query string and parameter list.
243+
244+
Returns:
245+
Tuple of (query_string, parameter_values) ready for database execution
246+
247+
Raises:
248+
ValueError: If there are any unfilled slots
249+
250+
Example:
251+
>>> frag = sql("SELECT * FROM users WHERE id = {}", 42)
252+
>>> frag.query()
253+
('SELECT * FROM users WHERE id = $1', [42])
254+
"""
137255
query, args = self.prep_query()
138256
placeholder_values = [arg.value for arg in args]
139257
return query, placeholder_values
140258

141259
def sqlalchemy_text(self) -> Any:
260+
"""Convert this fragment to a SQLAlchemy TextClause.
261+
262+
Renders the fragment into a SQLAlchemy TextClause with bound parameters.
263+
Placeholder values will be bound with bindparams. Unfilled slots will be
264+
included as unbound parameters.
265+
266+
Returns:
267+
SQLAlchemy TextClause object
268+
269+
Raises:
270+
ImportError: If SQLAlchemy is not installed
271+
272+
Example:
273+
>>> frag = sql("SELECT * FROM users WHERE id = {}", 42)
274+
>>> text_clause = frag.sqlalchemy_text()
275+
>>> # Can be used with SQLAlchemy engine.execute(text_clause)
276+
"""
142277
return sqlalchemy_text_from_fragment(self)
143278

144279
def prepare(self) -> tuple[str, Callable[..., list[Any]]]:
280+
"""Prepare fragment for use with prepared statements.
281+
282+
Returns a query string and a function that generates parameter lists.
283+
The query string contains numbered placeholders, and the function takes
284+
**kwargs for any unfilled slots and returns the complete parameter list.
285+
286+
Returns:
287+
Tuple of (query_string, parameter_generator_function)
288+
289+
Example:
290+
>>> template = sql("UPDATE users SET name={name}, age={age} WHERE id < {}", 100)
291+
>>> query, param_func = template.prepare()
292+
>>> query
293+
'UPDATE users SET name=$1, age=$2 WHERE id < $3'
294+
>>> param_func(name="Alice", age=25)
295+
['Alice', 25, 100]
296+
>>> param_func(name="Bob", age=30)
297+
['Bob', 30, 100]
298+
"""
145299
query, args = self.prep_query(allow_slots=True)
146300
env = {}
147301
func = [
@@ -159,10 +313,47 @@ def prepare(self) -> tuple[str, Callable[..., list[Any]]]:
159313
return query, env["generate_args"] # type: ignore
160314

161315
def __iter__(self) -> Iterator[Any]:
316+
"""Make Fragment iterable for use with asyncpg and similar drivers.
317+
318+
Returns an iterator that yields the query string followed by all parameter
319+
values. This matches the (query, *args) calling convention of asyncpg.
320+
321+
Yields:
322+
Query string, then each parameter value
323+
324+
Example:
325+
>>> frag = sql("SELECT * FROM users WHERE id = {} AND name = {}", 42, "Alice")
326+
>>> list(frag)
327+
['SELECT * FROM users WHERE id = $1 AND name = $2', 42, 'Alice']
328+
>>> # Can be used directly with asyncpg
329+
>>> await conn.fetch(*frag)
330+
"""
162331
sql, args = self.query()
163332
return iter((sql, *args))
164333

165334
def join(self, parts: Iterable["Fragment"]) -> "Fragment":
335+
"""Join multiple fragments using this fragment as a separator.
336+
337+
Creates a new fragment by joining the provided fragments with this fragment
338+
as the separator between them.
339+
340+
Args:
341+
parts: Iterable of Fragment objects to join
342+
343+
Returns:
344+
New Fragment with parts joined by this fragment
345+
346+
Example:
347+
>>> separator = sql(" AND ")
348+
>>> conditions = [sql("a = {}", 1), sql("b = {}", 2), sql("c = {}", 3)]
349+
>>> result = separator.join(conditions)
350+
>>> list(result)
351+
['a = $1 AND b = $2 AND c = $3', 1, 2, 3]
352+
353+
>>> # More commonly used for CASE statements
354+
>>> clauses = [sql("WHEN {} THEN {}", x, y) for x, y in [("a", 1), ("b", 2)]]
355+
>>> case = sql("CASE {clauses} END", clauses=sql(" ").join(clauses))
356+
"""
166357
return Fragment(list(join_parts(parts, infix=self)))
167358

168359

0 commit comments

Comments
 (0)