sql-athame 0.4.0a11__py3-none-any.whl → 0.4.0a13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sql_athame/base.py CHANGED
@@ -23,6 +23,14 @@ auto_numbered_re = re.compile(r"[A-Za-z0-9_]")
23
23
 
24
24
 
25
25
  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
+ """
26
34
  return not auto_numbered_re.match(field_name)
27
35
 
28
36
 
@@ -31,6 +39,16 @@ def process_slot_value(
31
39
  value: Any,
32
40
  placeholders: dict[str, Placeholder],
33
41
  ) -> 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
+ """
34
52
  if isinstance(value, Fragment):
35
53
  return value
36
54
  else:
@@ -41,10 +59,43 @@ def process_slot_value(
41
59
 
42
60
  @dataclasses.dataclass
43
61
  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
+ """
86
+
44
87
  __slots__ = ["parts"]
45
88
  parts: list[Part]
46
89
 
47
90
  def flatten_into(self, parts: list[FlatPart]) -> None:
91
+ """Recursively flatten this fragment into a list of flat parts.
92
+
93
+ This method traverses nested fragments and adds all parts to the provided list.
94
+ String parts are not combined here - that's done in flatten().
95
+
96
+ Args:
97
+ parts: List to append flattened parts to
98
+ """
48
99
  for part in self.parts:
49
100
  if isinstance(part, Fragment):
50
101
  part.flatten_into(parts)
@@ -52,6 +103,21 @@ class Fragment:
52
103
  parts.append(part)
53
104
 
54
105
  def compile(self) -> Callable[..., "Fragment"]:
106
+ """Create an optimized function for filling slots in this fragment.
107
+
108
+ Returns a compiled function that when called with **kwargs will create a new
109
+ Fragment equivalent to calling self.fill(**kwargs). The compilation process
110
+ does as much work as possible up front, making repeated slot filling much faster.
111
+
112
+ Returns:
113
+ Function that takes **kwargs and returns a Fragment with slots filled
114
+
115
+ Example:
116
+ >>> template = sql("SELECT * FROM users WHERE name = {name} AND age > {age}")
117
+ >>> compiled_template = template.compile()
118
+ >>> query1 = compiled_template(name="Alice", age=25)
119
+ >>> query2 = compiled_template(name="Bob", age=30)
120
+ """
55
121
  flattened = self.flatten()
56
122
  env = dict(
57
123
  process_slot_value=process_slot_value,
@@ -77,6 +143,14 @@ class Fragment:
77
143
  return env["compiled"] # type: ignore
78
144
 
79
145
  def flatten(self) -> "Fragment":
146
+ """Create a flattened version of this fragment.
147
+
148
+ Recursively flattens all nested fragments and combines adjacent string parts
149
+ into single strings for efficiency.
150
+
151
+ Returns:
152
+ New Fragment with no nested fragments and adjacent strings combined
153
+ """
80
154
  parts: list[FlatPart] = []
81
155
  self.flatten_into(parts)
82
156
  out_parts: list[Part] = []
@@ -88,6 +162,24 @@ class Fragment:
88
162
  return Fragment(out_parts)
89
163
 
90
164
  def fill(self, **kwargs: Any) -> "Fragment":
165
+ """Create a new fragment by filling any empty slots with provided values.
166
+
167
+ Searches for Slot objects in this fragment and replaces them with the
168
+ corresponding values from kwargs. If a value is a Fragment, it's substituted
169
+ in-place; otherwise it becomes a placeholder.
170
+
171
+ Args:
172
+ **kwargs: Named values to fill into slots
173
+
174
+ Returns:
175
+ New Fragment with slots filled
176
+
177
+ Example:
178
+ >>> template = sql("SELECT * FROM {table} WHERE id = {id}")
179
+ >>> query = template.fill(table=sql.identifier("users"), id=42)
180
+ >>> list(query)
181
+ ['SELECT * FROM "users" WHERE id = $1', 42]
182
+ """
91
183
  parts: list[Part] = []
92
184
  self.flatten_into(cast(list[FlatPart], parts))
93
185
  placeholders: dict[str, Placeholder] = {}
@@ -109,6 +201,20 @@ class Fragment:
109
201
  ) -> tuple[str, list[Placeholder]]: ... # pragma: no cover
110
202
 
111
203
  def prep_query(self, allow_slots: bool = False) -> tuple[str, list[Any]]:
204
+ """Prepare the fragment for query execution.
205
+
206
+ Flattens the fragment and converts placeholders to numbered parameters ($1, $2, etc.)
207
+ suitable for database drivers like asyncpg.
208
+
209
+ Args:
210
+ allow_slots: If True, allows unfilled slots; if False, raises ValueError for unfilled slots
211
+
212
+ Returns:
213
+ Tuple of (query_string, parameter_objects)
214
+
215
+ Raises:
216
+ ValueError: If allow_slots is False and there are unfilled slots
217
+ """
112
218
  parts: list[FlatPart] = []
113
219
  self.flatten_into(parts)
114
220
  args: list[Union[Placeholder, Slot]] = []
@@ -134,14 +240,63 @@ class Fragment:
134
240
  return "".join(out_parts).strip(), args
135
241
 
136
242
  def query(self) -> tuple[str, list[Any]]:
243
+ """Render the fragment into a query string and parameter list.
244
+
245
+ Returns:
246
+ Tuple of (query_string, parameter_values) ready for database execution
247
+
248
+ Raises:
249
+ ValueError: If there are any unfilled slots
250
+
251
+ Example:
252
+ >>> frag = sql("SELECT * FROM users WHERE id = {}", 42)
253
+ >>> frag.query()
254
+ ('SELECT * FROM users WHERE id = $1', [42])
255
+ """
137
256
  query, args = self.prep_query()
138
257
  placeholder_values = [arg.value for arg in args]
139
258
  return query, placeholder_values
140
259
 
141
260
  def sqlalchemy_text(self) -> Any:
261
+ """Convert this fragment to a SQLAlchemy TextClause.
262
+
263
+ Renders the fragment into a SQLAlchemy TextClause with bound parameters.
264
+ Placeholder values will be bound with bindparams. Unfilled slots will be
265
+ included as unbound parameters.
266
+
267
+ Returns:
268
+ SQLAlchemy TextClause object
269
+
270
+ Raises:
271
+ ImportError: If SQLAlchemy is not installed
272
+
273
+ Example:
274
+ >>> frag = sql("SELECT * FROM users WHERE id = {}", 42)
275
+ >>> text_clause = frag.sqlalchemy_text()
276
+ >>> # Can be used with SQLAlchemy engine.execute(text_clause)
277
+ """
142
278
  return sqlalchemy_text_from_fragment(self)
143
279
 
144
280
  def prepare(self) -> tuple[str, Callable[..., list[Any]]]:
281
+ """Prepare fragment for use with prepared statements.
282
+
283
+ Returns a query string and a function that generates parameter lists.
284
+ The query string contains numbered placeholders, and the function takes
285
+ **kwargs for any unfilled slots and returns the complete parameter list.
286
+
287
+ Returns:
288
+ Tuple of (query_string, parameter_generator_function)
289
+
290
+ Example:
291
+ >>> template = sql("UPDATE users SET name={name}, age={age} WHERE id < {}", 100)
292
+ >>> query, param_func = template.prepare()
293
+ >>> query
294
+ 'UPDATE users SET name=$1, age=$2 WHERE id < $3'
295
+ >>> param_func(name="Alice", age=25)
296
+ ['Alice', 25, 100]
297
+ >>> param_func(name="Bob", age=30)
298
+ ['Bob', 30, 100]
299
+ """
145
300
  query, args = self.prep_query(allow_slots=True)
146
301
  env = {}
147
302
  func = [
@@ -159,17 +314,95 @@ class Fragment:
159
314
  return query, env["generate_args"] # type: ignore
160
315
 
161
316
  def __iter__(self) -> Iterator[Any]:
317
+ """Make Fragment iterable for use with asyncpg and similar drivers.
318
+
319
+ Returns an iterator that yields the query string followed by all parameter
320
+ values. This matches the (query, *args) calling convention of asyncpg.
321
+
322
+ Yields:
323
+ Query string, then each parameter value
324
+
325
+ Example:
326
+ >>> frag = sql("SELECT * FROM users WHERE id = {} AND name = {}", 42, "Alice")
327
+ >>> list(frag)
328
+ ['SELECT * FROM users WHERE id = $1 AND name = $2', 42, 'Alice']
329
+ >>> # Can be used directly with asyncpg
330
+ >>> await conn.fetch(*frag)
331
+ """
162
332
  sql, args = self.query()
163
333
  return iter((sql, *args))
164
334
 
165
335
  def join(self, parts: Iterable["Fragment"]) -> "Fragment":
336
+ """Join multiple fragments using this fragment as a separator.
337
+
338
+ Creates a new fragment by joining the provided fragments with this fragment
339
+ as the separator between them.
340
+
341
+ Args:
342
+ parts: Iterable of Fragment objects to join
343
+
344
+ Returns:
345
+ New Fragment with parts joined by this fragment
346
+
347
+ Example:
348
+ >>> separator = sql(" AND ")
349
+ >>> conditions = [sql("a = {}", 1), sql("b = {}", 2), sql("c = {}", 3)]
350
+ >>> result = separator.join(conditions)
351
+ >>> list(result)
352
+ ['a = $1 AND b = $2 AND c = $3', 1, 2, 3]
353
+
354
+ >>> # More commonly used for CASE statements
355
+ >>> clauses = [sql("WHEN {} THEN {}", x, y) for x, y in [("a", 1), ("b", 2)]]
356
+ >>> case = sql("CASE {clauses} END", clauses=sql(" ").join(clauses))
357
+ """
166
358
  return Fragment(list(join_parts(parts, infix=self)))
167
359
 
168
360
 
169
361
  class SQLFormatter:
362
+ """Main SQL formatting class providing the sql() function and utility methods.
363
+
364
+ This class is instantiated as the global 'sql' object that provides the primary
365
+ interface for building SQL fragments. It supports format string syntax with
366
+ placeholders and provides utility methods for common SQL operations.
367
+ """
368
+
170
369
  def __call__(
171
370
  self, fmt: str, *args: Any, preserve_formatting: bool = False, **kwargs: Any
172
371
  ) -> Fragment:
372
+ """Create a SQL Fragment from a format string with placeholders.
373
+
374
+ The format string contains literal SQL and may contain positional references
375
+ marked by {} and named references marked by {name}. Positional references
376
+ must have matching arguments in *args. Named references may have matching
377
+ arguments in **kwargs; if not provided, they remain as named slots to be
378
+ filled later.
379
+
380
+ If a referenced argument is a Fragment, it is substituted into the SQL
381
+ along with its embedded placeholders. Otherwise, it becomes a placeholder value.
382
+
383
+ Args:
384
+ fmt: SQL format string with {} placeholders
385
+ *args: Positional arguments for {} placeholders
386
+ preserve_formatting: If True, preserve whitespace; if False, normalize whitespace
387
+ **kwargs: Named arguments for {name} placeholders
388
+
389
+ Returns:
390
+ Fragment containing the SQL with placeholders
391
+
392
+ Raises:
393
+ ValueError: If there are unfilled positional arguments
394
+
395
+ Example:
396
+ >>> sql("SELECT * FROM users WHERE id = {}", 42)
397
+ Fragment(['SELECT * FROM users WHERE id = ', Placeholder('0', 42)])
398
+
399
+ >>> sql("SELECT * FROM users WHERE id = {id} AND name = {name}", id=42, name="Alice")
400
+ Fragment(['SELECT * FROM users WHERE id = ', Placeholder('id', 42), ' AND name = ', Placeholder('name', 'Alice')])
401
+
402
+ >>> # Fragments can be embedded
403
+ >>> where_clause = sql("active = {}", True)
404
+ >>> sql("SELECT * FROM users WHERE {}", where_clause)
405
+ """
173
406
  if not preserve_formatting:
174
407
  fmt = newline_whitespace_re.sub(" ", fmt)
175
408
  fmtr = string.Formatter()
@@ -198,23 +431,110 @@ class SQLFormatter:
198
431
 
199
432
  @staticmethod
200
433
  def value(value: Any) -> Fragment:
434
+ """Create a Fragment with a single placeholder value.
435
+
436
+ Equivalent to sql("{}", value) but more explicit.
437
+
438
+ Args:
439
+ value: The value to create a placeholder for
440
+
441
+ Returns:
442
+ Fragment containing a single placeholder
443
+
444
+ Example:
445
+ >>> sql.value(42)
446
+ Fragment([Placeholder('value', 42)])
447
+ """
201
448
  placeholder = Placeholder("value", value)
202
449
  return Fragment([placeholder])
203
450
 
204
451
  @staticmethod
205
452
  def escape(value: Any) -> Fragment:
453
+ """Create a Fragment with a value escaped and embedded into the SQL.
454
+
455
+ Unlike placeholders, escaped values are embedded directly into the SQL text.
456
+ Types currently supported are strings, floats, ints, UUIDs, None, and
457
+ sequences of the above. Use with caution and only for trusted values.
458
+
459
+ Args:
460
+ value: The value to escape and embed
461
+
462
+ Returns:
463
+ Fragment with the escaped value as literal SQL
464
+
465
+ Example:
466
+ >>> list(sql("SELECT * FROM tbl WHERE qty = ANY({})", sql.escape([1, 3, 5])))
467
+ ['SELECT * FROM tbl WHERE qty = ANY(ARRAY[1, 3, 5])']
468
+
469
+ >>> # Compare to placeholder version:
470
+ >>> list(sql("SELECT * FROM tbl WHERE qty = ANY({})", [1, 3, 5]))
471
+ ['SELECT * FROM tbl WHERE qty = ANY($1)', [1, 3, 5]]
472
+
473
+ Note:
474
+ Burning invariant values into the query can potentially help the query optimizer.
475
+ """
206
476
  return lit(escape(value))
207
477
 
208
478
  @staticmethod
209
479
  def slot(name: str) -> Fragment:
480
+ """Create a Fragment with a single empty slot.
481
+
482
+ Equivalent to sql("{name}") but more explicit.
483
+
484
+ Args:
485
+ name: The name of the slot
486
+
487
+ Returns:
488
+ Fragment containing a single slot
489
+
490
+ Example:
491
+ >>> template = sql("SELECT * FROM users WHERE {}", sql.slot("condition"))
492
+ >>> query = template.fill(condition=sql("active = {}", True))
493
+ """
210
494
  return Fragment([Slot(name)])
211
495
 
212
496
  @staticmethod
213
497
  def literal(text: str) -> Fragment:
498
+ """Create a Fragment with literal SQL text.
499
+
500
+ No substitution of any kind is performed. Be very careful of SQL injection
501
+ when using this method.
502
+
503
+ Args:
504
+ text: Raw SQL text to include
505
+
506
+ Returns:
507
+ Fragment containing the literal SQL
508
+
509
+ Warning:
510
+ Only use with trusted SQL text to avoid SQL injection vulnerabilities.
511
+
512
+ Example:
513
+ >>> sql.literal("ORDER BY created_at DESC")
514
+ Fragment(['ORDER BY created_at DESC'])
515
+ """
214
516
  return Fragment([text])
215
517
 
216
518
  @staticmethod
217
519
  def identifier(name: str, prefix: Optional[str] = None) -> Fragment:
520
+ """Create a Fragment with a quoted SQL identifier.
521
+
522
+ Creates a properly quoted identifier name, optionally with a dotted prefix
523
+ for schema or table qualification.
524
+
525
+ Args:
526
+ name: The identifier name to quote
527
+ prefix: Optional prefix (schema, table, etc.)
528
+
529
+ Returns:
530
+ Fragment containing the quoted identifier
531
+
532
+ Example:
533
+ >>> list(sql("SELECT {col} FROM {table}",
534
+ ... col=sql.identifier("user_name"),
535
+ ... table=sql.identifier("users", prefix="public")))
536
+ ['SELECT "user_name" FROM "public"."users"']
537
+ """
218
538
  if prefix:
219
539
  return lit(f"{quote_identifier(prefix)}.{quote_identifier(name)}")
220
540
  else:
@@ -227,6 +547,24 @@ class SQLFormatter:
227
547
  def all(self, *parts: Fragment) -> Fragment: ... # pragma: no cover
228
548
 
229
549
  def all(self, *parts) -> Fragment: # type: ignore
550
+ """Join fragments with AND, returning TRUE if no parts provided.
551
+
552
+ Creates a SQL Fragment joining the fragments in parts together with AND.
553
+ If parts is empty, returns TRUE. Each fragment is wrapped in parentheses.
554
+
555
+ Args:
556
+ *parts: Fragment objects to join, or single iterable of fragments
557
+
558
+ Returns:
559
+ Fragment containing the AND-joined conditions
560
+
561
+ Example:
562
+ >>> where = [sql("a = {}", 42), sql("x <> {}", "foo")]
563
+ >>> list(sql("SELECT * FROM tbl WHERE {}", sql.all(where)))
564
+ ['SELECT * FROM tbl WHERE (a = $1) AND (x <> $2)', 42, 'foo']
565
+ >>> list(sql("SELECT * FROM tbl WHERE {}", sql.all([])))
566
+ ['SELECT * FROM tbl WHERE TRUE']
567
+ """
230
568
  if parts and not isinstance(parts[0], Fragment):
231
569
  parts = parts[0]
232
570
  return any_all(list(parts), "AND", "TRUE")
@@ -238,6 +576,24 @@ class SQLFormatter:
238
576
  def any(self, *parts: Fragment) -> Fragment: ... # pragma: no cover
239
577
 
240
578
  def any(self, *parts) -> Fragment: # type: ignore
579
+ """Join fragments with OR, returning FALSE if no parts provided.
580
+
581
+ Creates a SQL Fragment joining the fragments in parts together with OR.
582
+ If parts is empty, returns FALSE. Each fragment is wrapped in parentheses.
583
+
584
+ Args:
585
+ *parts: Fragment objects to join, or single iterable of fragments
586
+
587
+ Returns:
588
+ Fragment containing the OR-joined conditions
589
+
590
+ Example:
591
+ >>> where = [sql("a = {}", 42), sql("x <> {}", "foo")]
592
+ >>> list(sql("SELECT * FROM tbl WHERE {}", sql.any(where)))
593
+ ['SELECT * FROM tbl WHERE (a = $1) OR (x <> $2)', 42, 'foo']
594
+ >>> list(sql("SELECT * FROM tbl WHERE {}", sql.any([])))
595
+ ['SELECT * FROM tbl WHERE FALSE']
596
+ """
241
597
  if parts and not isinstance(parts[0], Fragment):
242
598
  parts = parts[0]
243
599
  return any_all(list(parts), "OR", "FALSE")
@@ -249,11 +605,49 @@ class SQLFormatter:
249
605
  def list(self, *parts: Fragment) -> Fragment: ... # pragma: no cover
250
606
 
251
607
  def list(self, *parts) -> Fragment: # type: ignore
608
+ """Join fragments with commas.
609
+
610
+ Creates a SQL Fragment joining the fragments in parts together with commas.
611
+ Commonly used for column lists, value lists, etc.
612
+
613
+ Args:
614
+ *parts: Fragment objects to join, or single iterable of fragments
615
+
616
+ Returns:
617
+ Fragment containing the comma-separated fragments
618
+
619
+ Example:
620
+ >>> cols = [sql.identifier("id"), sql.identifier("name"), sql.identifier("email")]
621
+ >>> list(sql("SELECT {} FROM users", sql.list(cols)))
622
+ ['SELECT "id", "name", "email" FROM users']
623
+ """
252
624
  if parts and not isinstance(parts[0], Fragment):
253
625
  parts = parts[0]
254
626
  return Fragment(list(join_parts(parts, infix=", ")))
255
627
 
256
628
  def unnest(self, data: Iterable[Sequence[Any]], types: Iterable[str]) -> Fragment:
629
+ """Create a Fragment containing an UNNEST expression with associated data.
630
+
631
+ The data is specified as tuples (in the "database columns" sense) in data,
632
+ and the PostgreSQL types must be specified in types. The data is transposed
633
+ into the correct form for UNNEST and embedded as placeholders.
634
+
635
+ Args:
636
+ data: Iterable of sequences, where each sequence represents a row
637
+ types: Iterable of PostgreSQL type names for each column
638
+
639
+ Returns:
640
+ Fragment containing UNNEST expression with typed array placeholders
641
+
642
+ Example:
643
+ >>> list(sql("SELECT * FROM {}", sql.unnest([("a", 1), ("b", 2), ("c", 3)], ["text", "integer"])))
644
+ ['SELECT * FROM UNNEST($1::text[], $2::integer[])', ('a', 'b', 'c'), (1, 2, 3)]
645
+
646
+ >>> # Useful for bulk operations
647
+ >>> users_data = [("Alice", 25), ("Bob", 30), ("Charlie", 35)]
648
+ >>> insert_query = sql("INSERT INTO users (name, age) SELECT * FROM {}",
649
+ ... sql.unnest(users_data, ["text", "integer"]))
650
+ """
257
651
  nested = [nest_for_type(x, t) for x, t in zip(zip(*data), types)]
258
652
  if not nested:
259
653
  nested = [nest_for_type([], t) for t in types]
@@ -261,16 +655,50 @@ class SQLFormatter:
261
655
 
262
656
 
263
657
  sql = SQLFormatter()
658
+ """Global SQLFormatter instance providing the main sql() function and utilities.
659
+
660
+ This is the primary interface for creating SQL fragments. Use it to:
661
+ - Create fragments: sql("SELECT * FROM users WHERE id = {}", user_id)
662
+ - Join fragments: sql.all([condition1, condition2])
663
+ - Create identifiers: sql.identifier("table_name")
664
+ - And much more
665
+
666
+ See SQLFormatter class documentation for all available methods.
667
+ """
264
668
 
265
669
 
266
670
  json_types = ("JSON", "JSONB")
267
671
 
268
672
 
269
673
  def is_json_type(typename: str) -> bool:
674
+ """Check if a type name represents a JSON type.
675
+
676
+ Args:
677
+ typename: PostgreSQL type name to check
678
+
679
+ Returns:
680
+ True if typename is JSON or JSONB (case insensitive)
681
+ """
270
682
  return typename.upper() in json_types
271
683
 
272
684
 
273
685
  def nest_for_type(data: Sequence[Any], typename: str) -> Fragment:
686
+ """Create a typed array fragment for UNNEST operations.
687
+
688
+ Converts a sequence of data into a properly typed PostgreSQL array fragment.
689
+ Handles JSON/JSONB types specially by converting objects to JSON strings.
690
+
691
+ Args:
692
+ data: Sequence of values to convert to array
693
+ typename: PostgreSQL type name for the array
694
+
695
+ Returns:
696
+ Fragment containing a typed array placeholder
697
+
698
+ Note:
699
+ For JSON/JSONB types, non-string values are converted to JSON strings
700
+ to work around asyncpg limitations.
701
+ """
274
702
  if is_json_type(typename):
275
703
  # https://github.com/MagicStack/asyncpg/issues/345
276
704
 
@@ -287,10 +715,33 @@ def nest_for_type(data: Sequence[Any], typename: str) -> Fragment:
287
715
 
288
716
 
289
717
  def lit(text: str) -> Fragment:
718
+ """Create a Fragment containing literal SQL text.
719
+
720
+ Convenience function equivalent to Fragment([text]).
721
+
722
+ Args:
723
+ text: Literal SQL text
724
+
725
+ Returns:
726
+ Fragment containing the literal text
727
+ """
290
728
  return Fragment([text])
291
729
 
292
730
 
293
731
  def any_all(frags: list[Fragment], op: str, base_case: str) -> Fragment:
732
+ """Join fragments with a logical operator, with a base case for empty lists.
733
+
734
+ Used by sql.all() and sql.any() to implement AND/OR joining with appropriate
735
+ base cases (TRUE for AND, FALSE for OR).
736
+
737
+ Args:
738
+ frags: List of fragments to join
739
+ op: Operator to use for joining ("AND" or "OR")
740
+ base_case: Value to return if frags is empty ("TRUE" or "FALSE")
741
+
742
+ Returns:
743
+ Fragment with fragments joined by operator, or base case if empty
744
+ """
294
745
  if not frags:
295
746
  return lit(base_case)
296
747
  parts = join_parts(frags, prefix="(", infix=f") {op} (", suffix=")")
@@ -303,6 +754,20 @@ def join_parts(
303
754
  prefix: Optional[Part] = None,
304
755
  suffix: Optional[Part] = None,
305
756
  ) -> Iterator[Part]:
757
+ """Join parts with a separator, optionally adding prefix and suffix.
758
+
759
+ Generator function that yields parts with infix separators between them,
760
+ and optional prefix/suffix parts.
761
+
762
+ Args:
763
+ parts: Parts to join
764
+ infix: Separator to place between parts
765
+ prefix: Optional part to yield first
766
+ suffix: Optional part to yield last
767
+
768
+ Yields:
769
+ Parts with separators, prefix, and suffix as appropriate
770
+ """
306
771
  if prefix:
307
772
  yield prefix
308
773
  for i, part in enumerate(parts):
@@ -314,5 +779,19 @@ def join_parts(
314
779
 
315
780
 
316
781
  def quote_identifier(name: str) -> str:
782
+ """Quote a SQL identifier with double quotes, escaping internal quotes.
783
+
784
+ Args:
785
+ name: Identifier name to quote
786
+
787
+ Returns:
788
+ Quoted identifier with internal quotes escaped
789
+
790
+ Example:
791
+ >>> quote_identifier("user_name")
792
+ '"user_name"'
793
+ >>> quote_identifier('table"with"quotes')
794
+ '"table""with""quotes"'
795
+ """
317
796
  quoted = name.replace('"', '""')
318
797
  return f'"{quoted}"'