Skip to content

Commit 253d6ea

Browse files
committed
Ensures the empty select is valid
1 parent 82f0319 commit 253d6ea

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
"""
2+
Integration tests for WOQL select() with empty variable list
3+
4+
Tests that select() with no arguments is valid and works correctly.
5+
This is needed for patterns like localize() where we want to hide
6+
all inner variables while still executing the inner query.
7+
"""
8+
import pytest
9+
10+
from terminusdb_client import Client
11+
from terminusdb_client.woqlquery.woql_query import WOQLQuery
12+
13+
14+
@pytest.fixture(scope="module")
15+
def select_empty_db():
16+
"""Create a client and test database for select empty tests."""
17+
# Use local server at 6363 (started with TERMINUSDB_AUTOLOGIN=true)
18+
client = Client("http://127.0.0.1:6363")
19+
client.connect()
20+
21+
db_name = "db__test_select_empty"
22+
23+
# Create test database if it doesn't exist
24+
if db_name not in client.list_databases():
25+
client.create_database(db_name)
26+
else:
27+
client.connect(db=db_name)
28+
29+
yield client
30+
31+
# Cleanup
32+
try:
33+
client.delete_database(db_name, "admin")
34+
except Exception:
35+
pass
36+
37+
38+
class TestWOQLSelectEmpty:
39+
"""Integration tests for WOQL select() with empty variable list."""
40+
41+
@pytest.fixture(autouse=True)
42+
def setup(self, select_empty_db):
43+
"""Setup client reference for each test."""
44+
self.client = select_empty_db
45+
46+
def test_select_no_args_returns_empty_bindings(self):
47+
"""select() with no arguments should be valid and return empty bindings."""
48+
# select() with no arguments should:
49+
# 1. Be valid syntax (not throw client-side error)
50+
# 2. Execute successfully on server
51+
# 3. Return bindings with no variables (empty dicts)
52+
53+
query = WOQLQuery().select(
54+
WOQLQuery().eq("v:X", WOQLQuery().string("test"))
55+
)
56+
57+
result = self.client.query(query)
58+
59+
assert result is not None
60+
assert "bindings" in result
61+
assert len(result["bindings"]) == 1
62+
63+
# The binding should be empty since select() hides all variables
64+
binding = result["bindings"][0]
65+
assert len(binding) == 0
66+
67+
def test_select_hides_inner_variables_outer_visible(self):
68+
"""select() should hide inner variables while outer variables remain visible."""
69+
# Pattern: outer variable is visible, inner variables are hidden by select()
70+
query = WOQLQuery().woql_and(
71+
WOQLQuery().eq("v:Outer", WOQLQuery().string("visible")),
72+
WOQLQuery().select(
73+
WOQLQuery().woql_and(
74+
WOQLQuery().eq("v:Inner", WOQLQuery().string("hidden")),
75+
# Use both variables to ensure query executes
76+
WOQLQuery().eq("v:Both", "v:Inner"),
77+
)
78+
),
79+
)
80+
81+
result = self.client.query(query)
82+
83+
assert result is not None
84+
assert "bindings" in result
85+
assert len(result["bindings"]) == 1
86+
87+
binding = result["bindings"][0]
88+
89+
# v:Outer should be visible (defined outside select())
90+
outer = binding.get("Outer") or binding.get("v:Outer")
91+
assert outer is not None
92+
assert outer.get("@value") == "visible"
93+
94+
# v:Inner and v:Both should NOT be visible (inside select())
95+
assert binding.get("Inner") is None
96+
assert binding.get("v:Inner") is None
97+
assert binding.get("Both") is None
98+
assert binding.get("v:Both") is None
99+
100+
def test_select_allows_outer_binding_through_unification(self):
101+
"""select() should hide inner variables but allow outer variables to be bound."""
102+
# Pattern: outer variable is visible and gets value through unification
103+
query = WOQLQuery().woql_and(
104+
WOQLQuery().eq("v:Outer", "v:Outer"),
105+
WOQLQuery().select(
106+
WOQLQuery().woql_and(
107+
WOQLQuery().eq("v:Inner", WOQLQuery().string("hidden")),
108+
WOQLQuery().eq("v:Inner_Shared_Value", WOQLQuery().string("visible")),
109+
# Use both variables to ensure query executes
110+
WOQLQuery().eq("v:Outer", "v:Inner_Shared_Value"),
111+
)
112+
),
113+
)
114+
115+
result = self.client.query(query)
116+
117+
assert result is not None
118+
assert "bindings" in result
119+
assert len(result["bindings"]) == 1
120+
121+
binding = result["bindings"][0]
122+
123+
# v:Outer should be visible (defined outside select())
124+
outer = binding.get("Outer") or binding.get("v:Outer")
125+
assert outer is not None
126+
assert outer.get("@value") == "visible"
127+
128+
# v:Inner should NOT be visible (inside select())
129+
assert binding.get("Inner") is None
130+
assert binding.get("v:Inner") is None
131+
132+
def test_select_generated_json_has_empty_variables_array(self):
133+
"""select() with no variables should generate JSON with empty variables array."""
134+
# Verify the WOQL builder generates correct JSON with empty variables array
135+
query = WOQLQuery().select(WOQLQuery().eq("v:X", WOQLQuery().string("test")))
136+
137+
json_query = query.to_dict()
138+
139+
assert json_query["@type"] == "Select"
140+
assert "variables" in json_query
141+
assert isinstance(json_query["variables"], list)
142+
assert len(json_query["variables"]) == 0
143+
144+
def test_two_localized_blocks_bind_same_variable_name_different_values(self):
145+
"""Two localized blocks can bind same variable name to different values."""
146+
# KEY TEST: Demonstrates select() scope isolation
147+
# Two separate localized blocks use the same internal variable name (v:temp)
148+
# but bind it to different values - proving they don't interfere
149+
150+
localized1, v1 = WOQLQuery().localize({
151+
"result": "v:result1",
152+
"temp": None, # local variable - same name in both blocks
153+
})
154+
155+
localized2, v2 = WOQLQuery().localize({
156+
"result": "v:result2",
157+
"temp": None, # local variable - same name, different scope
158+
})
159+
160+
query = WOQLQuery().woql_and(
161+
# First localized block: temp = "first", result1 = temp
162+
localized1(
163+
WOQLQuery().woql_and(
164+
WOQLQuery().eq(v1.temp, WOQLQuery().string("first")),
165+
WOQLQuery().eq(v1.result, v1.temp),
166+
)
167+
),
168+
# Second localized block: temp = "second", result2 = temp
169+
localized2(
170+
WOQLQuery().woql_and(
171+
WOQLQuery().eq(v2.temp, WOQLQuery().string("second")),
172+
WOQLQuery().eq(v2.result, v2.temp),
173+
)
174+
),
175+
)
176+
177+
result = self.client.query(query)
178+
179+
assert result is not None
180+
assert "bindings" in result
181+
assert len(result["bindings"]) == 1
182+
183+
binding = result["bindings"][0]
184+
185+
# v:result1 should be "first" (from first localized block)
186+
result1 = binding.get("result1") or binding.get("v:result1")
187+
assert result1 is not None
188+
assert result1.get("@value") == "first"
189+
190+
# v:result2 should be "second" (from second localized block)
191+
result2 = binding.get("result2") or binding.get("v:result2")
192+
assert result2 is not None
193+
assert result2.get("@value") == "second"
194+
195+
# The temp variables should NOT be visible (they're local)
196+
assert binding.get("temp") is None
197+
assert binding.get("v:temp") is None
198+
199+
def test_localize_pattern_hides_local_exposes_outer(self):
200+
"""localize pattern: select() hides local variables, eq() exposes outer params."""
201+
# This is the real use case: localize() pattern
202+
# - Outer parameters are bound via eq() OUTSIDE select()
203+
# - Local variables are hidden by select()
204+
205+
# Simulate what localize() should do with select() (not select(''))
206+
outer_param = "v:param_unique_123"
207+
local_var = "v:local_unique_456"
208+
209+
query = WOQLQuery().woql_and(
210+
# Outer eq() bindings - these should be visible
211+
WOQLQuery().eq("v:MyParam", outer_param),
212+
# select() with no args - hides everything inside
213+
WOQLQuery().select(
214+
WOQLQuery().woql_and(
215+
# Bind the unique param to a value
216+
WOQLQuery().eq(outer_param, WOQLQuery().string("param_value")),
217+
# Use a local variable (should be hidden)
218+
WOQLQuery().eq(local_var, WOQLQuery().string("local_value")),
219+
)
220+
),
221+
)
222+
223+
result = self.client.query(query)
224+
225+
assert result is not None
226+
assert "bindings" in result
227+
assert len(result["bindings"]) == 1
228+
229+
binding = result["bindings"][0]
230+
231+
# v:MyParam should be visible (eq() is outside select())
232+
# and unified with outer_param which was bound inside
233+
my_param = binding.get("MyParam") or binding.get("v:MyParam")
234+
assert my_param is not None
235+
assert my_param.get("@value") == "param_value"

0 commit comments

Comments
 (0)