Skip to content

Commit d6cd65a

Browse files
authored
Merge pull request #465 from terminusdb/select-with-no-variables
Select with no variables
2 parents e57a809 + 190a14e commit d6cd65a

File tree

1 file changed

+239
-0
lines changed

1 file changed

+239
-0
lines changed
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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+
9+
import pytest
10+
11+
from terminusdb_client import Client
12+
from terminusdb_client.woqlquery.woql_query import WOQLQuery
13+
14+
15+
@pytest.fixture(scope="module")
16+
def select_empty_db(docker_url):
17+
"""Create a client and test database for select empty tests."""
18+
client = Client(docker_url)
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(WOQLQuery().eq("v:X", WOQLQuery().string("test")))
54+
55+
result = self.client.query(query)
56+
57+
assert result is not None
58+
assert "bindings" in result
59+
assert len(result["bindings"]) == 1
60+
61+
# The binding should be empty since select() hides all variables
62+
binding = result["bindings"][0]
63+
assert len(binding) == 0
64+
65+
def test_select_hides_inner_variables_outer_visible(self):
66+
"""select() should hide inner variables while outer variables remain visible."""
67+
# Pattern: outer variable is visible, inner variables are hidden by select()
68+
query = WOQLQuery().woql_and(
69+
WOQLQuery().eq("v:Outer", WOQLQuery().string("visible")),
70+
WOQLQuery().select(
71+
WOQLQuery().woql_and(
72+
WOQLQuery().eq("v:Inner", WOQLQuery().string("hidden")),
73+
# Use both variables to ensure query executes
74+
WOQLQuery().eq("v:Both", "v:Inner"),
75+
)
76+
),
77+
)
78+
79+
result = self.client.query(query)
80+
81+
assert result is not None
82+
assert "bindings" in result
83+
assert len(result["bindings"]) == 1
84+
85+
binding = result["bindings"][0]
86+
87+
# v:Outer should be visible (defined outside select())
88+
outer = binding.get("Outer") or binding.get("v:Outer")
89+
assert outer is not None
90+
assert outer.get("@value") == "visible"
91+
92+
# v:Inner and v:Both should NOT be visible (inside select())
93+
assert binding.get("Inner") is None
94+
assert binding.get("v:Inner") is None
95+
assert binding.get("Both") is None
96+
assert binding.get("v:Both") is None
97+
98+
def test_select_allows_outer_binding_through_unification(self):
99+
"""select() should hide inner variables but allow outer variables to be bound."""
100+
# Pattern: outer variable is visible and gets value through unification
101+
query = WOQLQuery().woql_and(
102+
WOQLQuery().eq("v:Outer", "v:Outer"),
103+
WOQLQuery().select(
104+
WOQLQuery().woql_and(
105+
WOQLQuery().eq("v:Inner", WOQLQuery().string("hidden")),
106+
WOQLQuery().eq(
107+
"v:Inner_Shared_Value", WOQLQuery().string("visible")
108+
),
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+
{
152+
"result": "v:result1",
153+
"temp": None, # local variable - same name in both blocks
154+
}
155+
)
156+
157+
localized2, v2 = WOQLQuery().localize(
158+
{
159+
"result": "v:result2",
160+
"temp": None, # local variable - same name, different scope
161+
}
162+
)
163+
164+
query = WOQLQuery().woql_and(
165+
# First localized block: temp = "first", result1 = temp
166+
localized1(
167+
WOQLQuery().woql_and(
168+
WOQLQuery().eq(v1.temp, WOQLQuery().string("first")),
169+
WOQLQuery().eq(v1.result, v1.temp),
170+
)
171+
),
172+
# Second localized block: temp = "second", result2 = temp
173+
localized2(
174+
WOQLQuery().woql_and(
175+
WOQLQuery().eq(v2.temp, WOQLQuery().string("second")),
176+
WOQLQuery().eq(v2.result, v2.temp),
177+
)
178+
),
179+
)
180+
181+
result = self.client.query(query)
182+
183+
assert result is not None
184+
assert "bindings" in result
185+
assert len(result["bindings"]) == 1
186+
187+
binding = result["bindings"][0]
188+
189+
# v:result1 should be "first" (from first localized block)
190+
result1 = binding.get("result1") or binding.get("v:result1")
191+
assert result1 is not None
192+
assert result1.get("@value") == "first"
193+
194+
# v:result2 should be "second" (from second localized block)
195+
result2 = binding.get("result2") or binding.get("v:result2")
196+
assert result2 is not None
197+
assert result2.get("@value") == "second"
198+
199+
# The temp variables should NOT be visible (they're local)
200+
assert binding.get("temp") is None
201+
assert binding.get("v:temp") is None
202+
203+
def test_localize_pattern_hides_local_exposes_outer(self):
204+
"""localize pattern: select() hides local variables, eq() exposes outer params."""
205+
# This is the real use case: localize() pattern
206+
# - Outer parameters are bound via eq() OUTSIDE select()
207+
# - Local variables are hidden by select()
208+
209+
# Simulate what localize() should do with select() (not select(''))
210+
outer_param = "v:param_unique_123"
211+
local_var = "v:local_unique_456"
212+
213+
query = WOQLQuery().woql_and(
214+
# Outer eq() bindings - these should be visible
215+
WOQLQuery().eq("v:MyParam", outer_param),
216+
# select() with no args - hides everything inside
217+
WOQLQuery().select(
218+
WOQLQuery().woql_and(
219+
# Bind the unique param to a value
220+
WOQLQuery().eq(outer_param, WOQLQuery().string("param_value")),
221+
# Use a local variable (should be hidden)
222+
WOQLQuery().eq(local_var, WOQLQuery().string("local_value")),
223+
)
224+
),
225+
)
226+
227+
result = self.client.query(query)
228+
229+
assert result is not None
230+
assert "bindings" in result
231+
assert len(result["bindings"]) == 1
232+
233+
binding = result["bindings"][0]
234+
235+
# v:MyParam should be visible (eq() is outside select())
236+
# and unified with outer_param which was bound inside
237+
my_param = binding.get("MyParam") or binding.get("v:MyParam")
238+
assert my_param is not None
239+
assert my_param.get("@value") == "param_value"

0 commit comments

Comments
 (0)