1 -- Evergreen DB patch 0686.schema.auditor_boost.sql
3 -- FIXME: insert description of change, if needed
8 -- check whether patch can be applied
9 SELECT evergreen.upgrade_deps_block_check('0686', :eg_version);
11 -- FIXME: add/check SQL statements to perform the upgrade
12 -- These three functions are for capturing, getting, and clearing user and workstation information
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];
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"}}];
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"});
32 CREATE OR REPLACE FUNCTION auditor.create_auditor_history ( sch TEXT, tbl TEXT ) RETURNS BOOL AS $creator$
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,
41 LIKE $$ || sch || $$.$$ || tbl || $$
46 $creator$ LANGUAGE 'plpgsql';
48 CREATE OR REPLACE FUNCTION auditor.create_auditor_func ( sch TEXT, tbl TEXT ) RETURNS BOOL AS $creator$
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;
59 CREATE OR REPLACE FUNCTION auditor.audit_$$ || sch || $$_$$ || tbl || $$_func ()
60 RETURNS TRIGGER AS $func$
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'),
69 OLD.$$ || array_to_string(column_list, ', OLD.') || $$
70 FROM auditor.get_audit_info();
73 $func$ LANGUAGE 'plpgsql';
77 $creator$ LANGUAGE 'plpgsql';
79 CREATE OR REPLACE FUNCTION auditor.create_auditor_lifecycle ( sch TEXT, tbl TEXT ) RETURNS BOOL AS $creator$
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;
90 CREATE VIEW auditor.$$ || sch || $$_$$ || tbl || $$_lifecycle AS
91 SELECT -1 AS audit_id,
96 $$ || array_to_string(column_list, ', ') || $$
97 FROM $$ || sch || $$.$$ || tbl || $$
99 SELECT audit_id, audit_time, audit_action, audit_user, audit_ws,
100 $$ || array_to_string(column_list, ', ') || $$
101 FROM auditor.$$ || sch || $$_$$ || tbl || $$_history;
105 $creator$ LANGUAGE 'plpgsql';
107 -- Corrects all column discrepencies between audit table and core table:
108 -- Adds missing columns
109 -- Removes leftover columns
111 -- Also, ensures all core auditor columns exist.
112 CREATE OR REPLACE FUNCTION auditor.fix_columns() RETURNS VOID AS $BODY$
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
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'
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'
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
139 JOIN pg_catalog.pg_attribute a ON a.attrelid IN (t.main_oid, t.audit_oid)
140 WHERE attnum > 0 AND NOT attisdropped
142 column_defs AS ( -- The motherload, every audit table and main table plus column names and defs.
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
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
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
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;
164 query:=query || $$,$$;
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;
169 IF alter_t THEN -- Open alter table = needs a semicolon
170 query:=query || $$; $$;
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.
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;$$;
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;
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.
194 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
197 query:=query || $$,$$;
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.
202 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
205 query:=query || $$,$$;
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.
210 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
213 query:=query || $$,$$;
215 query:=query || $$ ALTER COLUMN $$ || cr.audit_column || $$ TYPE $$ || cr.main_column_def;
218 FOR core_column IN SELECT DISTINCT unnest(auditor_cores) LOOP -- Repeat this outside of the loop to catch the last table
220 query:=query || $$ALTER TABLE auditor.$$ || current_audit_table;
223 query:=query || $$,$$;
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;
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.
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;$$;
243 $BODY$ LANGUAGE plpgsql;
245 -- Update it all routine
246 CREATE OR REPLACE FUNCTION auditor.update_auditors() RETURNS boolean AS $BODY$
252 -- Drop Lifecycle view(s) before potential column changes
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 || $$;$$;
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'
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'
277 SELECT main_namespace, main_table FROM table_set LOOP
279 PERFORM auditor.create_auditor_func(table_schema, table_name);
280 PERFORM auditor.create_auditor_lifecycle(table_schema, table_name);
284 $BODY$ LANGUAGE plpgsql;
286 -- Go ahead and update them all now
287 SELECT auditor.update_auditors();