Skip to content

Commit 99dd1dc

Browse files
committed
Add rdf:List operators
1 parent 31b9aae commit 99dd1dc

2 files changed

Lines changed: 979 additions & 0 deletions

File tree

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
"""
2+
Integration tests for WOQL rdf:List operations.
3+
4+
These tests verify the rdflist operations:
5+
- rdflist_list, rdflist_peek, rdflist_last, rdflist_member, rdflist_length
6+
- rdflist_pop, rdflist_push, rdflist_append
7+
- rdflist_insert, rdflist_drop, rdflist_clear
8+
- rdflist_swap, rdflist_reverse
9+
- rdflist_empty, rdflist_is_empty, rdflist_slice
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 extract_values(result_list):
21+
"""Extract raw values from a list of typed literals."""
22+
if not result_list:
23+
return []
24+
return [
25+
item["@value"] if isinstance(item, dict) and "@value" in item else item
26+
for item in result_list
27+
]
28+
29+
30+
def create_test_list(client, values):
31+
"""Create a test rdf:List with the given values and return the list head IRI."""
32+
if not values:
33+
raise ValueError("Cannot create empty list with this helper")
34+
35+
# Build query to create the list
36+
query_parts = []
37+
38+
# Create cells with deterministic IDs
39+
cell_ids = [f"terminusdb://data/Cons/test_{i}" for i in range(len(values))]
40+
41+
for i, (cell_id, value) in enumerate(zip(cell_ids, values)):
42+
query_parts.append(WOQLQuery().add_triple(cell_id, "rdf:type", "rdf:List"))
43+
query_parts.append(
44+
WOQLQuery().add_triple(
45+
cell_id,
46+
"rdf:first",
47+
{"@type": "xsd:string", "@value": value},
48+
)
49+
)
50+
if i < len(values) - 1:
51+
query_parts.append(
52+
WOQLQuery().add_triple(cell_id, "rdf:rest", cell_ids[i + 1])
53+
)
54+
else:
55+
query_parts.append(WOQLQuery().add_triple(cell_id, "rdf:rest", "rdf:nil"))
56+
57+
create_query = WOQLQuery().woql_and(*query_parts)
58+
client.query(create_query)
59+
60+
# Return the head cell IRI
61+
return cell_ids[0]
62+
63+
64+
class TestWOQLRdfListOperations:
65+
"""Tests for WOQL rdf:List operations."""
66+
67+
@pytest.fixture(autouse=True)
68+
def setup_teardown(self, docker_url):
69+
"""Setup and teardown for each test."""
70+
# Try without auth first (AUTOLOGIN mode), fallback to admin/root
71+
try:
72+
self.client = Client(docker_url, user_agent=test_user_agent)
73+
self.client.connect()
74+
except Exception:
75+
# Fallback to authenticated connection
76+
self.client = Client(
77+
docker_url, user="admin", key="root", user_agent=test_user_agent
78+
)
79+
self.client.connect()
80+
81+
self.db_name = "test_woql_rdflist_operations"
82+
83+
# Create database for tests
84+
if self.db_name in self.client.list_databases():
85+
self.client.delete_database(self.db_name)
86+
self.client.create_database(self.db_name)
87+
88+
yield
89+
90+
# Cleanup
91+
self.client.delete_database(self.db_name)
92+
93+
def test_rdflist_list_collects_all_elements(self):
94+
"""Test rdflist_list collects all elements into array."""
95+
list_head = create_test_list(self.client, ["A", "B", "C"])
96+
97+
query = WOQLQuery().rdflist_list(list_head, "v:all")
98+
result = self.client.query(query)
99+
100+
assert len(result["bindings"]) == 1
101+
assert extract_values(result["bindings"][0]["all"]) == ["A", "B", "C"]
102+
103+
def test_rdflist_peek_gets_first_element(self):
104+
"""Test rdflist_peek gets the first element."""
105+
list_head = create_test_list(self.client, ["First", "Second", "Third"])
106+
107+
query = WOQLQuery().rdflist_peek(list_head, "v:first")
108+
result = self.client.query(query)
109+
110+
assert len(result["bindings"]) == 1
111+
first = result["bindings"][0].get("first") or result["bindings"][0].get("v:first")
112+
assert first["@value"] == "First"
113+
114+
def test_rdflist_last_gets_last_element(self):
115+
"""Test rdflist_last gets the last element."""
116+
list_head = create_test_list(self.client, ["First", "Second", "Last"])
117+
118+
query = WOQLQuery().rdflist_last(list_head, "v:last")
119+
result = self.client.query(query)
120+
121+
assert len(result["bindings"]) == 1
122+
last = result["bindings"][0].get("last") or result["bindings"][0].get("v:last")
123+
assert last["@value"] == "Last"
124+
125+
def test_rdflist_member_iterates_elements(self):
126+
"""Test rdflist_member yields one binding per element."""
127+
list_head = create_test_list(self.client, ["X", "Y", "Z"])
128+
129+
query = WOQLQuery().rdflist_member(list_head, "v:item")
130+
result = self.client.query(query)
131+
132+
assert len(result["bindings"]) == 3
133+
items = [
134+
(b.get("item") or b.get("v:item"))["@value"]
135+
for b in result["bindings"]
136+
]
137+
assert items == ["X", "Y", "Z"]
138+
139+
def test_rdflist_length_counts_elements(self):
140+
"""Test rdflist_length returns correct count."""
141+
list_head = create_test_list(self.client, ["A", "B", "C", "D"])
142+
143+
query = WOQLQuery().rdflist_length(list_head, "v:len")
144+
result = self.client.query(query)
145+
146+
assert len(result["bindings"]) == 1
147+
length = result["bindings"][0].get("len") or result["bindings"][0].get("v:len")
148+
assert int(length["@value"]) == 4
149+
150+
def test_rdflist_push_adds_to_front(self):
151+
"""Test rdflist_push adds element to front in-place."""
152+
list_head = create_test_list(self.client, ["B", "C"])
153+
154+
# Push "A" to front
155+
push_query = WOQLQuery().rdflist_push(
156+
list_head, {"@type": "xsd:string", "@value": "A"}
157+
)
158+
self.client.query(push_query)
159+
160+
# Verify list is now [A, B, C]
161+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
162+
result = self.client.query(verify_query)
163+
assert extract_values(result["bindings"][0]["all"]) == ["A", "B", "C"]
164+
165+
def test_rdflist_pop_removes_from_front(self):
166+
"""Test rdflist_pop removes and returns first element."""
167+
list_head = create_test_list(self.client, ["X", "Y", "Z"])
168+
169+
# Get first element before pop
170+
peek_query = WOQLQuery().rdflist_peek(list_head, "v:first")
171+
peek_result = self.client.query(peek_query)
172+
first_val = peek_result["bindings"][0]["first"]["@value"]
173+
assert first_val == "X"
174+
175+
# Pop first element (write operation)
176+
pop_query = WOQLQuery().rdflist_pop(list_head, "v:popped")
177+
self.client.query(pop_query)
178+
179+
# Verify list is now [Y, Z]
180+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
181+
verify_result = self.client.query(verify_query)
182+
assert extract_values(verify_result["bindings"][0]["all"]) == ["Y", "Z"]
183+
184+
def test_rdflist_append_adds_to_end(self):
185+
"""Test rdflist_append adds element to end."""
186+
list_head = create_test_list(self.client, ["A", "B"])
187+
188+
# Append "C" to end
189+
append_query = WOQLQuery().rdflist_append(
190+
list_head, {"@type": "xsd:string", "@value": "C"}, "v:new_cell"
191+
)
192+
self.client.query(append_query)
193+
194+
# Verify list is now [A, B, C]
195+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
196+
result = self.client.query(verify_query)
197+
assert extract_values(result["bindings"][0]["all"]) == ["A", "B", "C"]
198+
199+
def test_rdflist_insert_at_position(self):
200+
"""Test rdflist_insert adds element at specified position."""
201+
list_head = create_test_list(self.client, ["A", "C", "D"])
202+
203+
# Insert "B" at position 1
204+
insert_query = WOQLQuery().rdflist_insert(
205+
list_head, 1, {"@type": "xsd:string", "@value": "B"}
206+
)
207+
self.client.query(insert_query)
208+
209+
# Verify list is now [A, B, C, D]
210+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
211+
result = self.client.query(verify_query)
212+
assert extract_values(result["bindings"][0]["all"]) == ["A", "B", "C", "D"]
213+
214+
def test_rdflist_insert_at_position_zero(self):
215+
"""Test rdflist_insert at position 0 works like push."""
216+
list_head = create_test_list(self.client, ["B", "C"])
217+
218+
# Insert "A" at position 0
219+
insert_query = WOQLQuery().rdflist_insert(
220+
list_head, 0, {"@type": "xsd:string", "@value": "A"}
221+
)
222+
self.client.query(insert_query)
223+
224+
# Verify list is now [A, B, C]
225+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
226+
result = self.client.query(verify_query)
227+
assert extract_values(result["bindings"][0]["all"]) == ["A", "B", "C"]
228+
229+
def test_rdflist_drop_removes_at_position(self):
230+
"""Test rdflist_drop removes element at specified position."""
231+
list_head = create_test_list(self.client, ["A", "B", "C", "D"])
232+
233+
# Drop element at position 1 (B)
234+
drop_query = WOQLQuery().rdflist_drop(list_head, 1)
235+
self.client.query(drop_query)
236+
237+
# Verify list is now [A, C, D]
238+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
239+
result = self.client.query(verify_query)
240+
assert extract_values(result["bindings"][0]["all"]) == ["A", "C", "D"]
241+
242+
def test_rdflist_drop_at_position_zero(self):
243+
"""Test rdflist_drop at position 0 removes first element."""
244+
list_head = create_test_list(self.client, ["X", "Y", "Z"])
245+
246+
# Drop element at position 0
247+
drop_query = WOQLQuery().rdflist_drop(list_head, 0)
248+
self.client.query(drop_query)
249+
250+
# Verify list is now [Y, Z]
251+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
252+
result = self.client.query(verify_query)
253+
assert extract_values(result["bindings"][0]["all"]) == ["Y", "Z"]
254+
255+
def test_rdflist_swap_exchanges_elements(self):
256+
"""Test rdflist_swap exchanges elements at two positions."""
257+
list_head = create_test_list(self.client, ["A", "B", "C", "D"])
258+
259+
# Swap positions 0 and 2 (A and C)
260+
swap_query = WOQLQuery().rdflist_swap(list_head, 0, 2)
261+
self.client.query(swap_query)
262+
263+
# Verify list is now [C, B, A, D]
264+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
265+
result = self.client.query(verify_query)
266+
assert extract_values(result["bindings"][0]["all"]) == ["C", "B", "A", "D"]
267+
268+
def test_rdflist_swap_same_position_is_noop(self):
269+
"""Test rdflist_swap with same position does nothing."""
270+
list_head = create_test_list(self.client, ["A", "B", "C"])
271+
272+
# Swap position 1 with itself
273+
swap_query = WOQLQuery().rdflist_swap(list_head, 1, 1)
274+
self.client.query(swap_query)
275+
276+
# Verify list is unchanged
277+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
278+
result = self.client.query(verify_query)
279+
assert extract_values(result["bindings"][0]["all"]) == ["A", "B", "C"]
280+
281+
def test_rdflist_clear_removes_all_elements(self):
282+
"""Test rdflist_clear removes all cons cells."""
283+
list_head = create_test_list(self.client, ["A", "B", "C"])
284+
285+
# Clear the list (write operation returns string)
286+
clear_query = WOQLQuery().rdflist_clear(list_head, "v:empty")
287+
self.client.query(clear_query)
288+
289+
# Verify list head no longer exists as rdf:List
290+
verify_query = WOQLQuery().triple(list_head, "rdf:type", "rdf:List")
291+
verify_result = self.client.query(verify_query)
292+
assert len(verify_result["bindings"]) == 0
293+
294+
def test_rdflist_empty_creates_nil_reference(self):
295+
"""Test rdflist_empty returns rdf:nil."""
296+
query = WOQLQuery().rdflist_empty("v:empty_list")
297+
result = self.client.query(query)
298+
299+
assert len(result["bindings"]) == 1
300+
empty = result["bindings"][0].get("empty_list") or result["bindings"][0].get("v:empty_list")
301+
assert empty == "rdf:nil"
302+
303+
def test_rdflist_is_empty_succeeds_for_nil(self):
304+
"""Test rdflist_is_empty succeeds for rdf:nil."""
305+
query = WOQLQuery().rdflist_is_empty("rdf:nil")
306+
result = self.client.query(query)
307+
308+
assert len(result["bindings"]) == 1
309+
310+
def test_rdflist_is_empty_fails_for_non_empty_list(self):
311+
"""Test rdflist_is_empty fails for non-empty list."""
312+
list_head = create_test_list(self.client, ["A"])
313+
314+
query = WOQLQuery().rdflist_is_empty(list_head)
315+
result = self.client.query(query)
316+
317+
assert len(result["bindings"]) == 0
318+
319+
def test_rdflist_slice_extracts_range(self):
320+
"""Test rdflist_slice extracts a range of elements."""
321+
list_head = create_test_list(self.client, ["A", "B", "C", "D", "E"])
322+
323+
# Get elements 1-3 (B, C, D)
324+
query = WOQLQuery().rdflist_slice(list_head, 1, 4, "v:slice")
325+
result = self.client.query(query)
326+
327+
assert len(result["bindings"]) == 1
328+
assert extract_values(result["bindings"][0]["slice"]) == ["B", "C", "D"]
329+
330+
def test_rdflist_slice_first_two(self):
331+
"""Test rdflist_slice gets first two elements."""
332+
list_head = create_test_list(self.client, ["X", "Y", "Z"])
333+
334+
query = WOQLQuery().rdflist_slice(list_head, 0, 2, "v:slice")
335+
result = self.client.query(query)
336+
337+
assert len(result["bindings"]) == 1
338+
assert extract_values(result["bindings"][0]["slice"]) == ["X", "Y"]
339+
340+
def test_rdflist_slice_single_element(self):
341+
"""Test rdflist_slice gets single element."""
342+
list_head = create_test_list(self.client, ["A", "B", "C"])
343+
344+
query = WOQLQuery().rdflist_slice(list_head, 1, 2, "v:slice")
345+
result = self.client.query(query)
346+
347+
assert len(result["bindings"]) == 1
348+
assert extract_values(result["bindings"][0]["slice"]) == ["B"]
349+
350+
def test_rdflist_slice_empty_range(self):
351+
"""Test rdflist_slice returns empty for start >= end."""
352+
list_head = create_test_list(self.client, ["A", "B", "C"])
353+
354+
query = WOQLQuery().rdflist_slice(list_head, 2, 2, "v:slice")
355+
result = self.client.query(query)
356+
357+
assert len(result["bindings"]) == 1
358+
assert result["bindings"][0]["slice"] == []
359+
360+
def test_rdflist_reverse_reverses_list(self):
361+
"""Test rdflist_reverse reverses the list in-place."""
362+
list_head = create_test_list(self.client, ["A", "B", "C", "D"])
363+
364+
# Reverse the list
365+
reverse_query = WOQLQuery().rdflist_reverse(list_head)
366+
self.client.query(reverse_query)
367+
368+
# Verify list is now [D, C, B, A]
369+
verify_query = WOQLQuery().rdflist_list(list_head, "v:all")
370+
result = self.client.query(verify_query)
371+
assert extract_values(result["bindings"][0]["all"]) == ["D", "C", "B", "A"]
372+
373+
def test_rdflist_insert_negative_position_raises(self):
374+
"""Test rdflist_insert raises for negative position."""
375+
with pytest.raises(ValueError, match="position >= 0"):
376+
WOQLQuery().rdflist_insert("v:list", -1, "value")
377+
378+
def test_rdflist_drop_negative_position_raises(self):
379+
"""Test rdflist_drop raises for negative position."""
380+
with pytest.raises(ValueError, match="position >= 0"):
381+
WOQLQuery().rdflist_drop("v:list", -1)
382+
383+
def test_rdflist_swap_negative_position_raises(self):
384+
"""Test rdflist_swap raises for negative position."""
385+
with pytest.raises(ValueError, match="positions >= 0"):
386+
WOQLQuery().rdflist_swap("v:list", -1, 0)
387+
388+
def test_rdflist_slice_negative_raises(self):
389+
"""Test rdflist_slice raises for negative indices."""
390+
with pytest.raises(ValueError, match="negative indices"):
391+
WOQLQuery().rdflist_slice("v:list", -1, 2, "v:result")

0 commit comments

Comments
 (0)