|
| 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