Skip to content

Commit 97a79b7

Browse files
committed
Range min/max query on list
1 parent ba1decf commit 97a79b7

3 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
Integration tests for WOQL RangeMin and RangeMax predicates.
3+
4+
Tests finding minimum and maximum values in lists:
5+
- Integer lists
6+
- Date lists
7+
- Single element
8+
- Empty list
9+
- Equal elements
10+
"""
11+
12+
import pytest
13+
14+
from terminusdb_client import Client
15+
from terminusdb_client.woqlquery.woql_query import WOQLQuery
16+
17+
test_user_agent = "terminusdb-client-python-tests"
18+
19+
20+
def intv(v):
21+
"""Build an xsd:integer typed literal."""
22+
return {"@type": "xsd:integer", "@value": v}
23+
24+
25+
def datv(v):
26+
"""Build an xsd:date typed literal."""
27+
return {"@type": "xsd:date", "@value": v}
28+
29+
30+
class TestRangeMin:
31+
"""Integration tests for RangeMin."""
32+
33+
@pytest.fixture(autouse=True)
34+
def setup_teardown(self, docker_url):
35+
self.client = Client(docker_url, user_agent=test_user_agent)
36+
self.client.connect()
37+
self.db_name = "test_range_min"
38+
if self.db_name in self.client.list_databases():
39+
self.client.delete_database(self.db_name)
40+
self.client.create_database(self.db_name)
41+
yield
42+
self.client.delete_database(self.db_name)
43+
44+
def test_min_integers(self):
45+
"""Minimum of [7, 2, 9, 1, 5] is 1."""
46+
query = WOQLQuery().range_min(
47+
[intv(7), intv(2), intv(9), intv(1), intv(5)], "v:m"
48+
)
49+
result = self.client.query(query)
50+
assert len(result["bindings"]) == 1
51+
assert result["bindings"][0]["m"]["@value"] == 1
52+
53+
def test_min_single_element(self):
54+
"""Single element list returns that element."""
55+
query = WOQLQuery().range_min([intv(42)], "v:m")
56+
result = self.client.query(query)
57+
assert len(result["bindings"]) == 1
58+
assert result["bindings"][0]["m"]["@value"] == 42
59+
60+
def test_min_empty_list(self):
61+
"""Empty list yields no bindings."""
62+
query = WOQLQuery().range_min([], "v:m")
63+
result = self.client.query(query)
64+
assert len(result["bindings"]) == 0
65+
66+
def test_min_dates(self):
67+
"""Minimum of dates."""
68+
query = WOQLQuery().range_min(
69+
[datv("2024-06-15"), datv("2024-01-01"), datv("2024-03-01")], "v:m"
70+
)
71+
result = self.client.query(query)
72+
assert len(result["bindings"]) == 1
73+
assert result["bindings"][0]["m"]["@value"] == "2024-01-01"
74+
75+
def test_min_equal_elements(self):
76+
"""All equal elements returns that element."""
77+
query = WOQLQuery().range_min([intv(3), intv(3), intv(3)], "v:m")
78+
result = self.client.query(query)
79+
assert len(result["bindings"]) == 1
80+
assert result["bindings"][0]["m"]["@value"] == 3
81+
82+
83+
class TestRangeMax:
84+
"""Integration tests for RangeMax."""
85+
86+
@pytest.fixture(autouse=True)
87+
def setup_teardown(self, docker_url):
88+
self.client = Client(docker_url, user_agent=test_user_agent)
89+
self.client.connect()
90+
self.db_name = "test_range_max"
91+
if self.db_name in self.client.list_databases():
92+
self.client.delete_database(self.db_name)
93+
self.client.create_database(self.db_name)
94+
yield
95+
self.client.delete_database(self.db_name)
96+
97+
def test_max_integers(self):
98+
"""Maximum of [7, 2, 9, 1, 5] is 9."""
99+
query = WOQLQuery().range_max(
100+
[intv(7), intv(2), intv(9), intv(1), intv(5)], "v:m"
101+
)
102+
result = self.client.query(query)
103+
assert len(result["bindings"]) == 1
104+
assert result["bindings"][0]["m"]["@value"] == 9
105+
106+
def test_max_dates(self):
107+
"""Maximum of dates."""
108+
query = WOQLQuery().range_max(
109+
[datv("2024-06-15"), datv("2024-01-01"), datv("2024-03-01")], "v:m"
110+
)
111+
result = self.client.query(query)
112+
assert len(result["bindings"]) == 1
113+
assert result["bindings"][0]["m"]["@value"] == "2024-06-15"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Unit tests for WOQLQuery.range_min and range_max JSON serialization."""
2+
3+
import pytest
4+
5+
from terminusdb_client.woqlquery.woql_query import WOQLQuery
6+
7+
8+
class TestRangeMinSerialization:
9+
"""Tests that range_min produces the correct WOQL JSON AST."""
10+
11+
def test_variable_result(self):
12+
"""List and variable result."""
13+
query = WOQLQuery().range_min("v:list", "v:m")
14+
expected = {
15+
"@type": "RangeMin",
16+
"list": {"@type": "Value", "variable": "list"},
17+
"result": {"@type": "Value", "variable": "m"},
18+
}
19+
assert query.to_dict() == expected
20+
21+
def test_raises_on_none_list(self):
22+
"""Raises ValueError when list is None."""
23+
with pytest.raises(ValueError, match="RangeMin takes two parameters"):
24+
WOQLQuery().range_min(None, "v:m")
25+
26+
def test_raises_on_none_result(self):
27+
"""Raises ValueError when result is None."""
28+
with pytest.raises(ValueError, match="RangeMin takes two parameters"):
29+
WOQLQuery().range_min("v:list", None)
30+
31+
32+
class TestRangeMaxSerialization:
33+
"""Tests that range_max produces the correct WOQL JSON AST."""
34+
35+
def test_variable_result(self):
36+
"""List and variable result."""
37+
query = WOQLQuery().range_max("v:list", "v:m")
38+
expected = {
39+
"@type": "RangeMax",
40+
"list": {"@type": "Value", "variable": "list"},
41+
"result": {"@type": "Value", "variable": "m"},
42+
}
43+
assert query.to_dict() == expected
44+
45+
def test_raises_on_none_list(self):
46+
"""Raises ValueError when list is None."""
47+
with pytest.raises(ValueError, match="RangeMax takes two parameters"):
48+
WOQLQuery().range_max(None, "v:m")
49+
50+
def test_raises_on_none_result(self):
51+
"""Raises ValueError when result is None."""
52+
with pytest.raises(ValueError, match="RangeMax takes two parameters"):
53+
WOQLQuery().range_max("v:list", None)

terminusdb_client/woqlquery/woql_query.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,6 +2470,60 @@ def interval_relation_typed(self, relation, x, y):
24702470
self._cursor["y"] = self._clean_object(y)
24712471
return self
24722472

2473+
def range_min(self, input_list, result):
2474+
"""Find the minimum value in a list using the standard ordering.
2475+
2476+
Works with any comparable types: numbers, dates, strings.
2477+
Empty list produces no bindings.
2478+
2479+
Parameters
2480+
----------
2481+
input_list : list or str or dict
2482+
the list of values to search
2483+
result : str or dict
2484+
variable or value for the minimum
2485+
2486+
Returns
2487+
-------
2488+
WOQLQuery object
2489+
query object that can be chained and/or execute
2490+
"""
2491+
if input_list is None or result is None:
2492+
raise ValueError("RangeMin takes two parameters")
2493+
if self._cursor.get("@type"):
2494+
self._wrap_cursor_with_and()
2495+
self._cursor["@type"] = "RangeMin"
2496+
self._cursor["list"] = self._clean_object(input_list)
2497+
self._cursor["result"] = self._clean_object(result)
2498+
return self
2499+
2500+
def range_max(self, input_list, result):
2501+
"""Find the maximum value in a list using the standard ordering.
2502+
2503+
Works with any comparable types: numbers, dates, strings.
2504+
Empty list produces no bindings.
2505+
2506+
Parameters
2507+
----------
2508+
input_list : list or str or dict
2509+
the list of values to search
2510+
result : str or dict
2511+
variable or value for the maximum
2512+
2513+
Returns
2514+
-------
2515+
WOQLQuery object
2516+
query object that can be chained and/or execute
2517+
"""
2518+
if input_list is None or result is None:
2519+
raise ValueError("RangeMax takes two parameters")
2520+
if self._cursor.get("@type"):
2521+
self._wrap_cursor_with_and()
2522+
self._cursor["@type"] = "RangeMax"
2523+
self._cursor["list"] = self._clean_object(input_list)
2524+
self._cursor["result"] = self._clean_object(result)
2525+
return self
2526+
24732527
def interval(self, start, end, interval_val):
24742528
"""Constructs or deconstructs a half-open xdd:dateTimeInterval [start, end).
24752529

0 commit comments

Comments
 (0)