Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions drivine/cypher/sql/oracle_cypher_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,13 +959,15 @@ def _map_keys(self, json: str) -> str:
bound first: the data guide's query reads a column of a row outside it, and not an expression of one. The
guide's paths are read by their places, as `temporal_elements` reads them: a `JSON_TABLE` over the guide of a
map that holds a list of temporal values worked out item by item stalls the parse as one over the list does
(#331, #347). They are so read for every map, not only one the statement makes hold such a list: over the guide,
which a subquery gives, they are read many times faster than `JSON_TABLE` reads them (`keys(n)` of 100,000
nodes: 0.4 s, not 4.8 s). Null has no keys, and gives null.
(#331, #347). They are so read for every map, not only one the statement makes hold such a list. The guide is
worked out by the lateral view itself, as its one aggregate over `DUAL`: a subquery that works it out, in that
view's select list, is worked out once, and every row then had the first row's keys
(liberation-data/drivine4j#414, here #257). drivine4j measured the keys of 100,000 maps so at 7 s, where a
`JSON_TABLE` over each guide takes 4.4 s. Null has no keys, and gives null.
"""

def keys(map_: str) -> str:
paths = self.temporal_elements(f"(SELECT JSON_DATAGUIDE({map_}, 2) FROM DUAL)", "dg")
paths = self.temporal_elements(f"JSON_DATAGUIDE({map_}, 2)", "dg")
return f"CASE WHEN {_kind(map_)} = 'object' THEN {_keys_of(paths)} END"

return self.bound([json], lambda it: keys(it[0]))
Expand Down
83 changes: 83 additions & 0 deletions tests/integration/test_cypher_compiled.py
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,89 @@ async def test_a_walk_read_through_a_with_then_sorted_by_to_string_in_a_second_w
]


async def test_keys_gives_each_rows_own_keys_of_a_node_a_relationship_a_map_or_a_parameter_wherever_it_is_read(
cypher_pm: PersistenceManager,
):
# On Oracle every row had the first row's keys (liberation-data/drivine4j#414, here #257): the map's data guide was
# worked out by a subquery in a lateral view's select list, which Oracle works out once. The spec's cases read one
# row's keys only. In a comprehension, a quantifier, a DISTINCT and an order as well.
async def rows(cypher: str, parameters: dict[str, Any] | None = None) -> list[Any]:
return await cypher_pm.query(spec(cypher, parameters))

await clear(cypher_pm)
await rows(
"CREATE (a:CyKeyed {id: 1, name: 'Andres', surname: 'Lopez'}), "
"(b:CyKeyed {id: 2, otherName: 'Andres', otherSurname: 'Lopez'}), (c:CyKeyed {id: 3, a: 1}), "
"(d:CyKeyed {id: 4, b: 2}), (a)-[:CY_KEYED {w: 1, p: 1}]->(b), (c)-[:CY_KEYED {w: 2, q: 2, z: 3}]->(d)"
)
assert await rows("MATCH (n:CyKeyed) UNWIND keys(n) AS k RETURN n.id, k ORDER BY n.id, k") == [
[1, "id"],
[1, "name"],
[1, "surname"],
[2, "id"],
[2, "otherName"],
[2, "otherSurname"],
[3, "a"],
[3, "id"],
[4, "b"],
[4, "id"],
]
# The TCK's Graph8 #2.
assert await rows("MATCH (n:CyKeyed) UNWIND keys(n) AS x RETURN DISTINCT x ORDER BY x") == [
"a",
"b",
"id",
"name",
"otherName",
"otherSurname",
"surname",
]
assert await rows("MATCH (n:CyKeyed) WITH n ORDER BY n.id RETURN collect(size(keys(n)))") == [[3, 3, 2, 2]]
assert await rows("MATCH (n:CyKeyed) RETURN 'b' IN keys(n) ORDER BY n.id") == [False, False, False, True]
assert await rows("MATCH (n:CyKeyed) RETURN [k IN keys(n) WHERE k IN ['a', 'b'] | k + '!'] ORDER BY n.id") == [
[],
[],
["a!"],
["b!"],
]
assert await rows("MATCH (:CyKeyed)-[r:CY_KEYED]->() UNWIND keys(r) AS k RETURN r.w, k ORDER BY r.w, k") == [
[1, "p"],
[1, "w"],
[2, "q"],
[2, "w"],
[2, "z"],
]
assert await rows("UNWIND [{a: 1}, {b: 2}] AS m RETURN keys(m)") == [["a"], ["b"]]
assert await rows("UNWIND $ms AS m RETURN keys(m)", {"ms": [{"a": 1}, {"b": 2}]}) == [["a"], ["b"]]
assert await rows("WITH [{a: 1}, {b: 2}] AS ms RETURN [m IN ms | keys(m)]") == [[["a"], ["b"]]]
assert await rows("UNWIND [{a: 1}, {b: 2}] AS m RETURN [x IN [m, {c: 3}] | keys(x)]") == [
[["a"], ["c"]],
[["b"], ["c"]],
]
assert await rows("UNWIND [{b: 1}, {a: 2}, {b: 3}] AS m RETURN DISTINCT keys(m) ORDER BY keys(m)") == [
["a"],
["b"],
]
assert await rows("UNWIND [{b: 1}, {a: 2}] AS m RETURN coalesce(m.a, m.b) ORDER BY keys(m)") == [2, 1]
# A quantifier over each row's keys in a WHERE, the keys of a relationship an OPTIONAL MATCH didn't find, and of
# maps in a list each row holds.
assert await rows(
"UNWIND $rows AS row MATCH (n:CyKeyed {id: row.id}) WHERE any(k IN keys(row.props) WHERE k = 'b') RETURN n.id",
{"rows": [{"id": 1, "props": {"a": 1}}, {"id": 4, "props": {"b": 2}}]},
) == [4]
assert await rows(
"MATCH (n:CyKeyed) OPTIONAL MATCH (n)-[r:CY_KEYED]->() RETURN n.id, size(keys(r)) ORDER BY n.id"
) == [[1, 2], [2, None], [3, 3], [4, None]]
assert await rows("UNWIND [[{a: 1}, {b: 2, c: 3}], [{d: 4, e: 5, f: 6}]] AS l RETURN [x IN l | size(keys(x))]") == [
[1, 2],
[3],
]
# A nested map's paths are in its parent's data guide too, and only those one step from the top are keys.
assert await rows(
"UNWIND [{a: {x: 1}}, {b: {y: 2}, c: 3}] AS m RETURN size(keys(m)), keys(coalesce(m.a, m.b))"
) == [[1, ["x"]], [2, ["y"]]]


async def test_lists_and_maps_that_hold_a_null_compare_item_by_item_null_where_no_pair_differs(
cypher_pm: PersistenceManager,
):
Expand Down
87 changes: 87 additions & 0 deletions tests/sync_integration/test_cypher_compiled.py
Original file line number Diff line number Diff line change
Expand Up @@ -2919,6 +2919,93 @@ def test_a_walk_read_through_a_with_then_sorted_by_to_string_in_a_second_with_ke
]


def test_keys_gives_each_rows_own_keys_of_a_node_a_relationship_a_map_or_a_parameter_wherever_it_is_read(
cypher_pm: PersistenceManager,
):
# On Oracle every row had the first row's keys (liberation-data/drivine4j#414, here #257): the map's data guide was
# worked out by a subquery in a lateral view's select list, which Oracle works out once. The spec's cases read one
# row's keys only. In a comprehension, a quantifier, a DISTINCT and an order as well.
def rows(cypher: str, parameters: dict[str, Any] | None = None) -> list[Any]:
return cypher_pm.query(spec(cypher, parameters))

clear(cypher_pm)
rows(
"CREATE (a:CyKeyed {id: 1, name: 'Andres', surname: 'Lopez'}), "
"(b:CyKeyed {id: 2, otherName: 'Andres', otherSurname: 'Lopez'}), (c:CyKeyed {id: 3, a: 1}), "
"(d:CyKeyed {id: 4, b: 2}), (a)-[:CY_KEYED {w: 1, p: 1}]->(b), (c)-[:CY_KEYED {w: 2, q: 2, z: 3}]->(d)"
)
assert rows("MATCH (n:CyKeyed) UNWIND keys(n) AS k RETURN n.id, k ORDER BY n.id, k") == [
[1, "id"],
[1, "name"],
[1, "surname"],
[2, "id"],
[2, "otherName"],
[2, "otherSurname"],
[3, "a"],
[3, "id"],
[4, "b"],
[4, "id"],
]
# The TCK's Graph8 #2.
assert rows("MATCH (n:CyKeyed) UNWIND keys(n) AS x RETURN DISTINCT x ORDER BY x") == [
"a",
"b",
"id",
"name",
"otherName",
"otherSurname",
"surname",
]
assert rows("MATCH (n:CyKeyed) WITH n ORDER BY n.id RETURN collect(size(keys(n)))") == [[3, 3, 2, 2]]
assert rows("MATCH (n:CyKeyed) RETURN 'b' IN keys(n) ORDER BY n.id") == [False, False, False, True]
assert rows("MATCH (n:CyKeyed) RETURN [k IN keys(n) WHERE k IN ['a', 'b'] | k + '!'] ORDER BY n.id") == [
[],
[],
["a!"],
["b!"],
]
assert rows("MATCH (:CyKeyed)-[r:CY_KEYED]->() UNWIND keys(r) AS k RETURN r.w, k ORDER BY r.w, k") == [
[1, "p"],
[1, "w"],
[2, "q"],
[2, "w"],
[2, "z"],
]
assert rows("UNWIND [{a: 1}, {b: 2}] AS m RETURN keys(m)") == [["a"], ["b"]]
assert rows("UNWIND $ms AS m RETURN keys(m)", {"ms": [{"a": 1}, {"b": 2}]}) == [["a"], ["b"]]
assert rows("WITH [{a: 1}, {b: 2}] AS ms RETURN [m IN ms | keys(m)]") == [[["a"], ["b"]]]
assert rows("UNWIND [{a: 1}, {b: 2}] AS m RETURN [x IN [m, {c: 3}] | keys(x)]") == [
[["a"], ["c"]],
[["b"], ["c"]],
]
assert rows("UNWIND [{b: 1}, {a: 2}, {b: 3}] AS m RETURN DISTINCT keys(m) ORDER BY keys(m)") == [
["a"],
["b"],
]
assert rows("UNWIND [{b: 1}, {a: 2}] AS m RETURN coalesce(m.a, m.b) ORDER BY keys(m)") == [2, 1]
# A quantifier over each row's keys in a WHERE, the keys of a relationship an OPTIONAL MATCH didn't find, and of
# maps in a list each row holds.
assert rows(
"UNWIND $rows AS row MATCH (n:CyKeyed {id: row.id}) WHERE any(k IN keys(row.props) WHERE k = 'b') RETURN n.id",
{"rows": [{"id": 1, "props": {"a": 1}}, {"id": 4, "props": {"b": 2}}]},
) == [4]
assert rows("MATCH (n:CyKeyed) OPTIONAL MATCH (n)-[r:CY_KEYED]->() RETURN n.id, size(keys(r)) ORDER BY n.id") == [
[1, 2],
[2, None],
[3, 3],
[4, None],
]
assert rows("UNWIND [[{a: 1}, {b: 2, c: 3}], [{d: 4, e: 5, f: 6}]] AS l RETURN [x IN l | size(keys(x))]") == [
[1, 2],
[3],
]
# A nested map's paths are in its parent's data guide too, and only those one step from the top are keys.
assert rows("UNWIND [{a: {x: 1}}, {b: {y: 2}, c: 3}] AS m RETURN size(keys(m)), keys(coalesce(m.a, m.b))") == [
[1, ["x"]],
[2, ["y"]],
]


def test_lists_and_maps_that_hold_a_null_compare_item_by_item_null_where_no_pair_differs(
cypher_pm: PersistenceManager,
):
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/test_cypher_zoned_list_sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def test_on_oracle_a_value_that_may_hold_temporal_values_and_a_map_of_them_and_i
keys = OracleCypherCompiler("g").compile(KEYED).sql
assert "PASSING dg_r.n - 1" in keys, keys
assert "JSON_TABLE((SELECT JSON_DATAGUIDE" not in keys, keys
# Worked out by the lateral view itself: a subquery in its select list is worked out once, for every row
# (liberation-data/drivine4j#414, here #257).
assert "LATERAL (SELECT /*+ NO_MERGE */ JSON_DATAGUIDE(" in keys, keys
assert "(SELECT JSON_DATAGUIDE" not in keys, keys


def test_on_oracle_a_list_of_temporal_values_a_with_takes_out_of_an_unwound_map_is_read_item_by_item_too():
Expand Down
Loading