Skip to content

Commit 5c8b91f

Browse files
committed
feat: Adds new filters to increase Postgrest parity
Adds like, ilike, is, match, imatch, and isdistinct filter operators to realtime subscriptions, along with a negate flag on user_defined_filter to support NOT variants (e.g. IS NOT, IS NOT DISTINCT FROM). This increases our parity with Postgrest filters
1 parent 6f3d262 commit 5c8b91f

22 files changed

Lines changed: 1034 additions & 339 deletions

bin/installcheck

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ REGRESS="${PGXS}/../test/regress/pg_regress"
4141
TESTS=$(ls ${TESTDIR}/sql | sed -e 's/\..*$//' | sort )
4242

4343
# Execute the test fixtures
44-
psql -v ON_ERROR_STOP=1 -f sql/setup.sql -f sql/walrus--0.1.sql -f sql/walrus_migration_0001*.sql -f sql/walrus_migration_0002*.sql -f sql/walrus_migration_0003*.sql -f sql/walrus_migration_0004*.sql -f sql/walrus_migration_0005*.sql -f sql/walrus_migration_0006*.sql -f sql/walrus_migration_0007*.sql -f sql/walrus_migration_0008*.sql -f sql/walrus_migration_0009*.sql -f sql/walrus_migration_0010*.sql -f sql/walrus_migration_0011*.sql -f sql/walrus_migration_0012*.sql -f sql/walrus_migration_0013*.sql -f test/fixtures.sql -d contrib_regression
44+
psql -v ON_ERROR_STOP=1 -f sql/setup.sql -f sql/walrus--0.1.sql -f sql/walrus_migration_0001*.sql -f sql/walrus_migration_0002*.sql -f sql/walrus_migration_0003*.sql -f sql/walrus_migration_0004*.sql -f sql/walrus_migration_0005*.sql -f sql/walrus_migration_0006*.sql -f sql/walrus_migration_0007*.sql -f sql/walrus_migration_0008*.sql -f sql/walrus_migration_0009*.sql -f sql/walrus_migration_0010*.sql -f sql/walrus_migration_0011*.sql -f sql/walrus_migration_0012*.sql -f sql/walrus_migration_0013*.sql -f sql/walrus_migration_0014*.sql -f test/fixtures.sql -d contrib_regression
4545

4646
# Run tests
4747
${REGRESS} --use-existing --dbname=contrib_regression --inputdir=${TESTDIR} ${TESTS}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
alter type realtime.equality_op add value 'like';
2+
alter type realtime.equality_op add value 'ilike';
3+
alter type realtime.equality_op add value 'is';
4+
alter type realtime.equality_op add value 'match';
5+
alter type realtime.equality_op add value 'imatch';
6+
alter type realtime.equality_op add value 'isdistinct';
7+
8+
alter type realtime.user_defined_filter add attribute negate boolean;
9+
10+
create or replace function realtime.check_equality_op(
11+
op realtime.equality_op,
12+
type_ regtype,
13+
val_1 text,
14+
val_2 text,
15+
negate boolean DEFAULT false
16+
)
17+
returns bool
18+
immutable
19+
language plpgsql
20+
as $$
21+
declare
22+
op_symbol text;
23+
res boolean;
24+
begin
25+
-- IS DISTINCT FROM / IS NOT DISTINCT FROM: infix, both sides typed literals
26+
if op = 'isdistinct' then
27+
execute format(
28+
'select %L::%s %s %L::%s',
29+
val_1,
30+
type_::text,
31+
case when negate then 'IS NOT DISTINCT FROM' else 'IS DISTINCT FROM' end,
32+
val_2,
33+
type_::text
34+
) into res;
35+
return res;
36+
end if;
37+
38+
-- IS requires a keyword RHS (NULL, TRUE, FALSE, UNKNOWN), not a typed literal
39+
if op = 'is' then
40+
if val_2 not in ('null', 'true', 'false', 'unknown') then
41+
raise exception 'invalid value for is filter: must be null, true, false, or unknown';
42+
end if;
43+
execute format(
44+
'select %L::%s %s %s',
45+
val_1,
46+
type_::text,
47+
case when negate then 'IS NOT' else 'IS' end,
48+
upper(val_2)
49+
) into res;
50+
return res;
51+
end if;
52+
53+
op_symbol = case
54+
when op = 'eq' then '='
55+
when op = 'neq' then '!='
56+
when op = 'lt' then '<'
57+
when op = 'lte' then '<='
58+
when op = 'gt' then '>'
59+
when op = 'gte' then '>='
60+
when op = 'in' then '= any'
61+
when op = 'like' then 'LIKE'
62+
when op = 'ilike' then 'ILIKE'
63+
when op = 'match' then '~'
64+
when op = 'imatch' then '~*'
65+
else null
66+
end;
67+
68+
if op_symbol is null then
69+
raise exception 'unsupported equality operator: %', op::text;
70+
end if;
71+
72+
execute format(
73+
'select %L::' || type_::text || ' ' || op_symbol
74+
|| ' ( %L::'
75+
|| (case when op = 'in' then type_::text || '[]' else type_::text end)
76+
|| ')', val_1, val_2
77+
) into res;
78+
79+
return case when negate then not res else res end;
80+
end;
81+
$$;
82+
83+
84+
create or replace function realtime.is_visible_through_filters(columns realtime.wal_column[], filters realtime.user_defined_filter[])
85+
returns bool
86+
language sql
87+
immutable
88+
as $$
89+
select
90+
coalesce(
91+
sum(
92+
realtime.check_equality_op(
93+
op:=f.op,
94+
type_:=col.type_oid::regtype,
95+
val_1:=col.value #>> '{}',
96+
val_2:=f.value,
97+
negate:=coalesce(f.negate, false)
98+
)::int
99+
) = count(1),
100+
true
101+
)
102+
from
103+
unnest(filters) f
104+
join unnest(columns) col
105+
on f.column_name = col.name;
106+
$$;
107+
108+
109+
create or replace function realtime.subscription_check_filters()
110+
returns trigger
111+
language plpgsql
112+
as $$
113+
declare
114+
col_names text[] = coalesce(
115+
array_agg(c.column_name order by c.ordinal_position),
116+
'{}'::text[]
117+
)
118+
from
119+
information_schema.columns c
120+
where
121+
format('%I.%I', c.table_schema, c.table_name)::regclass = new.entity
122+
and pg_catalog.has_column_privilege(
123+
(new.claims ->> 'role'),
124+
format('%I.%I', c.table_schema, c.table_name)::regclass,
125+
c.column_name,
126+
'SELECT'
127+
);
128+
table_col_names text[] = coalesce(
129+
array_agg(pa.attname),
130+
'{}'::text[]
131+
)
132+
from
133+
pg_attribute pa
134+
where
135+
pa.attrelid = new.entity
136+
and pa.attnum > 0;
137+
filter realtime.user_defined_filter;
138+
col_type regtype;
139+
in_val jsonb;
140+
selected_col text;
141+
begin
142+
for filter in select * from unnest(new.filters) loop
143+
if not filter.column_name = any(col_names) then
144+
raise exception 'invalid column for filter %', filter.column_name;
145+
end if;
146+
147+
col_type = (
148+
select atttypid::regtype
149+
from pg_catalog.pg_attribute
150+
where attrelid = new.entity
151+
and attname = filter.column_name
152+
);
153+
if col_type is null then
154+
raise exception 'failed to lookup type for column %', filter.column_name;
155+
end if;
156+
157+
if filter.op = 'in'::realtime.equality_op then
158+
in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);
159+
if coalesce(jsonb_array_length(in_val), 0) > 100 then
160+
raise exception 'too many values for `in` filter. Maximum 100';
161+
end if;
162+
elsif filter.op = 'is'::realtime.equality_op then
163+
if filter.value not in ('null', 'true', 'false', 'unknown') then
164+
raise exception 'invalid value for is filter: must be null, true, false, or unknown';
165+
end if;
166+
else
167+
perform realtime.cast(filter.value, col_type);
168+
end if;
169+
end loop;
170+
171+
if new.selected_columns is not null then
172+
for selected_col in select * from unnest(new.selected_columns) loop
173+
if not selected_col = any(col_names) then
174+
raise exception 'invalid column for select %', selected_col;
175+
end if;
176+
end loop;
177+
end if;
178+
179+
new.filters = coalesce(
180+
array_agg(f order by f.column_name, f.op, f.value),
181+
'{}'
182+
) from unnest(new.filters) f;
183+
184+
new.selected_columns = (
185+
select array_agg(c order by c)
186+
from unnest(new.selected_columns) c
187+
);
188+
189+
return new;
190+
end;
191+
$$;

test/expected/issue_40_quoted_regtype.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ select
2525
'role', 'authenticated',
2626
'sub', seed_uuid(2)::text
2727
),
28-
array[('primary_color', 'eq', 'RED')::realtime.user_defined_filter];
28+
array[('primary_color', 'eq', 'RED', null)::realtime.user_defined_filter];
2929
insert into public.notes(id, primary_color)
3030
values
3131
(1, 'RED'), -- matches filter

0 commit comments

Comments
 (0)