flowquery 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +5 -1
- package/dist/graph/relationship.js.map +1 -1
- package/dist/parsing/base_parser.d.ts +1 -1
- package/dist/parsing/base_parser.d.ts.map +1 -1
- package/dist/parsing/base_parser.js.map +1 -1
- package/dist/parsing/expressions/operator.d.ts +37 -1
- package/dist/parsing/expressions/operator.d.ts.map +1 -1
- package/dist/parsing/expressions/operator.js +121 -2
- package/dist/parsing/expressions/operator.js.map +1 -1
- package/dist/parsing/expressions/reference.d.ts +1 -0
- package/dist/parsing/expressions/reference.d.ts.map +1 -1
- package/dist/parsing/expressions/reference.js +3 -0
- package/dist/parsing/expressions/reference.js.map +1 -1
- package/dist/parsing/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/string_distance.d.ts +7 -0
- package/dist/parsing/functions/string_distance.d.ts.map +1 -0
- package/dist/parsing/functions/string_distance.js +84 -0
- package/dist/parsing/functions/string_distance.js.map +1 -0
- package/dist/parsing/parser.d.ts +6 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +127 -15
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/keyword.d.ts +4 -1
- package/dist/tokenization/keyword.d.ts.map +1 -1
- package/dist/tokenization/keyword.js +3 -0
- package/dist/tokenization/keyword.js.map +1 -1
- package/dist/tokenization/token.d.ts +6 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +18 -0
- package/dist/tokenization/token.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/relationship.py +5 -1
- package/flowquery-py/src/parsing/expressions/__init__.py +4 -0
- package/flowquery-py/src/parsing/expressions/operator.py +102 -0
- package/flowquery-py/src/parsing/functions/__init__.py +2 -0
- package/flowquery-py/src/parsing/functions/string_distance.py +88 -0
- package/flowquery-py/src/parsing/parser.py +120 -10
- package/flowquery-py/src/tokenization/keyword.py +3 -0
- package/flowquery-py/src/tokenization/token.py +21 -0
- package/flowquery-py/tests/compute/test_runner.py +406 -1
- package/flowquery-py/tests/parsing/test_expression.py +121 -1
- package/flowquery-py/tests/parsing/test_parser.py +203 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/relationship.ts +4 -1
- package/src/parsing/base_parser.ts +1 -1
- package/src/parsing/expressions/operator.ts +129 -1
- package/src/parsing/expressions/reference.ts +8 -5
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/string_distance.ts +80 -0
- package/src/parsing/parser.ts +138 -14
- package/src/tokenization/keyword.ts +3 -0
- package/src/tokenization/token.ts +24 -0
- package/tests/compute/runner.test.ts +379 -0
- package/tests/parsing/expression.test.ts +150 -16
- package/tests/parsing/parser.test.ts +200 -0
|
@@ -418,6 +418,42 @@ class TestRunner:
|
|
|
418
418
|
assert len(results) == 1
|
|
419
419
|
assert results[0] == {"replace": "hexxo"}
|
|
420
420
|
|
|
421
|
+
@pytest.mark.asyncio
|
|
422
|
+
async def test_string_distance_function(self):
|
|
423
|
+
"""Test string_distance function."""
|
|
424
|
+
runner = Runner('RETURN string_distance("kitten", "sitting") as dist')
|
|
425
|
+
await runner.run()
|
|
426
|
+
results = runner.results
|
|
427
|
+
assert len(results) == 1
|
|
428
|
+
assert results[0]["dist"] == pytest.approx(3 / 7)
|
|
429
|
+
|
|
430
|
+
@pytest.mark.asyncio
|
|
431
|
+
async def test_string_distance_identical_strings(self):
|
|
432
|
+
"""Test string_distance function with identical strings."""
|
|
433
|
+
runner = Runner('RETURN string_distance("hello", "hello") as dist')
|
|
434
|
+
await runner.run()
|
|
435
|
+
results = runner.results
|
|
436
|
+
assert len(results) == 1
|
|
437
|
+
assert results[0] == {"dist": 0}
|
|
438
|
+
|
|
439
|
+
@pytest.mark.asyncio
|
|
440
|
+
async def test_string_distance_empty_string(self):
|
|
441
|
+
"""Test string_distance function with empty string."""
|
|
442
|
+
runner = Runner('RETURN string_distance("", "abc") as dist')
|
|
443
|
+
await runner.run()
|
|
444
|
+
results = runner.results
|
|
445
|
+
assert len(results) == 1
|
|
446
|
+
assert results[0] == {"dist": 1}
|
|
447
|
+
|
|
448
|
+
@pytest.mark.asyncio
|
|
449
|
+
async def test_string_distance_both_empty(self):
|
|
450
|
+
"""Test string_distance function with both empty strings."""
|
|
451
|
+
runner = Runner('RETURN string_distance("", "") as dist')
|
|
452
|
+
await runner.run()
|
|
453
|
+
results = runner.results
|
|
454
|
+
assert len(results) == 1
|
|
455
|
+
assert results[0] == {"dist": 0}
|
|
456
|
+
|
|
421
457
|
@pytest.mark.asyncio
|
|
422
458
|
async def test_f_string_with_escaped_braces(self):
|
|
423
459
|
"""Test f-string with escaped braces."""
|
|
@@ -1138,6 +1174,99 @@ class TestRunner:
|
|
|
1138
1174
|
with pytest.raises(ValueError, match="Circular relationship detected"):
|
|
1139
1175
|
await match.run()
|
|
1140
1176
|
|
|
1177
|
+
@pytest.mark.asyncio
|
|
1178
|
+
async def test_multi_hop_match_with_min_hops_constraint_1(self):
|
|
1179
|
+
"""Test multi-hop match with min hops constraint *1.."""
|
|
1180
|
+
await Runner(
|
|
1181
|
+
"""
|
|
1182
|
+
CREATE VIRTUAL (:MinHop1Person) AS {
|
|
1183
|
+
unwind [
|
|
1184
|
+
{id: 1, name: 'Person 1'},
|
|
1185
|
+
{id: 2, name: 'Person 2'},
|
|
1186
|
+
{id: 3, name: 'Person 3'},
|
|
1187
|
+
{id: 4, name: 'Person 4'}
|
|
1188
|
+
] as record
|
|
1189
|
+
RETURN record.id as id, record.name as name
|
|
1190
|
+
}
|
|
1191
|
+
"""
|
|
1192
|
+
).run()
|
|
1193
|
+
await Runner(
|
|
1194
|
+
"""
|
|
1195
|
+
CREATE VIRTUAL (:MinHop1Person)-[:KNOWS]-(:MinHop1Person) AS {
|
|
1196
|
+
unwind [
|
|
1197
|
+
{left_id: 1, right_id: 2},
|
|
1198
|
+
{left_id: 2, right_id: 3},
|
|
1199
|
+
{left_id: 3, right_id: 4}
|
|
1200
|
+
] as record
|
|
1201
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1202
|
+
}
|
|
1203
|
+
"""
|
|
1204
|
+
).run()
|
|
1205
|
+
match = Runner(
|
|
1206
|
+
"""
|
|
1207
|
+
MATCH (a:MinHop1Person)-[:KNOWS*1..]->(b:MinHop1Person)
|
|
1208
|
+
RETURN a.name AS name1, b.name AS name2
|
|
1209
|
+
"""
|
|
1210
|
+
)
|
|
1211
|
+
await match.run()
|
|
1212
|
+
results = match.results
|
|
1213
|
+
# *1.. means at least 1 hop, so no zero-hop (self) matches
|
|
1214
|
+
# Person 1: 1-hop to P2, 2-hop to P3, 3-hop to P4
|
|
1215
|
+
# Person 2: 1-hop to P3, 2-hop to P4
|
|
1216
|
+
# Person 3: 1-hop to P4
|
|
1217
|
+
# Person 4: no outgoing edges
|
|
1218
|
+
assert len(results) == 6
|
|
1219
|
+
assert results[0] == {"name1": "Person 1", "name2": "Person 2"}
|
|
1220
|
+
assert results[1] == {"name1": "Person 1", "name2": "Person 3"}
|
|
1221
|
+
assert results[2] == {"name1": "Person 1", "name2": "Person 4"}
|
|
1222
|
+
assert results[3] == {"name1": "Person 2", "name2": "Person 3"}
|
|
1223
|
+
assert results[4] == {"name1": "Person 2", "name2": "Person 4"}
|
|
1224
|
+
assert results[5] == {"name1": "Person 3", "name2": "Person 4"}
|
|
1225
|
+
|
|
1226
|
+
@pytest.mark.asyncio
|
|
1227
|
+
async def test_multi_hop_match_with_min_hops_constraint_2(self):
|
|
1228
|
+
"""Test multi-hop match with min hops constraint *2.."""
|
|
1229
|
+
await Runner(
|
|
1230
|
+
"""
|
|
1231
|
+
CREATE VIRTUAL (:MinHop2Person) AS {
|
|
1232
|
+
unwind [
|
|
1233
|
+
{id: 1, name: 'Person 1'},
|
|
1234
|
+
{id: 2, name: 'Person 2'},
|
|
1235
|
+
{id: 3, name: 'Person 3'},
|
|
1236
|
+
{id: 4, name: 'Person 4'}
|
|
1237
|
+
] as record
|
|
1238
|
+
RETURN record.id as id, record.name as name
|
|
1239
|
+
}
|
|
1240
|
+
"""
|
|
1241
|
+
).run()
|
|
1242
|
+
await Runner(
|
|
1243
|
+
"""
|
|
1244
|
+
CREATE VIRTUAL (:MinHop2Person)-[:KNOWS]-(:MinHop2Person) AS {
|
|
1245
|
+
unwind [
|
|
1246
|
+
{left_id: 1, right_id: 2},
|
|
1247
|
+
{left_id: 2, right_id: 3},
|
|
1248
|
+
{left_id: 3, right_id: 4}
|
|
1249
|
+
] as record
|
|
1250
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1251
|
+
}
|
|
1252
|
+
"""
|
|
1253
|
+
).run()
|
|
1254
|
+
match = Runner(
|
|
1255
|
+
"""
|
|
1256
|
+
MATCH (a:MinHop2Person)-[:KNOWS*2..]->(b:MinHop2Person)
|
|
1257
|
+
RETURN a.name AS name1, b.name AS name2
|
|
1258
|
+
"""
|
|
1259
|
+
)
|
|
1260
|
+
await match.run()
|
|
1261
|
+
results = match.results
|
|
1262
|
+
# *2.. means at least 2 hops
|
|
1263
|
+
# Person 1: 2-hop to P3, 3-hop to P4
|
|
1264
|
+
# Person 2: 2-hop to P4
|
|
1265
|
+
assert len(results) == 3
|
|
1266
|
+
assert results[0] == {"name1": "Person 1", "name2": "Person 3"}
|
|
1267
|
+
assert results[1] == {"name1": "Person 1", "name2": "Person 4"}
|
|
1268
|
+
assert results[2] == {"name1": "Person 2", "name2": "Person 4"}
|
|
1269
|
+
|
|
1141
1270
|
@pytest.mark.asyncio
|
|
1142
1271
|
async def test_multi_hop_match_with_variable_length_relationships(self):
|
|
1143
1272
|
"""Test multi-hop match with variable length relationships."""
|
|
@@ -1669,4 +1798,280 @@ class TestRunner:
|
|
|
1669
1798
|
await runner.run()
|
|
1670
1799
|
results = runner.results
|
|
1671
1800
|
assert len(results) == 1
|
|
1672
|
-
assert results[0] == {"name1": "Node 1", "name2": "Node 2"}
|
|
1801
|
+
assert results[0] == {"name1": "Node 1", "name2": "Node 2"}
|
|
1802
|
+
|
|
1803
|
+
@pytest.mark.asyncio
|
|
1804
|
+
async def test_match_with_node_reference_passed_through_with(self):
|
|
1805
|
+
"""Test that node variables passed through WITH can be re-referenced in subsequent MATCH."""
|
|
1806
|
+
await Runner("""
|
|
1807
|
+
CREATE VIRTUAL (:WithRefUser) AS {
|
|
1808
|
+
UNWIND [
|
|
1809
|
+
{id: 1, name: 'Alice', mail: 'alice@test.com', jobTitle: 'CEO'},
|
|
1810
|
+
{id: 2, name: 'Bob', mail: 'bob@test.com', jobTitle: 'VP'},
|
|
1811
|
+
{id: 3, name: 'Carol', mail: 'carol@test.com', jobTitle: 'VP'},
|
|
1812
|
+
{id: 4, name: 'Dave', mail: 'dave@test.com', jobTitle: 'Engineer'}
|
|
1813
|
+
] AS record
|
|
1814
|
+
RETURN record.id AS id, record.name AS name, record.mail AS mail, record.jobTitle AS jobTitle
|
|
1815
|
+
}
|
|
1816
|
+
""").run()
|
|
1817
|
+
await Runner("""
|
|
1818
|
+
CREATE VIRTUAL (:WithRefUser)-[:MANAGES]-(:WithRefUser) AS {
|
|
1819
|
+
UNWIND [
|
|
1820
|
+
{left_id: 1, right_id: 2},
|
|
1821
|
+
{left_id: 1, right_id: 3},
|
|
1822
|
+
{left_id: 2, right_id: 4}
|
|
1823
|
+
] AS record
|
|
1824
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
1825
|
+
}
|
|
1826
|
+
""").run()
|
|
1827
|
+
runner = Runner("""
|
|
1828
|
+
MATCH (ceo:WithRefUser)-[:MANAGES]->(dr1:WithRefUser)
|
|
1829
|
+
WHERE ceo.jobTitle = 'CEO'
|
|
1830
|
+
WITH ceo, dr1
|
|
1831
|
+
MATCH (ceo)-[:MANAGES]->(dr2:WithRefUser)
|
|
1832
|
+
WHERE dr1.mail <> dr2.mail
|
|
1833
|
+
RETURN ceo.name AS ceo, dr1.name AS dr1, dr2.name AS dr2
|
|
1834
|
+
""")
|
|
1835
|
+
await runner.run()
|
|
1836
|
+
results = runner.results
|
|
1837
|
+
# CEO (Alice) manages Bob and Carol. All distinct pairs:
|
|
1838
|
+
# (Alice, Bob, Carol) and (Alice, Carol, Bob)
|
|
1839
|
+
assert len(results) == 2
|
|
1840
|
+
assert results[0] == {"ceo": "Alice", "dr1": "Bob", "dr2": "Carol"}
|
|
1841
|
+
assert results[1] == {"ceo": "Alice", "dr1": "Carol", "dr2": "Bob"}
|
|
1842
|
+
|
|
1843
|
+
async def test_match_with_node_reference_reuse_with_label(self):
|
|
1844
|
+
"""Test that reusing a node variable with a label creates a NodeReference, not a new node."""
|
|
1845
|
+
await Runner("""
|
|
1846
|
+
CREATE VIRTUAL (:RefLabelUser) AS {
|
|
1847
|
+
UNWIND [
|
|
1848
|
+
{id: 1, name: 'Alice', jobTitle: 'CEO'},
|
|
1849
|
+
{id: 2, name: 'Bob', jobTitle: 'VP'},
|
|
1850
|
+
{id: 3, name: 'Carol', jobTitle: 'VP'},
|
|
1851
|
+
{id: 4, name: 'Dave', jobTitle: 'Engineer'}
|
|
1852
|
+
] AS record
|
|
1853
|
+
RETURN record.id AS id, record.name AS name, record.jobTitle AS jobTitle
|
|
1854
|
+
}
|
|
1855
|
+
""").run()
|
|
1856
|
+
await Runner("""
|
|
1857
|
+
CREATE VIRTUAL (:RefLabelUser)-[:MANAGES]-(:RefLabelUser) AS {
|
|
1858
|
+
UNWIND [
|
|
1859
|
+
{left_id: 1, right_id: 2},
|
|
1860
|
+
{left_id: 1, right_id: 3},
|
|
1861
|
+
{left_id: 2, right_id: 4}
|
|
1862
|
+
] AS record
|
|
1863
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
1864
|
+
}
|
|
1865
|
+
""").run()
|
|
1866
|
+
# Uses (ceo:RefLabelUser) with label in both MATCH clauses.
|
|
1867
|
+
# Previously this would create a new node instead of a NodeReference.
|
|
1868
|
+
runner = Runner("""
|
|
1869
|
+
MATCH (ceo:RefLabelUser)-[:MANAGES]->(dr1:RefLabelUser)
|
|
1870
|
+
WHERE ceo.jobTitle = 'CEO'
|
|
1871
|
+
WITH ceo, dr1
|
|
1872
|
+
MATCH (ceo:RefLabelUser)-[:MANAGES]->(dr2:RefLabelUser)
|
|
1873
|
+
WHERE dr1.name <> dr2.name
|
|
1874
|
+
RETURN ceo.name AS ceo, dr1.name AS dr1, dr2.name AS dr2
|
|
1875
|
+
""")
|
|
1876
|
+
await runner.run()
|
|
1877
|
+
results = runner.results
|
|
1878
|
+
assert len(results) == 2
|
|
1879
|
+
assert results[0] == {"ceo": "Alice", "dr1": "Bob", "dr2": "Carol"}
|
|
1880
|
+
assert results[1] == {"ceo": "Alice", "dr1": "Carol", "dr2": "Bob"}
|
|
1881
|
+
|
|
1882
|
+
@pytest.mark.asyncio
|
|
1883
|
+
async def test_where_with_is_null(self):
|
|
1884
|
+
"""Test WHERE with IS NULL."""
|
|
1885
|
+
runner = Runner("""
|
|
1886
|
+
unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: null}] as person
|
|
1887
|
+
with person.name as name, person.age as age
|
|
1888
|
+
where age IS NULL
|
|
1889
|
+
return name
|
|
1890
|
+
""")
|
|
1891
|
+
await runner.run()
|
|
1892
|
+
results = runner.results
|
|
1893
|
+
assert len(results) == 1
|
|
1894
|
+
assert results[0] == {"name": "Bob"}
|
|
1895
|
+
|
|
1896
|
+
@pytest.mark.asyncio
|
|
1897
|
+
async def test_where_with_is_not_null(self):
|
|
1898
|
+
"""Test WHERE with IS NOT NULL."""
|
|
1899
|
+
runner = Runner("""
|
|
1900
|
+
unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: null}] as person
|
|
1901
|
+
with person.name as name, person.age as age
|
|
1902
|
+
where age IS NOT NULL
|
|
1903
|
+
return name, age
|
|
1904
|
+
""")
|
|
1905
|
+
await runner.run()
|
|
1906
|
+
results = runner.results
|
|
1907
|
+
assert len(results) == 1
|
|
1908
|
+
assert results[0] == {"name": "Alice", "age": 30}
|
|
1909
|
+
|
|
1910
|
+
@pytest.mark.asyncio
|
|
1911
|
+
async def test_where_with_is_not_null_multiple_results(self):
|
|
1912
|
+
"""Test WHERE with IS NOT NULL filters multiple results."""
|
|
1913
|
+
runner = Runner("""
|
|
1914
|
+
unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: null}, {name: 'Carol', age: 25}] as person
|
|
1915
|
+
with person.name as name, person.age as age
|
|
1916
|
+
where age IS NOT NULL
|
|
1917
|
+
return name, age
|
|
1918
|
+
""")
|
|
1919
|
+
await runner.run()
|
|
1920
|
+
results = runner.results
|
|
1921
|
+
assert len(results) == 2
|
|
1922
|
+
assert results[0] == {"name": "Alice", "age": 30}
|
|
1923
|
+
assert results[1] == {"name": "Carol", "age": 25}
|
|
1924
|
+
|
|
1925
|
+
@pytest.mark.asyncio
|
|
1926
|
+
async def test_where_with_in_list_check(self):
|
|
1927
|
+
"""Test WHERE with IN list check."""
|
|
1928
|
+
runner = Runner("""
|
|
1929
|
+
unwind range(1, 10) as n
|
|
1930
|
+
with n
|
|
1931
|
+
where n IN [2, 4, 6, 8]
|
|
1932
|
+
return n
|
|
1933
|
+
""")
|
|
1934
|
+
await runner.run()
|
|
1935
|
+
results = runner.results
|
|
1936
|
+
assert len(results) == 4
|
|
1937
|
+
assert [r["n"] for r in results] == [2, 4, 6, 8]
|
|
1938
|
+
|
|
1939
|
+
@pytest.mark.asyncio
|
|
1940
|
+
async def test_where_with_not_in_list_check(self):
|
|
1941
|
+
"""Test WHERE with NOT IN list check."""
|
|
1942
|
+
runner = Runner("""
|
|
1943
|
+
unwind range(1, 5) as n
|
|
1944
|
+
with n
|
|
1945
|
+
where n NOT IN [2, 4]
|
|
1946
|
+
return n
|
|
1947
|
+
""")
|
|
1948
|
+
await runner.run()
|
|
1949
|
+
results = runner.results
|
|
1950
|
+
assert len(results) == 3
|
|
1951
|
+
assert [r["n"] for r in results] == [1, 3, 5]
|
|
1952
|
+
|
|
1953
|
+
@pytest.mark.asyncio
|
|
1954
|
+
async def test_where_with_in_string_list(self):
|
|
1955
|
+
"""Test WHERE with IN string list."""
|
|
1956
|
+
runner = Runner("""
|
|
1957
|
+
unwind ['apple', 'banana', 'cherry', 'date'] as fruit
|
|
1958
|
+
with fruit
|
|
1959
|
+
where fruit IN ['banana', 'date']
|
|
1960
|
+
return fruit
|
|
1961
|
+
""")
|
|
1962
|
+
await runner.run()
|
|
1963
|
+
results = runner.results
|
|
1964
|
+
assert len(results) == 2
|
|
1965
|
+
assert [r["fruit"] for r in results] == ["banana", "date"]
|
|
1966
|
+
|
|
1967
|
+
@pytest.mark.asyncio
|
|
1968
|
+
async def test_where_with_in_combined_with_and(self):
|
|
1969
|
+
"""Test WHERE with IN combined with AND."""
|
|
1970
|
+
runner = Runner("""
|
|
1971
|
+
unwind range(1, 20) as n
|
|
1972
|
+
with n
|
|
1973
|
+
where n IN [1, 5, 10, 15, 20] AND n > 5
|
|
1974
|
+
return n
|
|
1975
|
+
""")
|
|
1976
|
+
await runner.run()
|
|
1977
|
+
results = runner.results
|
|
1978
|
+
assert len(results) == 3
|
|
1979
|
+
assert [r["n"] for r in results] == [10, 15, 20]
|
|
1980
|
+
|
|
1981
|
+
@pytest.mark.asyncio
|
|
1982
|
+
async def test_where_with_contains(self):
|
|
1983
|
+
"""Test WHERE with CONTAINS."""
|
|
1984
|
+
runner = Runner("""
|
|
1985
|
+
unwind ['apple', 'banana', 'grape', 'pineapple'] as fruit
|
|
1986
|
+
with fruit
|
|
1987
|
+
where fruit CONTAINS 'apple'
|
|
1988
|
+
return fruit
|
|
1989
|
+
""")
|
|
1990
|
+
await runner.run()
|
|
1991
|
+
results = runner.results
|
|
1992
|
+
assert len(results) == 2
|
|
1993
|
+
assert [r["fruit"] for r in results] == ["apple", "pineapple"]
|
|
1994
|
+
|
|
1995
|
+
@pytest.mark.asyncio
|
|
1996
|
+
async def test_where_with_not_contains(self):
|
|
1997
|
+
"""Test WHERE with NOT CONTAINS."""
|
|
1998
|
+
runner = Runner("""
|
|
1999
|
+
unwind ['apple', 'banana', 'grape', 'pineapple'] as fruit
|
|
2000
|
+
with fruit
|
|
2001
|
+
where fruit NOT CONTAINS 'apple'
|
|
2002
|
+
return fruit
|
|
2003
|
+
""")
|
|
2004
|
+
await runner.run()
|
|
2005
|
+
results = runner.results
|
|
2006
|
+
assert len(results) == 2
|
|
2007
|
+
assert [r["fruit"] for r in results] == ["banana", "grape"]
|
|
2008
|
+
|
|
2009
|
+
@pytest.mark.asyncio
|
|
2010
|
+
async def test_where_with_starts_with(self):
|
|
2011
|
+
"""Test WHERE with STARTS WITH."""
|
|
2012
|
+
runner = Runner("""
|
|
2013
|
+
unwind ['apple', 'apricot', 'banana', 'avocado'] as fruit
|
|
2014
|
+
with fruit
|
|
2015
|
+
where fruit STARTS WITH 'ap'
|
|
2016
|
+
return fruit
|
|
2017
|
+
""")
|
|
2018
|
+
await runner.run()
|
|
2019
|
+
results = runner.results
|
|
2020
|
+
assert len(results) == 2
|
|
2021
|
+
assert [r["fruit"] for r in results] == ["apple", "apricot"]
|
|
2022
|
+
|
|
2023
|
+
@pytest.mark.asyncio
|
|
2024
|
+
async def test_where_with_not_starts_with(self):
|
|
2025
|
+
"""Test WHERE with NOT STARTS WITH."""
|
|
2026
|
+
runner = Runner("""
|
|
2027
|
+
unwind ['apple', 'apricot', 'banana', 'avocado'] as fruit
|
|
2028
|
+
with fruit
|
|
2029
|
+
where fruit NOT STARTS WITH 'ap'
|
|
2030
|
+
return fruit
|
|
2031
|
+
""")
|
|
2032
|
+
await runner.run()
|
|
2033
|
+
results = runner.results
|
|
2034
|
+
assert len(results) == 2
|
|
2035
|
+
assert [r["fruit"] for r in results] == ["banana", "avocado"]
|
|
2036
|
+
|
|
2037
|
+
@pytest.mark.asyncio
|
|
2038
|
+
async def test_where_with_ends_with(self):
|
|
2039
|
+
"""Test WHERE with ENDS WITH."""
|
|
2040
|
+
runner = Runner("""
|
|
2041
|
+
unwind ['apple', 'pineapple', 'banana', 'grape'] as fruit
|
|
2042
|
+
with fruit
|
|
2043
|
+
where fruit ENDS WITH 'ple'
|
|
2044
|
+
return fruit
|
|
2045
|
+
""")
|
|
2046
|
+
await runner.run()
|
|
2047
|
+
results = runner.results
|
|
2048
|
+
assert len(results) == 2
|
|
2049
|
+
assert [r["fruit"] for r in results] == ["apple", "pineapple"]
|
|
2050
|
+
|
|
2051
|
+
@pytest.mark.asyncio
|
|
2052
|
+
async def test_where_with_not_ends_with(self):
|
|
2053
|
+
"""Test WHERE with NOT ENDS WITH."""
|
|
2054
|
+
runner = Runner("""
|
|
2055
|
+
unwind ['apple', 'pineapple', 'banana', 'grape'] as fruit
|
|
2056
|
+
with fruit
|
|
2057
|
+
where fruit NOT ENDS WITH 'ple'
|
|
2058
|
+
return fruit
|
|
2059
|
+
""")
|
|
2060
|
+
await runner.run()
|
|
2061
|
+
results = runner.results
|
|
2062
|
+
assert len(results) == 2
|
|
2063
|
+
assert [r["fruit"] for r in results] == ["banana", "grape"]
|
|
2064
|
+
|
|
2065
|
+
@pytest.mark.asyncio
|
|
2066
|
+
async def test_where_with_contains_combined_with_and(self):
|
|
2067
|
+
"""Test WHERE with CONTAINS combined with AND."""
|
|
2068
|
+
runner = Runner("""
|
|
2069
|
+
unwind ['apple', 'pineapple', 'applesauce', 'banana'] as fruit
|
|
2070
|
+
with fruit
|
|
2071
|
+
where fruit CONTAINS 'apple' AND fruit STARTS WITH 'pine'
|
|
2072
|
+
return fruit
|
|
2073
|
+
""")
|
|
2074
|
+
await runner.run()
|
|
2075
|
+
results = runner.results
|
|
2076
|
+
assert len(results) == 1
|
|
2077
|
+
assert results[0]["fruit"] == "pineapple"
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
from flowquery.parsing.expressions.expression import Expression
|
|
5
5
|
from flowquery.parsing.expressions.operator import (
|
|
6
|
-
Add, Subtract, Multiply, Power, GreaterThan, And
|
|
6
|
+
Add, Subtract, Multiply, Power, GreaterThan, And, Is, IsNot,
|
|
7
|
+
Contains, NotContains, StartsWith, NotStartsWith, EndsWith, NotEndsWith,
|
|
7
8
|
)
|
|
8
9
|
from flowquery.parsing.expressions.number import Number
|
|
10
|
+
from flowquery.parsing.expressions.string import String
|
|
11
|
+
from flowquery.parsing.components.null import Null
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class TestExpression:
|
|
@@ -47,3 +50,120 @@ class TestExpression:
|
|
|
47
50
|
expression.add_node(Number("1"))
|
|
48
51
|
expression.finish()
|
|
49
52
|
assert expression.value() == 1
|
|
53
|
+
|
|
54
|
+
def test_is_null_with_null_value(self):
|
|
55
|
+
"""Test IS NULL with null value."""
|
|
56
|
+
expression = Expression()
|
|
57
|
+
expression.add_node(Null())
|
|
58
|
+
expression.add_node(Is())
|
|
59
|
+
expression.add_node(Null())
|
|
60
|
+
expression.finish()
|
|
61
|
+
assert expression.value() == 1
|
|
62
|
+
|
|
63
|
+
def test_is_null_with_non_null_value(self):
|
|
64
|
+
"""Test IS NULL with non-null value."""
|
|
65
|
+
expression = Expression()
|
|
66
|
+
expression.add_node(Number("42"))
|
|
67
|
+
expression.add_node(Is())
|
|
68
|
+
expression.add_node(Null())
|
|
69
|
+
expression.finish()
|
|
70
|
+
assert expression.value() == 0
|
|
71
|
+
|
|
72
|
+
def test_is_not_null_with_non_null_value(self):
|
|
73
|
+
"""Test IS NOT NULL with non-null value."""
|
|
74
|
+
expression = Expression()
|
|
75
|
+
expression.add_node(Number("42"))
|
|
76
|
+
expression.add_node(IsNot())
|
|
77
|
+
expression.add_node(Null())
|
|
78
|
+
expression.finish()
|
|
79
|
+
assert expression.value() == 1
|
|
80
|
+
|
|
81
|
+
def test_is_not_null_with_null_value(self):
|
|
82
|
+
"""Test IS NOT NULL with null value."""
|
|
83
|
+
expression = Expression()
|
|
84
|
+
expression.add_node(Null())
|
|
85
|
+
expression.add_node(IsNot())
|
|
86
|
+
expression.add_node(Null())
|
|
87
|
+
expression.finish()
|
|
88
|
+
assert expression.value() == 0
|
|
89
|
+
|
|
90
|
+
def test_contains_with_matching_substring(self):
|
|
91
|
+
"""Test CONTAINS with matching substring."""
|
|
92
|
+
expression = Expression()
|
|
93
|
+
expression.add_node(String("pineapple"))
|
|
94
|
+
expression.add_node(Contains())
|
|
95
|
+
expression.add_node(String("apple"))
|
|
96
|
+
expression.finish()
|
|
97
|
+
assert expression.value() == 1
|
|
98
|
+
|
|
99
|
+
def test_contains_with_non_matching_substring(self):
|
|
100
|
+
"""Test CONTAINS with non-matching substring."""
|
|
101
|
+
expression = Expression()
|
|
102
|
+
expression.add_node(String("banana"))
|
|
103
|
+
expression.add_node(Contains())
|
|
104
|
+
expression.add_node(String("apple"))
|
|
105
|
+
expression.finish()
|
|
106
|
+
assert expression.value() == 0
|
|
107
|
+
|
|
108
|
+
def test_not_contains(self):
|
|
109
|
+
"""Test NOT CONTAINS."""
|
|
110
|
+
expression = Expression()
|
|
111
|
+
expression.add_node(String("banana"))
|
|
112
|
+
expression.add_node(NotContains())
|
|
113
|
+
expression.add_node(String("apple"))
|
|
114
|
+
expression.finish()
|
|
115
|
+
assert expression.value() == 1
|
|
116
|
+
|
|
117
|
+
def test_starts_with_matching_prefix(self):
|
|
118
|
+
"""Test STARTS WITH matching prefix."""
|
|
119
|
+
expression = Expression()
|
|
120
|
+
expression.add_node(String("pineapple"))
|
|
121
|
+
expression.add_node(StartsWith())
|
|
122
|
+
expression.add_node(String("pine"))
|
|
123
|
+
expression.finish()
|
|
124
|
+
assert expression.value() == 1
|
|
125
|
+
|
|
126
|
+
def test_starts_with_non_matching_prefix(self):
|
|
127
|
+
"""Test STARTS WITH non-matching prefix."""
|
|
128
|
+
expression = Expression()
|
|
129
|
+
expression.add_node(String("pineapple"))
|
|
130
|
+
expression.add_node(StartsWith())
|
|
131
|
+
expression.add_node(String("apple"))
|
|
132
|
+
expression.finish()
|
|
133
|
+
assert expression.value() == 0
|
|
134
|
+
|
|
135
|
+
def test_not_starts_with(self):
|
|
136
|
+
"""Test NOT STARTS WITH."""
|
|
137
|
+
expression = Expression()
|
|
138
|
+
expression.add_node(String("pineapple"))
|
|
139
|
+
expression.add_node(NotStartsWith())
|
|
140
|
+
expression.add_node(String("apple"))
|
|
141
|
+
expression.finish()
|
|
142
|
+
assert expression.value() == 1
|
|
143
|
+
|
|
144
|
+
def test_ends_with_matching_suffix(self):
|
|
145
|
+
"""Test ENDS WITH matching suffix."""
|
|
146
|
+
expression = Expression()
|
|
147
|
+
expression.add_node(String("pineapple"))
|
|
148
|
+
expression.add_node(EndsWith())
|
|
149
|
+
expression.add_node(String("apple"))
|
|
150
|
+
expression.finish()
|
|
151
|
+
assert expression.value() == 1
|
|
152
|
+
|
|
153
|
+
def test_ends_with_non_matching_suffix(self):
|
|
154
|
+
"""Test ENDS WITH non-matching suffix."""
|
|
155
|
+
expression = Expression()
|
|
156
|
+
expression.add_node(String("pineapple"))
|
|
157
|
+
expression.add_node(EndsWith())
|
|
158
|
+
expression.add_node(String("banana"))
|
|
159
|
+
expression.finish()
|
|
160
|
+
assert expression.value() == 0
|
|
161
|
+
|
|
162
|
+
def test_not_ends_with(self):
|
|
163
|
+
"""Test NOT ENDS WITH."""
|
|
164
|
+
expression = Expression()
|
|
165
|
+
expression.add_node(String("pineapple"))
|
|
166
|
+
expression.add_node(NotEndsWith())
|
|
167
|
+
expression.add_node(String("banana"))
|
|
168
|
+
expression.finish()
|
|
169
|
+
assert expression.value() == 1
|