-
Notifications
You must be signed in to change notification settings - Fork 2
/
variant.sql
162 lines (134 loc) · 5.33 KB
/
variant.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
BEGIN;
CREATE SCHEMA IF NOT EXISTS variants;
SET LOCAL search_path TO variants, meta, public;
--- Add a variant to tagged union. The variant's primary key must be columns
--- of the same type in the same order as the tagged union's. (They don't need
--- to have the same names.)
CREATE FUNCTION variant(base regclass, variant regclass) RETURNS void AS $code$
DECLARE
view_name text := quote_ident(tablename(base)||'*');
trigger_base text := tablename(base)||':'||tablename(variant);
insert_trigger text := quote_ident(trigger_base||'/i');
update_trigger text := quote_ident(trigger_base||'/u');
delete_trigger text := quote_ident(trigger_base||'/d');
ns text := quote_ident(schemaname(variant));
tabs regclass[];
selects text[];
BEGIN
SELECT tables INTO tabs FROM variants WHERE tab = base;
tabs := COALESCE(tabs, ARRAY[]::regclass[]) || variant;
--- Collect table information now, before changing the search path, to use
--- for rebuilding the view, later.
WITH expanded AS
(SELECT tab,
array_agg('NULL::'||tab)
OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS a,
array_agg('NULL::'||tab)
OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS z
FROM unnest(tabs) AS _(tab))
SELECT array_agg($$
SELECT $$|| quote_cols(cols) ||$$,
tableoid::regclass,
$$|| fields ||$$
FROM $$|| tab ||$$ AS tab
$$) INTO STRICT selects
FROM pk NATURAL JOIN expanded,
array_to_string((a[1:cardinality(a)-1] || ARRAY['(tab)'])
|| z[2:cardinality(z)], ', ', 'NULL') AS fields;
--- Update metadata table.
DELETE FROM variants WHERE tab = base;
INSERT INTO variants VALUES (base, tabs);
--- Mark base as a variant type.
BEGIN
EXECUTE $$
ALTER TABLE $$|| base ||$$ INHERIT variants.variant;
$$;
EXCEPTION WHEN duplicate_table THEN END;
--- Strange but true: duplicate_table is thrown when we try to inherit from
--- a table we already inherit from.
EXECUTE $$
SET LOCAL search_path TO $$|| ns ||$$, public;
----- Setup the foreign key linking variant to base.
--- Ensures constraint will validate at the end of the transaction.
INSERT INTO $$|| base ||$$ SELECT $$||
quote_cols(pk(variant))
||$$ FROM $$|| variant ||$$;
ALTER TABLE $$|| variant ||$$ ADD FOREIGN KEY ($$||
quote_cols(pk(variant))
||$$)
REFERENCES $$|| base ||$$
ON UPDATE CASCADE ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;
----- Create the triggers that propagate changes to base.
CREATE OR REPLACE FUNCTION $$|| insert_trigger ||$$()
RETURNS trigger AS $t$
BEGIN
INSERT INTO $$|| base ||$$ VALUES ($$|| inserter(pk(variant)) ||$$);
RETURN NEW;
END
$t$ LANGUAGE plpgsql;
CREATE TRIGGER $$|| insert_trigger ||$$
BEFORE INSERT ON $$|| variant ||$$ FOR EACH ROW
EXECUTE PROCEDURE $$|| insert_trigger ||$$();
CREATE OR REPLACE FUNCTION $$|| update_trigger ||$$()
RETURNS trigger AS $t$
BEGIN
UPDATE $$|| base || setter(pk(base), pk(variant)) ||$$;
RETURN NEW;
END
$t$ LANGUAGE plpgsql;
CREATE TRIGGER $$|| update_trigger ||$$ AFTER UPDATE OF $$||
quote_cols(pk(variant))
||$$ ON $$|| variant ||$$ FOR EACH ROW
EXECUTE PROCEDURE $$|| update_trigger ||$$();
CREATE OR REPLACE FUNCTION $$|| delete_trigger ||$$()
RETURNS trigger AS $t$
BEGIN
DELETE FROM $$|| base || deleter(pk(base), pk(variant)) ||$$;
RETURN OLD;
END
$t$ LANGUAGE plpgsql;
CREATE TRIGGER $$|| delete_trigger ||$$
AFTER DELETE ON $$|| variant ||$$ FOR EACH ROW
EXECUTE PROCEDURE $$|| delete_trigger ||$$();
----- Rebuild the view.
CREATE OR REPLACE VIEW $$|| view_name ||$$ ($$||
quote_cols(pk(base))
||$$, type, $$||
quote_cols(tabs)
||$$) AS$$|| array_to_string(selects, ' UNION ALL')||$$
$$;
END
$code$ LANGUAGE plpgsql SET search_path FROM CURRENT;
CREATE TABLE variants (
tab regclass NOT NULL,
tables regclass[] NOT NULL DEFAULT '{}'
);
--- Marker table -- every variant base inherits from this table.
CREATE TABLE variant ();
CREATE FUNCTION quote_cols(cols name[]) RETURNS text AS $$
SELECT string_agg(quote_ident(col), ', ') FROM unnest(cols) AS col
$$ LANGUAGE sql IMMUTABLE STRICT;
CREATE FUNCTION quote_cols(cols regclass[]) RETURNS text AS $$
SELECT string_agg(quote_ident(tablename(col)), ', ') FROM unnest(cols) AS col
$$ LANGUAGE sql IMMUTABLE STRICT;
CREATE FUNCTION inserter(cols name[])
RETURNS text AS $$
SELECT string_agg('NEW.'||quote_ident(col), ', ') FROM unnest(cols) AS col
$$ LANGUAGE sql IMMUTABLE STRICT;
CREATE FUNCTION setter(left_cols name[], right_cols name[])
RETURNS text AS $$
SELECT ' SET '||string_agg(ql||' = NEW.'||qr, ', ')
||' WHERE '||string_agg(ql||' = OLD.'||qr, ', ')
FROM unnest(left_cols, right_cols) AS _(left_col, right_col),
quote_ident(left_col) AS ql,
quote_ident(right_col) AS qr
$$ LANGUAGE sql IMMUTABLE STRICT;
CREATE FUNCTION deleter(left_cols name[], right_cols name[])
RETURNS text AS $$
SELECT ' WHERE '||string_agg(ql||' = OLD.'||qr, ', ')
FROM unnest(left_cols, right_cols) AS _(left_col, right_col),
quote_ident(left_col) AS ql,
quote_ident(right_col) AS qr
$$ LANGUAGE sql IMMUTABLE STRICT;
END;