iceaxe 0.7.1__tar.gz → 0.7.2__tar.gz
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.
- {iceaxe-0.7.1/iceaxe.egg-info → iceaxe-0.7.2}/PKG-INFO +1 -1
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_queries.py +159 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/functions.py +527 -1
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/queries.py +5 -1
- {iceaxe-0.7.1 → iceaxe-0.7.2/iceaxe.egg-info}/PKG-INFO +1 -1
- {iceaxe-0.7.1 → iceaxe-0.7.2}/pyproject.toml +1 -1
- {iceaxe-0.7.1 → iceaxe-0.7.2}/LICENSE +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/MANIFEST.in +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/README.md +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/benchmarks/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/benchmarks/test_bulk_insert.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/benchmarks/test_select.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/conf_models.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/conftest.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/docker_helpers.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/helpers.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/migrations/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/migrations/conftest.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/migrations/test_action_sorter.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/migrations/test_generator.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/migrations/test_generics.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/mountaineer/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/mountaineer/dependencies/test_core.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/schemas/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/schemas/test_actions.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/schemas/test_cli.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/schemas/test_db_memory_serializer.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/schemas/test_db_serializer.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/schemas/test_db_stubs.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_alias.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_base.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_comparison.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_field.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_helpers.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_modifications.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_queries_str.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_session.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/__tests__/test_text_search.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/alias_values.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/base.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/comparison.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/field.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/generics.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/io.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/logging.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/action_sorter.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/cli.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/client_io.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/generator.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/migration.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/migrations/migrator.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/modifications.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/mountaineer/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/mountaineer/cli.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/mountaineer/config.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/mountaineer/dependencies/core.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/postgres.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/py.typed +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/queries_str.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/schemas/__init__.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/schemas/actions.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/schemas/cli.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/schemas/db_memory_serializer.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/schemas/db_serializer.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/schemas/db_stubs.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/session.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/session_optimized.c +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/session_optimized.pyx +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/sql_types.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe/typing.py +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe.egg-info/SOURCES.txt +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe.egg-info/dependency_links.txt +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe.egg-info/requires.txt +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/iceaxe.egg-info/top_level.txt +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/setup.cfg +0 -0
- {iceaxe-0.7.1 → iceaxe-0.7.2}/setup.py +0 -0
|
@@ -5,6 +5,7 @@ import pytest
|
|
|
5
5
|
|
|
6
6
|
from iceaxe.__tests__.conf_models import (
|
|
7
7
|
ArtifactDemo,
|
|
8
|
+
ComplexDemo,
|
|
8
9
|
Employee,
|
|
9
10
|
FunctionDemoModel,
|
|
10
11
|
UserDemo,
|
|
@@ -316,6 +317,13 @@ def test_function_transformations():
|
|
|
316
317
|
[],
|
|
317
318
|
)
|
|
318
319
|
|
|
320
|
+
# Test unnest function
|
|
321
|
+
new_query = QueryBuilder().select(func.unnest(ComplexDemo.string_list))
|
|
322
|
+
assert new_query.build() == (
|
|
323
|
+
'SELECT unnest("complexdemo"."string_list") AS aggregate_0 FROM "complexdemo"',
|
|
324
|
+
[],
|
|
325
|
+
)
|
|
326
|
+
|
|
319
327
|
# Test type conversion functions
|
|
320
328
|
new_query = QueryBuilder().select(
|
|
321
329
|
(
|
|
@@ -337,6 +345,157 @@ def test_function_transformations():
|
|
|
337
345
|
)
|
|
338
346
|
|
|
339
347
|
|
|
348
|
+
def test_array_operators():
|
|
349
|
+
# Test ANY operator
|
|
350
|
+
new_query = (
|
|
351
|
+
QueryBuilder()
|
|
352
|
+
.select(ComplexDemo)
|
|
353
|
+
.where(func.any(ComplexDemo.string_list) == "python")
|
|
354
|
+
)
|
|
355
|
+
assert new_query.build() == (
|
|
356
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
357
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE \'python\' = ANY("complexdemo"."string_list")',
|
|
358
|
+
[],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Test ALL operator
|
|
362
|
+
new_query = (
|
|
363
|
+
QueryBuilder()
|
|
364
|
+
.select(ComplexDemo)
|
|
365
|
+
.where(func.all(ComplexDemo.string_list) == "active")
|
|
366
|
+
)
|
|
367
|
+
assert new_query.build() == (
|
|
368
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
369
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE \'active\' = ALL("complexdemo"."string_list")',
|
|
370
|
+
[],
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Test array_contains operator (@>)
|
|
374
|
+
new_query = (
|
|
375
|
+
QueryBuilder()
|
|
376
|
+
.select(ComplexDemo)
|
|
377
|
+
.where(
|
|
378
|
+
func.array_contains(ComplexDemo.string_list, ["python", "django"]) == True # noqa: E712
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
assert new_query.build() == (
|
|
382
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
383
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE "complexdemo"."string_list" @> ARRAY[\'python\',\'django\'] = $1',
|
|
384
|
+
[True],
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Test array_contained_by operator (<@)
|
|
388
|
+
new_query = (
|
|
389
|
+
QueryBuilder()
|
|
390
|
+
.select(ComplexDemo)
|
|
391
|
+
.where(
|
|
392
|
+
func.array_contained_by( # noqa: E712
|
|
393
|
+
ComplexDemo.string_list, ["python", "java", "go", "rust"]
|
|
394
|
+
)
|
|
395
|
+
== True
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
assert new_query.build() == (
|
|
399
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
400
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE "complexdemo"."string_list" <@ ARRAY[\'python\',\'java\',\'go\',\'rust\'] = $1',
|
|
401
|
+
[True],
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Test array_overlaps operator (&&)
|
|
405
|
+
new_query = (
|
|
406
|
+
QueryBuilder()
|
|
407
|
+
.select(ComplexDemo)
|
|
408
|
+
.where(
|
|
409
|
+
func.array_overlaps( # noqa: E712
|
|
410
|
+
ComplexDemo.string_list, ["python", "data-science", "ml"]
|
|
411
|
+
)
|
|
412
|
+
== True
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
assert new_query.build() == (
|
|
416
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
417
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE "complexdemo"."string_list" && ARRAY[\'python\',\'data-science\',\'ml\'] = $1',
|
|
418
|
+
[True],
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_array_comparison_operators():
|
|
423
|
+
# Test ANY with different operators
|
|
424
|
+
new_query = (
|
|
425
|
+
QueryBuilder()
|
|
426
|
+
.select(ComplexDemo)
|
|
427
|
+
.where(func.any(ComplexDemo.string_list) != "inactive")
|
|
428
|
+
)
|
|
429
|
+
assert new_query.build() == (
|
|
430
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
431
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE \'inactive\' != ANY("complexdemo"."string_list")',
|
|
432
|
+
[],
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Test ALL with >= operator
|
|
436
|
+
new_query = (
|
|
437
|
+
QueryBuilder()
|
|
438
|
+
.select(ComplexDemo)
|
|
439
|
+
.where(func.all(ComplexDemo.string_list) >= "a")
|
|
440
|
+
)
|
|
441
|
+
assert new_query.build() == (
|
|
442
|
+
'SELECT "complexdemo"."id" AS "complexdemo_id", "complexdemo"."string_list" AS "complexdemo_string_list", '
|
|
443
|
+
'"complexdemo"."json_data" AS "complexdemo_json_data" FROM "complexdemo" WHERE \'a\' >= ALL("complexdemo"."string_list")',
|
|
444
|
+
[],
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_array_manipulation_functions():
|
|
449
|
+
# Test array_append
|
|
450
|
+
new_query = QueryBuilder().select(
|
|
451
|
+
func.array_append(ComplexDemo.string_list, "new-tag")
|
|
452
|
+
)
|
|
453
|
+
assert new_query.build() == (
|
|
454
|
+
'SELECT array_append("complexdemo"."string_list", \'new-tag\') AS aggregate_0 FROM "complexdemo"',
|
|
455
|
+
[],
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Test array_prepend
|
|
459
|
+
new_query = QueryBuilder().select(
|
|
460
|
+
func.array_prepend("featured", ComplexDemo.string_list)
|
|
461
|
+
)
|
|
462
|
+
assert new_query.build() == (
|
|
463
|
+
'SELECT array_prepend(\'featured\', "complexdemo"."string_list") AS aggregate_0 FROM "complexdemo"',
|
|
464
|
+
[],
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Test array_cat with field - this would require a join in practice
|
|
468
|
+
# For now, let's test with a simpler case using the same table
|
|
469
|
+
# or we could test array_cat with a literal array which is more common
|
|
470
|
+
|
|
471
|
+
# Test array_cat with literal array
|
|
472
|
+
new_query = QueryBuilder().select(
|
|
473
|
+
func.array_cat(ComplexDemo.string_list, ["admin", "superuser"])
|
|
474
|
+
)
|
|
475
|
+
assert new_query.build() == (
|
|
476
|
+
'SELECT array_cat("complexdemo"."string_list", ARRAY[\'admin\',\'superuser\']) AS aggregate_0 FROM "complexdemo"',
|
|
477
|
+
[],
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Test array_position
|
|
481
|
+
new_query = QueryBuilder().select(
|
|
482
|
+
func.array_position(ComplexDemo.string_list, "python")
|
|
483
|
+
)
|
|
484
|
+
assert new_query.build() == (
|
|
485
|
+
'SELECT array_position("complexdemo"."string_list", \'python\') AS aggregate_0 FROM "complexdemo"',
|
|
486
|
+
[],
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Test array_remove
|
|
490
|
+
new_query = QueryBuilder().select(
|
|
491
|
+
func.array_remove(ComplexDemo.string_list, "deprecated")
|
|
492
|
+
)
|
|
493
|
+
assert new_query.build() == (
|
|
494
|
+
'SELECT array_remove("complexdemo"."string_list", \'deprecated\') AS aggregate_0 FROM "complexdemo"',
|
|
495
|
+
[],
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
340
499
|
def test_invalid_where_condition():
|
|
341
500
|
with pytest.raises(ValueError):
|
|
342
501
|
QueryBuilder().select(UserDemo.id).where("invalid condition") # type: ignore
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Any, Literal, Type, TypeVar, cast
|
|
5
|
+
from typing import Any, Generic, Literal, Type, TypeVar, cast
|
|
6
6
|
|
|
7
7
|
from iceaxe.base import (
|
|
8
8
|
DBFieldClassDefinition,
|
|
@@ -193,6 +193,140 @@ class TSVectorFunctionMetadata(FunctionMetadata):
|
|
|
193
193
|
return self
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
class BooleanExpression(FieldComparison):
|
|
197
|
+
"""
|
|
198
|
+
A FieldComparison that represents a complete boolean expression.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(self, expression: FunctionMetadata):
|
|
202
|
+
# Initialize with dummy values since we override to_query
|
|
203
|
+
super().__init__(left=expression, comparison=ComparisonType.EQ, right=True)
|
|
204
|
+
self.expression = expression
|
|
205
|
+
|
|
206
|
+
def to_query(self, start: int = 1) -> tuple[QueryLiteral, list[Any]]:
|
|
207
|
+
"""
|
|
208
|
+
Return the expression directly without additional comparison.
|
|
209
|
+
"""
|
|
210
|
+
return self.expression.literal, []
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ArrayComparison(Generic[T], ComparisonBase[bool]):
|
|
214
|
+
"""
|
|
215
|
+
Provides comparison methods for SQL array operations (ANY/ALL).
|
|
216
|
+
This class enables ergonomic syntax for array comparisons.
|
|
217
|
+
|
|
218
|
+
```python {{sticky: True}}
|
|
219
|
+
# Using ArrayComparison for ANY operations:
|
|
220
|
+
func.any(Article.tags) == 'python'
|
|
221
|
+
func.any(User.follower_ids) == current_user_id
|
|
222
|
+
|
|
223
|
+
# Using ArrayComparison for ALL operations:
|
|
224
|
+
func.all(Project.member_statuses) == 'active'
|
|
225
|
+
func.all(Student.test_scores) >= 70
|
|
226
|
+
```
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self, array_metadata: FunctionMetadata, operation: Literal["ANY", "ALL"]
|
|
231
|
+
):
|
|
232
|
+
"""
|
|
233
|
+
Initialize an ArrayComparison object.
|
|
234
|
+
|
|
235
|
+
:param array_metadata: The metadata for the array field
|
|
236
|
+
:param operation: The SQL operation (ANY or ALL)
|
|
237
|
+
"""
|
|
238
|
+
self.array_metadata = array_metadata
|
|
239
|
+
self.operation = operation
|
|
240
|
+
|
|
241
|
+
def __eq__(self, other: T) -> BooleanExpression: # type: ignore
|
|
242
|
+
"""
|
|
243
|
+
Creates an equality comparison with the array elements.
|
|
244
|
+
|
|
245
|
+
:param other: The value to compare with array elements
|
|
246
|
+
:return: A boolean expression object that resolves to a boolean
|
|
247
|
+
"""
|
|
248
|
+
return self._create_comparison(other, "=")
|
|
249
|
+
|
|
250
|
+
def __ne__(self, other: T) -> BooleanExpression: # type: ignore
|
|
251
|
+
"""
|
|
252
|
+
Creates a not-equal comparison with the array elements.
|
|
253
|
+
|
|
254
|
+
:param other: The value to compare with array elements
|
|
255
|
+
:return: A boolean expression object that resolves to a boolean
|
|
256
|
+
"""
|
|
257
|
+
return self._create_comparison(other, "!=")
|
|
258
|
+
|
|
259
|
+
def __lt__(self, other: T) -> BooleanExpression:
|
|
260
|
+
"""
|
|
261
|
+
Creates a less-than comparison with the array elements.
|
|
262
|
+
|
|
263
|
+
:param other: The value to compare with array elements
|
|
264
|
+
:return: A boolean expression object that resolves to a boolean
|
|
265
|
+
"""
|
|
266
|
+
return self._create_comparison(other, "<")
|
|
267
|
+
|
|
268
|
+
def __le__(self, other: T) -> BooleanExpression:
|
|
269
|
+
"""
|
|
270
|
+
Creates a less-than-or-equal comparison with the array elements.
|
|
271
|
+
|
|
272
|
+
:param other: The value to compare with array elements
|
|
273
|
+
:return: A boolean expression object that resolves to a boolean
|
|
274
|
+
"""
|
|
275
|
+
return self._create_comparison(other, "<=")
|
|
276
|
+
|
|
277
|
+
def __gt__(self, other: T) -> BooleanExpression:
|
|
278
|
+
"""
|
|
279
|
+
Creates a greater-than comparison with the array elements.
|
|
280
|
+
|
|
281
|
+
:param other: The value to compare with array elements
|
|
282
|
+
:return: A boolean expression object that resolves to a boolean
|
|
283
|
+
"""
|
|
284
|
+
return self._create_comparison(other, ">")
|
|
285
|
+
|
|
286
|
+
def __ge__(self, other: T) -> BooleanExpression:
|
|
287
|
+
"""
|
|
288
|
+
Creates a greater-than-or-equal comparison with the array elements.
|
|
289
|
+
|
|
290
|
+
:param other: The value to compare with array elements
|
|
291
|
+
:return: A boolean expression object that resolves to a boolean
|
|
292
|
+
"""
|
|
293
|
+
return self._create_comparison(other, ">=")
|
|
294
|
+
|
|
295
|
+
def _create_comparison(self, value: T, operator: str) -> BooleanExpression:
|
|
296
|
+
"""
|
|
297
|
+
Internal method to create the comparison SQL.
|
|
298
|
+
|
|
299
|
+
:param value: The value to compare with array elements
|
|
300
|
+
:param operator: The SQL comparison operator
|
|
301
|
+
:return: A boolean expression object that resolves to a boolean
|
|
302
|
+
"""
|
|
303
|
+
# Handle simple values
|
|
304
|
+
if isinstance(value, (str, int, float, bool)):
|
|
305
|
+
value_literal = f"'{value}'" if isinstance(value, str) else str(value)
|
|
306
|
+
else:
|
|
307
|
+
# For complex values, use the metadata
|
|
308
|
+
value_metadata = FunctionBuilder._column_to_metadata(value)
|
|
309
|
+
value_literal = str(value_metadata.literal)
|
|
310
|
+
|
|
311
|
+
# Create the comparison as a FunctionMetadata
|
|
312
|
+
result = FunctionMetadata(
|
|
313
|
+
literal=QueryLiteral(
|
|
314
|
+
f"{value_literal} {operator} {self.operation}({self.array_metadata.literal})"
|
|
315
|
+
),
|
|
316
|
+
original_field=self.array_metadata.original_field,
|
|
317
|
+
)
|
|
318
|
+
# Return a BooleanExpression that generates the SQL directly
|
|
319
|
+
return BooleanExpression(result)
|
|
320
|
+
|
|
321
|
+
def to_query(self) -> tuple[QueryLiteral, list[Any]]:
|
|
322
|
+
"""
|
|
323
|
+
Converts the array comparison to its SQL representation.
|
|
324
|
+
|
|
325
|
+
:return: A tuple of the SQL query string and list of parameter values
|
|
326
|
+
"""
|
|
327
|
+
return self.array_metadata.literal, []
|
|
328
|
+
|
|
329
|
+
|
|
196
330
|
class FunctionBuilder:
|
|
197
331
|
"""
|
|
198
332
|
Builder class for SQL aggregate functions and other SQL operations.
|
|
@@ -595,6 +729,398 @@ class FunctionBuilder:
|
|
|
595
729
|
)
|
|
596
730
|
return cast(str, metadata)
|
|
597
731
|
|
|
732
|
+
def unnest(self, field: list[T]) -> T:
|
|
733
|
+
"""
|
|
734
|
+
Expands an array into a set of rows.
|
|
735
|
+
|
|
736
|
+
:param field: The array field to unnest
|
|
737
|
+
:return: A function metadata object that resolves to the element type
|
|
738
|
+
|
|
739
|
+
```python {{sticky: True}}
|
|
740
|
+
# Unnest an array column
|
|
741
|
+
tags = await conn.execute(select(func.unnest(Article.tags)))
|
|
742
|
+
|
|
743
|
+
# Use with joins
|
|
744
|
+
result = await conn.execute(
|
|
745
|
+
select((User.name, func.unnest(User.favorite_colors)))
|
|
746
|
+
.where(User.id == user_id)
|
|
747
|
+
)
|
|
748
|
+
```
|
|
749
|
+
"""
|
|
750
|
+
metadata = self._column_to_metadata(field)
|
|
751
|
+
metadata.literal = QueryLiteral(f"unnest({metadata.literal})")
|
|
752
|
+
return cast(T, metadata)
|
|
753
|
+
|
|
754
|
+
# Array Operators and Functions
|
|
755
|
+
def any(self, array_field: list[T]) -> ArrayComparison[T]:
|
|
756
|
+
"""
|
|
757
|
+
Creates an ANY array comparison that can be used with comparison operators.
|
|
758
|
+
|
|
759
|
+
:param array_field: The array field to check against
|
|
760
|
+
:return: An ArrayComparison object that supports comparison operators
|
|
761
|
+
|
|
762
|
+
```python {{sticky: True}}
|
|
763
|
+
# Check if 'python' is in the tags array
|
|
764
|
+
has_python = await conn.execute(
|
|
765
|
+
select(Article)
|
|
766
|
+
.where(func.any(Article.tags) == 'python')
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Check if user id is in the follower_ids array
|
|
770
|
+
is_follower = await conn.execute(
|
|
771
|
+
select(User)
|
|
772
|
+
.where(func.any(User.follower_ids) == current_user_id)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Check if any score is above threshold
|
|
776
|
+
has_passing = await conn.execute(
|
|
777
|
+
select(Student)
|
|
778
|
+
.where(func.any(Student.test_scores) >= 70)
|
|
779
|
+
)
|
|
780
|
+
```
|
|
781
|
+
"""
|
|
782
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
783
|
+
return ArrayComparison(array_metadata, "ANY")
|
|
784
|
+
|
|
785
|
+
def all(self, array_field: list[T]) -> ArrayComparison[T]:
|
|
786
|
+
"""
|
|
787
|
+
Creates an ALL array comparison that can be used with comparison operators.
|
|
788
|
+
|
|
789
|
+
:param array_field: The array field to check against
|
|
790
|
+
:return: An ArrayComparison object that supports comparison operators
|
|
791
|
+
|
|
792
|
+
```python {{sticky: True}}
|
|
793
|
+
# Check if all elements in status array are 'active'
|
|
794
|
+
all_active = await conn.execute(
|
|
795
|
+
select(Project)
|
|
796
|
+
.where(func.all(Project.member_statuses) == 'active')
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
# Check if all scores are above threshold
|
|
800
|
+
all_passing = await conn.execute(
|
|
801
|
+
select(Student)
|
|
802
|
+
.where(func.all(Student.test_scores) >= 70)
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# Check if all values are non-null
|
|
806
|
+
all_present = await conn.execute(
|
|
807
|
+
select(Survey)
|
|
808
|
+
.where(func.all(Survey.responses) != None)
|
|
809
|
+
)
|
|
810
|
+
```
|
|
811
|
+
"""
|
|
812
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
813
|
+
return ArrayComparison(array_metadata, "ALL")
|
|
814
|
+
|
|
815
|
+
def array_contains(
|
|
816
|
+
self, array_field: list[T], contained: list[T]
|
|
817
|
+
) -> FunctionMetadata:
|
|
818
|
+
"""
|
|
819
|
+
Creates an array contains comparison using the @> operator.
|
|
820
|
+
Checks if the array field contains all elements of the contained array.
|
|
821
|
+
|
|
822
|
+
:param array_field: The array field to check
|
|
823
|
+
:param contained: The array that should be contained
|
|
824
|
+
:return: A function metadata object that resolves to a boolean
|
|
825
|
+
|
|
826
|
+
```python {{sticky: True}}
|
|
827
|
+
# Check if tags contain both 'python' and 'django'
|
|
828
|
+
has_both = await conn.execute(
|
|
829
|
+
select(Article)
|
|
830
|
+
.where(func.array_contains(Article.tags, ['python', 'django']) == True)
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# Check if permissions contain required permissions
|
|
834
|
+
has_perms = await conn.execute(
|
|
835
|
+
select(User)
|
|
836
|
+
.where(func.array_contains(User.permissions, ['read', 'write']) == True)
|
|
837
|
+
)
|
|
838
|
+
```
|
|
839
|
+
"""
|
|
840
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
841
|
+
|
|
842
|
+
# Convert the contained list to PostgreSQL array syntax
|
|
843
|
+
if all(isinstance(x, str) for x in contained):
|
|
844
|
+
contained_literal = "ARRAY[" + ",".join(f"'{x}'" for x in contained) + "]"
|
|
845
|
+
else:
|
|
846
|
+
contained_literal = "ARRAY[" + ",".join(str(x) for x in contained) + "]"
|
|
847
|
+
|
|
848
|
+
# Create the @> comparison as a FunctionMetadata
|
|
849
|
+
array_metadata.literal = QueryLiteral(
|
|
850
|
+
f"{array_metadata.literal} @> {contained_literal}"
|
|
851
|
+
)
|
|
852
|
+
return array_metadata
|
|
853
|
+
|
|
854
|
+
def array_contained_by(
|
|
855
|
+
self, array_field: list[T], container: list[T]
|
|
856
|
+
) -> FunctionMetadata:
|
|
857
|
+
"""
|
|
858
|
+
Creates an array contained by comparison using the <@ operator.
|
|
859
|
+
Checks if all elements of the array field are contained in the container array.
|
|
860
|
+
|
|
861
|
+
:param array_field: The array field to check
|
|
862
|
+
:param container: The array that should contain all elements
|
|
863
|
+
:return: A function metadata object that resolves to a boolean
|
|
864
|
+
|
|
865
|
+
```python {{sticky: True}}
|
|
866
|
+
# Check if user's skills are all from allowed skills
|
|
867
|
+
valid_skills = await conn.execute(
|
|
868
|
+
select(User)
|
|
869
|
+
.where(func.array_contained_by(User.skills, ['python', 'java', 'go', 'rust']) == True)
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Check if selected options are all from valid options
|
|
873
|
+
valid_selection = await conn.execute(
|
|
874
|
+
select(Survey)
|
|
875
|
+
.where(func.array_contained_by(Survey.selected, [1, 2, 3, 4, 5]) == True)
|
|
876
|
+
)
|
|
877
|
+
```
|
|
878
|
+
"""
|
|
879
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
880
|
+
|
|
881
|
+
# Convert the container list to PostgreSQL array syntax
|
|
882
|
+
if all(isinstance(x, str) for x in container):
|
|
883
|
+
container_literal = "ARRAY[" + ",".join(f"'{x}'" for x in container) + "]"
|
|
884
|
+
else:
|
|
885
|
+
container_literal = "ARRAY[" + ",".join(str(x) for x in container) + "]"
|
|
886
|
+
|
|
887
|
+
# Create the <@ comparison as a FunctionMetadata
|
|
888
|
+
array_metadata.literal = QueryLiteral(
|
|
889
|
+
f"{array_metadata.literal} <@ {container_literal}"
|
|
890
|
+
)
|
|
891
|
+
return array_metadata
|
|
892
|
+
|
|
893
|
+
def array_overlaps(self, array_field: list[T], other: list[T]) -> FunctionMetadata:
|
|
894
|
+
"""
|
|
895
|
+
Creates an array overlap comparison using the && operator.
|
|
896
|
+
Checks if the arrays have any elements in common.
|
|
897
|
+
|
|
898
|
+
:param array_field: The first array field
|
|
899
|
+
:param other: The second array to check for overlap
|
|
900
|
+
:return: A function metadata object that resolves to a boolean
|
|
901
|
+
|
|
902
|
+
```python {{sticky: True}}
|
|
903
|
+
# Check if article tags overlap with user interests
|
|
904
|
+
matching_interests = await conn.execute(
|
|
905
|
+
select(Article)
|
|
906
|
+
.where(func.array_overlaps(Article.tags, ['python', 'data-science', 'ml']) == True)
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Check if available times overlap
|
|
910
|
+
has_common_time = await conn.execute(
|
|
911
|
+
select(Meeting)
|
|
912
|
+
.where(func.array_overlaps(Meeting.available_slots, [1, 2, 3]) == True)
|
|
913
|
+
)
|
|
914
|
+
```
|
|
915
|
+
"""
|
|
916
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
917
|
+
|
|
918
|
+
# Convert the other list to PostgreSQL array syntax
|
|
919
|
+
if all(isinstance(x, str) for x in other):
|
|
920
|
+
other_literal = "ARRAY[" + ",".join(f"'{x}'" for x in other) + "]"
|
|
921
|
+
else:
|
|
922
|
+
other_literal = "ARRAY[" + ",".join(str(x) for x in other) + "]"
|
|
923
|
+
|
|
924
|
+
# Create the && comparison as a FunctionMetadata
|
|
925
|
+
array_metadata.literal = QueryLiteral(
|
|
926
|
+
f"{array_metadata.literal} && {other_literal}"
|
|
927
|
+
)
|
|
928
|
+
return array_metadata
|
|
929
|
+
|
|
930
|
+
def array_append(self, array_field: list[T], element: T) -> list[T]:
|
|
931
|
+
"""
|
|
932
|
+
Appends an element to the end of an array.
|
|
933
|
+
|
|
934
|
+
:param array_field: The array field to append to
|
|
935
|
+
:param element: The element to append
|
|
936
|
+
:return: A function metadata object that resolves to an array
|
|
937
|
+
|
|
938
|
+
```python {{sticky: True}}
|
|
939
|
+
# Append a tag to the array
|
|
940
|
+
updated = await conn.execute(
|
|
941
|
+
update(Article)
|
|
942
|
+
.set(tags=func.array_append(Article.tags, 'new-tag'))
|
|
943
|
+
.where(Article.id == article_id)
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
# Select with appended element
|
|
947
|
+
with_extra = await conn.execute(
|
|
948
|
+
select(func.array_append(User.skills, 'python'))
|
|
949
|
+
)
|
|
950
|
+
```
|
|
951
|
+
"""
|
|
952
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
953
|
+
|
|
954
|
+
# Handle element literal
|
|
955
|
+
if isinstance(element, str):
|
|
956
|
+
element_literal = f"'{element}'"
|
|
957
|
+
elif isinstance(element, (int, float, bool)):
|
|
958
|
+
element_literal = str(element)
|
|
959
|
+
else:
|
|
960
|
+
element_metadata = self._column_to_metadata(element)
|
|
961
|
+
element_literal = str(element_metadata.literal)
|
|
962
|
+
|
|
963
|
+
array_metadata.literal = QueryLiteral(
|
|
964
|
+
f"array_append({array_metadata.literal}, {element_literal})"
|
|
965
|
+
)
|
|
966
|
+
return cast(list[T], array_metadata)
|
|
967
|
+
|
|
968
|
+
def array_prepend(self, element: T, array_field: list[T]) -> list[T]:
|
|
969
|
+
"""
|
|
970
|
+
Prepends an element to the beginning of an array.
|
|
971
|
+
|
|
972
|
+
:param element: The element to prepend
|
|
973
|
+
:param array_field: The array field to prepend to
|
|
974
|
+
:return: A function metadata object that resolves to an array
|
|
975
|
+
|
|
976
|
+
```python {{sticky: True}}
|
|
977
|
+
# Prepend a tag to the array
|
|
978
|
+
updated = await conn.execute(
|
|
979
|
+
update(Article)
|
|
980
|
+
.set(tags=func.array_prepend('featured', Article.tags))
|
|
981
|
+
.where(Article.id == article_id)
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
# Select with prepended element
|
|
985
|
+
with_prefix = await conn.execute(
|
|
986
|
+
select(func.array_prepend('beginner', User.skill_levels))
|
|
987
|
+
)
|
|
988
|
+
```
|
|
989
|
+
"""
|
|
990
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
991
|
+
|
|
992
|
+
# Handle element literal
|
|
993
|
+
if isinstance(element, str):
|
|
994
|
+
element_literal = f"'{element}'"
|
|
995
|
+
elif isinstance(element, (int, float, bool)):
|
|
996
|
+
element_literal = str(element)
|
|
997
|
+
else:
|
|
998
|
+
element_metadata = self._column_to_metadata(element)
|
|
999
|
+
element_literal = str(element_metadata.literal)
|
|
1000
|
+
|
|
1001
|
+
array_metadata.literal = QueryLiteral(
|
|
1002
|
+
f"array_prepend({element_literal}, {array_metadata.literal})"
|
|
1003
|
+
)
|
|
1004
|
+
return cast(list[T], array_metadata)
|
|
1005
|
+
|
|
1006
|
+
def array_cat(self, array1: list[T], array2: list[T]) -> list[T]:
|
|
1007
|
+
"""
|
|
1008
|
+
Concatenates two arrays.
|
|
1009
|
+
|
|
1010
|
+
:param array1: The first array
|
|
1011
|
+
:param array2: The second array
|
|
1012
|
+
:return: A function metadata object that resolves to an array
|
|
1013
|
+
|
|
1014
|
+
```python {{sticky: True}}
|
|
1015
|
+
# Concatenate two arrays
|
|
1016
|
+
combined_tags = await conn.execute(
|
|
1017
|
+
select(func.array_cat(Article.tags, Article.categories))
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Merge user permissions
|
|
1021
|
+
all_perms = await conn.execute(
|
|
1022
|
+
update(User)
|
|
1023
|
+
.set(permissions=func.array_cat(User.permissions, ['admin', 'superuser']))
|
|
1024
|
+
.where(User.id == user_id)
|
|
1025
|
+
)
|
|
1026
|
+
```
|
|
1027
|
+
"""
|
|
1028
|
+
array1_metadata = self._column_to_metadata(array1)
|
|
1029
|
+
|
|
1030
|
+
# Handle array2 - could be a field or a literal list
|
|
1031
|
+
if isinstance(array2, list):
|
|
1032
|
+
# Convert literal list to PostgreSQL array
|
|
1033
|
+
if all(isinstance(x, str) for x in array2):
|
|
1034
|
+
array2_literal = "ARRAY[" + ",".join(f"'{x}'" for x in array2) + "]"
|
|
1035
|
+
else:
|
|
1036
|
+
array2_literal = "ARRAY[" + ",".join(str(x) for x in array2) + "]"
|
|
1037
|
+
else:
|
|
1038
|
+
array2_metadata = self._column_to_metadata(array2)
|
|
1039
|
+
array2_literal = str(array2_metadata.literal)
|
|
1040
|
+
|
|
1041
|
+
array1_metadata.literal = QueryLiteral(
|
|
1042
|
+
f"array_cat({array1_metadata.literal}, {array2_literal})"
|
|
1043
|
+
)
|
|
1044
|
+
return cast(list[T], array1_metadata)
|
|
1045
|
+
|
|
1046
|
+
def array_position(self, array_field: list[T], element: T) -> int:
|
|
1047
|
+
"""
|
|
1048
|
+
Returns the position of the first occurrence of an element in an array (1-based).
|
|
1049
|
+
Returns NULL if the element is not found.
|
|
1050
|
+
|
|
1051
|
+
:param array_field: The array field to search in
|
|
1052
|
+
:param element: The element to find
|
|
1053
|
+
:return: A function metadata object that resolves to an integer
|
|
1054
|
+
|
|
1055
|
+
```python {{sticky: True}}
|
|
1056
|
+
# Find position of a tag
|
|
1057
|
+
position = await conn.execute(
|
|
1058
|
+
select(func.array_position(Article.tags, 'python'))
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
# Find position in a list of ids
|
|
1062
|
+
rank = await conn.execute(
|
|
1063
|
+
select(func.array_position(Contest.winner_ids, User.id))
|
|
1064
|
+
.where(Contest.id == contest_id)
|
|
1065
|
+
)
|
|
1066
|
+
```
|
|
1067
|
+
"""
|
|
1068
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
1069
|
+
|
|
1070
|
+
# Handle element literal
|
|
1071
|
+
if isinstance(element, str):
|
|
1072
|
+
element_literal = f"'{element}'"
|
|
1073
|
+
elif isinstance(element, (int, float, bool)):
|
|
1074
|
+
element_literal = str(element)
|
|
1075
|
+
else:
|
|
1076
|
+
element_metadata = self._column_to_metadata(element)
|
|
1077
|
+
element_literal = str(element_metadata.literal)
|
|
1078
|
+
|
|
1079
|
+
array_metadata.literal = QueryLiteral(
|
|
1080
|
+
f"array_position({array_metadata.literal}, {element_literal})"
|
|
1081
|
+
)
|
|
1082
|
+
return cast(int, array_metadata)
|
|
1083
|
+
|
|
1084
|
+
def array_remove(self, array_field: list[T], element: T) -> list[T]:
|
|
1085
|
+
"""
|
|
1086
|
+
Removes all occurrences of an element from an array.
|
|
1087
|
+
|
|
1088
|
+
:param array_field: The array field to remove from
|
|
1089
|
+
:param element: The element to remove
|
|
1090
|
+
:return: A function metadata object that resolves to an array
|
|
1091
|
+
|
|
1092
|
+
```python {{sticky: True}}
|
|
1093
|
+
# Remove a tag from the array
|
|
1094
|
+
updated = await conn.execute(
|
|
1095
|
+
update(Article)
|
|
1096
|
+
.set(tags=func.array_remove(Article.tags, 'deprecated'))
|
|
1097
|
+
.where(Article.id == article_id)
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Remove a skill from user's skill list
|
|
1101
|
+
cleaned = await conn.execute(
|
|
1102
|
+
update(User)
|
|
1103
|
+
.set(skills=func.array_remove(User.skills, 'obsolete-skill'))
|
|
1104
|
+
.where(User.id == user_id)
|
|
1105
|
+
)
|
|
1106
|
+
```
|
|
1107
|
+
"""
|
|
1108
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
1109
|
+
|
|
1110
|
+
# Handle element literal
|
|
1111
|
+
if isinstance(element, str):
|
|
1112
|
+
element_literal = f"'{element}'"
|
|
1113
|
+
elif isinstance(element, (int, float, bool)):
|
|
1114
|
+
element_literal = str(element)
|
|
1115
|
+
else:
|
|
1116
|
+
element_metadata = self._column_to_metadata(element)
|
|
1117
|
+
element_literal = str(element_metadata.literal)
|
|
1118
|
+
|
|
1119
|
+
array_metadata.literal = QueryLiteral(
|
|
1120
|
+
f"array_remove({array_metadata.literal}, {element_literal})"
|
|
1121
|
+
)
|
|
1122
|
+
return cast(list[T], array_metadata)
|
|
1123
|
+
|
|
598
1124
|
# Type Conversion Functions
|
|
599
1125
|
def cast(self, field: Any, type_name: Type[T]) -> T:
|
|
600
1126
|
"""
|
|
@@ -11,7 +11,11 @@ from iceaxe.base import (
|
|
|
11
11
|
DBModelMetaclass,
|
|
12
12
|
TableBase,
|
|
13
13
|
)
|
|
14
|
-
from iceaxe.comparison import
|
|
14
|
+
from iceaxe.comparison import (
|
|
15
|
+
ComparisonGroupType,
|
|
16
|
+
FieldComparison,
|
|
17
|
+
FieldComparisonGroup,
|
|
18
|
+
)
|
|
15
19
|
from iceaxe.functions import FunctionMetadata
|
|
16
20
|
from iceaxe.queries_str import (
|
|
17
21
|
QueryElementBase,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|