Skip to content

Commit ae19ea7

Browse files
authored
Bound per-row memory in writable clauses (#2517)
SET and REMOVE create EState-owned tuple slots for every updated entity and run transient builders in an executor-lifetime memory context. DELETE RLS slots have the same ownership problem, while MERGE SET shares the update path. Large writable statements therefore retain slots and scratch until executor shutdown. Use standalone slots for per-row writes and RLS checks, release them explicitly, and allocate update/delete scratch in the writable CustomScan's per-tuple context. Reset both CustomScan and EState per-tuple contexts at the corresponding row boundaries while retaining node-lifetime slots in the EState. Initialize stored and virtual generated-column metadata with the same lifetime as the one-row ResultRelInfo used by SET, REMOVE, and DELETE. Copy properties for paths retained by MERGE's cross-row de-duplication state into the query-lifetime context, and fully release rejected path arrays. Reset connected-edge RLS expression scratch for each edge in both DETACH DELETE scan paths. Add generated-column batch and DELETE coverage, eager multi-row MERGE coverage, terminal and non-terminal repeated-path coverage, and function-based connected-edge RLS coverage for both endpoint indexes and the sequential-scan fallback.
1 parent 2e2120b commit ae19ea7

12 files changed

Lines changed: 734 additions & 13 deletions

File tree

‎regress/expected/cypher_merge.out‎

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,113 @@ $$) AS (src agtype, last agtype);
22312231
"Anchor" | "Anchor"
22322232
(1 row)
22332233

2234+
-- Exercise eager MERGE ON CREATE and ON MATCH SET over consecutive rows.
2235+
SELECT * FROM cypher('merge_actions', $$
2236+
UNWIND [1, 2, 3] AS ident
2237+
MERGE (n:Batch {id: ident})
2238+
ON CREATE SET n.payload = {nested: [ident, {active: true}]}
2239+
RETURN n.id, n.payload
2240+
$$) AS (id agtype, payload agtype);
2241+
id | payload
2242+
----+-----------------------------------
2243+
1 | {"nested": [1, {"active": true}]}
2244+
2 | {"nested": [2, {"active": true}]}
2245+
3 | {"nested": [3, {"active": true}]}
2246+
(3 rows)
2247+
2248+
SELECT * FROM cypher('merge_actions', $$
2249+
UNWIND [1, 2, 3] AS ident
2250+
MERGE (n:Batch {id: ident})
2251+
ON MATCH SET n.payload = {updated: [ident, false]}
2252+
RETURN n.id, n.payload
2253+
$$) AS (id agtype, payload agtype);
2254+
id | payload
2255+
----+-------------------------
2256+
1 | {"updated": [1, false]}
2257+
2 | {"updated": [2, false]}
2258+
3 | {"updated": [3, false]}
2259+
(3 rows)
2260+
2261+
-- Reuse a path created by an earlier input row.
2262+
SELECT * FROM cypher('merge_actions', $$
2263+
UNWIND [1, 2, 1] AS ident
2264+
MERGE (n:DedupBatch {
2265+
id: ident,
2266+
payload: {nested: [ident]}
2267+
})
2268+
RETURN ident, n.id, n.payload
2269+
$$) AS (input agtype, id agtype, payload agtype);
2270+
input | id | payload
2271+
-------+----+-----------------
2272+
1 | 1 | {"nested": [1]}
2273+
2 | 2 | {"nested": [2]}
2274+
1 | 1 | {"nested": [1]}
2275+
(3 rows)
2276+
2277+
SELECT * FROM cypher('merge_actions', $$
2278+
MATCH (n:DedupBatch)
2279+
RETURN count(n)
2280+
$$) AS (count agtype);
2281+
count
2282+
-------
2283+
2
2284+
(1 row)
2285+
2286+
-- Reuse a path created by an earlier input row in terminal MERGE.
2287+
SELECT * FROM cypher('merge_actions', $$
2288+
UNWIND [3, 4, 3] AS ident
2289+
MERGE (:DedupBatch {
2290+
id: ident,
2291+
payload: {nested: [ident]}
2292+
})
2293+
$$) AS (result agtype);
2294+
result
2295+
--------
2296+
(0 rows)
2297+
2298+
SELECT * FROM cypher('merge_actions', $$
2299+
MATCH (n:DedupBatch)
2300+
RETURN count(n)
2301+
$$) AS (count agtype);
2302+
count
2303+
-------
2304+
4
2305+
(1 row)
2306+
2307+
-- Exercise repeated duplicate candidates in eager and terminal MERGE.
2308+
SELECT * FROM cypher('merge_actions', $$
2309+
UNWIND range(1, 1000) AS ident
2310+
MERGE (n:DedupBatch {
2311+
id: 20,
2312+
payload: {nested: [20]}
2313+
})
2314+
RETURN count(n)
2315+
$$) AS (count agtype);
2316+
count
2317+
-------
2318+
1000
2319+
(1 row)
2320+
2321+
SELECT * FROM cypher('merge_actions', $$
2322+
UNWIND range(1, 1000) AS ident
2323+
MERGE (:DedupBatch {
2324+
id: 21,
2325+
payload: {nested: [21]}
2326+
})
2327+
$$) AS (result agtype);
2328+
result
2329+
--------
2330+
(0 rows)
2331+
2332+
SELECT * FROM cypher('merge_actions', $$
2333+
MATCH (n:DedupBatch)
2334+
RETURN count(n)
2335+
$$) AS (count agtype);
2336+
count
2337+
-------
2338+
6
2339+
(1 row)
2340+
22342341
-- cleanup
22352342
SELECT * FROM cypher('merge_actions', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
22362343
a
@@ -2241,12 +2348,14 @@ SELECT * FROM cypher('merge_actions', $$ MATCH (n) DETACH DELETE n $$) AS (a agt
22412348
-- delete graphs
22422349
--
22432350
SELECT drop_graph('merge_actions', true);
2244-
NOTICE: drop cascades to 5 other objects
2351+
NOTICE: drop cascades to 7 other objects
22452352
DETAIL: drop cascades to table merge_actions._ag_label_vertex
22462353
drop cascades to table merge_actions._ag_label_edge
22472354
drop cascades to table merge_actions."Person"
22482355
drop cascades to table merge_actions."KNOWS"
22492356
drop cascades to table merge_actions."on"
2357+
drop cascades to table merge_actions."Batch"
2358+
drop cascades to table merge_actions."DedupBatch"
22502359
NOTICE: graph "merge_actions" has been dropped
22512360
drop_graph
22522361
------------

‎regress/expected/generated_columns.out‎

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,58 @@ SELECT category, properties FROM generated_columns."Product" ORDER BY category N
104104
| {"type": "no-cat"}
105105
(3 rows)
106106

107+
-- Exercise generated columns through multi-row CREATE, SET, and MERGE.
108+
SELECT * FROM cypher('generated_columns', $$
109+
UNWIND ['batch-a', 'batch-b', 'batch-c'] AS category
110+
CREATE (p:Product {category: category, batch: 'create'})
111+
RETURN count(p)
112+
$$) AS (count agtype);
113+
count
114+
-------
115+
3
116+
(1 row)
117+
118+
SELECT * FROM cypher('generated_columns', $$
119+
MATCH (p:Product)
120+
SET p.checked = true
121+
RETURN count(p)
122+
$$) AS (count agtype);
123+
count
124+
-------
125+
6
126+
(1 row)
127+
128+
SELECT * FROM cypher('generated_columns', $$
129+
UNWIND ['merge-a', 'merge-b', 'merge-c'] AS category
130+
MERGE (p:Product {category: category})
131+
ON CREATE SET p.batch = 'merge-create'
132+
RETURN count(p)
133+
$$) AS (count agtype);
134+
count
135+
-------
136+
3
137+
(1 row)
138+
139+
SELECT * FROM cypher('generated_columns', $$
140+
UNWIND ['merge-a', 'merge-b', 'merge-c'] AS category
141+
MERGE (p:Product {category: category})
142+
ON MATCH SET p.batch = 'merge-match'
143+
RETURN count(p)
144+
$$) AS (count agtype);
145+
count
146+
-------
147+
3
148+
(1 row)
149+
150+
SELECT count(*) AS rows,
151+
bool_and(category IS NOT DISTINCT FROM
152+
agtype_access_operator(properties, '"category"')::varchar(25)) AS synchronized
153+
FROM generated_columns."Product";
154+
rows | synchronized
155+
------+--------------
156+
12 | t
157+
(1 row)
158+
107159
--
108160
-- GENERATED ALWAYS ... STORED column on an edge label
109161
--
@@ -123,7 +175,7 @@ SELECT * FROM cypher('generated_columns', $$
123175
$$) AS (r agtype);
124176
r
125177
----------------------------------------------------------------------------------------------------------------------------------------
126-
{"id": 1125899906842625, "label": "REL", "end_id": 844424930131973, "start_id": 844424930131972, "properties": {"kind": "link"}}::edge
178+
{"id": 1125899906842625, "label": "REL", "end_id": 844424930131982, "start_id": 844424930131981, "properties": {"kind": "link"}}::edge
127179
(1 row)
128180

129181
SELECT kind FROM generated_columns."REL";
@@ -203,17 +255,105 @@ FROM generated_columns."T";
203255
t
204256
(1 row)
205257

258+
-- Exercise SET and DELETE on stored and virtual generated columns.
259+
SELECT create_vlabel('generated_columns', 'DeleteStored');
260+
NOTICE: VLabel "DeleteStored" has been created
261+
create_vlabel
262+
---------------
263+
264+
(1 row)
265+
266+
ALTER TABLE generated_columns."DeleteStored"
267+
ADD COLUMN marker integer GENERATED ALWAYS AS (1) STORED;
268+
SELECT * FROM cypher('generated_columns', $$
269+
UNWIND [1, 2, 3] AS ident
270+
CREATE (n:DeleteStored {id: ident})
271+
RETURN count(n)
272+
$$) AS (count agtype);
273+
count
274+
-------
275+
3
276+
(1 row)
277+
278+
SELECT * FROM cypher('generated_columns', $$
279+
MATCH (n:DeleteStored)
280+
DELETE n
281+
$$) AS (n agtype);
282+
n
283+
---
284+
(0 rows)
285+
286+
SELECT count(*) FROM generated_columns."DeleteStored";
287+
count
288+
-------
289+
0
290+
(1 row)
291+
292+
SELECT create_vlabel('generated_columns', 'DeleteVirtual');
293+
NOTICE: VLabel "DeleteVirtual" has been created
294+
create_vlabel
295+
---------------
296+
297+
(1 row)
298+
299+
ALTER TABLE generated_columns."DeleteVirtual"
300+
ADD COLUMN marker integer GENERATED ALWAYS AS (1) VIRTUAL;
301+
SELECT * FROM cypher('generated_columns', $$
302+
UNWIND [1, 2, 3] AS ident
303+
CREATE (n:DeleteVirtual {id: ident})
304+
RETURN count(n)
305+
$$) AS (count agtype);
306+
count
307+
-------
308+
3
309+
(1 row)
310+
311+
SELECT * FROM cypher('generated_columns', $$
312+
MATCH (n:DeleteVirtual)
313+
SET n.checked = true
314+
RETURN count(n)
315+
$$) AS (count agtype);
316+
count
317+
-------
318+
3
319+
(1 row)
320+
321+
SELECT bool_and(marker = 1) AS virtual_ok
322+
FROM generated_columns."DeleteVirtual";
323+
virtual_ok
324+
------------
325+
t
326+
(1 row)
327+
328+
SELECT * FROM cypher('generated_columns', $$
329+
MATCH (n:DeleteVirtual)
330+
DELETE n
331+
RETURN count(n)
332+
$$) AS (count agtype);
333+
count
334+
-------
335+
3
336+
(1 row)
337+
338+
SELECT count(*) FROM generated_columns."DeleteVirtual";
339+
count
340+
-------
341+
0
342+
(1 row)
343+
206344
--
207345
-- Cleanup
208346
--
209347
SELECT drop_graph('generated_columns', true);
210-
NOTICE: drop cascades to 6 other objects
348+
NOTICE: drop cascades to 8 other objects
211349
DETAIL: drop cascades to table generated_columns._ag_label_vertex
212350
drop cascades to table generated_columns._ag_label_edge
213351
drop cascades to table generated_columns."Product"
214352
drop cascades to table generated_columns."REL"
215353
drop cascades to table generated_columns."Plain"
216354
drop cascades to table generated_columns."T"
355+
drop cascades to table generated_columns."DeleteStored"
356+
drop cascades to table generated_columns."DeleteVirtual"
217357
NOTICE: graph "generated_columns" has been dropped
218358
drop_graph
219359
------------

‎regress/expected/security.out‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,91 @@ $$) AS (since agtype);
14401440
-------
14411441
(0 rows)
14421442

1443+
-- Exercise function-based edge RLS through the default endpoint indexes.
1444+
SELECT * FROM cypher('rls_graph', $$
1445+
CREATE (:Person {name: 'DetachFnIndexHub', owner: 'rls_user1', department: 'DetachFn'})
1446+
$$) AS (a agtype);
1447+
a
1448+
---
1449+
(0 rows)
1450+
1451+
SELECT * FROM cypher('rls_graph', $$
1452+
MATCH (hub:Person {name: 'DetachFnIndexHub'})
1453+
UNWIND range(1, 16) AS ident
1454+
CREATE (hub)-[:KNOWS {owner: 'rls_user1'}]->
1455+
(:Person {owner: 'rls_user1', department: 'DetachFn', leaf: ident})
1456+
$$) AS (a agtype);
1457+
a
1458+
---
1459+
(0 rows)
1460+
1461+
SET ROLE rls_user1;
1462+
SELECT * FROM cypher('rls_graph', $$
1463+
MATCH (p:Person {name: 'DetachFnIndexHub'}) DETACH DELETE p
1464+
$$) AS (a agtype);
1465+
a
1466+
---
1467+
(0 rows)
1468+
1469+
RESET ROLE;
1470+
SELECT * FROM cypher('rls_graph', $$
1471+
MATCH ()-[k:KNOWS]->() WHERE k.owner = 'rls_user1' RETURN count(k)
1472+
$$) AS (count agtype);
1473+
count
1474+
-------
1475+
0
1476+
(1 row)
1477+
1478+
-- Drop the endpoint indexes to exercise the connected-edge sequential scan.
1479+
DO $$
1480+
DECLARE
1481+
index_to_drop regclass;
1482+
BEGIN
1483+
FOR index_to_drop IN
1484+
SELECT indexrelid
1485+
FROM pg_index
1486+
WHERE indrelid = 'rls_graph."KNOWS"'::regclass
1487+
AND indnatts = 1
1488+
AND indkey[0] IN (2, 3)
1489+
LOOP
1490+
EXECUTE format('DROP INDEX %s', index_to_drop);
1491+
END LOOP;
1492+
END
1493+
$$;
1494+
SELECT * FROM cypher('rls_graph', $$
1495+
CREATE (:Person {name: 'DetachFnSeqHub', owner: 'rls_user1', department: 'DetachFn'})
1496+
$$) AS (a agtype);
1497+
a
1498+
---
1499+
(0 rows)
1500+
1501+
SELECT * FROM cypher('rls_graph', $$
1502+
MATCH (hub:Person {name: 'DetachFnSeqHub'})
1503+
UNWIND range(1, 16) AS ident
1504+
CREATE (hub)-[:KNOWS {owner: 'rls_user1'}]->
1505+
(:Person {owner: 'rls_user1', department: 'DetachFn', seq_leaf: ident})
1506+
$$) AS (a agtype);
1507+
a
1508+
---
1509+
(0 rows)
1510+
1511+
SET ROLE rls_user1;
1512+
SELECT * FROM cypher('rls_graph', $$
1513+
MATCH (p:Person {name: 'DetachFnSeqHub'}) DETACH DELETE p
1514+
$$) AS (a agtype);
1515+
a
1516+
---
1517+
(0 rows)
1518+
1519+
RESET ROLE;
1520+
SELECT * FROM cypher('rls_graph', $$
1521+
MATCH ()-[k:KNOWS]->() WHERE k.owner = 'rls_user1' RETURN count(k)
1522+
$$) AS (count agtype);
1523+
count
1524+
-------
1525+
0
1526+
(1 row)
1527+
14431528
-- cleanup
14441529
DROP POLICY detach_fn_knows_owner ON rls_graph."KNOWS";
14451530
DROP POLICY detach_fn_person_all ON rls_graph."Person";

0 commit comments

Comments
 (0)