Skip to content

Commit 155f239

Browse files
committed
init: add postgrest fixtures and queries
0 parents  commit 155f239

18 files changed

Lines changed: 5737 additions & 0 deletions

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*.control
2+
regression.*
3+
results/
4+
*.so
5+
*.o
6+
*.bc
7+
*.diffs
8+
pgbench_log.*
9+
.history
10+
tags
11+
pgrst_schema_cache--*.sql
12+
!pgrst_schema_cache--*--*.sql

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2025 Steve Chavez
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included
12+
in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Makefile

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
EXTENSION = pgrst_schema_cache
2+
EXTVERSION = 1.0.0
3+
4+
DATA = $(wildcard sql/*--*.sql)
5+
6+
EXTRA_CLEAN = sql/$(EXTENSION)--$(EXTVERSION).sql $(EXTENSION).control
7+
8+
TESTS = $(wildcard test/sql/*.sql)
9+
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))
10+
REGRESS_OPTS = --use-existing --inputdir=test
11+
12+
PG_CONFIG = pg_config
13+
14+
build: $(BUILD_DIR)/.gitignore
15+
16+
$(BUILD_DIR)/.gitignore: sql/$(EXTENSION)--$(EXTVERSION).sql $(EXTENSION).control
17+
mkdir -p $(BUILD_DIR)/extension
18+
cp $(EXTENSION).control $(BUILD_DIR)/extension
19+
cp sql/$(EXTENSION)--$(EXTVERSION).sql $(BUILD_DIR)/extension
20+
echo "*" > $(BUILD_DIR)/.gitignore
21+
22+
sql/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql
23+
cp $< $@
24+
25+
$(EXTENSION).control: $(EXTENSION).control.in
26+
sed "s/@EXTVERSION@/$(EXTVERSION)/g" $(EXTENSION).control.in > $@
27+
28+
PGXS := $(shell $(PG_CONFIG) --pgxs)
29+
include $(PGXS)
30+
31+
.PHONY: test
32+
test:
33+
make installcheck

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# pgrst_schema_cache
2+

pgrst_schema_cache.control.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
default_version = '@EXTVERSION@'
2+
relocatable = true
3+
module_pathname = '$libdir/pgrst_schema_cache'

shell.nix

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
with import (builtins.fetchTarball {
2+
name = "2025-06-16";
3+
url = "https://github.com/NixOS/nixpkgs/archive/e6f23dc08d3624daab7094b701aa3954923c6bbb.tar.gz";
4+
sha256 = "sha256:0m0xmk8sjb5gv2pq7s8w7qxf7qggqsd3rxzv3xrqkhfimy2x7bnx";
5+
}) {};
6+
mkShellNoCC {
7+
buildInputs =
8+
let
9+
xpg = import (fetchFromGitHub {
10+
owner = "steve-chavez";
11+
repo = "xpg";
12+
rev = "v1.6.1";
13+
sha256 = "sha256-SHvjrW8AgZN8sNxK7Qu78TTzfm543r1dTyvBKxIYwOE=";
14+
});
15+
in [
16+
xpg.xpg
17+
gcc15
18+
];
19+
shellHook = ''
20+
export HISTFILE=.history
21+
'';
22+
}

sql/pgrst_schema_cache--1.0.0.sql

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
-- we need this type otherwise the all_m2o_and_o2o_rels fails with
2+
-- ERROR: column "cols_and_fcols" has pseudo-type record[]
3+
-- This is due to the array_agg(row(...)) which gives a record[], which is a pseudotype
4+
-- and pg doesn't allow it as a view column.
5+
create type ref_pair as (col text, ref text);
6+
7+
-- same query as PostgREST's allM2OandO2ORels
8+
create view all_m2o_and_o2o_rels as
9+
with
10+
pks_uniques_cols as (
11+
select
12+
conrelid,
13+
array_agg(key order by key) as cols
14+
from pg_constraint,
15+
lateral unnest(conkey) as _(key)
16+
where
17+
contype in ('p', 'u')
18+
and connamespace <> 'pg_catalog'::regnamespace
19+
group by oid, conrelid
20+
)
21+
select
22+
ns1.nspname as table_schema,
23+
tab.relname as table_name,
24+
ns2.nspname as foreign_table_schema,
25+
other.relname as foreign_table_name,
26+
traint.conrelid = traint.confrelid as is_self,
27+
traint.conname as constraint_name,
28+
column_info.cols_and_fcols,
29+
(column_info.cols in (select cols from pks_uniques_cols where conrelid = traint.conrelid)) as one_to_one
30+
from pg_constraint traint
31+
join lateral (
32+
select
33+
array_agg(row(cols.attname, refs.attname)::ref_pair order by ord) as cols_and_fcols,
34+
array_agg(cols.attnum order by cols.attnum) as cols
35+
from unnest(traint.conkey, traint.confkey) with ordinality as _(col, ref, ord)
36+
join pg_attribute cols on cols.attrelid = traint.conrelid and cols.attnum = col
37+
join pg_attribute refs on refs.attrelid = traint.confrelid and refs.attnum = ref
38+
) as column_info on true
39+
join pg_namespace ns1 on ns1.oid = traint.connamespace
40+
join pg_class tab on tab.oid = traint.conrelid
41+
join pg_class other on other.oid = traint.confrelid
42+
join pg_namespace ns2 on ns2.oid = other.relnamespace
43+
where traint.contype = 'f'
44+
and traint.conparentid = 0
45+
order by traint.conrelid, traint.conname;
46+
47+
-- type for the array of (column_name, view_columns[]), otherwise the creation of the function would fail
48+
CREATE TYPE col_dep AS (
49+
col text,
50+
view_columns text[]
51+
);
52+
53+
-- Same query as PostgREST's allViewsKeyDependencies
54+
-- Returns all the views' primary keys and foreign keys dependencies
55+
-- query explanation at:
56+
-- * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19
57+
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
58+
create or replace function all_views_key_dependencies
59+
( db_schemas regnamespace[] default '{public}'
60+
, db_extra_search_path regnamespace[] default '{public}'
61+
)
62+
returns table (
63+
table_schema text,
64+
table_name text,
65+
view_schema text,
66+
view_name text,
67+
constraint_name text,
68+
constraint_type text,
69+
column_dependencies col_dep[] -- array of (col, view_columns[])
70+
) as $func$
71+
with recursive
72+
pks_fks as (
73+
-- pk + fk referencing col
74+
select
75+
contype::text as contype,
76+
conname,
77+
array_length(conkey, 1) as ncol,
78+
conrelid as resorigtbl,
79+
col as resorigcol,
80+
ord
81+
from pg_constraint
82+
left join lateral unnest(conkey) with ordinality as _(col, ord) on true
83+
where contype IN ('p', 'f')
84+
union
85+
-- fk referenced col
86+
select
87+
concat(contype, '_ref') as contype,
88+
conname,
89+
array_length(confkey, 1) as ncol,
90+
confrelid,
91+
col,
92+
ord
93+
from pg_constraint
94+
left join lateral unnest(confkey) with ordinality as _(col, ord) on true
95+
where contype='f'
96+
),
97+
views as (
98+
select
99+
c.oid as view_id,
100+
c.relnamespace as view_schema_id,
101+
n.nspname as view_schema,
102+
c.relname as view_name,
103+
r.ev_action as view_definition
104+
from pg_class c
105+
join pg_namespace n on n.oid = c.relnamespace
106+
join pg_rewrite r on r.ev_class = c.oid
107+
where c.relkind in ('v', 'm') and c.relnamespace = ANY($1 || $2)
108+
),
109+
transform_json as (
110+
select
111+
view_id, view_schema_id, view_schema, view_name,
112+
-- the following formatting is without indentation on purpose
113+
-- to allow simple diffs, with less whitespace noise
114+
replace(
115+
replace(
116+
replace(
117+
replace(
118+
replace(
119+
replace(
120+
replace(
121+
regexp_replace(
122+
replace(
123+
replace(
124+
replace(
125+
replace(
126+
replace(
127+
replace(
128+
replace(
129+
replace(
130+
replace(
131+
replace(
132+
replace(
133+
view_definition::text,
134+
-- This conversion to json is heavily optimized for performance.
135+
-- The general idea is to use as few regexp_replace() calls as possible.
136+
-- Simple replace() is a lot faster, so we jump through some hoops
137+
-- to be able to use regexp_replace() only once.
138+
-- This has been tested against a huge schema with 250+ different views.
139+
-- The unit tests do NOT reflect all possible inputs. Be careful when changing this!
140+
-- -----------------------------------------------
141+
-- pattern | replacement | flags
142+
-- -----------------------------------------------
143+
-- `<>` in pg_node_tree is the same as `null` in JSON, but due to very poor performance of json_typeof
144+
-- we need to make this an empty array here to prevent json_array_elements from throwing an error
145+
-- when the targetList is null.
146+
-- We'll need to put it first, to make the node protection below work for node lists that start with
147+
-- null: `(<> ...`, too. This is the case for coldefexprs, when the first column does not have a default value.
148+
'<>' , '()'
149+
-- `,` is not part of the pg_node_tree format, but used in the regex.
150+
-- This removes all `,` that might be part of column names.
151+
), ',' , ''
152+
-- The same applies for `{` and `}`, although those are used a lot in pg_node_tree.
153+
-- We remove the escaped ones, which might be part of column names again.
154+
), E'\\{' , ''
155+
), E'\\}' , ''
156+
-- The fields we need are formatted as json manually to protect them from the regex.
157+
), ' :targetList ' , ',"targetList":'
158+
), ' :resno ' , ',"resno":'
159+
), ' :resorigtbl ' , ',"resorigtbl":'
160+
), ' :resorigcol ' , ',"resorigcol":'
161+
-- Make the regex also match the node type, e.g. `{QUERY ...`, to remove it in one pass.
162+
), '{' , '{ :'
163+
-- Protect node lists, which start with `({` or `((` from the greedy regex.
164+
-- The extra `{` is removed again later.
165+
), '((' , '{(('
166+
), '({' , '{({'
167+
-- This regex removes all unused fields to avoid the need to format all of them correctly.
168+
-- This leads to a smaller json result as well.
169+
-- Removal stops at `,` for used fields (see above) and `}` for the end of the current node.
170+
-- Nesting can't be parsed correctly with a regex, so we stop at `{` as well and
171+
-- add an empty key for the followig node.
172+
), ' :[^}{,]+' , ',"":' , 'g'
173+
-- For performance, the regex also added those empty keys when hitting a `,` or `}`.
174+
-- Those are removed next.
175+
), ',"":}' , '}'
176+
), ',"":,' , ','
177+
-- This reverses the "node list protection" from above.
178+
), '{(' , '('
179+
-- Every key above has been added with a `,` so far. The first key in an object doesn't need it.
180+
), '{,' , '{'
181+
-- pg_node_tree has `()` around lists, but JSON uses `[]`
182+
), '(' , '['
183+
), ')' , ']'
184+
-- pg_node_tree has ` ` between list items, but JSON uses `,`
185+
), ' ' , ','
186+
)::json as view_definition
187+
from views
188+
),
189+
target_entries as(
190+
select
191+
view_id, view_schema_id, view_schema, view_name,
192+
json_array_elements(view_definition->0->'targetList') as entry
193+
from transform_json
194+
),
195+
results as(
196+
select
197+
view_id, view_schema_id, view_schema, view_name,
198+
(entry->>'resno')::int as view_column,
199+
(entry->>'resorigtbl')::oid as resorigtbl,
200+
(entry->>'resorigcol')::int as resorigcol
201+
from target_entries
202+
),
203+
-- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE
204+
-- Can be replaced with CYCLE clause once PG v13 is EOL.
205+
recursion(view_id, view_schema_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(
206+
select
207+
r.*,
208+
false,
209+
ARRAY[resorigtbl]
210+
from results r
211+
where view_schema_id = ANY ($1)
212+
union all
213+
select
214+
view.view_id,
215+
view.view_schema_id,
216+
view.view_schema,
217+
view.view_name,
218+
view.view_column,
219+
tab.resorigtbl,
220+
tab.resorigcol,
221+
tab.resorigtbl = ANY(path),
222+
path || tab.resorigtbl
223+
from recursion view
224+
join results tab on view.resorigtbl=tab.view_id and view.resorigcol=tab.view_column
225+
where not is_cycle
226+
),
227+
repeated_references as(
228+
select
229+
view_id,
230+
view_schema,
231+
view_name,
232+
resorigtbl,
233+
resorigcol,
234+
array_agg(attname) as view_columns
235+
from recursion
236+
join pg_attribute vcol on vcol.attrelid = view_id and vcol.attnum = view_column
237+
group by
238+
view_id,
239+
view_schema,
240+
view_name,
241+
resorigtbl,
242+
resorigcol
243+
)
244+
select
245+
sch.nspname as table_schema,
246+
tbl.relname as table_name,
247+
rep.view_schema,
248+
rep.view_name,
249+
pks_fks.conname as constraint_name,
250+
pks_fks.contype as constraint_type,
251+
array_agg(row(col.attname, view_columns)::col_dep order by pks_fks.ord) as column_dependencies
252+
from repeated_references rep
253+
join pks_fks using (resorigtbl, resorigcol)
254+
join pg_class tbl on tbl.oid = rep.resorigtbl
255+
join pg_attribute col on col.attrelid = tbl.oid and col.attnum = rep.resorigcol
256+
join pg_namespace sch on sch.oid = tbl.relnamespace
257+
group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conname, pks_fks.contype, pks_fks.ncol
258+
-- make sure we only return key for which all columns are referenced in the view - no partial PKs or FKs
259+
having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
260+
$func$
261+
language sql stable;

0 commit comments

Comments
 (0)