Skip to content

Commit bd52fd1

Browse files
committed
Restore contsel/contjoinsel for containment & key-existence operators (#2356)
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. Restore the lighter `contsel`/`contjoinsel` helpers used by PostgreSQL core's jsonb operators (`@>`, `<@`, `?` on jsonb), which matches upstream's long-standing precedent for the same operator family. 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 "no leaked matchingsel" aggregate guard and functional smoke for all 10 operators. The guard catches future regressions if a new operator is added without the right selectivity helper. * `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 774e781 (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.
1 parent 54e19fa commit bd52fd1

8 files changed

Lines changed: 312 additions & 39 deletions

File tree

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ REGRESS = scan \
180180
map_projection \
181181
direct_field_access \
182182
security \
183-
reserved_keyword_alias
183+
reserved_keyword_alias \
184+
containment_selectivity
184185

185186
ifneq ($(EXTRA_TESTS),)
186187
REGRESS += $(EXTRA_TESTS)

age--1.7.0--y.y.y.sql

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,38 @@ BEGIN
459459
END LOOP;
460460
END;
461461
$$;
462+
463+
464+
--
465+
-- Issue #2356: restore lightweight selectivity functions for containment
466+
-- and key-existence operators.
467+
--
468+
-- The PG14+ branches of AGE bound RESTRICT=matchingsel / JOIN=matchingjoinsel
469+
-- on @>, <@, @>>, <<@, ?, ?|, ?&. matchingsel is built for pattern operators
470+
-- (LIKE / regex) and invokes the operator's underlying support function on
471+
-- pg_statistic MCVs during planning. For agtype that re-runs agtype_contains
472+
-- per MCV, which can dominate planning time on point queries (TPS regression
473+
-- reported on PG18). PostgreSQL core itself binds @>/<@/? on jsonb to
474+
-- contsel/contjoinsel for the same reason; this aligns AGE with that
475+
-- precedent.
476+
--
477+
ALTER OPERATOR ag_catalog.@>(agtype, agtype)
478+
SET (RESTRICT = contsel, JOIN = contjoinsel);
479+
ALTER OPERATOR ag_catalog.<@(agtype, agtype)
480+
SET (RESTRICT = contsel, JOIN = contjoinsel);
481+
ALTER OPERATOR ag_catalog.@>>(agtype, agtype)
482+
SET (RESTRICT = contsel, JOIN = contjoinsel);
483+
ALTER OPERATOR ag_catalog.<<@(agtype, agtype)
484+
SET (RESTRICT = contsel, JOIN = contjoinsel);
485+
ALTER OPERATOR ag_catalog.?(agtype, text)
486+
SET (RESTRICT = contsel, JOIN = contjoinsel);
487+
ALTER OPERATOR ag_catalog.?(agtype, agtype)
488+
SET (RESTRICT = contsel, JOIN = contjoinsel);
489+
ALTER OPERATOR ag_catalog.?|(agtype, text[])
490+
SET (RESTRICT = contsel, JOIN = contjoinsel);
491+
ALTER OPERATOR ag_catalog.?|(agtype, agtype)
492+
SET (RESTRICT = contsel, JOIN = contjoinsel);
493+
ALTER OPERATOR ag_catalog.?&(agtype, text[])
494+
SET (RESTRICT = contsel, JOIN = contjoinsel);
495+
ALTER OPERATOR ag_catalog.?&(agtype, agtype)
496+
SET (RESTRICT = contsel, JOIN = contjoinsel);
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
/*
20+
* Regression coverage for issue #2356:
21+
* The containment (@>, <@, @>>, <<@) and key-existence (?, ?|, ?&)
22+
* operators on agtype must be bound to the lightweight selectivity
23+
* helpers contsel / contjoinsel during planning. Earlier PG14+
24+
* branches used matchingsel / matchingjoinsel, which caused planning
25+
* to invoke agtype_contains() against pg_statistic MCVs and produced
26+
* a 30%+ planning-time regression on point queries (severe TPS drop
27+
* reported on the PG18 branch).
28+
*
29+
* This test pins the bindings by querying pg_operator directly. If
30+
* someone re-introduces matchingsel here, the test diff is loud and
31+
* precise.
32+
*/
33+
LOAD 'age';
34+
SET search_path TO ag_catalog;
35+
-- Selectivity helpers for the four containment operators.
36+
SELECT o.oprname,
37+
pg_catalog.format_type(o.oprleft, NULL) AS lhs,
38+
pg_catalog.format_type(o.oprright, NULL) AS rhs,
39+
o.oprrest::text AS restrict_fn,
40+
o.oprjoin::text AS join_fn
41+
FROM pg_catalog.pg_operator o
42+
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
43+
WHERE n.nspname = 'ag_catalog'
44+
AND o.oprname IN ('@>', '<@', '@>>', '<<@')
45+
ORDER BY o.oprname, lhs, rhs;
46+
oprname | lhs | rhs | restrict_fn | join_fn
47+
---------+--------+--------+-------------+-------------
48+
<<@ | agtype | agtype | contsel | contjoinsel
49+
<@ | agtype | agtype | contsel | contjoinsel
50+
@> | agtype | agtype | contsel | contjoinsel
51+
@>> | agtype | agtype | contsel | contjoinsel
52+
(4 rows)
53+
54+
-- Selectivity helpers for all key-existence operator overloads
55+
-- (right-hand side may be text, text[], or agtype).
56+
SELECT o.oprname,
57+
pg_catalog.format_type(o.oprleft, NULL) AS lhs,
58+
pg_catalog.format_type(o.oprright, NULL) AS rhs,
59+
o.oprrest::text AS restrict_fn,
60+
o.oprjoin::text AS join_fn
61+
FROM pg_catalog.pg_operator o
62+
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
63+
WHERE n.nspname = 'ag_catalog'
64+
AND o.oprname IN ('?', '?|', '?&')
65+
ORDER BY o.oprname, lhs, rhs;
66+
oprname | lhs | rhs | restrict_fn | join_fn
67+
---------+--------+--------+-------------+-------------
68+
? | agtype | agtype | contsel | contjoinsel
69+
? | agtype | text | contsel | contjoinsel
70+
?& | agtype | agtype | contsel | contjoinsel
71+
?& | agtype | text[] | contsel | contjoinsel
72+
?| | agtype | agtype | contsel | contjoinsel
73+
?| | agtype | text[] | contsel | contjoinsel
74+
(6 rows)
75+
76+
-- A single aggregate guard: there must be NO operator in ag_catalog whose
77+
-- selectivity is still bound to matchingsel / matchingjoinsel. This is the
78+
-- catch-all that keeps issue #2356 from silently regressing if a future
79+
-- operator is added.
80+
SELECT COUNT(*) AS leaked_matchingsel_bindings
81+
FROM pg_catalog.pg_operator o
82+
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
83+
WHERE n.nspname = 'ag_catalog'
84+
AND (o.oprrest::text IN ('matchingsel')
85+
OR o.oprjoin::text IN ('matchingjoinsel'));
86+
leaked_matchingsel_bindings
87+
-----------------------------
88+
0
89+
(1 row)
90+
91+
-- Smoke test: each operator still works functionally. Selectivity binding
92+
-- only affects the planner; this guards against an inadvertent operator
93+
-- removal as part of any future cleanup.
94+
SELECT '{"a":1,"b":2}'::agtype @> '{"a":1}'::agtype AS contains_yes;
95+
contains_yes
96+
--------------
97+
t
98+
(1 row)
99+
100+
SELECT '{"a":1}'::agtype <@ '{"a":1,"b":2}'::agtype AS contained_yes;
101+
contained_yes
102+
---------------
103+
t
104+
(1 row)
105+
106+
SELECT '{"a":{"b":1}}'::agtype @>> '{"a":{"b":1}}'::agtype AS top_contains_yes;
107+
top_contains_yes
108+
------------------
109+
t
110+
(1 row)
111+
112+
SELECT '{"a":{"b":1}}'::agtype <<@ '{"a":{"b":1}}'::agtype AS top_contained_yes;
113+
top_contained_yes
114+
-------------------
115+
t
116+
(1 row)
117+
118+
SELECT '{"a":1}'::agtype ? 'a'::text AS exists_text_yes;
119+
exists_text_yes
120+
-----------------
121+
t
122+
(1 row)
123+
124+
SELECT '{"a":1}'::agtype ? '"a"'::agtype AS exists_agtype_yes;
125+
exists_agtype_yes
126+
-------------------
127+
t
128+
(1 row)
129+
130+
SELECT '{"a":1,"b":2}'::agtype ?| ARRAY['a','c'] AS exists_any_text_yes;
131+
exists_any_text_yes
132+
---------------------
133+
t
134+
(1 row)
135+
136+
SELECT '{"a":1,"b":2}'::agtype ?| '["a","c"]'::agtype AS exists_any_agtype_yes;
137+
exists_any_agtype_yes
138+
-----------------------
139+
t
140+
(1 row)
141+
142+
SELECT '{"a":1,"b":2}'::agtype ?& ARRAY['a','b'] AS exists_all_text_yes;
143+
exists_all_text_yes
144+
---------------------
145+
t
146+
(1 row)
147+
148+
SELECT '{"a":1,"b":2}'::agtype ?& '["a","b"]'::agtype AS exists_all_agtype_yes;
149+
exists_all_agtype_yes
150+
-----------------------
151+
t
152+
(1 row)
153+

regress/expected/cypher_match.out

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,15 +2407,15 @@ SELECT * FROM cypher('cypher_match', $$ MATCH (a {name:a.name}) MATCH (a {age:a.
24072407
SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: u.relationship}]->(b) RETURN p $$) as (a agtype);
24082408
a
24092409
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2410-
[{"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
24112410
[{"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
2411+
[{"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
24122412
(2 rows)
24132413

24142414
SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: u.relationship, years: u.years}]->(b) RETURN p $$) as (a agtype);
24152415
a
24162416
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2417-
[{"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
24182417
[{"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
2418+
[{"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
24192419
(2 rows)
24202420

24212421
SELECT * FROM cypher('cypher_match', $$ MATCH p=(a {name:a.name})-[u {relationship: u.relationship}]->(b {age:b.age}) RETURN p $$) as (a agtype);
@@ -3398,19 +3398,17 @@ SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[
33983398
(1 row)
33993399

34003400
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);
3401-
QUERY PLAN
3402-
-------------------------------------------------------------------------------------------------------------------------------
3403-
Hash Join
3404-
Hash Cond: (y.id = _age_default_alias_0.end_id)
3405-
-> Seq Scan on "Product" y
3406-
-> Hash
3407-
-> Hash Join
3408-
Hash Cond: (x.id = _age_default_alias_0.start_id)
3409-
-> Seq Scan on "Customer" x
3410-
-> Hash
3411-
-> Seq Scan on bought _age_default_alias_0
3412-
Filter: (properties @>> '{"addr": {"city": "Vancouver", "street": 30}, "store": "Amazon"}'::agtype)
3413-
(10 rows)
3401+
QUERY PLAN
3402+
-------------------------------------------------------------------------------------------------------------------
3403+
Nested Loop
3404+
-> Nested Loop
3405+
-> Seq Scan on bought _age_default_alias_0
3406+
Filter: (properties @>> '{"addr": {"city": "Vancouver", "street": 30}, "store": "Amazon"}'::agtype)
3407+
-> Index Only Scan using "Customer_pkey" on "Customer" x
3408+
Index Cond: (id = _age_default_alias_0.start_id)
3409+
-> Index Only Scan using "Product_pkey" on "Product" y
3410+
Index Cond: (id = _age_default_alias_0.end_id)
3411+
(8 rows)
34143412

34153413
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);
34163414
QUERY PLAN

regress/expected/cypher_vle.out

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,8 @@ SELECT prepend_node('list01', 'b');
726726
SELECT * FROM show_list_use_vle('list01');
727727
node
728728
-----------------------------------------------------------------------------------
729-
{"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
730729
{"id": 1407374883553281, "label": "node", "properties": {"content": "a"}}::vertex
730+
{"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
731731
(2 rows)
732732

733733
-- prepend a node 'c'
@@ -741,9 +741,9 @@ SELECT prepend_node('list01', 'c');
741741
SELECT * FROM show_list_use_vle('list01');
742742
node
743743
-----------------------------------------------------------------------------------
744-
{"id": 1407374883553283, "label": "node", "properties": {"content": "c"}}::vertex
745-
{"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
746744
{"id": 1407374883553281, "label": "node", "properties": {"content": "a"}}::vertex
745+
{"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
746+
{"id": 1407374883553283, "label": "node", "properties": {"content": "c"}}::vertex
747747
(3 rows)
748748

749749
DROP FUNCTION show_list_use_vle;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/*
21+
* Regression coverage for issue #2356:
22+
* The containment (@>, <@, @>>, <<@) and key-existence (?, ?|, ?&)
23+
* operators on agtype must be bound to the lightweight selectivity
24+
* helpers contsel / contjoinsel during planning. Earlier PG14+
25+
* branches used matchingsel / matchingjoinsel, which caused planning
26+
* to invoke agtype_contains() against pg_statistic MCVs and produced
27+
* a 30%+ planning-time regression on point queries (severe TPS drop
28+
* reported on the PG18 branch).
29+
*
30+
* This test pins the bindings by querying pg_operator directly. If
31+
* someone re-introduces matchingsel here, the test diff is loud and
32+
* precise.
33+
*/
34+
35+
LOAD 'age';
36+
SET search_path TO ag_catalog;
37+
38+
-- Selectivity helpers for the four containment operators.
39+
SELECT o.oprname,
40+
pg_catalog.format_type(o.oprleft, NULL) AS lhs,
41+
pg_catalog.format_type(o.oprright, NULL) AS rhs,
42+
o.oprrest::text AS restrict_fn,
43+
o.oprjoin::text AS join_fn
44+
FROM pg_catalog.pg_operator o
45+
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
46+
WHERE n.nspname = 'ag_catalog'
47+
AND o.oprname IN ('@>', '<@', '@>>', '<<@')
48+
ORDER BY o.oprname, lhs, rhs;
49+
50+
-- Selectivity helpers for all key-existence operator overloads
51+
-- (right-hand side may be text, text[], or agtype).
52+
SELECT o.oprname,
53+
pg_catalog.format_type(o.oprleft, NULL) AS lhs,
54+
pg_catalog.format_type(o.oprright, NULL) AS rhs,
55+
o.oprrest::text AS restrict_fn,
56+
o.oprjoin::text AS join_fn
57+
FROM pg_catalog.pg_operator o
58+
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
59+
WHERE n.nspname = 'ag_catalog'
60+
AND o.oprname IN ('?', '?|', '?&')
61+
ORDER BY o.oprname, lhs, rhs;
62+
63+
-- A single aggregate guard: there must be NO operator in ag_catalog whose
64+
-- selectivity is still bound to matchingsel / matchingjoinsel. This is the
65+
-- catch-all that keeps issue #2356 from silently regressing if a future
66+
-- operator is added.
67+
SELECT COUNT(*) AS leaked_matchingsel_bindings
68+
FROM pg_catalog.pg_operator o
69+
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
70+
WHERE n.nspname = 'ag_catalog'
71+
AND (o.oprrest::text IN ('matchingsel')
72+
OR o.oprjoin::text IN ('matchingjoinsel'));
73+
74+
-- Smoke test: each operator still works functionally. Selectivity binding
75+
-- only affects the planner; this guards against an inadvertent operator
76+
-- removal as part of any future cleanup.
77+
SELECT '{"a":1,"b":2}'::agtype @> '{"a":1}'::agtype AS contains_yes;
78+
SELECT '{"a":1}'::agtype <@ '{"a":1,"b":2}'::agtype AS contained_yes;
79+
SELECT '{"a":{"b":1}}'::agtype @>> '{"a":{"b":1}}'::agtype AS top_contains_yes;
80+
SELECT '{"a":{"b":1}}'::agtype <<@ '{"a":{"b":1}}'::agtype AS top_contained_yes;
81+
SELECT '{"a":1}'::agtype ? 'a'::text AS exists_text_yes;
82+
SELECT '{"a":1}'::agtype ? '"a"'::agtype AS exists_agtype_yes;
83+
SELECT '{"a":1,"b":2}'::agtype ?| ARRAY['a','c'] AS exists_any_text_yes;
84+
SELECT '{"a":1,"b":2}'::agtype ?| '["a","c"]'::agtype AS exists_any_agtype_yes;
85+
SELECT '{"a":1,"b":2}'::agtype ?& ARRAY['a','b'] AS exists_all_text_yes;
86+
SELECT '{"a":1,"b":2}'::agtype ?& '["a","b"]'::agtype AS exists_all_agtype_yes;

0 commit comments

Comments
 (0)