Skip to content

Commit a9be500

Browse files
committed
Add RDF List operators
1 parent 519af6b commit a9be500

File tree

4 files changed

+1566
-1
lines changed

4 files changed

+1566
-1
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""
2+
Unit tests for WOQL localize() - verifying JSON structure only.
3+
These tests do NOT connect to a database - they only verify the generated WOQL JSON.
4+
5+
These tests align with the JavaScript client's woqlLocalize.spec.js tests.
6+
"""
7+
import pytest
8+
from terminusdb_client.woqlquery import (
9+
WOQLQuery, Var, VarsUnique, _reset_unique_var_counter
10+
)
11+
12+
13+
class TestWOQLLocalize:
14+
"""Test suite for WOQL localize() method."""
15+
16+
def setup_method(self):
17+
"""Reset unique var counter before each test for predictable names."""
18+
_reset_unique_var_counter(0)
19+
20+
def test_hide_local_variables_from_outer_scope(self):
21+
"""Should hide local variables from outer scope (basic test)."""
22+
(localized, v) = WOQLQuery().localize({
23+
'local_only': None,
24+
})
25+
query = WOQLQuery().woql_and(
26+
WOQLQuery().triple('v:x', 'v:y', 'v:z'),
27+
localized(
28+
WOQLQuery().triple(v.local_only, 'knows', 'v:someone'),
29+
),
30+
)
31+
32+
result = query.to_dict()
33+
34+
# Check that query has proper structure
35+
assert result['@type'] == 'And'
36+
assert isinstance(result['and'], list)
37+
# Second element should be Select
38+
assert result['and'][1]['@type'] == 'Select'
39+
assert isinstance(result['and'][1]['variables'], list)
40+
41+
# CRITICAL: select("") creates variables:[] to hide all local variables
42+
assert result['and'][1]['variables'] == []
43+
44+
def test_bind_outer_parameters_via_eq(self):
45+
"""Should bind outer parameters via eq() clauses."""
46+
(localized, v) = WOQLQuery().localize({
47+
'param1': 'v:input1',
48+
'param2': 'v:input2',
49+
'local': None,
50+
})
51+
52+
query = localized(
53+
WOQLQuery().triple(v.param1, 'knows', v.param2),
54+
)
55+
56+
result = query.to_dict()
57+
58+
# Structure: And with eq bindings + Select wrapper
59+
assert result['@type'] == 'And'
60+
and_clauses = result['and']
61+
62+
# Should have eq bindings before the select
63+
eq_count = sum(1 for c in and_clauses if c.get('@type') == 'Equals')
64+
assert eq_count >= 2 # At least 2 eq bindings for 2 outer params
65+
66+
# Last element should be Select
67+
select_clauses = [c for c in and_clauses if c.get('@type') == 'Select']
68+
assert len(select_clauses) >= 1
69+
70+
def test_functional_mode(self):
71+
"""Should work in functional mode."""
72+
(localized, v) = WOQLQuery().localize({
73+
'x': 'v:external_x',
74+
'temp': None,
75+
})
76+
77+
query = localized(
78+
WOQLQuery().woql_and(
79+
WOQLQuery().eq(v.temp, {'@type': 'xsd:string', '@value': 'intermediate'}),
80+
WOQLQuery().triple(v.x, 'knows', v.temp),
81+
),
82+
)
83+
84+
result = query.to_dict()
85+
86+
# Structure should be And with eq + Select
87+
assert result['@type'] == 'And'
88+
and_clauses = result['and']
89+
90+
# Should have eq binding for external_x
91+
eq_count = sum(1 for c in and_clauses if c.get('@type') == 'Equals')
92+
assert eq_count >= 1
93+
94+
# Should have Select with empty variables
95+
select_clauses = [c for c in and_clauses if c.get('@type') == 'Select']
96+
assert len(select_clauses) >= 1
97+
assert select_clauses[-1]['variables'] == []
98+
99+
def test_generate_unique_variable_names(self):
100+
"""Should generate unique variable names."""
101+
_reset_unique_var_counter(0)
102+
103+
(localized1, v1) = WOQLQuery().localize({
104+
'var1': None,
105+
})
106+
107+
(localized2, v2) = WOQLQuery().localize({
108+
'var1': None,
109+
})
110+
111+
# Variable names should be different between calls
112+
assert v1.var1.name != v2.var1.name
113+
114+
def test_handle_only_local_variables(self):
115+
"""Should handle only local variables (no outer bindings)."""
116+
(localized, v) = WOQLQuery().localize({
117+
'local1': None,
118+
'local2': None,
119+
})
120+
121+
query = localized(
122+
WOQLQuery().triple(v.local1, 'knows', v.local2),
123+
)
124+
125+
result = query.to_dict()
126+
127+
# With no outer bindings, should be just Select
128+
assert result['@type'] == 'Select'
129+
assert result['variables'] == []
130+
131+
# Query should be directly the triple (wrapped in And from select)
132+
assert result['query']['@type'] in ['Triple', 'And']
133+
134+
def test_handle_only_outer_parameters(self):
135+
"""Should handle only outer parameters (no local variables)."""
136+
(localized, v) = WOQLQuery().localize({
137+
'param1': 'v:input1',
138+
'param2': 'v:input2',
139+
})
140+
141+
query = localized(
142+
WOQLQuery().triple(v.param1, 'knows', v.param2),
143+
)
144+
145+
result = query.to_dict()
146+
assert result['@type'] == 'And'
147+
# Should have eq bindings + Select
148+
assert len(result['and']) >= 3
149+
150+
def test_preserve_variable_types(self):
151+
"""Should preserve variable types (Var instances)."""
152+
(localized, v) = WOQLQuery().localize({
153+
'input': 'v:x',
154+
'local': None,
155+
})
156+
157+
# v.input and v.local should be Var instances
158+
assert isinstance(v.input, Var)
159+
assert isinstance(v.local, Var)
160+
161+
def test_handle_empty_parameter_specification(self):
162+
"""Should handle empty parameter specification."""
163+
(localized, v) = WOQLQuery().localize({})
164+
165+
query = localized(
166+
WOQLQuery().triple('v:s', 'v:p', 'v:o'),
167+
)
168+
169+
result = query.to_dict()
170+
assert result['@type'] == 'Select'
171+
assert result['variables'] == []
172+
173+
# No eq bindings, just the query
174+
assert 'query' in result
175+
176+
177+
class TestVarsUnique:
178+
"""Test suite for VarsUnique class."""
179+
180+
def setup_method(self):
181+
"""Reset unique var counter before each test."""
182+
_reset_unique_var_counter(0)
183+
184+
def test_generates_unique_names(self):
185+
"""VarsUnique should generate unique variable names."""
186+
v1 = VarsUnique('x', 'y')
187+
v2 = VarsUnique('x', 'y')
188+
189+
# Names should have counter suffix
190+
assert '_' in v1.x.name
191+
assert '_' in v1.y.name
192+
193+
# Different calls should have different names
194+
assert v1.x.name != v2.x.name
195+
assert v1.y.name != v2.y.name
196+
197+
def test_counter_increments(self):
198+
"""Counter should increment for each variable."""
199+
_reset_unique_var_counter(0)
200+
v = VarsUnique('a', 'b', 'c')
201+
202+
# Each variable should have incrementing suffix
203+
assert v.a.name == 'a_1'
204+
assert v.b.name == 'b_2'
205+
assert v.c.name == 'c_3'
206+
207+
def test_reset_counter(self):
208+
"""Counter reset should work correctly."""
209+
_reset_unique_var_counter(100)
210+
v = VarsUnique('test')
211+
assert v.test.name == 'test_101'
212+
213+
def test_var_instances(self):
214+
"""VarsUnique should create Var instances."""
215+
v = VarsUnique('foo', 'bar')
216+
assert isinstance(v.foo, Var)
217+
assert isinstance(v.bar, Var)
218+
219+
def test_to_dict(self):
220+
"""Var.to_dict should return proper WOQL structure."""
221+
_reset_unique_var_counter(0)
222+
v = VarsUnique('myvar')
223+
result = v.myvar.to_dict()
224+
225+
assert result == {
226+
'@type': 'Value',
227+
'variable': 'myvar_1'
228+
}

0 commit comments

Comments
 (0)