This is an automated email from the ASF dual-hosted git repository.

jrgemignani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git


The following commit(s) were added to refs/heads/master by this push:
     new 639c8d5a Restore contsel/contjoinsel for containment & key-existence 
operators (#2356) (#2417)
639c8d5a is described below

commit 639c8d5ac1cc930095caccb58b90dd65d9035e12
Author: Prashant Chinnam <[email protected]>
AuthorDate: Mon Jun 8 16:55:50 2026 -0700

    Restore contsel/contjoinsel for containment & key-existence operators 
(#2356) (#2417)
    
    The containment (`@>`, `<@`, `@>>`, `<<@`) and key-existence (`?`, `?|`,
    `?&`) operators on `agtype` were bound to `matchingsel`/`matchingjoinsel`
    on the PG14+ source tree. `matchingsel` is built for pattern operators
    (LIKE/regex) and during planning invokes the operator's underlying
    function (`agtype_contains`) once per `pg_statistic` MCV. With realistic
    statistics targets that produces a planner-time regression that
    dominates simple OLTP-style point queries.
    
    Rebind those operators to the lighter `contsel`/`contjoinsel` estimators,
    which return fixed selectivity constants without invoking the operator
    function during planning. This is a deliberate planning-speed vs.
    estimate-accuracy trade-off. Note it DIVERGES from PostgreSQL core, which
    keeps jsonb's `@>`, `<@`, `?`, `?|`, `?&` on `matchingsel`/`matchingjoinsel`
    (verified on REL_16/17/18_STABLE in `pg_operator.dat`); it is an
    AGE-specific choice favoring workloads where these operators appear in
    selective point lookups. A future improvement could add a custom `agtype`
    selectivity function that is both cheap and statistics-aware.
    
    Changes:
    
    * `sql/agtype_operators.sql`, `sql/agtype_exists.sql`: 10 operators
      flipped from `matchingsel`/`matchingjoinsel` to
      `contsel`/`contjoinsel`.
    * `age--1.7.0--y.y.y.sql`: appended `ALTER OPERATOR ... SET (RESTRICT,
      JOIN)` for all 10 operators so existing installs flip on
      `ALTER EXTENSION age UPDATE`.
    * `regress/sql/containment_selectivity.sql` (+ `expected/.out`): pin
      the bindings via `pg_operator`, plus a scoped "no leaked matchingsel"
      guard and functional smoke for all 10 operators. Adds an upgrade-path
      assertion that simulates a stale (pre-fix) install, replays the
      shipped `ALTER OPERATOR` block, and confirms every overload flips to
      `contsel`/`contjoinsel` (run in a rolled-back transaction).
    * `regress/expected/cypher_match.out`, `regress/expected/cypher_vle.out`:
      refresh expected to reflect new (and better) plan shapes that the
      lower-selectivity helper produces — `test_enable_containment` now
      picks Nested Loop + Index Only Scans over a Seq Scan/Hash Join, and
      two `MATCH p=...` and `show_list_use_vle` queries flip row order
      (queries had no `ORDER BY`; result set is unchanged, only ordering).
    * `Makefile`: register `containment_selectivity` in `REGRESS`.
    
    Validation:
    
    * Build: clean, `-Werror`.
    * Regression: 36/37 tests pass under `EXTRA_TESTS="pgvector
      fuzzystrmatch pg_trgm"`. Only `age_upgrade` fails — pre-existing on
      master at 774e781b (verified by `git stash && installcheck`).
    * Reporter's exact methodology (LDBC-SNB-style snb_graph + pgbench on
      `bench_message_content`) reproduces the regression and the fix:
    
      | Metric                     | matchingsel | contsel | Delta |
      |----------------------------|-------------|---------|-------|
      | EXPLAIN planning time (ms) | 1.42        | 0.97    | -32%  |
      | EXPLAIN execution time (ms)| 0.34        | 0.31    | ~equal|
      | pgbench TPS (8c x 30s)     | 5247        | 7378    | +40.6%|
    
      Run with `default_statistics_target = 1000` to populate MCV lists,
      matching the reporter's analyzed-graph conditions.
    
    * Upgrade path: validated end-to-end during the benchmark — operator
      bindings were flipped from `matchingsel` -> `contsel` via the same
      `ALTER OPERATOR` statements the upgrade SQL ships, while operators
      remained functional throughout.
    
    Driver workflows (python/go/node/jdbc) intentionally not run: this PR
    only adjusts pg_operator selectivity metadata. There is no C, type, or
    protocol change that drivers could observe.
    
    Closes #2356.
---
 Makefile                                     |   3 +-
 age--1.7.0--y.y.y.sql                        |  38 +++++
 regress/expected/containment_selectivity.out | 245 +++++++++++++++++++++++++++
 regress/expected/cypher_match.out            |  30 ++--
 regress/expected/cypher_vle.out              |   8 +-
 regress/sql/containment_selectivity.sql      | 156 +++++++++++++++++
 regress/sql/cypher_match.sql                 |   6 +-
 regress/sql/cypher_vle.sql                   |   2 +-
 sql/agtype_exists.sql                        |  24 +--
 sql/agtype_operators.sql                     |  16 +-
 10 files changed, 483 insertions(+), 45 deletions(-)

diff --git a/Makefile b/Makefile
index b79dbd3b..f2a0b9a6 100644
--- a/Makefile
+++ b/Makefile
@@ -183,7 +183,8 @@ REGRESS = scan \
           direct_field_access \
           security \
           reserved_keyword_alias \
-          agtype_jsonb_cast
+          agtype_jsonb_cast \
+          containment_selectivity
 
 ifneq ($(EXTRA_TESTS),)
   REGRESS += $(EXTRA_TESTS)
diff --git a/age--1.7.0--y.y.y.sql b/age--1.7.0--y.y.y.sql
index 74b84d60..a4cac0c5 100644
--- a/age--1.7.0--y.y.y.sql
+++ b/age--1.7.0--y.y.y.sql
@@ -762,3 +762,41 @@ CREATE FUNCTION ag_catalog._agehash_self_test()
     VOLATILE
     PARALLEL UNSAFE
 AS 'MODULE_PATHNAME';
+
+--
+-- Issue #2356: rebind containment and key-existence operators to the
+-- lightweight contsel / contjoinsel selectivity estimators.
+--
+-- @>, <@, @>>, <<@, ?, ?|, ?& on agtype were bound to matchingsel /
+-- matchingjoinsel. During planning matchingsel invokes the operator's
+-- underlying function (agtype_contains) once per pg_statistic MCV entry and
+-- histogram bin; with a realistic default_statistics_target that planning
+-- cost dominates simple point queries (the regression reported in #2356).
+--
+-- contsel / contjoinsel return fixed selectivity constants without calling the
+-- operator function, so planning is constant-time. This is a deliberate
+-- planning-speed vs. estimate-accuracy trade-off. Note it DIVERGES from
+-- PostgreSQL core, which keeps jsonb's @>, <@, ?, ?|, ?& on matchingsel /
+-- matchingjoinsel; it is an AGE-specific choice favoring workloads where these
+-- operators appear in selective point lookups.
+--
+ALTER OPERATOR ag_catalog.@>(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.<@(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.@>>(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.<<@(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, text)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, text[])
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, text[])
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, agtype)
+    SET (RESTRICT = contsel, JOIN = contjoinsel);
diff --git a/regress/expected/containment_selectivity.out 
b/regress/expected/containment_selectivity.out
new file mode 100644
index 00000000..a0626d55
--- /dev/null
+++ b/regress/expected/containment_selectivity.out
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/*
+ * Regression coverage for issue #2356:
+ *   The containment (@>, <@, @>>, <<@) and key-existence (?, ?|, ?&)
+ *   operators on agtype must be bound to the lightweight selectivity
+ *   helpers contsel / contjoinsel during planning. Earlier PG14+
+ *   branches used matchingsel / matchingjoinsel, which caused planning
+ *   to invoke agtype_contains() against pg_statistic MCVs and produced
+ *   a 30%+ planning-time regression on point queries (severe TPS drop
+ *   reported on the PG18 branch).
+ *
+ *   This test pins the bindings by querying pg_operator directly. If
+ *   someone re-introduces matchingsel here, the test diff is loud and
+ *   precise.
+ */
+LOAD 'age';
+SET search_path TO ag_catalog;
+-- Selectivity helpers for the four containment operators.
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@')
+ORDER  BY o.oprname, lhs, rhs;
+ oprname |  lhs   |  rhs   | restrict_fn |   join_fn   
+---------+--------+--------+-------------+-------------
+ <<@     | agtype | agtype | contsel     | contjoinsel
+ <@      | agtype | agtype | contsel     | contjoinsel
+ @>      | agtype | agtype | contsel     | contjoinsel
+ @>>     | agtype | agtype | contsel     | contjoinsel
+(4 rows)
+
+-- Selectivity helpers for all key-existence operator overloads
+-- (right-hand side may be text, text[], or agtype).
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('?', '?|', '?&')
+ORDER  BY o.oprname, lhs, rhs;
+ oprname |  lhs   |  rhs   | restrict_fn |   join_fn   
+---------+--------+--------+-------------+-------------
+ ?       | agtype | agtype | contsel     | contjoinsel
+ ?       | agtype | text   | contsel     | contjoinsel
+ ?&      | agtype | agtype | contsel     | contjoinsel
+ ?&      | agtype | text[] | contsel     | contjoinsel
+ ?|      | agtype | agtype | contsel     | contjoinsel
+ ?|      | agtype | text[] | contsel     | contjoinsel
+(6 rows)
+
+-- Scoped guard for issue #2356: assert that none of the specific containment
+-- and key-existence operators on agtype are bound to matchingsel /
+-- matchingjoinsel. We deliberately limit the check to these operator names
+-- (rather than every operator in ag_catalog) so unrelated operators that
+-- legitimately use matchingsel for their own semantics are not affected by
+-- this regression test.
+SELECT COUNT(*) AS leaked_matchingsel_bindings
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@', '?', '?|', '?&')
+  AND  (o.oprrest::text  = 'matchingsel'
+        OR o.oprjoin::text = 'matchingjoinsel');
+ leaked_matchingsel_bindings 
+-----------------------------
+                           0
+(1 row)
+
+-- Smoke test: each operator still works functionally. Selectivity binding
+-- only affects the planner; this guards against an inadvertent operator
+-- removal as part of any future cleanup.
+SELECT '{"a":1,"b":2}'::agtype @>  '{"a":1}'::agtype             AS 
contains_yes;
+ contains_yes 
+--------------
+ t
+(1 row)
+
+SELECT '{"a":1}'::agtype       <@  '{"a":1,"b":2}'::agtype       AS 
contained_yes;
+ contained_yes 
+---------------
+ t
+(1 row)
+
+SELECT '{"a":{"b":1}}'::agtype @>> '{"a":{"b":1}}'::agtype       AS 
top_contains_yes;
+ top_contains_yes 
+------------------
+ t
+(1 row)
+
+SELECT '{"a":{"b":1}}'::agtype <<@ '{"a":{"b":1}}'::agtype       AS 
top_contained_yes;
+ top_contained_yes 
+-------------------
+ t
+(1 row)
+
+SELECT '{"a":1}'::agtype       ?   'a'::text                     AS 
exists_text_yes;
+ exists_text_yes 
+-----------------
+ t
+(1 row)
+
+SELECT '{"a":1}'::agtype       ?   '"a"'::agtype                 AS 
exists_agtype_yes;
+ exists_agtype_yes 
+-------------------
+ t
+(1 row)
+
+SELECT '{"a":1,"b":2}'::agtype ?|  ARRAY['a','c']                AS 
exists_any_text_yes;
+ exists_any_text_yes 
+---------------------
+ t
+(1 row)
+
+SELECT '{"a":1,"b":2}'::agtype ?|  '["a","c"]'::agtype           AS 
exists_any_agtype_yes;
+ exists_any_agtype_yes 
+-----------------------
+ t
+(1 row)
+
+SELECT '{"a":1,"b":2}'::agtype ?&  ARRAY['a','b']                AS 
exists_all_text_yes;
+ exists_all_text_yes 
+---------------------
+ t
+(1 row)
+
+SELECT '{"a":1,"b":2}'::agtype ?&  '["a","b"]'::agtype           AS 
exists_all_agtype_yes;
+ exists_all_agtype_yes 
+-----------------------
+ t
+(1 row)
+
+-- Upgrade-path assertion for issue #2356.
+--
+-- The checks above cover a FRESH install: contsel / contjoinsel come straight
+-- from agtype_operators.sql and agtype_exists.sql. Existing installs instead
+-- pick up the fix from the ALTER OPERATOR ... SET (RESTRICT, JOIN) block that
+-- age--1.7.0--y.y.y.sql ships and "ALTER EXTENSION age UPDATE" replays. 
Nothing
+-- above exercises that block, so a silent regression in it would go unnoticed.
+--
+-- We replay the shipped ALTER OPERATOR statements directly rather than running
+-- ALTER EXTENSION age UPDATE: the dev upgrade script targets the placeholder
+-- version "y.y.y" and is not a stable version-chain target inside the
+-- regression harness. The whole section runs in a transaction that is rolled
+-- back, so it observes the flip without permanently mutating the operator
+-- catalog (PostgreSQL DDL is transactional).
+BEGIN;
+-- Simulate a stale (pre-fix) install: force all ten overloads back onto
+-- matchingsel / matchingjoinsel.
+ALTER OPERATOR ag_catalog.@>(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.<@(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.@>>(agtype, agtype)  SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.<<@(agtype, agtype)  SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, text)      SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, agtype)    SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, text[])   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, text[])   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+-- Stale state: every overload now reports matchingsel / matchingjoinsel.
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@', '?', '?|', '?&')
+ORDER  BY o.oprname, lhs, rhs;
+ oprname |  lhs   |  rhs   | restrict_fn |     join_fn     
+---------+--------+--------+-------------+-----------------
+ <<@     | agtype | agtype | matchingsel | matchingjoinsel
+ <@      | agtype | agtype | matchingsel | matchingjoinsel
+ ?       | agtype | agtype | matchingsel | matchingjoinsel
+ ?       | agtype | text   | matchingsel | matchingjoinsel
+ ?&      | agtype | agtype | matchingsel | matchingjoinsel
+ ?&      | agtype | text[] | matchingsel | matchingjoinsel
+ ?|      | agtype | agtype | matchingsel | matchingjoinsel
+ ?|      | agtype | text[] | matchingsel | matchingjoinsel
+ @>      | agtype | agtype | matchingsel | matchingjoinsel
+ @>>     | agtype | agtype | matchingsel | matchingjoinsel
+(10 rows)
+
+-- Replay the exact ALTER OPERATOR block shipped in age--1.7.0--y.y.y.sql.
+ALTER OPERATOR ag_catalog.@>(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.<@(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.@>>(agtype, agtype)  SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.<<@(agtype, agtype)  SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, text)      SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, agtype)    SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, text[])   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, text[])   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+-- After the upgrade replay every overload is back on contsel / contjoinsel.
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@', '?', '?|', '?&')
+ORDER  BY o.oprname, lhs, rhs;
+ oprname |  lhs   |  rhs   | restrict_fn |   join_fn   
+---------+--------+--------+-------------+-------------
+ <<@     | agtype | agtype | contsel     | contjoinsel
+ <@      | agtype | agtype | contsel     | contjoinsel
+ ?       | agtype | agtype | contsel     | contjoinsel
+ ?       | agtype | text   | contsel     | contjoinsel
+ ?&      | agtype | agtype | contsel     | contjoinsel
+ ?&      | agtype | text[] | contsel     | contjoinsel
+ ?|      | agtype | agtype | contsel     | contjoinsel
+ ?|      | agtype | text[] | contsel     | contjoinsel
+ @>      | agtype | agtype | contsel     | contjoinsel
+ @>>     | agtype | agtype | contsel     | contjoinsel
+(10 rows)
+
+ROLLBACK;
diff --git a/regress/expected/cypher_match.out 
b/regress/expected/cypher_match.out
index 33c728f2..ab51486b 100644
--- a/regress/expected/cypher_match.out
+++ b/regress/expected/cypher_match.out
@@ -2404,21 +2404,21 @@ SELECT * FROM cypher('cypher_match', $$ MATCH (a 
{name:a.name}) MATCH (a {age:a.
  {"id": 281474976710659, "label": "", "properties": {"age": 3, "name": 
"orphan"}}::vertex
 (3 rows)
 
-SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship}]->(b) RETURN p $$) as (a agtype);
+SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship}]->(b) RETURN p ORDER BY id(u) $$) as (a agtype);
                                                                                
                                                                                
      a                                                                         
                                                                                
            
 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": 
"T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 
281474976710666, "start_id": 281474976710661, "properties": {"years": 3, 
"relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", 
"properties": {"age": 6}}::vertex]::path
  [{"id": 281474976710659, "label": "", "properties": {"age": 3, "name": 
"orphan"}}::vertex, {"id": 4785074604081154, "label": "knows", "end_id": 
281474976710666, "start_id": 281474976710659, "properties": {"years": 4, 
"relationship": "enemies"}}::edge, {"id": 281474976710666, "label": "", 
"properties": {"age": 6}}::vertex]::path
 (2 rows)
 
-SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship, years: u.years}]->(b) RETURN p $$) as (a agtype);
+SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship, years: u.years}]->(b) RETURN p ORDER BY id(u) $$) as (a agtype);
                                                                                
                                                                                
      a                                                                         
                                                                                
            
 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": 
"T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 
281474976710666, "start_id": 281474976710661, "properties": {"years": 3, 
"relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", 
"properties": {"age": 6}}::vertex]::path
  [{"id": 281474976710659, "label": "", "properties": {"age": 3, "name": 
"orphan"}}::vertex, {"id": 4785074604081154, "label": "knows", "end_id": 
281474976710666, "start_id": 281474976710659, "properties": {"years": 4, 
"relationship": "enemies"}}::edge, {"id": 281474976710666, "label": "", 
"properties": {"age": 6}}::vertex]::path
 (2 rows)
 
-SELECT * FROM cypher('cypher_match', $$ MATCH p=(a {name:a.name})-[u 
{relationship: u.relationship}]->(b {age:b.age}) RETURN p $$) as (a agtype);
+SELECT * FROM cypher('cypher_match', $$ MATCH p=(a {name:a.name})-[u 
{relationship: u.relationship}]->(b {age:b.age}) RETURN p ORDER BY id(u) $$) as 
(a agtype);
                                                                                
                                                                                
      a                                                                         
                                                                                
            
 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": 
"T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 
281474976710666, "start_id": 281474976710661, "properties": {"years": 3, 
"relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", 
"properties": {"age": 6}}::vertex]::path
@@ -3398,19 +3398,17 @@ SELECT count(*) FROM cypher('test_enable_containment', 
$$ MATCH p=(x:Customer)-[
 (1 row)
 
 SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH 
(x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 
30}}]->(y:Product) RETURN 0 $$) as (a agtype);
-                                                          QUERY PLAN           
                                                
--------------------------------------------------------------------------------------------------------------------------------
- Hash Join
-   Hash Cond: (y.id = _age_default_alias_0.end_id)
-   ->  Seq Scan on "Product" y
-   ->  Hash
-         ->  Hash Join
-               Hash Cond: (x.id = _age_default_alias_0.start_id)
-               ->  Seq Scan on "Customer" x
-               ->  Hash
-                     ->  Seq Scan on bought _age_default_alias_0
-                           Filter: (properties @>> '{"addr": {"city": 
"Vancouver", "street": 30}, "store": "Amazon"}'::agtype)
-(10 rows)
+                                                    QUERY PLAN                 
                                    
+-------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+   ->  Nested Loop
+         ->  Seq Scan on bought _age_default_alias_0
+               Filter: (properties @>> '{"addr": {"city": "Vancouver", 
"street": 30}, "store": "Amazon"}'::agtype)
+         ->  Index Only Scan using "Customer_pkey" on "Customer" x
+               Index Cond: (id = _age_default_alias_0.start_id)
+   ->  Index Only Scan using "Product_pkey" on "Product" y
+         Index Cond: (id = _age_default_alias_0.end_id)
+(8 rows)
 
 SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH 
(x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 
'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN 0 $$) as (a 
agtype);
                                                                              
QUERY PLAN                                                                      
        
diff --git a/regress/expected/cypher_vle.out b/regress/expected/cypher_vle.out
index 27e3bb82..a09cd4aa 100644
--- a/regress/expected/cypher_vle.out
+++ b/regress/expected/cypher_vle.out
@@ -691,7 +691,7 @@ BEGIN
     RETURN QUERY
     SELECT * FROM cypher('mygraph', $CYPHER$
         MATCH (h:head {name: $list_name})-[e:next*]->(v:node)
-        RETURN v
+        RETURN v ORDER BY id(v)
     $CYPHER$, ag_param) AS (node agtype);
 END $$;
 -- create a list
@@ -726,8 +726,8 @@ SELECT prepend_node('list01', 'b');
 SELECT * FROM show_list_use_vle('list01');
                                        node                                    
    
 
-----------------------------------------------------------------------------------
- {"id": 1407374883553282, "label": "node", "properties": {"content": 
"b"}}::vertex
  {"id": 1407374883553281, "label": "node", "properties": {"content": 
"a"}}::vertex
+ {"id": 1407374883553282, "label": "node", "properties": {"content": 
"b"}}::vertex
 (2 rows)
 
 -- prepend a node 'c'
@@ -741,9 +741,9 @@ SELECT prepend_node('list01', 'c');
 SELECT * FROM show_list_use_vle('list01');
                                        node                                    
    
 
-----------------------------------------------------------------------------------
- {"id": 1407374883553283, "label": "node", "properties": {"content": 
"c"}}::vertex
- {"id": 1407374883553282, "label": "node", "properties": {"content": 
"b"}}::vertex
  {"id": 1407374883553281, "label": "node", "properties": {"content": 
"a"}}::vertex
+ {"id": 1407374883553282, "label": "node", "properties": {"content": 
"b"}}::vertex
+ {"id": 1407374883553283, "label": "node", "properties": {"content": 
"c"}}::vertex
 (3 rows)
 
 DROP FUNCTION show_list_use_vle;
diff --git a/regress/sql/containment_selectivity.sql 
b/regress/sql/containment_selectivity.sql
new file mode 100755
index 00000000..c35ad3fc
--- /dev/null
+++ b/regress/sql/containment_selectivity.sql
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Regression coverage for issue #2356:
+ *   The containment (@>, <@, @>>, <<@) and key-existence (?, ?|, ?&)
+ *   operators on agtype must be bound to the lightweight selectivity
+ *   helpers contsel / contjoinsel during planning. Earlier PG14+
+ *   branches used matchingsel / matchingjoinsel, which caused planning
+ *   to invoke agtype_contains() against pg_statistic MCVs and produced
+ *   a 30%+ planning-time regression on point queries (severe TPS drop
+ *   reported on the PG18 branch).
+ *
+ *   This test pins the bindings by querying pg_operator directly. If
+ *   someone re-introduces matchingsel here, the test diff is loud and
+ *   precise.
+ */
+
+LOAD 'age';
+SET search_path TO ag_catalog;
+
+-- Selectivity helpers for the four containment operators.
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@')
+ORDER  BY o.oprname, lhs, rhs;
+
+-- Selectivity helpers for all key-existence operator overloads
+-- (right-hand side may be text, text[], or agtype).
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('?', '?|', '?&')
+ORDER  BY o.oprname, lhs, rhs;
+
+-- Scoped guard for issue #2356: assert that none of the specific containment
+-- and key-existence operators on agtype are bound to matchingsel /
+-- matchingjoinsel. We deliberately limit the check to these operator names
+-- (rather than every operator in ag_catalog) so unrelated operators that
+-- legitimately use matchingsel for their own semantics are not affected by
+-- this regression test.
+SELECT COUNT(*) AS leaked_matchingsel_bindings
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@', '?', '?|', '?&')
+  AND  (o.oprrest::text  = 'matchingsel'
+        OR o.oprjoin::text = 'matchingjoinsel');
+
+-- Smoke test: each operator still works functionally. Selectivity binding
+-- only affects the planner; this guards against an inadvertent operator
+-- removal as part of any future cleanup.
+SELECT '{"a":1,"b":2}'::agtype @>  '{"a":1}'::agtype             AS 
contains_yes;
+SELECT '{"a":1}'::agtype       <@  '{"a":1,"b":2}'::agtype       AS 
contained_yes;
+SELECT '{"a":{"b":1}}'::agtype @>> '{"a":{"b":1}}'::agtype       AS 
top_contains_yes;
+SELECT '{"a":{"b":1}}'::agtype <<@ '{"a":{"b":1}}'::agtype       AS 
top_contained_yes;
+SELECT '{"a":1}'::agtype       ?   'a'::text                     AS 
exists_text_yes;
+SELECT '{"a":1}'::agtype       ?   '"a"'::agtype                 AS 
exists_agtype_yes;
+SELECT '{"a":1,"b":2}'::agtype ?|  ARRAY['a','c']                AS 
exists_any_text_yes;
+SELECT '{"a":1,"b":2}'::agtype ?|  '["a","c"]'::agtype           AS 
exists_any_agtype_yes;
+SELECT '{"a":1,"b":2}'::agtype ?&  ARRAY['a','b']                AS 
exists_all_text_yes;
+SELECT '{"a":1,"b":2}'::agtype ?&  '["a","b"]'::agtype           AS 
exists_all_agtype_yes;
+
+-- Upgrade-path assertion for issue #2356.
+--
+-- The checks above cover a FRESH install: contsel / contjoinsel come straight
+-- from agtype_operators.sql and agtype_exists.sql. Existing installs instead
+-- pick up the fix from the ALTER OPERATOR ... SET (RESTRICT, JOIN) block that
+-- age--1.7.0--y.y.y.sql ships and "ALTER EXTENSION age UPDATE" replays. 
Nothing
+-- above exercises that block, so a silent regression in it would go unnoticed.
+--
+-- We replay the shipped ALTER OPERATOR statements directly rather than running
+-- ALTER EXTENSION age UPDATE: the dev upgrade script targets the placeholder
+-- version "y.y.y" and is not a stable version-chain target inside the
+-- regression harness. The whole section runs in a transaction that is rolled
+-- back, so it observes the flip without permanently mutating the operator
+-- catalog (PostgreSQL DDL is transactional).
+BEGIN;
+
+-- Simulate a stale (pre-fix) install: force all ten overloads back onto
+-- matchingsel / matchingjoinsel.
+ALTER OPERATOR ag_catalog.@>(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.<@(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.@>>(agtype, agtype)  SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.<<@(agtype, agtype)  SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, text)      SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, agtype)    SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, text[])   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, text[])   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, agtype)   SET (RESTRICT = matchingsel, 
JOIN = matchingjoinsel);
+
+-- Stale state: every overload now reports matchingsel / matchingjoinsel.
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@', '?', '?|', '?&')
+ORDER  BY o.oprname, lhs, rhs;
+
+-- Replay the exact ALTER OPERATOR block shipped in age--1.7.0--y.y.y.sql.
+ALTER OPERATOR ag_catalog.@>(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.<@(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.@>>(agtype, agtype)  SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.<<@(agtype, agtype)  SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, text)      SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?(agtype, agtype)    SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, text[])   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?|(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, text[])   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+ALTER OPERATOR ag_catalog.?&(agtype, agtype)   SET (RESTRICT = contsel, JOIN = 
contjoinsel);
+
+-- After the upgrade replay every overload is back on contsel / contjoinsel.
+SELECT o.oprname,
+       pg_catalog.format_type(o.oprleft,  NULL) AS lhs,
+       pg_catalog.format_type(o.oprright, NULL) AS rhs,
+       o.oprrest::text  AS restrict_fn,
+       o.oprjoin::text  AS join_fn
+FROM   pg_catalog.pg_operator o
+JOIN   pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
+WHERE  n.nspname = 'ag_catalog'
+  AND  o.oprname IN ('@>', '<@', '@>>', '<<@', '?', '?|', '?&')
+ORDER  BY o.oprname, lhs, rhs;
+
+ROLLBACK;
diff --git a/regress/sql/cypher_match.sql b/regress/sql/cypher_match.sql
index e56aafac..8da012ff 100644
--- a/regress/sql/cypher_match.sql
+++ b/regress/sql/cypher_match.sql
@@ -1068,9 +1068,9 @@ SELECT * FROM cypher('cypher_match', $$ MATCH (a 
{name:a.name}) RETURN a $$) as
 SELECT * FROM cypher('cypher_match', $$ MATCH (a {name:a.name, age:a.age}) 
RETURN a $$) as (a agtype);
 SELECT * FROM cypher('cypher_match', $$ MATCH (a {name:a.name}) MATCH (a 
{age:a.age}) RETURN a $$) as (a agtype);
 
-SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship}]->(b) RETURN p $$) as (a agtype);
-SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship, years: u.years}]->(b) RETURN p $$) as (a agtype);
-SELECT * FROM cypher('cypher_match', $$ MATCH p=(a {name:a.name})-[u 
{relationship: u.relationship}]->(b {age:b.age}) RETURN p $$) as (a agtype);
+SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship}]->(b) RETURN p ORDER BY id(u) $$) as (a agtype);
+SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: 
u.relationship, years: u.years}]->(b) RETURN p ORDER BY id(u) $$) as (a agtype);
+SELECT * FROM cypher('cypher_match', $$ MATCH p=(a {name:a.name})-[u 
{relationship: u.relationship}]->(b {age:b.age}) RETURN p ORDER BY id(u) $$) as 
(a agtype);
 
 SELECT * FROM cypher('cypher_match', $$ CREATE () WITH * MATCH (x{n0:x.n1}) 
RETURN 0 $$) as (a agtype);
 
diff --git a/regress/sql/cypher_vle.sql b/regress/sql/cypher_vle.sql
index c960aa7a..b121234e 100644
--- a/regress/sql/cypher_vle.sql
+++ b/regress/sql/cypher_vle.sql
@@ -264,7 +264,7 @@ BEGIN
     RETURN QUERY
     SELECT * FROM cypher('mygraph', $CYPHER$
         MATCH (h:head {name: $list_name})-[e:next*]->(v:node)
-        RETURN v
+        RETURN v ORDER BY id(v)
     $CYPHER$, ag_param) AS (node agtype);
 END $$;
 
diff --git a/sql/agtype_exists.sql b/sql/agtype_exists.sql
index 441af175..f4ec4660 100644
--- a/sql/agtype_exists.sql
+++ b/sql/agtype_exists.sql
@@ -32,8 +32,8 @@ CREATE OPERATOR ? (
   LEFTARG = agtype,
   RIGHTARG = text,
   FUNCTION = ag_catalog.agtype_exists,
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_exists_agtype(agtype, agtype)
@@ -48,8 +48,8 @@ CREATE OPERATOR ? (
   LEFTARG = agtype,
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_exists_agtype,
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_exists_any(agtype, text[])
@@ -64,8 +64,8 @@ CREATE OPERATOR ?| (
   LEFTARG = agtype,
   RIGHTARG = text[],
   FUNCTION = ag_catalog.agtype_exists_any,
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_exists_any_agtype(agtype, agtype)
@@ -80,8 +80,8 @@ CREATE OPERATOR ?| (
   LEFTARG = agtype,
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_exists_any_agtype,
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_exists_all(agtype, text[])
@@ -96,8 +96,8 @@ CREATE OPERATOR ?& (
   LEFTARG = agtype,
   RIGHTARG = text[],
   FUNCTION = ag_catalog.agtype_exists_all,
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_exists_all_agtype(agtype, agtype)
@@ -112,6 +112,6 @@ CREATE OPERATOR ?& (
   LEFTARG = agtype,
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_exists_all_agtype,
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
diff --git a/sql/agtype_operators.sql b/sql/agtype_operators.sql
index 36fedfe8..3fbc52f3 100644
--- a/sql/agtype_operators.sql
+++ b/sql/agtype_operators.sql
@@ -33,8 +33,8 @@ CREATE OPERATOR @> (
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_contains,
   COMMUTATOR = '<@',
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_contained_by(agtype, agtype)
@@ -50,8 +50,8 @@ CREATE OPERATOR <@ (
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_contained_by,
   COMMUTATOR = '@>',
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_contains_top_level(agtype, agtype)
@@ -67,8 +67,8 @@ CREATE OPERATOR @>> (
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_contains_top_level,
   COMMUTATOR = '<<@',
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
 
 CREATE FUNCTION ag_catalog.agtype_contained_by_top_level(agtype, agtype)
@@ -84,6 +84,6 @@ CREATE OPERATOR <<@ (
   RIGHTARG = agtype,
   FUNCTION = ag_catalog.agtype_contained_by_top_level,
   COMMUTATOR = '@>>',
-  RESTRICT = matchingsel,
-  JOIN = matchingjoinsel
+  RESTRICT = contsel,
+  JOIN = contjoinsel
 );
\ No newline at end of file


Reply via email to