Skip to content

Commit bf65c48

Browse files
committed
Add follow-up regression coverage for pattern expressions (#2360)
Addresses the non-blocking test-coverage follow-ups from the review: pattern expressions in additional contexts opened up by allowing anonymous_path as an expr_atom. New cases (all verified against a PG18 build): - Single-node pattern on a bound variable (a:Label). Documented as an EXISTS existence check, NOT an openCypher label predicate: a matching label is always true, and a non-matching label hits AGE's pre-existing "multiple labels for variable" restriction (captured as expected error). - Pattern expressions inside list and map literals. - Pattern expressions as function arguments: collect() shows correct per-row booleans; count() counts all rows (non-null bool) -- documented so the value is not mistaken for a bug. - Pattern expression in OPTIONAL MATCH ... WHERE (null-preserving). - EXISTS() and a bare pattern combined in one predicate. make installcheck: 33/33 green.
1 parent 0e720ea commit bf65c48

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

regress/expected/pattern_expression.out

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,132 @@ $$) AS (result agtype);
313313
"Alice"
314314
(1 row)
315315

316+
--
317+
-- Follow-up coverage (review #2360): pattern expressions in additional
318+
-- expression contexts opened up by allowing anonymous_path as an expr_atom.
319+
--
320+
--
321+
-- Single-node pattern on an already-bound variable: (a:Label)
322+
--
323+
-- NOTE: this is an EXISTS existence check on the bound variable, NOT an
324+
-- openCypher label predicate. A matching label is therefore always true
325+
-- (the variable is already bound), and a *different* label is rejected by
326+
-- AGE's pre-existing "multiple labels for variable" restriction rather than
327+
-- evaluating to false. Both behaviours are captured here so any future change
328+
-- to single-node-pattern semantics is caught by this test.
329+
SELECT * FROM cypher('pattern_expr', $$
330+
MATCH (a:Person)
331+
RETURN a.name, (a:Person)
332+
ORDER BY a.name
333+
$$) AS (name agtype, is_person agtype);
334+
name | is_person
335+
-----------+-----------
336+
"Alice" | true
337+
"Bob" | true
338+
"Charlie" | true
339+
"Dave" | true
340+
(4 rows)
341+
342+
-- A non-matching label errors (pre-existing limitation, not a regression)
343+
SELECT * FROM cypher('pattern_expr', $$
344+
MATCH (a:Person)
345+
RETURN a.name, (a:Animal)
346+
ORDER BY a.name
347+
$$) AS (name agtype, is_animal agtype);
348+
ERROR: multiple labels for variable 'a' are not supported
349+
LINE 3: RETURN a.name, (a:Animal)
350+
^
351+
--
352+
-- Pattern expressions inside a list literal
353+
--
354+
SELECT * FROM cypher('pattern_expr', $$
355+
MATCH (a:Person)
356+
RETURN a.name, [(a)-[:KNOWS]->(:Person), (a)-[:WORKS_WITH]->(:Person)]
357+
ORDER BY a.name
358+
$$) AS (name agtype, flags agtype);
359+
name | flags
360+
-----------+----------------
361+
"Alice" | [true, true]
362+
"Bob" | [false, false]
363+
"Charlie" | [false, false]
364+
"Dave" | [false, false]
365+
(4 rows)
366+
367+
--
368+
-- Pattern expressions inside a map literal
369+
--
370+
SELECT * FROM cypher('pattern_expr', $$
371+
MATCH (a:Person)
372+
RETURN a.name, {knows: (a)-[:KNOWS]->(:Person), works: (a)-[:WORKS_WITH]->(:Person)}
373+
ORDER BY a.name
374+
$$) AS (name agtype, m agtype);
375+
name | m
376+
-----------+----------------------------------
377+
"Alice" | {"knows": true, "works": true}
378+
"Bob" | {"knows": false, "works": false}
379+
"Charlie" | {"knows": false, "works": false}
380+
"Dave" | {"knows": false, "works": false}
381+
(4 rows)
382+
383+
--
384+
-- Pattern expressions as function arguments
385+
--
386+
-- collect() shows the per-row boolean values are correct (ORDER BY before
387+
-- the aggregate so the collected order is deterministic across scan plans).
388+
SELECT * FROM cypher('pattern_expr', $$
389+
MATCH (a:Person)
390+
WITH a ORDER BY a.name
391+
RETURN collect((a)-[:KNOWS]->(:Person))
392+
$$) AS (vals agtype);
393+
vals
394+
-----------------------------
395+
[true, false, false, false]
396+
(1 row)
397+
398+
-- count() counts non-null values; a boolean (including false) is non-null,
399+
-- so this counts every row rather than only the matching ones. This is the
400+
-- expected SQL aggregate semantics, documented here so the value is not
401+
-- mistaken for a bug.
402+
SELECT * FROM cypher('pattern_expr', $$
403+
MATCH (a:Person)
404+
RETURN count((a)-[:KNOWS]->(:Person))
405+
$$) AS (c agtype);
406+
c
407+
---
408+
4
409+
(1 row)
410+
411+
--
412+
-- Pattern expression in OPTIONAL MATCH ... WHERE (null-preserving)
413+
--
414+
SELECT * FROM cypher('pattern_expr', $$
415+
MATCH (a:Person)
416+
OPTIONAL MATCH (b:Person) WHERE (a)-[:KNOWS]->(b)
417+
RETURN a.name, b.name
418+
ORDER BY a.name, b.name
419+
$$) AS (a agtype, b agtype);
420+
a | b
421+
-----------+-------
422+
"Alice" | "Bob"
423+
"Bob" |
424+
"Charlie" |
425+
"Dave" |
426+
(4 rows)
427+
428+
--
429+
-- EXISTS() and a bare pattern combined in a single predicate
430+
--
431+
SELECT * FROM cypher('pattern_expr', $$
432+
MATCH (a:Person)
433+
WHERE EXISTS((a)-[:KNOWS]->(:Person)) AND (a)-[:WORKS_WITH]->(:Person)
434+
RETURN a.name
435+
ORDER BY a.name
436+
$$) AS (name agtype);
437+
name
438+
---------
439+
"Alice"
440+
(1 row)
441+
316442
--
317443
-- Cleanup
318444
--

regress/sql/pattern_expression.sql

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,91 @@ SELECT * FROM cypher('pattern_expr', $$
214214
ORDER BY name
215215
$$) AS (result agtype);
216216

217+
--
218+
-- Follow-up coverage (review #2360): pattern expressions in additional
219+
-- expression contexts opened up by allowing anonymous_path as an expr_atom.
220+
--
221+
222+
--
223+
-- Single-node pattern on an already-bound variable: (a:Label)
224+
--
225+
-- NOTE: this is an EXISTS existence check on the bound variable, NOT an
226+
-- openCypher label predicate. A matching label is therefore always true
227+
-- (the variable is already bound), and a *different* label is rejected by
228+
-- AGE's pre-existing "multiple labels for variable" restriction rather than
229+
-- evaluating to false. Both behaviours are captured here so any future change
230+
-- to single-node-pattern semantics is caught by this test.
231+
SELECT * FROM cypher('pattern_expr', $$
232+
MATCH (a:Person)
233+
RETURN a.name, (a:Person)
234+
ORDER BY a.name
235+
$$) AS (name agtype, is_person agtype);
236+
237+
-- A non-matching label errors (pre-existing limitation, not a regression)
238+
SELECT * FROM cypher('pattern_expr', $$
239+
MATCH (a:Person)
240+
RETURN a.name, (a:Animal)
241+
ORDER BY a.name
242+
$$) AS (name agtype, is_animal agtype);
243+
244+
--
245+
-- Pattern expressions inside a list literal
246+
--
247+
SELECT * FROM cypher('pattern_expr', $$
248+
MATCH (a:Person)
249+
RETURN a.name, [(a)-[:KNOWS]->(:Person), (a)-[:WORKS_WITH]->(:Person)]
250+
ORDER BY a.name
251+
$$) AS (name agtype, flags agtype);
252+
253+
--
254+
-- Pattern expressions inside a map literal
255+
--
256+
SELECT * FROM cypher('pattern_expr', $$
257+
MATCH (a:Person)
258+
RETURN a.name, {knows: (a)-[:KNOWS]->(:Person), works: (a)-[:WORKS_WITH]->(:Person)}
259+
ORDER BY a.name
260+
$$) AS (name agtype, m agtype);
261+
262+
--
263+
-- Pattern expressions as function arguments
264+
--
265+
-- collect() shows the per-row boolean values are correct (ORDER BY before
266+
-- the aggregate so the collected order is deterministic across scan plans).
267+
SELECT * FROM cypher('pattern_expr', $$
268+
MATCH (a:Person)
269+
WITH a ORDER BY a.name
270+
RETURN collect((a)-[:KNOWS]->(:Person))
271+
$$) AS (vals agtype);
272+
273+
-- count() counts non-null values; a boolean (including false) is non-null,
274+
-- so this counts every row rather than only the matching ones. This is the
275+
-- expected SQL aggregate semantics, documented here so the value is not
276+
-- mistaken for a bug.
277+
SELECT * FROM cypher('pattern_expr', $$
278+
MATCH (a:Person)
279+
RETURN count((a)-[:KNOWS]->(:Person))
280+
$$) AS (c agtype);
281+
282+
--
283+
-- Pattern expression in OPTIONAL MATCH ... WHERE (null-preserving)
284+
--
285+
SELECT * FROM cypher('pattern_expr', $$
286+
MATCH (a:Person)
287+
OPTIONAL MATCH (b:Person) WHERE (a)-[:KNOWS]->(b)
288+
RETURN a.name, b.name
289+
ORDER BY a.name, b.name
290+
$$) AS (a agtype, b agtype);
291+
292+
--
293+
-- EXISTS() and a bare pattern combined in a single predicate
294+
--
295+
SELECT * FROM cypher('pattern_expr', $$
296+
MATCH (a:Person)
297+
WHERE EXISTS((a)-[:KNOWS]->(:Person)) AND (a)-[:WORKS_WITH]->(:Person)
298+
RETURN a.name
299+
ORDER BY a.name
300+
$$) AS (name agtype);
301+
217302
--
218303
-- Cleanup
219304
--

0 commit comments

Comments
 (0)