iceaxe 0.7.1__cp313-cp313-macosx_11_0_arm64.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.

Potentially problematic release.


This version of iceaxe might be problematic. Click here for more details.

Files changed (75) hide show
  1. iceaxe/__init__.py +20 -0
  2. iceaxe/__tests__/__init__.py +0 -0
  3. iceaxe/__tests__/benchmarks/__init__.py +0 -0
  4. iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
  5. iceaxe/__tests__/benchmarks/test_select.py +114 -0
  6. iceaxe/__tests__/conf_models.py +133 -0
  7. iceaxe/__tests__/conftest.py +204 -0
  8. iceaxe/__tests__/docker_helpers.py +208 -0
  9. iceaxe/__tests__/helpers.py +268 -0
  10. iceaxe/__tests__/migrations/__init__.py +0 -0
  11. iceaxe/__tests__/migrations/conftest.py +36 -0
  12. iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
  13. iceaxe/__tests__/migrations/test_generator.py +140 -0
  14. iceaxe/__tests__/migrations/test_generics.py +91 -0
  15. iceaxe/__tests__/mountaineer/__init__.py +0 -0
  16. iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
  17. iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
  18. iceaxe/__tests__/schemas/__init__.py +0 -0
  19. iceaxe/__tests__/schemas/test_actions.py +1264 -0
  20. iceaxe/__tests__/schemas/test_cli.py +25 -0
  21. iceaxe/__tests__/schemas/test_db_memory_serializer.py +1525 -0
  22. iceaxe/__tests__/schemas/test_db_serializer.py +398 -0
  23. iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
  24. iceaxe/__tests__/test_alias.py +83 -0
  25. iceaxe/__tests__/test_base.py +52 -0
  26. iceaxe/__tests__/test_comparison.py +383 -0
  27. iceaxe/__tests__/test_field.py +11 -0
  28. iceaxe/__tests__/test_helpers.py +9 -0
  29. iceaxe/__tests__/test_modifications.py +151 -0
  30. iceaxe/__tests__/test_queries.py +605 -0
  31. iceaxe/__tests__/test_queries_str.py +173 -0
  32. iceaxe/__tests__/test_session.py +1511 -0
  33. iceaxe/__tests__/test_text_search.py +287 -0
  34. iceaxe/alias_values.py +67 -0
  35. iceaxe/base.py +350 -0
  36. iceaxe/comparison.py +560 -0
  37. iceaxe/field.py +250 -0
  38. iceaxe/functions.py +906 -0
  39. iceaxe/generics.py +140 -0
  40. iceaxe/io.py +107 -0
  41. iceaxe/logging.py +91 -0
  42. iceaxe/migrations/__init__.py +5 -0
  43. iceaxe/migrations/action_sorter.py +98 -0
  44. iceaxe/migrations/cli.py +228 -0
  45. iceaxe/migrations/client_io.py +62 -0
  46. iceaxe/migrations/generator.py +404 -0
  47. iceaxe/migrations/migration.py +86 -0
  48. iceaxe/migrations/migrator.py +101 -0
  49. iceaxe/modifications.py +176 -0
  50. iceaxe/mountaineer/__init__.py +10 -0
  51. iceaxe/mountaineer/cli.py +74 -0
  52. iceaxe/mountaineer/config.py +46 -0
  53. iceaxe/mountaineer/dependencies/__init__.py +6 -0
  54. iceaxe/mountaineer/dependencies/core.py +67 -0
  55. iceaxe/postgres.py +133 -0
  56. iceaxe/py.typed +0 -0
  57. iceaxe/queries.py +1455 -0
  58. iceaxe/queries_str.py +294 -0
  59. iceaxe/schemas/__init__.py +0 -0
  60. iceaxe/schemas/actions.py +864 -0
  61. iceaxe/schemas/cli.py +30 -0
  62. iceaxe/schemas/db_memory_serializer.py +705 -0
  63. iceaxe/schemas/db_serializer.py +346 -0
  64. iceaxe/schemas/db_stubs.py +525 -0
  65. iceaxe/session.py +860 -0
  66. iceaxe/session_optimized.c +12035 -0
  67. iceaxe/session_optimized.cpython-313-darwin.so +0 -0
  68. iceaxe/session_optimized.pyx +212 -0
  69. iceaxe/sql_types.py +148 -0
  70. iceaxe/typing.py +73 -0
  71. iceaxe-0.7.1.dist-info/METADATA +261 -0
  72. iceaxe-0.7.1.dist-info/RECORD +75 -0
  73. iceaxe-0.7.1.dist-info/WHEEL +6 -0
  74. iceaxe-0.7.1.dist-info/licenses/LICENSE +21 -0
  75. iceaxe-0.7.1.dist-info/top_level.txt +1 -0
iceaxe/comparison.py ADDED
@@ -0,0 +1,560 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
4
+ from typing import Any, Generic, Self, Sequence, TypeVar
5
+
6
+ from iceaxe.queries_str import QueryElementBase, QueryLiteral
7
+ from iceaxe.typing import is_column, is_comparison, is_comparison_group
8
+
9
+ T = TypeVar("T", bound="ComparisonBase")
10
+ J = TypeVar("J")
11
+
12
+
13
+ class ComparisonType(StrEnum):
14
+ """
15
+ Enumeration of SQL comparison operators used in query conditions.
16
+ These operators are used to build WHERE clauses and other conditional expressions.
17
+
18
+ ```python {{sticky: True}}
19
+ # Using comparison operators in queries:
20
+ query = select(User).where(
21
+ User.name == "John", # Uses ComparisonType.EQ
22
+ User.age >= 21, # Uses ComparisonType.GE
23
+ User.status.in_(["active", "pending"]) # Uses ComparisonType.IN
24
+ )
25
+ ```
26
+ """
27
+
28
+ EQ = "="
29
+ """
30
+ Equal to comparison
31
+ """
32
+
33
+ NE = "!="
34
+ """
35
+ Not equal to comparison
36
+ """
37
+
38
+ LT = "<"
39
+ """
40
+ Less than comparison
41
+ """
42
+
43
+ LE = "<="
44
+ """
45
+ Less than or equal to comparison
46
+ """
47
+
48
+ GT = ">"
49
+ """
50
+ Greater than comparison
51
+ """
52
+
53
+ GE = ">="
54
+ """
55
+ Greater than or equal to comparison
56
+ """
57
+
58
+ IN = "IN"
59
+ """
60
+ Check if value is in a list of values
61
+ """
62
+
63
+ NOT_IN = "NOT IN"
64
+ """
65
+ Check if value is not in a list of values
66
+ """
67
+
68
+ LIKE = "LIKE"
69
+ """
70
+ Pattern matching with wildcards. Supports cases like:
71
+ - "John%" (matches "John Doe", "Johnny", etc.)
72
+ """
73
+
74
+ NOT_LIKE = "NOT LIKE"
75
+ """
76
+ Negated pattern matching with wildcards. Supports cases like:
77
+ - "John%" (matches "Amy", "Bob", etc.)
78
+ """
79
+
80
+ ILIKE = "ILIKE"
81
+ """
82
+ Case-insensitive pattern matching. Supports cases like:
83
+ - "john%" (matches "John Doe", "johnny", etc.)
84
+ """
85
+
86
+ NOT_ILIKE = "NOT ILIKE"
87
+ """
88
+ Negated case-insensitive pattern matching. Supports cases like:
89
+ - "john%" (matches "Amy", "Bob", etc.)
90
+ """
91
+
92
+ IS = "IS"
93
+ """
94
+ NULL comparison
95
+ """
96
+
97
+ IS_NOT = "IS NOT"
98
+ """
99
+ NOT NULL comparison
100
+ """
101
+
102
+ IS_DISTINCT_FROM = "IS DISTINCT FROM"
103
+ """
104
+ IS DISTINCT FROM comparison
105
+ """
106
+
107
+ IS_NOT_DISTINCT_FROM = "IS NOT DISTINCT FROM"
108
+ """
109
+ IS NOT DISTINCT FROM comparison
110
+ """
111
+
112
+
113
+ class ComparisonGroupType(StrEnum):
114
+ """
115
+ Enumeration of logical operators used to combine multiple comparisons in SQL queries.
116
+ These operators allow building complex conditions by combining multiple WHERE clauses.
117
+
118
+ ```python {{sticky: True}}
119
+ # Combining multiple conditions:
120
+ query = select(User).where(
121
+ and_(
122
+ User.age >= 21,
123
+ User.status == "active"
124
+ )
125
+ )
126
+
127
+ # Using OR conditions:
128
+ query = select(User).where(
129
+ or_(
130
+ User.role == "admin",
131
+ User.permissions.contains("manage_users")
132
+ )
133
+ )
134
+ ```
135
+ """
136
+
137
+ AND = "AND"
138
+ """
139
+ Logical AND operator, all conditions must be true
140
+ """
141
+
142
+ OR = "OR"
143
+ """
144
+ Logical OR operator, at least one condition must be true
145
+ """
146
+
147
+
148
+ @dataclass
149
+ class FieldComparison(Generic[T]):
150
+ """
151
+ Represents a single SQL comparison operation between a field and a value or another field.
152
+ This class is typically created through the comparison operators (==, !=, >, <, etc.) on database fields.
153
+
154
+ ```python {{sticky: True}}
155
+ # These expressions create FieldComparison objects:
156
+ User.age >= 21
157
+ User.status.in_(["active", "pending"])
158
+ User.name.like("%John%")
159
+
160
+ # Direct instantiation (rarely needed):
161
+ comparison = FieldComparison(
162
+ left=User.age,
163
+ comparison=ComparisonType.GE,
164
+ right=21
165
+ )
166
+ ```
167
+ """
168
+
169
+ left: T
170
+ """
171
+ The left side of the comparison (typically a database field)
172
+ """
173
+
174
+ comparison: ComparisonType
175
+ """
176
+ The type of comparison to perform
177
+ """
178
+
179
+ right: T | Any
180
+ """
181
+ The right side of the comparison (can be a value or another field)
182
+ """
183
+
184
+ python_expression: bool = False
185
+ """
186
+ Implicit comparisons are created from Python expressions (like col1 == col2). If this
187
+ flag is False, it means the user explicitly used one of the column() helper functions like
188
+ .equals(), .not_equals(), etc.
189
+
190
+ """
191
+
192
+ def to_query(self, start: int = 1) -> tuple[QueryLiteral, list[Any]]:
193
+ """
194
+ Converts the comparison to its SQL representation.
195
+
196
+ :param start: The starting index for query parameters, defaults to 1
197
+ :return: A tuple of the SQL query string and list of parameter values
198
+ """
199
+ variables = []
200
+
201
+ field, left_vars = self.left.to_query()
202
+ variables += left_vars
203
+
204
+ value: QueryElementBase
205
+ comparison = self.comparison
206
+ if is_column(self.right):
207
+ # Support comparison to other fields (both identifiers)
208
+ value, right_vars = self.right.to_query()
209
+ variables += right_vars
210
+ else:
211
+ variable_offset = str(len(variables) + start)
212
+
213
+ if self.right is None:
214
+ # "None" values are not supported as query variables
215
+ value = QueryLiteral("NULL")
216
+ elif self.comparison in (ComparisonType.IN, ComparisonType.NOT_IN):
217
+ variables.append(self.right)
218
+ comparison_map = {
219
+ ComparisonType.IN: (ComparisonType.EQ, "ANY"),
220
+ ComparisonType.NOT_IN: (ComparisonType.NE, "ALL"),
221
+ }
222
+ comparison, operator = comparison_map[self.comparison]
223
+ value = QueryLiteral(f"{operator}(${variable_offset})")
224
+ else:
225
+ # Support comparison to static values
226
+ variables.append(self.right)
227
+ value = QueryLiteral(f"${variable_offset}")
228
+
229
+ return QueryLiteral(f"{field} {comparison.value} {value}"), variables
230
+
231
+ def force_join_constraints(self):
232
+ """
233
+ Set the context of the comparison to be used in a join. This places certain constraints
234
+ on the comparison operations that can be applied, like using equals for columns instead
235
+ of IS DISTINCT FROM.
236
+
237
+ """
238
+ comparison = self.comparison
239
+
240
+ # Only if we were created implicitly should we modify the comparison type
241
+ if self.python_expression:
242
+ if self.comparison == ComparisonType.IS_DISTINCT_FROM:
243
+ comparison = ComparisonType.NE
244
+ elif self.comparison == ComparisonType.IS_NOT_DISTINCT_FROM:
245
+ comparison = ComparisonType.EQ
246
+
247
+ return FieldComparison(left=self.left, comparison=comparison, right=self.right)
248
+
249
+
250
+ @dataclass
251
+ class FieldComparisonGroup:
252
+ """
253
+ Represents a group of field comparisons combined with a logical operator (AND/OR).
254
+ This class is typically created through the and_() and or_() functions.
255
+
256
+ ```python {{sticky: True}}
257
+ # Using and_() to create an AND group:
258
+ query = select(User).where(
259
+ and_(
260
+ User.age >= 21,
261
+ User.status == "active",
262
+ or_(
263
+ User.role == "admin",
264
+ User.permissions.contains("manage_users")
265
+ )
266
+ )
267
+ )
268
+
269
+ # Direct instantiation (rarely needed):
270
+ group = FieldComparisonGroup(
271
+ type=ComparisonGroupType.AND,
272
+ elements=[
273
+ User.age >= 21,
274
+ User.status == "active"
275
+ ]
276
+ )
277
+ ```
278
+ """
279
+
280
+ type: ComparisonGroupType
281
+ """
282
+ The type of logical operator to use (AND/OR)
283
+
284
+ """
285
+
286
+ elements: list["FieldComparison | FieldComparisonGroup"]
287
+ """
288
+ List of comparisons or nested comparison groups to combine
289
+ """
290
+
291
+ def to_query(self, start: int = 1) -> tuple[QueryLiteral, list[Any]]:
292
+ """
293
+ Converts the comparison group to its SQL representation.
294
+
295
+ :param start: The starting index for query parameters, defaults to 1
296
+ :return: A tuple of the SQL query string and list of parameter values
297
+ """
298
+ queries = ""
299
+ all_variables = []
300
+
301
+ for i, element in enumerate(self.elements):
302
+ if i > 0:
303
+ queries += f" {self.type.value} "
304
+
305
+ if is_comparison(element):
306
+ query, variables = element.to_query(start=start + len(all_variables))
307
+ queries += f"{query}"
308
+ all_variables += variables
309
+ elif is_comparison_group(element):
310
+ query, variables = element.to_query(start=start + len(all_variables))
311
+ queries += f"({query})"
312
+ all_variables += variables
313
+ else:
314
+ raise ValueError(f"Unexpected element type: {type(element)}")
315
+
316
+ return QueryLiteral(queries), all_variables
317
+
318
+
319
+ class ComparisonBase(ABC, Generic[J]):
320
+ """
321
+ Abstract base class for database fields that can be used in comparisons.
322
+ Provides standard comparison operators and methods for SQL query generation.
323
+
324
+ This class implements Python's comparison magic methods (__eq__, __ne__, etc.)
325
+ to enable natural syntax for building SQL queries. It also provides additional
326
+ methods for SQL-specific operations like IN, LIKE, and NULL comparisons.
327
+
328
+ ```python {{sticky: True}}
329
+ # ComparisonBase enables these operations on database fields:
330
+ User.age >= 21
331
+ User.status == "active"
332
+ User.name.like("%John%")
333
+ User.role.in_(["admin", "moderator"])
334
+ User.deleted_at.is_(None)
335
+ ```
336
+ """
337
+
338
+ def __eq__(self, other): # type: ignore
339
+ """
340
+ Implements equality comparison, closer to Python's == operator.
341
+ Maps to SQL '=' or 'IS' for NULL comparisons.
342
+ Maps to SQL 'IS NOT DISTINCT FROM' for column comparisons.
343
+
344
+ :param other: Value to compare against
345
+ :return: A field comparison object
346
+ """
347
+ raw_comparison: bool | None = None
348
+ if other is None:
349
+ raw_comparison = self.is_(None)
350
+ elif is_column(other):
351
+ raw_comparison = self.is_not_distinct_from(other)
352
+
353
+ comparison: FieldComparison[Self] = (
354
+ raw_comparison if raw_comparison is not None else self.equals(other)
355
+ ) # type: ignore
356
+ comparison.python_expression = True
357
+ return comparison
358
+
359
+ def __ne__(self, other): # type: ignore
360
+ """
361
+ Implements inequality comparison, closer to Python's != operator.
362
+ Maps to SQL '!=' or 'IS NOT' for NULL comparisons.
363
+ Maps to SQL 'IS DISTINCT FROM' for column comparisons.
364
+
365
+ :param other: Value to compare against
366
+ :return: A field comparison object
367
+ """
368
+ raw_comparison: bool | None = None
369
+ if other is None:
370
+ raw_comparison = self.is_not(None)
371
+ elif is_column(other):
372
+ raw_comparison = self.is_distinct_from(other)
373
+
374
+ comparison: FieldComparison[Self] = (
375
+ raw_comparison if raw_comparison is not None else self.not_equals(other)
376
+ ) # type: ignore
377
+ comparison.python_expression = True
378
+ return comparison
379
+
380
+ def __lt__(self, other):
381
+ """
382
+ Implements less than comparison (<).
383
+ Maps to SQL '<'.
384
+
385
+ :param other: Value to compare against
386
+ :return: A field comparison object
387
+ """
388
+ comparison = self._compare(ComparisonType.LT, other)
389
+ comparison.python_expression = True
390
+ return comparison
391
+
392
+ def __le__(self, other):
393
+ """
394
+ Implements less than or equal comparison (<=).
395
+ Maps to SQL '<='.
396
+
397
+ :param other: Value to compare against
398
+ :return: A field comparison object
399
+ """
400
+ comparison = self._compare(ComparisonType.LE, other)
401
+ comparison.python_expression = True
402
+ return comparison
403
+
404
+ def __gt__(self, other):
405
+ """
406
+ Implements greater than comparison (>).
407
+ Maps to SQL '>'.
408
+
409
+ :param other: Value to compare against
410
+ :return: A field comparison object
411
+ """
412
+ comparison = self._compare(ComparisonType.GT, other)
413
+ comparison.python_expression = True
414
+ return comparison
415
+
416
+ def __ge__(self, other):
417
+ """
418
+ Implements greater than or equal comparison (>=).
419
+ Maps to SQL '>='.
420
+
421
+ :param other: Value to compare against
422
+ :return: A field comparison object
423
+ """
424
+ comparison = self._compare(ComparisonType.GE, other)
425
+ comparison.python_expression = True
426
+ return comparison
427
+
428
+ def equals(self, other: Any) -> bool:
429
+ """
430
+ Implements equality comparison (==).
431
+
432
+ :param other: Value to compare against
433
+ :return: A field comparison object
434
+ """
435
+ return self._compare(ComparisonType.EQ, other) # type: ignore
436
+
437
+ def not_equals(self, other: Any) -> bool:
438
+ """
439
+ Implements inequality comparison (!=).
440
+
441
+ :param other: Value to compare against
442
+ :return: A field comparison object
443
+ """
444
+ return self._compare(ComparisonType.NE, other) # type: ignore
445
+
446
+ def is_(self, other: Any) -> bool:
447
+ """
448
+ Implements SQL IS operator.
449
+ Checks if the field's value is NULL.
450
+ """
451
+ return self._compare(ComparisonType.IS, other) # type: ignore
452
+
453
+ def is_not(self, other: Any) -> bool:
454
+ """
455
+ Implements SQL IS NOT operator.
456
+ Checks if the field's value is not NULL.
457
+ """
458
+ return self._compare(ComparisonType.IS_NOT, other) # type: ignore
459
+
460
+ def is_distinct_from(self, other: Any) -> bool:
461
+ """
462
+ Implements SQL IS DISTINCT FROM operator.
463
+ Checks if the field's value is distinct from another value.
464
+ """
465
+ return self._compare(ComparisonType.IS_DISTINCT_FROM, other) # type: ignore
466
+
467
+ def is_not_distinct_from(self, other: Any) -> bool:
468
+ """
469
+ Implements SQL IS NOT DISTINCT FROM operator.
470
+ Checks if the field's value is not distinct from another value.
471
+ """
472
+ return self._compare(ComparisonType.IS_NOT_DISTINCT_FROM, other) # type: ignore
473
+
474
+ def in_(self, other: Sequence[J]) -> bool:
475
+ """
476
+ Implements SQL IN operator.
477
+ Checks if the field's value is in a sequence of values.
478
+
479
+ :param other: Sequence of values to check against
480
+ :return: A field comparison object
481
+ """
482
+ return self._compare(ComparisonType.IN, other) # type: ignore
483
+
484
+ def not_in(self, other: Sequence[J]) -> bool:
485
+ """
486
+ Implements SQL NOT IN operator.
487
+ Checks if the field's value is not in a sequence of values.
488
+
489
+ :param other: Sequence of values to check against
490
+ :return: A field comparison object
491
+ """
492
+ return self._compare(ComparisonType.NOT_IN, other) # type: ignore
493
+
494
+ def like(
495
+ self: "ComparisonBase[str] | ComparisonBase[str | None]", other: str
496
+ ) -> bool:
497
+ """
498
+ Implements SQL LIKE operator for pattern matching.
499
+ Case-sensitive string pattern matching.
500
+
501
+ :param other: Pattern to match against
502
+ :return: A field comparison object
503
+ """
504
+ return self._compare(ComparisonType.LIKE, other) # type: ignore
505
+
506
+ def not_like(
507
+ self: "ComparisonBase[str] | ComparisonBase[str | None]", other: str
508
+ ) -> bool:
509
+ """
510
+ Implements SQL NOT LIKE operator.
511
+ Case-sensitive string pattern non-matching.
512
+
513
+ :param other: Pattern to match against
514
+ :return: A field comparison object
515
+ """
516
+ return self._compare(ComparisonType.NOT_LIKE, other) # type: ignore
517
+
518
+ def ilike(
519
+ self: "ComparisonBase[str] | ComparisonBase[str | None]", other: str
520
+ ) -> bool:
521
+ """
522
+ Implements PostgreSQL ILIKE operator.
523
+ Case-insensitive string pattern matching.
524
+
525
+ :param other: Pattern to match against
526
+ :return: A field comparison object
527
+ """
528
+ return self._compare(ComparisonType.ILIKE, other) # type: ignore
529
+
530
+ def not_ilike(
531
+ self: "ComparisonBase[str] | ComparisonBase[str | None]", other: str
532
+ ) -> bool:
533
+ """
534
+ Implements PostgreSQL NOT ILIKE operator.
535
+ Case-insensitive string pattern non-matching.
536
+
537
+ :param other: Pattern to match against
538
+ :return: A field comparison object
539
+ """
540
+ return self._compare(ComparisonType.NOT_ILIKE, other) # type: ignore
541
+
542
+ def _compare(self, comparison: ComparisonType, other: Any) -> FieldComparison[Self]:
543
+ """
544
+ Internal method to create a field comparison.
545
+
546
+ :param comparison: Type of comparison to create
547
+ :param other: Value to compare against
548
+ :return: A field comparison object
549
+ """
550
+ return FieldComparison(left=self, comparison=comparison, right=other)
551
+
552
+ @abstractmethod
553
+ def to_query(self) -> tuple["QueryLiteral", list[Any]]:
554
+ """
555
+ Abstract method to convert the field to its SQL representation.
556
+ Must be implemented by subclasses.
557
+
558
+ :return: A tuple of the SQL query string and list of parameter values
559
+ """
560
+ pass