]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/0686.schema.auditor_boost.sql
Stamping upgrade scripts for Vandelay default match set, with minor adjustments
[working/Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / 0686.schema.auditor_boost.sql
1 -- Evergreen DB patch 0686.schema.auditor_boost.sql
2 --
3 -- FIXME: insert description of change, if needed
4 --
5 BEGIN;
6
7
8 -- check whether patch can be applied
9 SELECT evergreen.upgrade_deps_block_check('0686', :eg_version);
10
11 -- FIXME: add/check SQL statements to perform the upgrade
12 -- These three functions are for capturing, getting, and clearing user and workstation information
13
14 -- Set the User AND workstation in one call. Tis faster. And less calls.
15 -- First argument is user, second is workstation
16 CREATE OR REPLACE FUNCTION auditor.set_audit_info(INT, INT) RETURNS VOID AS $$
17     $_SHARED{"eg_audit_user"} = $_[0];
18     $_SHARED{"eg_audit_ws"} = $_[1];
19 $$ LANGUAGE plperl;
20
21 -- Get the User AND workstation in one call. Less calls, useful for joins ;)
22 CREATE OR REPLACE FUNCTION auditor.get_audit_info() RETURNS TABLE (eg_user INT, eg_ws INT) AS $$
23     return [{eg_user => $_SHARED{"eg_audit_user"}, eg_ws => $_SHARED{"eg_audit_ws"}}];
24 $$ LANGUAGE plperl;
25
26 -- Clear the audit info, for whatever reason
27 CREATE OR REPLACE FUNCTION auditor.clear_audit_info() RETURNS VOID AS $$
28     delete($_SHARED{"eg_audit_user"});
29     delete($_SHARED{"eg_audit_ws"});
30 $$ LANGUAGE plperl;
31
32 CREATE OR REPLACE FUNCTION auditor.create_auditor_history ( sch TEXT, tbl TEXT ) RETURNS BOOL AS $creator$
33 BEGIN
34     EXECUTE $$
35         CREATE TABLE auditor.$$ || sch || $$_$$ || tbl || $$_history (
36             audit_id    BIGINT                          PRIMARY KEY,
37             audit_time  TIMESTAMP WITH TIME ZONE        NOT NULL,
38             audit_action        TEXT                            NOT NULL,
39             audit_user  INT,
40             audit_ws    INT,
41             LIKE $$ || sch || $$.$$ || tbl || $$
42         );
43     $$;
44         RETURN TRUE;
45 END;
46 $creator$ LANGUAGE 'plpgsql';
47
48 CREATE OR REPLACE FUNCTION auditor.create_auditor_func    ( sch TEXT, tbl TEXT ) RETURNS BOOL AS $creator$
49 DECLARE
50     column_list TEXT[];
51 BEGIN
52     SELECT INTO column_list array_agg(a.attname)
53         FROM pg_catalog.pg_attribute a
54             JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
55             JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
56         WHERE relkind = 'r' AND n.nspname = sch AND c.relname = tbl AND a.attnum > 0 AND NOT a.attisdropped;
57
58     EXECUTE $$
59         CREATE OR REPLACE FUNCTION auditor.audit_$$ || sch || $$_$$ || tbl || $$_func ()
60         RETURNS TRIGGER AS $func$
61         BEGIN
62             INSERT INTO auditor.$$ || sch || $$_$$ || tbl || $$_history ( audit_id, audit_time, audit_action, audit_user, audit_ws, $$
63             || array_to_string(column_list, ', ') || $$ )
64                 SELECT  nextval('auditor.$$ || sch || $$_$$ || tbl || $$_pkey_seq'),
65                     now(),
66                     SUBSTR(TG_OP,1,1),
67                     eg_user,
68                     eg_ws,
69                     OLD.$$ || array_to_string(column_list, ', OLD.') || $$
70                 FROM auditor.get_audit_info();
71             RETURN NULL;
72         END;
73         $func$ LANGUAGE 'plpgsql';
74     $$;
75     RETURN TRUE;
76 END;
77 $creator$ LANGUAGE 'plpgsql';
78
79 CREATE OR REPLACE FUNCTION auditor.create_auditor_lifecycle     ( sch TEXT, tbl TEXT ) RETURNS BOOL AS $creator$
80 DECLARE
81     column_list TEXT[];
82 BEGIN
83     SELECT INTO column_list array_agg(a.attname)
84         FROM pg_catalog.pg_attribute a
85             JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
86             JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
87         WHERE relkind = 'r' AND n.nspname = sch AND c.relname = tbl AND a.attnum > 0 AND NOT a.attisdropped;
88
89     EXECUTE $$
90         CREATE VIEW auditor.$$ || sch || $$_$$ || tbl || $$_lifecycle AS
91             SELECT -1 AS audit_id,
92                    now() AS audit_time,
93                    '-' AS audit_action,
94                    -1 AS audit_user,
95                    -1 AS audit_ws,
96                    $$ || array_to_string(column_list, ', ') || $$
97               FROM $$ || sch || $$.$$ || tbl || $$
98                 UNION ALL
99             SELECT audit_id, audit_time, audit_action, audit_user, audit_ws,
100             $$ || array_to_string(column_list, ', ') || $$
101               FROM auditor.$$ || sch || $$_$$ || tbl || $$_history;
102     $$;
103     RETURN TRUE;
104 END;
105 $creator$ LANGUAGE 'plpgsql';
106
107 -- Corrects all column discrepencies between audit table and core table:
108 -- Adds missing columns
109 -- Removes leftover columns
110 -- Updates types
111 -- Also, ensures all core auditor columns exist.
112 CREATE OR REPLACE FUNCTION auditor.fix_columns() RETURNS VOID AS $BODY$
113 DECLARE
114     current_table TEXT = ''; -- Storage for post-loop main table name
115     current_audit_table TEXT = ''; -- Storage for post-loop audit table name
116     query TEXT = ''; -- Storage for built query
117     cr RECORD; -- column record object
118     alter_t BOOL = false; -- Has the alter table command been appended yet
119     auditor_cores TEXT[] = ARRAY[]::TEXT[]; -- Core auditor function list (filled inside of loop)
120     core_column TEXT; -- The current core column we are adding
121 BEGIN
122     FOR cr IN
123         WITH audit_tables AS ( -- Basic grab of auditor tables. Anything in the auditor namespace, basically. With oids.
124             SELECT c.oid AS audit_oid, c.relname AS audit_table
125             FROM pg_catalog.pg_class c
126             JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
127             WHERE relkind='r' AND nspname = 'auditor'
128         ),
129         table_set AS ( -- Union of auditor tables with their "main" tables. With oids.
130             SELECT a.audit_oid, a.audit_table, c.oid AS main_oid, n.nspname as main_namespace, c.relname as main_table
131             FROM pg_catalog.pg_class c
132             JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
133             JOIN audit_tables a ON a.audit_table = n.nspname || '_' || c.relname || '_history'
134             WHERE relkind = 'r'
135         ),
136         column_lists AS ( -- All columns associated with the auditor or main table, grouped by the main table's oid.
137             SELECT DISTINCT ON (main_oid, attname) t.main_oid, a.attname
138             FROM table_set t
139             JOIN pg_catalog.pg_attribute a ON a.attrelid IN (t.main_oid, t.audit_oid)
140             WHERE attnum > 0 AND NOT attisdropped
141         ),
142         column_defs AS ( -- The motherload, every audit table and main table plus column names and defs.
143             SELECT audit_table,
144                    main_namespace,
145                    main_table,
146                    a.attname AS main_column, -- These two will be null for columns that have since been deleted, or for auditor core columns
147                    pg_catalog.format_type(a.atttypid, a.atttypmod) AS main_column_def,
148                    b.attname AS audit_column, -- These two will be null for columns that have since been added
149                    pg_catalog.format_type(b.atttypid, b.atttypmod) AS audit_column_def
150             FROM table_set t
151             JOIN column_lists c USING (main_oid)
152             LEFT JOIN pg_catalog.pg_attribute a ON a.attname = c.attname AND a.attrelid = t.main_oid AND a.attnum > 0 AND NOT a.attisdropped
153             LEFT JOIN pg_catalog.pg_attribute b ON b.attname = c.attname AND b.attrelid = t.audit_oid AND b.attnum > 0 AND NOT b.attisdropped
154         )
155         -- Nice sorted output from the above
156         SELECT * FROM column_defs WHERE main_column_def IS DISTINCT FROM audit_column_def ORDER BY main_namespace, main_table, main_column, audit_column
157     LOOP
158         IF current_table <> (cr.main_namespace || '.' || cr.main_table) THEN -- New table?
159             FOR core_column IN SELECT DISTINCT unnest(auditor_cores) LOOP -- Update missing core auditor columns
160                 IF NOT alter_t THEN -- Add ALTER TABLE if we haven't already
161                     query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
162                     alter_t:=TRUE;
163                 ELSE
164                     query:=query || $$,$$;
165                 END IF;
166                 -- Bit of a sneaky bit here. Create audit_id as a bigserial so it gets automatic values and doesn't complain about nulls when becoming a PRIMARY KEY.
167                 query:=query || $$ ADD COLUMN $$ || CASE WHEN core_column = 'audit_id bigint' THEN $$audit_id bigserial PRIMARY KEY$$ ELSE core_column END;
168             END LOOP;
169             IF alter_t THEN -- Open alter table = needs a semicolon
170                 query:=query || $$; $$;
171                 alter_t:=FALSE;
172                 IF 'audit_id bigint' = ANY(auditor_cores) THEN -- We added a primary key...
173                     -- Fun! Drop the default on audit_id, drop the auto-created sequence, create a new one, and set the current value
174                     -- For added fun, we have to execute in chunks due to the parser checking setval/currval arguments at parse time.
175                     EXECUTE query;
176                     EXECUTE $$ALTER TABLE auditor.$$ || current_audit_table || $$ ALTER COLUMN audit_id DROP DEFAULT; $$ ||
177                         $$CREATE SEQUENCE auditor.$$ || current_audit_table || $$_pkey_seq;$$;
178                     EXECUTE $$SELECT setval('auditor.$$ || current_audit_table || $$_pkey_seq', currval('auditor.$$ || current_audit_table || $$_audit_id_seq')); $$ ||
179                         $$DROP SEQUENCE auditor.$$ || current_audit_table || $$_audit_id_seq;$$;
180                     query:='';
181                 END IF;
182             END IF;
183             -- New table means we reset the list of needed auditor core columns
184             auditor_cores = ARRAY['audit_id bigint', 'audit_time timestamp with time zone', 'audit_action text', 'audit_user integer', 'audit_ws integer'];
185             -- And store some values for use later, because we can't rely on cr in all places.
186             current_table:=cr.main_namespace || '.' || cr.main_table;
187             current_audit_table:=cr.audit_table;
188         END IF;
189         IF cr.main_column IS NULL AND cr.audit_column LIKE 'audit_%' THEN -- Core auditor column?
190             -- Remove core from list of cores
191             SELECT INTO auditor_cores array_agg(core) FROM unnest(auditor_cores) AS core WHERE core != (cr.audit_column || ' ' || cr.audit_column_def);
192         ELSIF cr.main_column IS NULL THEN -- Main column doesn't exist, and it isn't an auditor column. Needs dropping from the auditor.
193             IF NOT alter_t THEN
194                 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
195                 alter_t:=TRUE;
196             ELSE
197                 query:=query || $$,$$;
198             END IF;
199             query:=query || $$ DROP COLUMN $$ || cr.audit_column;
200         ELSIF cr.audit_column IS NULL AND cr.main_column IS NOT NULL THEN -- New column auditor doesn't have. Add it.
201             IF NOT alter_t THEN
202                 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
203                 alter_t:=TRUE;
204             ELSE
205                 query:=query || $$,$$;
206             END IF;
207             query:=query || $$ ADD COLUMN $$ || cr.main_column || $$ $$ || cr.main_column_def;
208         ELSIF cr.main_column IS NOT NULL AND cr.audit_column IS NOT NULL THEN -- Both sides have this column, but types differ. Fix that.
209             IF NOT alter_t THEN
210                 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
211                 alter_t:=TRUE;
212             ELSE
213                 query:=query || $$,$$;
214             END IF;
215             query:=query || $$ ALTER COLUMN $$ || cr.audit_column || $$ TYPE $$ || cr.main_column_def;
216         END IF;
217     END LOOP;
218     FOR core_column IN SELECT DISTINCT unnest(auditor_cores) LOOP -- Repeat this outside of the loop to catch the last table
219         IF NOT alter_t THEN
220             query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
221             alter_t:=TRUE;
222         ELSE
223             query:=query || $$,$$;
224         END IF;
225         -- Bit of a sneaky bit here. Create audit_id as a bigserial so it gets automatic values and doesn't complain about nulls when becoming a PRIMARY KEY.
226         query:=query || $$ ADD COLUMN $$ || CASE WHEN core_column = 'audit_id bigint' THEN $$audit_id bigserial PRIMARY KEY$$ ELSE core_column END;
227     END LOOP;
228     IF alter_t THEN -- Open alter table = needs a semicolon
229         query:=query || $$;$$;
230         IF 'audit_id bigint' = ANY(auditor_cores) THEN -- We added a primary key...
231             -- Fun! Drop the default on audit_id, drop the auto-created sequence, create a new one, and set the current value
232             -- For added fun, we have to execute in chunks due to the parser checking setval/currval arguments at parse time.
233             EXECUTE query;
234             EXECUTE $$ALTER TABLE auditor.$$ || current_audit_table || $$ ALTER COLUMN audit_id DROP DEFAULT; $$ ||
235                 $$CREATE SEQUENCE auditor.$$ || current_audit_table || $$_pkey_seq;$$;
236             EXECUTE $$SELECT setval('auditor.$$ || current_audit_table || $$_pkey_seq', currval('auditor.$$ || current_audit_table || $$_audit_id_seq')); $$ ||
237                 $$DROP SEQUENCE auditor.$$ || current_audit_table || $$_audit_id_seq;$$;
238             query:='';
239         END IF;
240     END IF;
241     EXECUTE query;
242 END;
243 $BODY$ LANGUAGE plpgsql;
244
245 -- Update it all routine
246 CREATE OR REPLACE FUNCTION auditor.update_auditors() RETURNS boolean AS $BODY$
247 DECLARE
248     auditor_name TEXT;
249     table_schema TEXT;
250     table_name TEXT;
251 BEGIN
252     -- Drop Lifecycle view(s) before potential column changes
253     FOR auditor_name IN
254         SELECT c.relname
255             FROM pg_catalog.pg_class c
256                 JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
257             WHERE relkind = 'v' AND n.nspname = 'auditor' LOOP
258         EXECUTE $$ DROP VIEW auditor.$$ || auditor_name || $$;$$;
259     END LOOP;
260     -- Fix all column discrepencies
261     PERFORM auditor.fix_columns();
262     -- Re-create trigger functions and lifecycle views
263     FOR table_schema, table_name IN
264         WITH audit_tables AS (
265             SELECT c.oid AS audit_oid, c.relname AS audit_table
266             FROM pg_catalog.pg_class c
267             JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
268             WHERE relkind='r' AND nspname = 'auditor'
269         ),
270         table_set AS (
271             SELECT a.audit_oid, a.audit_table, c.oid AS main_oid, n.nspname as main_namespace, c.relname as main_table
272             FROM pg_catalog.pg_class c
273             JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
274             JOIN audit_tables a ON a.audit_table = n.nspname || '_' || c.relname || '_history'
275             WHERE relkind = 'r'
276         )
277         SELECT main_namespace, main_table FROM table_set LOOP
278         
279         PERFORM auditor.create_auditor_func(table_schema, table_name);
280         PERFORM auditor.create_auditor_lifecycle(table_schema, table_name);
281     END LOOP;
282     RETURN TRUE;
283 END;
284 $BODY$ LANGUAGE plpgsql;
285
286 -- Go ahead and update them all now
287 SELECT auditor.update_auditors();
288
289
290 COMMIT;