From 5e31a5b75a7d6bd3430d1d48acf0e7eefe39acf2 Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Tue, 1 Sep 2026 08:42:36 +0800 Subject: [PATCH 1/5] test(parser): cover redundant static edge type filter Add an EXPLAIN regression that expects a statically typed relationship predicate to produce the same filter-free plan as the equivalent MATCH without the redundant type check.\n\nRefs #2481 --- regress/expected/expr.out | 32 ++++++++++++++++++++++++++++++++ regress/sql/expr.sql | 7 +++++++ 2 files changed, 39 insertions(+) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 806a6f65c..ad961108e 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -9743,6 +9743,38 @@ $$) AS (name agtype); (2 rows) -- type() comparison on edge +SELECT * FROM cypher('accessor_opt', $$ + EXPLAIN (VERBOSE, COSTS OFF) + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'KNOWS' + RETURN id(r) +$$) AS (plan agtype); + QUERY PLAN +----------------------------------------------------------------------------------------------------------------- + Merge Join + Output: (r.id)::agtype + Merge Cond: (_age_default_alias_1.id = r.end_id) + -> Merge Append + Sort Key: _age_default_alias_1.id + -> Index Only Scan using _ag_label_vertex_pkey on accessor_opt._ag_label_vertex _age_default_alias_1_1 + Output: _age_default_alias_1_1.id + -> Index Only Scan using "Person_pkey" on accessor_opt."Person" _age_default_alias_1_2 + Output: _age_default_alias_1_2.id + -> Materialize + Output: r.id, r.end_id + -> Nested Loop + Output: r.id, r.end_id + -> Index Scan using "KNOWS_end_id_idx" on accessor_opt."KNOWS" r + Output: r.id, r.start_id, r.end_id, r.properties + -> Append + -> Seq Scan on accessor_opt._ag_label_vertex _age_default_alias_0_1 + Output: _age_default_alias_0_1.id + Filter: (_age_default_alias_0_1.id = r.start_id) + -> Index Only Scan using "Person_pkey" on accessor_opt."Person" _age_default_alias_0_2 + Output: _age_default_alias_0_2.id + Index Cond: (_age_default_alias_0_2.id = r.start_id) +(22 rows) + SELECT * FROM cypher('accessor_opt', $$ MATCH ()-[r]->() WHERE type(r) = 'KNOWS' diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index d4d900a1c..0f6735e09 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -3895,6 +3895,13 @@ SELECT * FROM cypher('accessor_opt', $$ $$) AS (name agtype); -- type() comparison on edge +SELECT * FROM cypher('accessor_opt', $$ + EXPLAIN (VERBOSE, COSTS OFF) + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'KNOWS' + RETURN id(r) +$$) AS (plan agtype); + SELECT * FROM cypher('accessor_opt', $$ MATCH ()-[r]->() WHERE type(r) = 'KNOWS' From 4d5707d6df82c7bfc714c4f07446104ba9af7238 Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Tue, 1 Sep 2026 10:02:03 +0800 Subject: [PATCH 2/5] perf(parser): elide redundant edge type filters Preserve non-redundant predicates and avoid mutating the original OPTIONAL MATCH WHERE tree. Refs #2481 --- regress/expected/expr.out | 66 ++++++++++++++ regress/sql/expr.sql | 46 ++++++++++ src/backend/parser/cypher_clause.c | 140 ++++++++++++++++++++++++++++- 3 files changed, 250 insertions(+), 2 deletions(-) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index ad961108e..fd99a26b6 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -9775,6 +9775,72 @@ $$) AS (plan agtype); Index Cond: (_age_default_alias_0_2.id = r.start_id) (22 rows) +-- A different relationship type is not redundant. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'LIKES' + RETURN count(r) +$$) AS (count agtype); + count +------- + 0 +(1 row) + +-- Removing the redundant conjunct must preserve the remaining predicate. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'KNOWS' AND r.since = 2020 + RETURN count(r) +$$) AS (count agtype); + count +------- + 1 +(1 row) + +-- A redundant predicate nested under OR must not be removed. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'KNOWS' OR r.since = -1 + RETURN count(r) +$$) AS (count agtype); + count +------- + 1 +(1 row) + +-- OPTIONAL MATCH must continue to emit unmatched left-hand rows. +SELECT * FROM cypher('accessor_opt', $$ + MATCH (n:Person) + OPTIONAL MATCH (n)-[r:KNOWS]->(m) + WHERE type(r) = 'KNOWS' + RETURN count(n), count(r) +$$) AS (nodes agtype, relationships agtype); + nodes | relationships +-------+--------------- + 2 | 1 +(1 row) + +-- A variable-length relationship is a list, so its type predicate is not +-- equivalent to the static relationship label and must remain untouched. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS*1..1]->() + WHERE type(r) = 'KNOWS' + RETURN count(r) +$$) AS (count agtype); +ERROR: type() argument must resolve to a scalar value +-- A relationship variable reused through WITH keeps its original binding. +SELECT * FROM cypher('accessor_opt', $$ + MATCH (a:Person)-[r:KNOWS]->(b) + WITH a, b, r + OPTIONAL MATCH (a)-[r]->(b) + WHERE type(r) = 'KNOWS' + RETURN count(r) +$$) AS (count agtype); + count +------- + 1 +(1 row) + SELECT * FROM cypher('accessor_opt', $$ MATCH ()-[r]->() WHERE type(r) = 'KNOWS' diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 0f6735e09..57060834c 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -3902,6 +3902,52 @@ SELECT * FROM cypher('accessor_opt', $$ RETURN id(r) $$) AS (plan agtype); +-- A different relationship type is not redundant. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'LIKES' + RETURN count(r) +$$) AS (count agtype); + +-- Removing the redundant conjunct must preserve the remaining predicate. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'KNOWS' AND r.since = 2020 + RETURN count(r) +$$) AS (count agtype); + +-- A redundant predicate nested under OR must not be removed. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE type(r) = 'KNOWS' OR r.since = -1 + RETURN count(r) +$$) AS (count agtype); + +-- OPTIONAL MATCH must continue to emit unmatched left-hand rows. +SELECT * FROM cypher('accessor_opt', $$ + MATCH (n:Person) + OPTIONAL MATCH (n)-[r:KNOWS]->(m) + WHERE type(r) = 'KNOWS' + RETURN count(n), count(r) +$$) AS (nodes agtype, relationships agtype); + +-- A variable-length relationship is a list, so its type predicate is not +-- equivalent to the static relationship label and must remain untouched. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS*1..1]->() + WHERE type(r) = 'KNOWS' + RETURN count(r) +$$) AS (count agtype); + +-- A relationship variable reused through WITH keeps its original binding. +SELECT * FROM cypher('accessor_opt', $$ + MATCH (a:Person)-[r:KNOWS]->(b) + WITH a, b, r + OPTIONAL MATCH (a)-[r]->(b) + WHERE type(r) = 'KNOWS' + RETURN count(r) +$$) AS (count agtype); + SELECT * FROM cypher('accessor_opt', $$ MATCH ()-[r]->() WHERE type(r) = 'KNOWS' diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index 147e3e74e..4d1376187 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -129,6 +129,10 @@ static List *transform_match_entities(cypher_parsestate *cpstate, Query *query, cypher_path *path); static void transform_match_pattern(cypher_parsestate *cpstate, Query *query, List *pattern, Node *where); +static Node *simplify_redundant_edge_type_qual(List *pattern, Node *where); +static bool is_redundant_edge_type_qual(List *pattern, Node *qual); +static const char *find_static_edge_label(List *pattern, + const char *variable_name); static List *transform_match_path(cypher_parsestate *cpstate, Query *query, cypher_path *path); static Expr *transform_cypher_edge(cypher_parsestate *cpstate, @@ -4009,6 +4013,7 @@ static RangeTblEntry *transform_cypher_optional_match_clause(cypher_parsestate * ParseNamespaceItem *jnsitem; cypher_match *match_self = (cypher_match *) clause->self; Node *saved_where = match_self->where; + Node *join_where; int i = 0; j->jointype = JOIN_LEFT; @@ -4063,11 +4068,14 @@ static RangeTblEntry *transform_cypher_optional_match_clause(cypher_parsestate * * out all matches for a given outer row, that outer row is still * emitted with nulls in the optional columns. */ - if (saved_where != NULL) + join_where = simplify_redundant_edge_type_qual(match_self->pattern, + saved_where); + + if (join_where != NULL) { Node *where_qual; - where_qual = transform_cypher_expr(cpstate, saved_where, + where_qual = transform_cypher_expr(cpstate, join_where, EXPR_KIND_WHERE); where_qual = coerce_to_boolean(pstate, where_qual, "WHERE"); j->quals = where_qual; @@ -4621,6 +4629,132 @@ static ParseNamespaceItem *transform_RangeFunction(cypher_parsestate *cpstate, return pnsi; } +static const char *find_static_edge_label(List *pattern, + const char *variable_name) +{ + ListCell *path_cell; + const char *label = NULL; + + foreach (path_cell, pattern) + { + cypher_path *path = lfirst(path_cell); + ListCell *entity_cell; + + foreach (entity_cell, path->path) + { + Node *entity = lfirst(entity_cell); + cypher_relationship *rel; + + if (!is_ag_node(entity, cypher_relationship)) + continue; + + rel = (cypher_relationship *)entity; + if (rel->name == NULL || + strcmp(rel->name, variable_name) != 0) + continue; + + if (rel->varlen != NULL || rel->parsed_label == NULL) + return NULL; + + if (label != NULL && strcmp(label, rel->parsed_label) != 0) + return NULL; + + label = rel->parsed_label; + } + } + + return label; +} + +static bool is_redundant_edge_type_qual(List *pattern, Node *qual) +{ + cypher_comparison_aexpr *comparison; + FuncCall *function; + ColumnRef *column; + A_Const *constant; + String *operator_name; + String *function_name; + String *variable_name; + const char *static_label; + + if (qual == NULL || !is_ag_node(qual, cypher_comparison_aexpr)) + return false; + + comparison = (cypher_comparison_aexpr *)qual; + if (comparison->kind != AEXPR_OP || + list_length(comparison->name) != 1) + return false; + + operator_name = linitial(comparison->name); + if (!IsA(operator_name, String) || + strcmp(strVal(operator_name), "=") != 0 || + !IsA(comparison->lexpr, FuncCall) || + !IsA(comparison->rexpr, A_Const)) + return false; + + function = (FuncCall *)comparison->lexpr; + constant = (A_Const *)comparison->rexpr; + if (list_length(function->funcname) != 1 || + list_length(function->args) != 1 || + constant->isnull || + constant->val.sval.type != T_String) + return false; + + function_name = linitial(function->funcname); + if (!IsA(function_name, String) || + pg_strcasecmp(strVal(function_name), "type") != 0 || + !IsA(linitial(function->args), ColumnRef)) + return false; + + column = linitial(function->args); + if (list_length(column->fields) != 1 || + !IsA(linitial(column->fields), String)) + return false; + + variable_name = linitial(column->fields); + static_label = find_static_edge_label(pattern, strVal(variable_name)); + + return static_label != NULL && + strcmp(static_label, strVal(&constant->val)) == 0; +} + +static Node *simplify_redundant_edge_type_qual(List *pattern, Node *where) +{ + BoolExpr *expression; + List *args = NIL; + ListCell *cell; + + if (where == NULL) + return NULL; + + if (is_redundant_edge_type_qual(pattern, where)) + return NULL; + + if (!IsA(where, BoolExpr)) + return where; + + expression = (BoolExpr *)where; + if (expression->boolop != AND_EXPR) + return where; + + foreach (cell, expression->args) + { + Node *arg = lfirst(cell); + + if (!is_redundant_edge_type_qual(pattern, arg)) + args = lappend(args, arg); + } + + if (args == NIL) + return NULL; + if (list_length(args) == 1) + return linitial(args); + if (list_length(args) == list_length(expression->args)) + return where; + + return (Node *)makeBoolExpr(AND_EXPR, args, expression->location); +} + static void transform_match_pattern(cypher_parsestate *cpstate, Query *query, List *pattern, Node *where) { @@ -4646,6 +4780,8 @@ static void transform_match_pattern(cypher_parsestate *cpstate, Query *query, quals = list_concat(quals, qual); } + where = simplify_redundant_edge_type_qual(pattern, where); + if (quals != NIL) { q = makeBoolExpr(AND_EXPR, quals, -1); From a8cb30bcb80fd2172f0658bbdabede844e1d188d Mon Sep 17 00:00:00 2001 From: linhongyu Date: Tue, 13 Oct 2026 11:36:54 +0800 Subject: [PATCH 3/5] fix(parser): do not elide type() qual on default label edge The default label edge (_ag_label_edge) scans child labels, so a `type(r) = '_ag_label_edge'` predicate is not redundant with the pattern edge. Exclude it from elision so the _ag_label_edge regression case returns rows consistently. Address review from MuhammadTahaNaveed on #2559. --- src/backend/parser/cypher_clause.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index 4d1376187..7e4b9512b 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -4714,8 +4714,15 @@ static bool is_redundant_edge_type_qual(List *pattern, Node *qual) variable_name = linitial(column->fields); static_label = find_static_edge_label(pattern, strVal(variable_name)); - return static_label != NULL && - strcmp(static_label, strVal(&constant->val)) == 0; + /* + * The default label edge (_ag_label_edge) scans child labels, so a + * `type(r) = '_ag_label_edge'` predicate is not redundant with the + * pattern edge and must not be elided. + */ + if (static_label == NULL || IS_DEFAULT_LABEL_EDGE(static_label)) + return false; + + return strcmp(static_label, strVal(&constant->val)) == 0; } static Node *simplify_redundant_edge_type_qual(List *pattern, Node *where) From 3d3741b3c8eb4708ff4ffb59c5c6a49b7c9d5f04 Mon Sep 17 00:00:00 2001 From: linhongyu Date: Tue, 13 Oct 2026 15:18:40 +0800 Subject: [PATCH 4/5] test(parser): cover default-label edge type() filter in expr.sql The default edge label (_ag_label_edge) scans every child edge label, so a `type(r) = '_ag_label_edge'` predicate is not redundant with the pattern edge. No edge is ever stored with this synthetic label, so the filtered result must stay empty. This captures the case requested in review on #2559. Before the parser fix this returned the child edge label ("P"); after it, the result is empty (0 rows). --- regress/expected/expr.out | 13 +++++++++++++ regress/sql/expr.sql | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index fd99a26b6..3f73fe57f 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -9841,6 +9841,19 @@ $$) AS (count agtype); 1 (1 row) +-- The default edge label (_ag_label_edge) scans every child edge label, so a +-- `type(r) = '_ag_label_edge'` predicate is not redundant with the pattern +-- edge and must not be elided. No edge is ever stored with this synthetic +-- label, so the result must stay empty. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:_ag_label_edge]->() + WHERE type(r) = '_ag_label_edge' + RETURN type(r) +$$) AS (edge_type agtype); + edge_type +----------- +(0 rows) + SELECT * FROM cypher('accessor_opt', $$ MATCH ()-[r]->() WHERE type(r) = 'KNOWS' diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 57060834c..38ad819d3 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -3948,6 +3948,16 @@ SELECT * FROM cypher('accessor_opt', $$ RETURN count(r) $$) AS (count agtype); +-- The default edge label (_ag_label_edge) scans every child edge label, so a +-- `type(r) = '_ag_label_edge'` predicate is not redundant with the pattern +-- edge and must not be elided. No edge is ever stored with this synthetic +-- label, so the result must stay empty. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:_ag_label_edge]->() + WHERE type(r) = '_ag_label_edge' + RETURN type(r) +$$) AS (edge_type agtype); + SELECT * FROM cypher('accessor_opt', $$ MATCH ()-[r]->() WHERE type(r) = 'KNOWS' From 917b68ea281484583fc13daa4ba257056dc1b08b Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Fri, 16 Oct 2026 01:50:40 +0800 Subject: [PATCH 5/5] perf(parser): recurse into nested AND when eliding redundant edge type() A predicate is redundant with the edge's static label whenever it sits in an AND-conjunct already implied by the pattern edge, regardless of parenthesization. Previously only the top-level AND args were inspected, so the semantically flat r.v > 0 AND type(r) = 'P' AND r.v < 4 elided the predicate while its parenthesized twin r.v > 0 AND (type(r) = 'P' AND r.v < 4) kept it -- a parentheses-dependent asymmetry. simplify_redundant_edge_type_qual now recurses into nested AND_EXPR groups, while deliberately stopping at OR_EXPR/NOT_EXPR boundaries (crossing an OR branch would be over-elision). A flag tracks elision that happens inside a nested group so the outer conjunction is rebuilt even when its own arg count is unchanged. Regress cases added to expr.sql: nested-AND count, nested-AND EXPLAIN (shows type() elided, siblings preserved), and an OR-group guard. --- regress/expected/expr.out | 61 ++++++++++++++++++++++++++++++ regress/sql/expr.sql | 26 +++++++++++++ src/backend/parser/cypher_clause.c | 25 +++++++++++- 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 3f73fe57f..65d079500 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -9864,6 +9864,67 @@ $$) AS (rid agtype); 1125899906842625 (1 row) +-- A redundant predication nested inside a parenthesized AND-conjunct is still +-- redundant, regardless of parenthesization: it must be elided while the +-- sibling predicate in the same nested group is preserved. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE r.since = 2020 AND (type(r) = 'KNOWS' AND r.since <> -1) + RETURN count(r) +$$) AS (count agtype); + count +------- + 1 +(1 row) + +-- The flat and nested forms above must produce identical plans (the redundant +-- conjunct is gone from both). +SELECT * FROM cypher('accessor_opt', $$ + EXPLAIN (VERBOSE, COSTS OFF) + MATCH ()-[r:KNOWS]->() + WHERE r.since = 2020 AND (type(r) = 'KNOWS' AND r.since <> -1) + RETURN count(r) +$$) AS (plan agtype); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Aggregate + Output: (count(ROW(r.id, _label_name('18222'::oid, r.id), r.end_id, r.start_id, r.properties)))::agtype + -> Hash Join + Output: r.id, r.end_id, r.start_id, r.properties + Hash Cond: (_age_default_alias_1.id = r.end_id) + -> Append + -> Seq Scan on accessor_opt._ag_label_vertex _age_default_alias_1_1 + Output: _age_default_alias_1_1.id + -> Seq Scan on accessor_opt."Person" _age_default_alias_1_2 + Output: _age_default_alias_1_2.id + -> Hash + Output: r.id, r.end_id, r.start_id, r.properties + -> Nested Loop + Output: r.id, r.end_id, r.start_id, r.properties + -> Seq Scan on accessor_opt."KNOWS" r + Output: r.id, r.start_id, r.end_id, r.properties + Filter: ((agtype_access_operator(VARIADIC ARRAY[r.properties, '"since"'::agtype]) = '2020'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[r.properties, '"since"'::agtype]) <> '-1'::agtype)) + -> Append + -> Seq Scan on accessor_opt._ag_label_vertex _age_default_alias_0_1 + Output: _age_default_alias_0_1.id + Filter: (_age_default_alias_0_1.id = r.start_id) + -> Index Only Scan using "Person_pkey" on accessor_opt."Person" _age_default_alias_0_2 + Output: _age_default_alias_0_2.id + Index Cond: (_age_default_alias_0_2.id = r.start_id) +(24 rows) + +-- A redundant predication inside a parenthesized OR-group must NOT be elided: +-- it still filters which rows reach the count. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE (type(r) = 'KNOWS' OR r.since = -1) + RETURN count(r) +$$) AS (count agtype); + count +------- + 1 +(1 row) + -- -- Accessors in ORDER BY -- diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 38ad819d3..bd2983947 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -3964,6 +3964,32 @@ SELECT * FROM cypher('accessor_opt', $$ RETURN id(r) $$) AS (rid agtype); +-- A redundant predication nested inside a parenthesized AND-conjunct is still +-- redundant, regardless of parenthesization: it must be elided while the +-- sibling predicate in the same nested group is preserved. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE r.since = 2020 AND (type(r) = 'KNOWS' AND r.since <> -1) + RETURN count(r) +$$) AS (count agtype); + +-- The flat and nested forms above must produce identical plans (the redundant +-- conjunct is gone from both). +SELECT * FROM cypher('accessor_opt', $$ + EXPLAIN (VERBOSE, COSTS OFF) + MATCH ()-[r:KNOWS]->() + WHERE r.since = 2020 AND (type(r) = 'KNOWS' AND r.since <> -1) + RETURN count(r) +$$) AS (plan agtype); + +-- A redundant predication inside a parenthesized OR-group must NOT be elided: +-- it still filters which rows reach the count. +SELECT * FROM cypher('accessor_opt', $$ + MATCH ()-[r:KNOWS]->() + WHERE (type(r) = 'KNOWS' OR r.since = -1) + RETURN count(r) +$$) AS (count agtype); + -- -- Accessors in ORDER BY -- diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index 7e4b9512b..64ed02397 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -4730,6 +4730,7 @@ static Node *simplify_redundant_edge_type_qual(List *pattern, Node *where) BoolExpr *expression; List *args = NIL; ListCell *cell; + bool changed = false; if (where == NULL) return NULL; @@ -4748,15 +4749,37 @@ static Node *simplify_redundant_edge_type_qual(List *pattern, Node *where) { Node *arg = lfirst(cell); + /* + * Recurse into nested AND-conjunctions: a `type(r) = '