flowquery 1.0.31 → 1.0.33
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/compute/flowquery.d.ts +43 -0
- package/dist/compute/flowquery.d.ts.map +1 -0
- package/dist/compute/flowquery.js +30 -0
- package/dist/compute/flowquery.js.map +1 -0
- package/dist/compute/runner.d.ts +0 -21
- package/dist/compute/runner.d.ts.map +1 -1
- package/dist/compute/runner.js.map +1 -1
- package/dist/flowquery.min.js +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +10 -10
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.d.ts +4 -4
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +13 -13
- package/dist/index.node.js.map +1 -1
- package/dist/parsing/context.d.ts +1 -0
- package/dist/parsing/context.d.ts.map +1 -1
- package/dist/parsing/context.js +5 -0
- package/dist/parsing/context.js.map +1 -1
- package/dist/parsing/expressions/operator.d.ts +2 -2
- package/dist/parsing/expressions/operator.d.ts.map +1 -1
- package/dist/parsing/expressions/operator.js +6 -1
- package/dist/parsing/expressions/operator.js.map +1 -1
- package/dist/parsing/operations/group_by.d.ts.map +1 -1
- package/dist/parsing/operations/group_by.js +8 -4
- package/dist/parsing/operations/group_by.js.map +1 -1
- package/dist/parsing/operations/match.d.ts +5 -1
- package/dist/parsing/operations/match.d.ts.map +1 -1
- package/dist/parsing/operations/match.js +25 -1
- package/dist/parsing/operations/match.js.map +1 -1
- package/dist/parsing/operations/union.d.ts +36 -0
- package/dist/parsing/operations/union.d.ts.map +1 -0
- package/dist/parsing/operations/union.js +121 -0
- package/dist/parsing/operations/union.js.map +1 -0
- package/dist/parsing/operations/union_all.d.ts +10 -0
- package/dist/parsing/operations/union_all.d.ts.map +1 -0
- package/dist/parsing/operations/union_all.js +17 -0
- package/dist/parsing/operations/union_all.js.map +1 -0
- package/dist/parsing/parser.d.ts +2 -3
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +72 -24
- package/dist/parsing/parser.js.map +1 -1
- package/dist/parsing/parser_state.d.ts +13 -0
- package/dist/parsing/parser_state.d.ts.map +1 -0
- package/dist/parsing/parser_state.js +27 -0
- package/dist/parsing/parser_state.js.map +1 -0
- 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/__init__.py +2 -0
- package/flowquery-py/src/compute/__init__.py +2 -1
- package/flowquery-py/src/compute/flowquery.py +68 -0
- package/flowquery-py/src/graph/node.py +1 -1
- package/flowquery-py/src/parsing/operations/__init__.py +4 -0
- package/flowquery-py/src/parsing/operations/group_by.py +3 -0
- package/flowquery-py/src/parsing/operations/match.py +24 -2
- package/flowquery-py/src/parsing/operations/union.py +115 -0
- package/flowquery-py/src/parsing/operations/union_all.py +17 -0
- package/flowquery-py/src/parsing/parser.py +68 -24
- package/flowquery-py/src/parsing/parser_state.py +26 -0
- 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 +557 -1
- package/flowquery-py/tests/parsing/test_parser.py +82 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/compute/flowquery.ts +46 -0
- package/src/compute/runner.ts +0 -24
- package/src/index.browser.ts +17 -14
- package/src/index.node.ts +21 -18
- package/src/parsing/context.ts +6 -0
- package/src/parsing/expressions/operator.ts +8 -3
- package/src/parsing/operations/group_by.ts +27 -19
- package/src/parsing/operations/match.ts +24 -1
- package/src/parsing/operations/union.ts +114 -0
- package/src/parsing/operations/union_all.ts +16 -0
- package/src/parsing/parser.ts +74 -23
- package/src/parsing/parser_state.ts +25 -0
- package/src/tokenization/keyword.ts +3 -0
- package/src/tokenization/token.ts +24 -0
- package/tests/compute/runner.test.ts +481 -0
- package/tests/parsing/parser.test.ts +76 -0
|
@@ -378,6 +378,21 @@ class TestRunner:
|
|
|
378
378
|
assert results[0] == {"i": 1, "sum": 12}
|
|
379
379
|
assert results[1] == {"i": 2, "sum": 12}
|
|
380
380
|
|
|
381
|
+
@pytest.mark.asyncio
|
|
382
|
+
async def test_aggregated_with_on_empty_result_set(self):
|
|
383
|
+
"""Test aggregated with on empty result set does not crash."""
|
|
384
|
+
runner = Runner(
|
|
385
|
+
"""
|
|
386
|
+
unwind [] as i
|
|
387
|
+
unwind [1, 2] as j
|
|
388
|
+
with i, count(j) as cnt
|
|
389
|
+
return i, cnt
|
|
390
|
+
"""
|
|
391
|
+
)
|
|
392
|
+
await runner.run()
|
|
393
|
+
results = runner.results
|
|
394
|
+
assert len(results) == 0
|
|
395
|
+
|
|
381
396
|
@pytest.mark.asyncio
|
|
382
397
|
async def test_aggregated_with_using_collect_and_return(self):
|
|
383
398
|
"""Test aggregated with using collect and return."""
|
|
@@ -1836,6 +1851,236 @@ class TestRunner:
|
|
|
1836
1851
|
assert len(results) == 1
|
|
1837
1852
|
assert results[0]["name"] == "Employee 1"
|
|
1838
1853
|
|
|
1854
|
+
@pytest.mark.asyncio
|
|
1855
|
+
async def test_optional_match_with_no_matching_relationship(self):
|
|
1856
|
+
"""Test optional match with no matching relationship returns null."""
|
|
1857
|
+
await Runner(
|
|
1858
|
+
"""
|
|
1859
|
+
CREATE VIRTUAL (:OptPerson) AS {
|
|
1860
|
+
unwind [
|
|
1861
|
+
{id: 1, name: 'Person 1'},
|
|
1862
|
+
{id: 2, name: 'Person 2'},
|
|
1863
|
+
{id: 3, name: 'Person 3'}
|
|
1864
|
+
] as record
|
|
1865
|
+
RETURN record.id as id, record.name as name
|
|
1866
|
+
}
|
|
1867
|
+
"""
|
|
1868
|
+
).run()
|
|
1869
|
+
await Runner(
|
|
1870
|
+
"""
|
|
1871
|
+
CREATE VIRTUAL (:OptPerson)-[:KNOWS]-(:OptPerson) AS {
|
|
1872
|
+
unwind [
|
|
1873
|
+
{left_id: 1, right_id: 2}
|
|
1874
|
+
] as record
|
|
1875
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1876
|
+
}
|
|
1877
|
+
"""
|
|
1878
|
+
).run()
|
|
1879
|
+
# Person 3 has no KNOWS relationship, so OPTIONAL MATCH should return null for friend
|
|
1880
|
+
match = Runner(
|
|
1881
|
+
"""
|
|
1882
|
+
MATCH (a:OptPerson)
|
|
1883
|
+
OPTIONAL MATCH (a)-[:KNOWS]->(b:OptPerson)
|
|
1884
|
+
RETURN a.name AS name, b AS friend
|
|
1885
|
+
"""
|
|
1886
|
+
)
|
|
1887
|
+
await match.run()
|
|
1888
|
+
results = match.results
|
|
1889
|
+
assert len(results) == 3
|
|
1890
|
+
assert results[0]["name"] == "Person 1"
|
|
1891
|
+
assert results[0]["friend"] is not None
|
|
1892
|
+
assert results[0]["friend"]["name"] == "Person 2"
|
|
1893
|
+
assert results[1]["name"] == "Person 2"
|
|
1894
|
+
assert results[1]["friend"] is None
|
|
1895
|
+
assert results[2]["name"] == "Person 3"
|
|
1896
|
+
assert results[2]["friend"] is None
|
|
1897
|
+
|
|
1898
|
+
@pytest.mark.asyncio
|
|
1899
|
+
async def test_optional_match_where_all_nodes_match(self):
|
|
1900
|
+
"""Test optional match where all nodes have matching relationships."""
|
|
1901
|
+
await Runner(
|
|
1902
|
+
"""
|
|
1903
|
+
CREATE VIRTUAL (:OptAllPerson) AS {
|
|
1904
|
+
unwind [
|
|
1905
|
+
{id: 1, name: 'Person 1'},
|
|
1906
|
+
{id: 2, name: 'Person 2'}
|
|
1907
|
+
] as record
|
|
1908
|
+
RETURN record.id as id, record.name as name
|
|
1909
|
+
}
|
|
1910
|
+
"""
|
|
1911
|
+
).run()
|
|
1912
|
+
await Runner(
|
|
1913
|
+
"""
|
|
1914
|
+
CREATE VIRTUAL (:OptAllPerson)-[:KNOWS]-(:OptAllPerson) AS {
|
|
1915
|
+
unwind [
|
|
1916
|
+
{left_id: 1, right_id: 2},
|
|
1917
|
+
{left_id: 2, right_id: 1}
|
|
1918
|
+
] as record
|
|
1919
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1920
|
+
}
|
|
1921
|
+
"""
|
|
1922
|
+
).run()
|
|
1923
|
+
# All persons have KNOWS relationships, so no null values
|
|
1924
|
+
match = Runner(
|
|
1925
|
+
"""
|
|
1926
|
+
MATCH (a:OptAllPerson)
|
|
1927
|
+
OPTIONAL MATCH (a)-[:KNOWS]->(b:OptAllPerson)
|
|
1928
|
+
RETURN a.name AS name, b AS friend
|
|
1929
|
+
"""
|
|
1930
|
+
)
|
|
1931
|
+
await match.run()
|
|
1932
|
+
results = match.results
|
|
1933
|
+
assert len(results) == 2
|
|
1934
|
+
assert results[0]["name"] == "Person 1"
|
|
1935
|
+
assert results[0]["friend"]["name"] == "Person 2"
|
|
1936
|
+
assert results[1]["name"] == "Person 2"
|
|
1937
|
+
assert results[1]["friend"]["name"] == "Person 1"
|
|
1938
|
+
|
|
1939
|
+
@pytest.mark.asyncio
|
|
1940
|
+
async def test_optional_match_with_no_data_returns_nulls(self):
|
|
1941
|
+
"""Test optional match with no matching data returns nulls."""
|
|
1942
|
+
await Runner(
|
|
1943
|
+
"""
|
|
1944
|
+
CREATE VIRTUAL (:OptNullPerson) AS {
|
|
1945
|
+
unwind [
|
|
1946
|
+
{id: 1, name: 'Person 1'},
|
|
1947
|
+
{id: 2, name: 'Person 2'}
|
|
1948
|
+
] as record
|
|
1949
|
+
RETURN record.id as id, record.name as name
|
|
1950
|
+
}
|
|
1951
|
+
"""
|
|
1952
|
+
).run()
|
|
1953
|
+
await Runner(
|
|
1954
|
+
"""
|
|
1955
|
+
CREATE VIRTUAL (:OptNullPerson)-[:KNOWS]-(:OptNullPerson) AS {
|
|
1956
|
+
unwind [] as record
|
|
1957
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1958
|
+
}
|
|
1959
|
+
"""
|
|
1960
|
+
).run()
|
|
1961
|
+
# KNOWS relationship type exists but has no data
|
|
1962
|
+
match = Runner(
|
|
1963
|
+
"""
|
|
1964
|
+
MATCH (a:OptNullPerson)
|
|
1965
|
+
OPTIONAL MATCH (a)-[:KNOWS]->(b:OptNullPerson)
|
|
1966
|
+
RETURN a.name AS name, b AS friend
|
|
1967
|
+
"""
|
|
1968
|
+
)
|
|
1969
|
+
await match.run()
|
|
1970
|
+
results = match.results
|
|
1971
|
+
assert len(results) == 2
|
|
1972
|
+
assert results[0]["name"] == "Person 1"
|
|
1973
|
+
assert results[0]["friend"] is None
|
|
1974
|
+
assert results[1]["name"] == "Person 2"
|
|
1975
|
+
assert results[1]["friend"] is None
|
|
1976
|
+
|
|
1977
|
+
@pytest.mark.asyncio
|
|
1978
|
+
async def test_optional_match_with_aggregation(self):
|
|
1979
|
+
"""Test optional match with aggregation (collect friends)."""
|
|
1980
|
+
await Runner(
|
|
1981
|
+
"""
|
|
1982
|
+
CREATE VIRTUAL (:OptAggPerson) AS {
|
|
1983
|
+
unwind [
|
|
1984
|
+
{id: 1, name: 'Person 1'},
|
|
1985
|
+
{id: 2, name: 'Person 2'},
|
|
1986
|
+
{id: 3, name: 'Person 3'}
|
|
1987
|
+
] as record
|
|
1988
|
+
RETURN record.id as id, record.name as name
|
|
1989
|
+
}
|
|
1990
|
+
"""
|
|
1991
|
+
).run()
|
|
1992
|
+
await Runner(
|
|
1993
|
+
"""
|
|
1994
|
+
CREATE VIRTUAL (:OptAggPerson)-[:KNOWS]-(:OptAggPerson) AS {
|
|
1995
|
+
unwind [
|
|
1996
|
+
{left_id: 1, right_id: 2},
|
|
1997
|
+
{left_id: 1, right_id: 3}
|
|
1998
|
+
] as record
|
|
1999
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2000
|
+
}
|
|
2001
|
+
"""
|
|
2002
|
+
).run()
|
|
2003
|
+
# Collect friends per person; Person 2 and 3 have no friends
|
|
2004
|
+
match = Runner(
|
|
2005
|
+
"""
|
|
2006
|
+
MATCH (a:OptAggPerson)
|
|
2007
|
+
OPTIONAL MATCH (a)-[:KNOWS]->(b:OptAggPerson)
|
|
2008
|
+
RETURN a.name AS name, collect(b) AS friends
|
|
2009
|
+
"""
|
|
2010
|
+
)
|
|
2011
|
+
await match.run()
|
|
2012
|
+
results = match.results
|
|
2013
|
+
assert len(results) == 3
|
|
2014
|
+
assert results[0]["name"] == "Person 1"
|
|
2015
|
+
assert len(results[0]["friends"]) == 2
|
|
2016
|
+
assert results[1]["name"] == "Person 2"
|
|
2017
|
+
assert len(results[1]["friends"]) == 1 # null is collected
|
|
2018
|
+
assert results[2]["name"] == "Person 3"
|
|
2019
|
+
assert len(results[2]["friends"]) == 1 # null is collected
|
|
2020
|
+
|
|
2021
|
+
@pytest.mark.asyncio
|
|
2022
|
+
async def test_standalone_optional_match_returns_data(self):
|
|
2023
|
+
"""Test standalone optional match returns data when label exists."""
|
|
2024
|
+
await Runner(
|
|
2025
|
+
"""
|
|
2026
|
+
CREATE VIRTUAL (:OptStandalonePerson) AS {
|
|
2027
|
+
unwind [
|
|
2028
|
+
{id: 1, name: 'Person 1'},
|
|
2029
|
+
{id: 2, name: 'Person 2'}
|
|
2030
|
+
] as record
|
|
2031
|
+
RETURN record.id as id, record.name as name
|
|
2032
|
+
}
|
|
2033
|
+
"""
|
|
2034
|
+
).run()
|
|
2035
|
+
await Runner(
|
|
2036
|
+
"""
|
|
2037
|
+
CREATE VIRTUAL (:OptStandalonePerson)-[:KNOWS]-(:OptStandalonePerson) AS {
|
|
2038
|
+
unwind [
|
|
2039
|
+
{left_id: 1, right_id: 2}
|
|
2040
|
+
] as record
|
|
2041
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2042
|
+
}
|
|
2043
|
+
"""
|
|
2044
|
+
).run()
|
|
2045
|
+
# Standalone OPTIONAL MATCH with relationship where only Person 1 has a match
|
|
2046
|
+
match = Runner(
|
|
2047
|
+
"""
|
|
2048
|
+
OPTIONAL MATCH (a:OptStandalonePerson)-[:KNOWS]->(b:OptStandalonePerson)
|
|
2049
|
+
RETURN a.name AS name, b.name AS friend
|
|
2050
|
+
"""
|
|
2051
|
+
)
|
|
2052
|
+
await match.run()
|
|
2053
|
+
results = match.results
|
|
2054
|
+
assert len(results) == 1
|
|
2055
|
+
assert results[0] == {"name": "Person 1", "friend": "Person 2"}
|
|
2056
|
+
|
|
2057
|
+
@pytest.mark.asyncio
|
|
2058
|
+
async def test_optional_match_returns_full_node_when_matched(self):
|
|
2059
|
+
"""Test optional match on existing label returns actual nodes."""
|
|
2060
|
+
await Runner(
|
|
2061
|
+
"""
|
|
2062
|
+
CREATE VIRTUAL (:OptFullPerson) AS {
|
|
2063
|
+
unwind [
|
|
2064
|
+
{id: 1, name: 'Person 1'},
|
|
2065
|
+
{id: 2, name: 'Person 2'}
|
|
2066
|
+
] as record
|
|
2067
|
+
RETURN record.id as id, record.name as name
|
|
2068
|
+
}
|
|
2069
|
+
"""
|
|
2070
|
+
).run()
|
|
2071
|
+
# OPTIONAL MATCH on existing label returns actual nodes
|
|
2072
|
+
match = Runner(
|
|
2073
|
+
"""
|
|
2074
|
+
OPTIONAL MATCH (n:OptFullPerson)
|
|
2075
|
+
RETURN n.name AS name
|
|
2076
|
+
"""
|
|
2077
|
+
)
|
|
2078
|
+
await match.run()
|
|
2079
|
+
results = match.results
|
|
2080
|
+
assert len(results) == 2
|
|
2081
|
+
assert results[0] == {"name": "Person 1"}
|
|
2082
|
+
assert results[1] == {"name": "Person 2"}
|
|
2083
|
+
|
|
1839
2084
|
@pytest.mark.asyncio
|
|
1840
2085
|
async def test_schema_returns_nodes_and_relationships_with_sample_data(self):
|
|
1841
2086
|
"""Test schema() returns nodes and relationships with sample data."""
|
|
@@ -2253,4 +2498,315 @@ class TestRunner:
|
|
|
2253
2498
|
names = [r["name"] for r in results]
|
|
2254
2499
|
assert "Person 1" in names
|
|
2255
2500
|
assert "Person 2" in names
|
|
2256
|
-
assert "Person 3" in names
|
|
2501
|
+
assert "Person 3" in names
|
|
2502
|
+
|
|
2503
|
+
# ============================================================
|
|
2504
|
+
# Add operator tests
|
|
2505
|
+
# ============================================================
|
|
2506
|
+
|
|
2507
|
+
@pytest.mark.asyncio
|
|
2508
|
+
async def test_add_two_integers(self):
|
|
2509
|
+
"""Test add two integers."""
|
|
2510
|
+
runner = Runner("return 1 + 2 as result")
|
|
2511
|
+
await runner.run()
|
|
2512
|
+
results = runner.results
|
|
2513
|
+
assert len(results) == 1
|
|
2514
|
+
assert results[0] == {"result": 3}
|
|
2515
|
+
|
|
2516
|
+
@pytest.mark.asyncio
|
|
2517
|
+
async def test_add_negative_number(self):
|
|
2518
|
+
"""Test add with a negative number."""
|
|
2519
|
+
runner = Runner("return -3 + 7 as result")
|
|
2520
|
+
await runner.run()
|
|
2521
|
+
results = runner.results
|
|
2522
|
+
assert len(results) == 1
|
|
2523
|
+
assert results[0] == {"result": 4}
|
|
2524
|
+
|
|
2525
|
+
@pytest.mark.asyncio
|
|
2526
|
+
async def test_add_to_negative_result(self):
|
|
2527
|
+
"""Test add to negative result."""
|
|
2528
|
+
runner = Runner("return 0 - 10 + 4 as result")
|
|
2529
|
+
await runner.run()
|
|
2530
|
+
results = runner.results
|
|
2531
|
+
assert len(results) == 1
|
|
2532
|
+
assert results[0] == {"result": -6}
|
|
2533
|
+
|
|
2534
|
+
@pytest.mark.asyncio
|
|
2535
|
+
async def test_add_zero(self):
|
|
2536
|
+
"""Test add zero."""
|
|
2537
|
+
runner = Runner("return 42 + 0 as result")
|
|
2538
|
+
await runner.run()
|
|
2539
|
+
results = runner.results
|
|
2540
|
+
assert len(results) == 1
|
|
2541
|
+
assert results[0] == {"result": 42}
|
|
2542
|
+
|
|
2543
|
+
@pytest.mark.asyncio
|
|
2544
|
+
async def test_add_floating_point_numbers(self):
|
|
2545
|
+
"""Test add floating point numbers."""
|
|
2546
|
+
runner = Runner("return 1.5 + 2.3 as result")
|
|
2547
|
+
await runner.run()
|
|
2548
|
+
results = runner.results
|
|
2549
|
+
assert len(results) == 1
|
|
2550
|
+
assert results[0]["result"] == pytest.approx(3.8)
|
|
2551
|
+
|
|
2552
|
+
@pytest.mark.asyncio
|
|
2553
|
+
async def test_add_integer_and_float(self):
|
|
2554
|
+
"""Test add integer and float."""
|
|
2555
|
+
runner = Runner("return 1 + 0.5 as result")
|
|
2556
|
+
await runner.run()
|
|
2557
|
+
results = runner.results
|
|
2558
|
+
assert len(results) == 1
|
|
2559
|
+
assert results[0]["result"] == pytest.approx(1.5)
|
|
2560
|
+
|
|
2561
|
+
@pytest.mark.asyncio
|
|
2562
|
+
async def test_add_strings(self):
|
|
2563
|
+
"""Test add strings."""
|
|
2564
|
+
runner = Runner('return "hello" + " world" as result')
|
|
2565
|
+
await runner.run()
|
|
2566
|
+
results = runner.results
|
|
2567
|
+
assert len(results) == 1
|
|
2568
|
+
assert results[0] == {"result": "hello world"}
|
|
2569
|
+
|
|
2570
|
+
@pytest.mark.asyncio
|
|
2571
|
+
async def test_add_empty_strings(self):
|
|
2572
|
+
"""Test add empty strings."""
|
|
2573
|
+
runner = Runner('return "" + "" as result')
|
|
2574
|
+
await runner.run()
|
|
2575
|
+
results = runner.results
|
|
2576
|
+
assert len(results) == 1
|
|
2577
|
+
assert results[0] == {"result": ""}
|
|
2578
|
+
|
|
2579
|
+
@pytest.mark.asyncio
|
|
2580
|
+
async def test_add_string_and_empty_string(self):
|
|
2581
|
+
"""Test add string and empty string."""
|
|
2582
|
+
runner = Runner('return "hello" + "" as result')
|
|
2583
|
+
await runner.run()
|
|
2584
|
+
results = runner.results
|
|
2585
|
+
assert len(results) == 1
|
|
2586
|
+
assert results[0] == {"result": "hello"}
|
|
2587
|
+
|
|
2588
|
+
@pytest.mark.asyncio
|
|
2589
|
+
async def test_add_two_lists(self):
|
|
2590
|
+
"""Test add two lists."""
|
|
2591
|
+
runner = Runner("return [1, 2] + [3, 4] as result")
|
|
2592
|
+
await runner.run()
|
|
2593
|
+
results = runner.results
|
|
2594
|
+
assert len(results) == 1
|
|
2595
|
+
assert results[0] == {"result": [1, 2, 3, 4]}
|
|
2596
|
+
|
|
2597
|
+
@pytest.mark.asyncio
|
|
2598
|
+
async def test_add_empty_list_to_list(self):
|
|
2599
|
+
"""Test add empty list to list."""
|
|
2600
|
+
runner = Runner("return [1, 2, 3] + [] as result")
|
|
2601
|
+
await runner.run()
|
|
2602
|
+
results = runner.results
|
|
2603
|
+
assert len(results) == 1
|
|
2604
|
+
assert results[0] == {"result": [1, 2, 3]}
|
|
2605
|
+
|
|
2606
|
+
@pytest.mark.asyncio
|
|
2607
|
+
async def test_add_two_empty_lists(self):
|
|
2608
|
+
"""Test add two empty lists."""
|
|
2609
|
+
runner = Runner("return [] + [] as result")
|
|
2610
|
+
await runner.run()
|
|
2611
|
+
results = runner.results
|
|
2612
|
+
assert len(results) == 1
|
|
2613
|
+
assert results[0] == {"result": []}
|
|
2614
|
+
|
|
2615
|
+
@pytest.mark.asyncio
|
|
2616
|
+
async def test_add_lists_with_mixed_types(self):
|
|
2617
|
+
"""Test add lists with mixed types."""
|
|
2618
|
+
runner = Runner('return [1, "a"] + [2, "b"] as result')
|
|
2619
|
+
await runner.run()
|
|
2620
|
+
results = runner.results
|
|
2621
|
+
assert len(results) == 1
|
|
2622
|
+
assert results[0] == {"result": [1, "a", 2, "b"]}
|
|
2623
|
+
|
|
2624
|
+
@pytest.mark.asyncio
|
|
2625
|
+
async def test_add_chained_three_numbers(self):
|
|
2626
|
+
"""Test add chained three numbers."""
|
|
2627
|
+
runner = Runner("return 1 + 2 + 3 as result")
|
|
2628
|
+
await runner.run()
|
|
2629
|
+
results = runner.results
|
|
2630
|
+
assert len(results) == 1
|
|
2631
|
+
assert results[0] == {"result": 6}
|
|
2632
|
+
|
|
2633
|
+
@pytest.mark.asyncio
|
|
2634
|
+
async def test_add_chained_multiple_numbers(self):
|
|
2635
|
+
"""Test add chained multiple numbers."""
|
|
2636
|
+
runner = Runner("return 10 + 20 + 30 + 40 as result")
|
|
2637
|
+
await runner.run()
|
|
2638
|
+
results = runner.results
|
|
2639
|
+
assert len(results) == 1
|
|
2640
|
+
assert results[0] == {"result": 100}
|
|
2641
|
+
|
|
2642
|
+
@pytest.mark.asyncio
|
|
2643
|
+
async def test_add_large_numbers(self):
|
|
2644
|
+
"""Test add large numbers."""
|
|
2645
|
+
runner = Runner("return 1000000 + 2000000 as result")
|
|
2646
|
+
await runner.run()
|
|
2647
|
+
results = runner.results
|
|
2648
|
+
assert len(results) == 1
|
|
2649
|
+
assert results[0] == {"result": 3000000}
|
|
2650
|
+
|
|
2651
|
+
@pytest.mark.asyncio
|
|
2652
|
+
async def test_add_with_unwind(self):
|
|
2653
|
+
"""Test add with unwind."""
|
|
2654
|
+
runner = Runner("unwind [1, 2, 3] as x return x + 10 as result")
|
|
2655
|
+
await runner.run()
|
|
2656
|
+
results = runner.results
|
|
2657
|
+
assert len(results) == 3
|
|
2658
|
+
assert results[0] == {"result": 11}
|
|
2659
|
+
assert results[1] == {"result": 12}
|
|
2660
|
+
assert results[2] == {"result": 13}
|
|
2661
|
+
|
|
2662
|
+
@pytest.mark.asyncio
|
|
2663
|
+
async def test_add_with_multiple_return_expressions(self):
|
|
2664
|
+
"""Test add with multiple return expressions."""
|
|
2665
|
+
runner = Runner("return 1 + 2 as sum1, 3 + 4 as sum2, 5 + 6 as sum3")
|
|
2666
|
+
await runner.run()
|
|
2667
|
+
results = runner.results
|
|
2668
|
+
assert len(results) == 1
|
|
2669
|
+
assert results[0] == {"sum1": 3, "sum2": 7, "sum3": 11}
|
|
2670
|
+
|
|
2671
|
+
@pytest.mark.asyncio
|
|
2672
|
+
async def test_add_mixed_with_other_operators(self):
|
|
2673
|
+
"""Test add mixed with other operators (precedence)."""
|
|
2674
|
+
runner = Runner("return 2 + 3 * 4 as result")
|
|
2675
|
+
await runner.run()
|
|
2676
|
+
results = runner.results
|
|
2677
|
+
assert len(results) == 1
|
|
2678
|
+
assert results[0] == {"result": 14}
|
|
2679
|
+
|
|
2680
|
+
@pytest.mark.asyncio
|
|
2681
|
+
async def test_add_with_parentheses(self):
|
|
2682
|
+
"""Test add with parentheses."""
|
|
2683
|
+
runner = Runner("return (2 + 3) * 4 as result")
|
|
2684
|
+
await runner.run()
|
|
2685
|
+
results = runner.results
|
|
2686
|
+
assert len(results) == 1
|
|
2687
|
+
assert results[0] == {"result": 20}
|
|
2688
|
+
|
|
2689
|
+
@pytest.mark.asyncio
|
|
2690
|
+
async def test_add_nested_lists(self):
|
|
2691
|
+
"""Test add nested lists."""
|
|
2692
|
+
runner = Runner("return [[1, 2]] + [[3, 4]] as result")
|
|
2693
|
+
await runner.run()
|
|
2694
|
+
results = runner.results
|
|
2695
|
+
assert len(results) == 1
|
|
2696
|
+
assert results[0] == {"result": [[1, 2], [3, 4]]}
|
|
2697
|
+
|
|
2698
|
+
@pytest.mark.asyncio
|
|
2699
|
+
async def test_add_with_with_clause(self):
|
|
2700
|
+
"""Test add with with clause."""
|
|
2701
|
+
runner = Runner("with 5 as a, 10 as b return a + b as result")
|
|
2702
|
+
await runner.run()
|
|
2703
|
+
results = runner.results
|
|
2704
|
+
assert len(results) == 1
|
|
2705
|
+
assert results[0] == {"result": 15}
|
|
2706
|
+
|
|
2707
|
+
# ============================================================
|
|
2708
|
+
# UNION and UNION ALL tests
|
|
2709
|
+
# ============================================================
|
|
2710
|
+
|
|
2711
|
+
@pytest.mark.asyncio
|
|
2712
|
+
async def test_union_with_simple_values(self):
|
|
2713
|
+
"""Test UNION with simple values."""
|
|
2714
|
+
runner = Runner("WITH 1 AS x RETURN x UNION WITH 2 AS x RETURN x")
|
|
2715
|
+
await runner.run()
|
|
2716
|
+
results = runner.results
|
|
2717
|
+
assert len(results) == 2
|
|
2718
|
+
assert results == [{"x": 1}, {"x": 2}]
|
|
2719
|
+
|
|
2720
|
+
@pytest.mark.asyncio
|
|
2721
|
+
async def test_union_removes_duplicates(self):
|
|
2722
|
+
"""Test UNION removes duplicates."""
|
|
2723
|
+
runner = Runner("WITH 1 AS x RETURN x UNION WITH 1 AS x RETURN x")
|
|
2724
|
+
await runner.run()
|
|
2725
|
+
results = runner.results
|
|
2726
|
+
assert len(results) == 1
|
|
2727
|
+
assert results == [{"x": 1}]
|
|
2728
|
+
|
|
2729
|
+
@pytest.mark.asyncio
|
|
2730
|
+
async def test_union_all_keeps_duplicates(self):
|
|
2731
|
+
"""Test UNION ALL keeps duplicates."""
|
|
2732
|
+
runner = Runner("WITH 1 AS x RETURN x UNION ALL WITH 1 AS x RETURN x")
|
|
2733
|
+
await runner.run()
|
|
2734
|
+
results = runner.results
|
|
2735
|
+
assert len(results) == 2
|
|
2736
|
+
assert results == [{"x": 1}, {"x": 1}]
|
|
2737
|
+
|
|
2738
|
+
@pytest.mark.asyncio
|
|
2739
|
+
async def test_union_with_multiple_columns(self):
|
|
2740
|
+
"""Test UNION with multiple columns."""
|
|
2741
|
+
runner = Runner(
|
|
2742
|
+
"WITH 1 AS a, 'hello' AS b RETURN a, b UNION WITH 2 AS a, 'world' AS b RETURN a, b"
|
|
2743
|
+
)
|
|
2744
|
+
await runner.run()
|
|
2745
|
+
results = runner.results
|
|
2746
|
+
assert len(results) == 2
|
|
2747
|
+
assert results == [
|
|
2748
|
+
{"a": 1, "b": "hello"},
|
|
2749
|
+
{"a": 2, "b": "world"},
|
|
2750
|
+
]
|
|
2751
|
+
|
|
2752
|
+
@pytest.mark.asyncio
|
|
2753
|
+
async def test_union_all_with_multiple_columns(self):
|
|
2754
|
+
"""Test chained UNION ALL with three branches."""
|
|
2755
|
+
runner = Runner(
|
|
2756
|
+
"WITH 1 AS a RETURN a UNION ALL WITH 2 AS a RETURN a UNION ALL WITH 3 AS a RETURN a"
|
|
2757
|
+
)
|
|
2758
|
+
await runner.run()
|
|
2759
|
+
results = runner.results
|
|
2760
|
+
assert len(results) == 3
|
|
2761
|
+
assert results == [{"a": 1}, {"a": 2}, {"a": 3}]
|
|
2762
|
+
|
|
2763
|
+
@pytest.mark.asyncio
|
|
2764
|
+
async def test_chained_union_removes_duplicates(self):
|
|
2765
|
+
"""Test chained UNION removes duplicates across all branches."""
|
|
2766
|
+
runner = Runner(
|
|
2767
|
+
"WITH 1 AS x RETURN x UNION WITH 2 AS x RETURN x UNION WITH 1 AS x RETURN x"
|
|
2768
|
+
)
|
|
2769
|
+
await runner.run()
|
|
2770
|
+
results = runner.results
|
|
2771
|
+
assert len(results) == 2
|
|
2772
|
+
assert results == [{"x": 1}, {"x": 2}]
|
|
2773
|
+
|
|
2774
|
+
@pytest.mark.asyncio
|
|
2775
|
+
async def test_union_with_unwind(self):
|
|
2776
|
+
"""Test UNION with UNWIND."""
|
|
2777
|
+
runner = Runner(
|
|
2778
|
+
"UNWIND [1, 2] AS x RETURN x UNION UNWIND [3, 4] AS x RETURN x"
|
|
2779
|
+
)
|
|
2780
|
+
await runner.run()
|
|
2781
|
+
results = runner.results
|
|
2782
|
+
assert len(results) == 4
|
|
2783
|
+
assert results == [{"x": 1}, {"x": 2}, {"x": 3}, {"x": 4}]
|
|
2784
|
+
|
|
2785
|
+
@pytest.mark.asyncio
|
|
2786
|
+
async def test_union_with_mismatched_columns(self):
|
|
2787
|
+
"""Test UNION with mismatched columns throws error."""
|
|
2788
|
+
runner = Runner("WITH 1 AS x RETURN x UNION WITH 2 AS y RETURN y")
|
|
2789
|
+
with pytest.raises(ValueError, match="All sub queries in a UNION must have the same return column names"):
|
|
2790
|
+
await runner.run()
|
|
2791
|
+
|
|
2792
|
+
@pytest.mark.asyncio
|
|
2793
|
+
async def test_union_with_empty_left_side(self):
|
|
2794
|
+
"""Test UNION with empty left side."""
|
|
2795
|
+
runner = Runner(
|
|
2796
|
+
"UNWIND [] AS x RETURN x UNION WITH 1 AS x RETURN x"
|
|
2797
|
+
)
|
|
2798
|
+
await runner.run()
|
|
2799
|
+
results = runner.results
|
|
2800
|
+
assert len(results) == 1
|
|
2801
|
+
assert results == [{"x": 1}]
|
|
2802
|
+
|
|
2803
|
+
@pytest.mark.asyncio
|
|
2804
|
+
async def test_union_with_empty_right_side(self):
|
|
2805
|
+
"""Test UNION with empty right side."""
|
|
2806
|
+
runner = Runner(
|
|
2807
|
+
"WITH 1 AS x RETURN x UNION UNWIND [] AS x RETURN x"
|
|
2808
|
+
)
|
|
2809
|
+
await runner.run()
|
|
2810
|
+
results = runner.results
|
|
2811
|
+
assert len(results) == 1
|
|
2812
|
+
assert results == [{"x": 1}]
|
|
@@ -1090,3 +1090,85 @@ class TestParser:
|
|
|
1090
1090
|
"----- Number (3)"
|
|
1091
1091
|
)
|
|
1092
1092
|
assert ast.print() == expected
|
|
1093
|
+
|
|
1094
|
+
def test_optional_match_operation(self):
|
|
1095
|
+
"""Test optional match operation."""
|
|
1096
|
+
parser = Parser()
|
|
1097
|
+
ast = parser.parse("OPTIONAL MATCH (n:Person) RETURN n")
|
|
1098
|
+
expected = (
|
|
1099
|
+
"ASTNode\n"
|
|
1100
|
+
"- OptionalMatch\n"
|
|
1101
|
+
"- Return\n"
|
|
1102
|
+
"-- Expression (n)\n"
|
|
1103
|
+
"--- Reference (n)"
|
|
1104
|
+
)
|
|
1105
|
+
assert ast.print() == expected
|
|
1106
|
+
match = ast.first_child()
|
|
1107
|
+
assert isinstance(match, Match)
|
|
1108
|
+
assert match.optional is True
|
|
1109
|
+
assert match.patterns[0].start_node is not None
|
|
1110
|
+
assert match.patterns[0].start_node.label == "Person"
|
|
1111
|
+
assert match.patterns[0].start_node.identifier == "n"
|
|
1112
|
+
|
|
1113
|
+
def test_optional_match_with_relationships(self):
|
|
1114
|
+
"""Test optional match with graph pattern including relationships."""
|
|
1115
|
+
parser = Parser()
|
|
1116
|
+
ast = parser.parse("OPTIONAL MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
|
|
1117
|
+
expected = (
|
|
1118
|
+
"ASTNode\n"
|
|
1119
|
+
"- OptionalMatch\n"
|
|
1120
|
+
"- Return\n"
|
|
1121
|
+
"-- Expression (a)\n"
|
|
1122
|
+
"--- Reference (a)\n"
|
|
1123
|
+
"-- Expression (b)\n"
|
|
1124
|
+
"--- Reference (b)"
|
|
1125
|
+
)
|
|
1126
|
+
assert ast.print() == expected
|
|
1127
|
+
match = ast.first_child()
|
|
1128
|
+
assert isinstance(match, Match)
|
|
1129
|
+
assert match.optional is True
|
|
1130
|
+
assert len(match.patterns[0].chain) == 3
|
|
1131
|
+
source = match.patterns[0].chain[0]
|
|
1132
|
+
relationship = match.patterns[0].chain[1]
|
|
1133
|
+
target = match.patterns[0].chain[2]
|
|
1134
|
+
assert source.identifier == "a"
|
|
1135
|
+
assert source.label == "Person"
|
|
1136
|
+
assert relationship.type == "KNOWS"
|
|
1137
|
+
assert target.identifier == "b"
|
|
1138
|
+
assert target.label == "Person"
|
|
1139
|
+
|
|
1140
|
+
def test_match_followed_by_optional_match(self):
|
|
1141
|
+
"""Test match followed by optional match."""
|
|
1142
|
+
parser = Parser()
|
|
1143
|
+
ast = parser.parse("MATCH (a:Person) OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) RETURN a, b")
|
|
1144
|
+
expected = (
|
|
1145
|
+
"ASTNode\n"
|
|
1146
|
+
"- Match\n"
|
|
1147
|
+
"- OptionalMatch\n"
|
|
1148
|
+
"- Return\n"
|
|
1149
|
+
"-- Expression (a)\n"
|
|
1150
|
+
"--- Reference (a)\n"
|
|
1151
|
+
"-- Expression (b)\n"
|
|
1152
|
+
"--- Reference (b)"
|
|
1153
|
+
)
|
|
1154
|
+
assert ast.print() == expected
|
|
1155
|
+
match = ast.first_child()
|
|
1156
|
+
assert isinstance(match, Match)
|
|
1157
|
+
assert match.optional is False
|
|
1158
|
+
optional_match = match.next
|
|
1159
|
+
assert isinstance(optional_match, Match)
|
|
1160
|
+
assert optional_match.optional is True
|
|
1161
|
+
|
|
1162
|
+
def test_regular_match_is_not_optional(self):
|
|
1163
|
+
"""Test that regular match is not optional."""
|
|
1164
|
+
parser = Parser()
|
|
1165
|
+
ast = parser.parse("MATCH (n:Person) RETURN n")
|
|
1166
|
+
match = ast.first_child()
|
|
1167
|
+
assert isinstance(match, Match)
|
|
1168
|
+
assert match.optional is False
|
|
1169
|
+
|
|
1170
|
+
def test_optional_without_match_throws_error(self):
|
|
1171
|
+
"""Test that OPTIONAL without MATCH throws error."""
|
|
1172
|
+
parser = Parser()
|
|
1173
|
+
with pytest.raises(Exception, match="Expected MATCH after OPTIONAL"):
|
|
1174
|
+
parser.parse("OPTIONAL RETURN 1")
|