2323
2424
2525def 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
4361class 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