better_record 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/scaffold.css +80 -0
  3. data/app/controllers/better_record/application_controller.rb +1 -0
  4. data/app/controllers/better_record/table_sizes_controller.rb +28 -0
  5. data/app/helpers/better_record/table_sizes_helper.rb +4 -0
  6. data/app/models/better_record/base.rb +16 -8
  7. data/app/models/better_record/current.rb +5 -0
  8. data/app/models/better_record/{audit.rb → logged_action.rb} +2 -2
  9. data/app/models/better_record/table_size.rb +20 -17
  10. data/{lib/better_record → config/initializers/active_record}/associations.rb +5 -8
  11. data/config/initializers/active_record/reflection.rb +21 -0
  12. data/config/initializers/boolean.rb +11 -0
  13. data/config/initializers/content_security_policy.rb +25 -0
  14. data/config/initializers/filter_parameter_logging.rb +10 -0
  15. data/config/initializers/inflections.rb +21 -0
  16. data/config/initializers/integer.rb +5 -0
  17. data/config/initializers/jazz_fingers.rb +7 -0
  18. data/config/initializers/mime_types.rb +4 -0
  19. data/config/initializers/money_type.rb +32 -0
  20. data/config/routes.rb +2 -0
  21. data/db/migrate/20180725160802_create_better_record_db_functions.rb +94 -0
  22. data/db/migrate/20180725201614_create_better_record_table_sizes.rb +24 -0
  23. data/db/postgres-audit-trigger.psql +347 -0
  24. data/lib/better_record.rb +14 -19
  25. data/lib/better_record/version.rb +1 -1
  26. data/lib/tasks/spec/attributes.rake +42 -0
  27. metadata +95 -9
  28. data/lib/generators/create_helper_functions/USAGE +0 -9
  29. data/lib/generators/create_helper_functions/create_helper_functions_generator.rb +0 -61
  30. data/lib/tasks/db/create_audits.rake +0 -0
@@ -0,0 +1,32 @@
1
+ class MoneyType < ActiveRecord::Type::Value
2
+ def cast(value)
3
+ return nil unless value
4
+ convert_to_money(value)
5
+ end
6
+
7
+ def deserialize(value)
8
+ super(convert_to_money(value))
9
+ end
10
+
11
+ def serialize(value)
12
+ new_val = convert_to_money(value)
13
+ super(new_val ? new_val.value : nil)
14
+ end
15
+
16
+ private
17
+ def convert_to_money(value)
18
+ return nil unless value
19
+ if (!value.kind_of?(Numeric))
20
+ begin
21
+ dollars_to_cents = (value.gsub(/\$/, '').presence || 0).to_d * 100
22
+ StoreAsInt.money(dollars_to_cents.to_i)
23
+ rescue
24
+ StoreAsInt::Money.new
25
+ end
26
+ else
27
+ StoreAsInt.money(value)
28
+ end
29
+ end
30
+ end
31
+
32
+ ActiveRecord::Type.register(:money_column, MoneyType)
data/config/routes.rb CHANGED
@@ -1,2 +1,4 @@
1
1
  BetterRecord::Engine.routes.draw do
2
+ root to: 'table_sizes#index'
3
+ resources :table_sizes, only: [ :index, :show ]
2
4
  end
@@ -0,0 +1,94 @@
1
+ class CreateBetterRecordDBFunctions < ActiveRecord::Migration[5.2]
2
+ def up
3
+ execute "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
4
+ execute "CREATE EXTENSION IF NOT EXISTS btree_gin;"
5
+ execute "CREATE EXTENSION IF NOT EXISTS pgcrypto;"
6
+
7
+ sql = ""
8
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-trigger.psql'), "r")
9
+ while (line = source.gets)
10
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
11
+ end
12
+ source.close
13
+
14
+ execute sql
15
+
16
+ execute <<-SQL
17
+ CREATE or REPLACE FUNCTION public.temp_table_exists( varchar)
18
+ RETURNS pg_catalog.bool AS
19
+ $$
20
+ BEGIN
21
+ /* check the table exist in database and is visible*/
22
+ PERFORM n.nspname, c.relname
23
+ FROM pg_catalog.pg_class c
24
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
25
+ WHERE n.nspname LIKE 'pg_temp_%' AND pg_catalog.pg_table_is_visible(c.oid)
26
+ AND relname = $1;
27
+
28
+ IF FOUND THEN
29
+ RETURN TRUE;
30
+ ELSE
31
+ RETURN FALSE;
32
+ END IF;
33
+
34
+ END;
35
+ $$
36
+ LANGUAGE 'plpgsql' VOLATILE
37
+ SQL
38
+
39
+ execute <<-SQL
40
+ CREATE OR REPLACE FUNCTION hash_password(password text)
41
+ RETURNS text AS
42
+ $BODY$
43
+ BEGIN
44
+ password = crypt(password, gen_salt('bf', 8));
45
+
46
+ RETURN password;
47
+ END;
48
+ $BODY$
49
+
50
+ LANGUAGE plpgsql;
51
+ SQL
52
+
53
+ execute <<-SQL
54
+ CREATE OR REPLACE FUNCTION validate_email(email text)
55
+ RETURNS text AS
56
+ $BODY$
57
+ BEGIN
58
+ IF email IS NOT NULL THEN
59
+ IF email !~* '\\A[^@\\s\\;]+@[^@\\s\\;]+\\.[^@\\s\\;]+\\Z' THEN
60
+ RAISE EXCEPTION 'Invalid E-mail format %', email
61
+ USING HINT = 'Please check your E-mail format.';
62
+ END IF ;
63
+ email = lower(email);
64
+ END IF ;
65
+
66
+ RETURN email;
67
+ END;
68
+ $BODY$
69
+ LANGUAGE plpgsql;
70
+ SQL
71
+
72
+ execute <<-SQL
73
+ CREATE OR REPLACE FUNCTION valid_email_trigger()
74
+ RETURNS TRIGGER AS
75
+ $BODY$
76
+ BEGIN
77
+ NEW.email = validate_email(NEW.email);
78
+
79
+ RETURN NEW;
80
+ END;
81
+ $BODY$
82
+ LANGUAGE plpgsql;
83
+ SQL
84
+
85
+ end
86
+
87
+ def down
88
+ execute 'DROP FUNCTION IF EXISTS valid_email_trigger();'
89
+ execute 'DROP FUNCTION IF EXISTS validate_email();'
90
+ execute 'DROP FUNCTION IF EXISTS temp_table_exists();'
91
+ execute "DROP FUNCTION IF EXISTS hash_password();"
92
+ execute "DROP SCHEMA IF EXISTS #{BetterRecord.db_audit_schema} CASCADE;"
93
+ end
94
+ end
@@ -0,0 +1,24 @@
1
+ class CreateBetterRecordTableSizes < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table "#{BetterRecord.db_audit_schema}.table_sizes", {id: false} do |t|
4
+ t.integer :oid, limit: 8
5
+ t.string :schema
6
+ t.string :name
7
+ t.float :apx_row_count
8
+ t.integer :total_bytes, limit: 8
9
+ t.integer :idx_bytes, limit: 8
10
+ t.integer :toast_bytes, limit: 8
11
+ t.integer :tbl_bytes, limit: 8
12
+ t.text :total
13
+ t.text :idx
14
+ t.text :toast
15
+ t.text :tbl
16
+ end
17
+
18
+ reversible do |d|
19
+ d.up do
20
+ execute "ALTER TABLE #{BetterRecord.db_audit_schema}.table_sizes ADD PRIMARY KEY (oid);"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,347 @@
1
+ -- An audit history is important on most tables. Provide an audit trigger that logs to
2
+ -- a dedicated audit table for the major relations.
3
+ --
4
+ -- This file should be generic and not depend on application roles or structures,
5
+ -- as it's being listed here:
6
+ --
7
+ -- https://wiki.postgresql.org/wiki/Audit_trigger_91plus
8
+ --
9
+ -- This trigger was originally based on
10
+ -- http://wiki.postgresql.org/wiki/Audit_trigger
11
+ -- but has been completely rewritten.
12
+ --
13
+ -- Should really be converted into a relocatable EXTENSION, with control and upgrade files.
14
+
15
+ CREATE EXTENSION IF NOT EXISTS hstore;
16
+
17
+ CREATE SCHEMA SELECTED_SCHEMA_NAME;
18
+ REVOKE ALL ON SCHEMA SELECTED_SCHEMA_NAME FROM public;
19
+
20
+ COMMENT ON SCHEMA SELECTED_SCHEMA_NAME IS 'Out-of-table audit/history logging tables and trigger functions';
21
+
22
+ --
23
+ -- Audited data. Lots of information is available, it's just a matter of how much
24
+ -- you really want to record. See:
25
+ --
26
+ -- http://www.postgresql.org/docs/9.1/static/functions-info.html
27
+ --
28
+ -- Remember, every column you add takes up more audit table space and slows audit
29
+ -- inserts.
30
+ --
31
+ -- Every index you add has a big impact too, so avoid adding indexes to the
32
+ -- audit table unless you REALLY need them. The hstore GIST indexes are
33
+ -- particularly expensive.
34
+ --
35
+ -- It is sometimes worth copying the audit table, or a coarse subset of it that
36
+ -- you're interested in, into a temporary table where you CREATE any useful
37
+ -- indexes and do your analysis.
38
+ --
39
+ CREATE TABLE SELECTED_SCHEMA_NAME.logged_actions (
40
+ event_id bigserial primary key,
41
+ schema_name text not null,
42
+ table_name text not null,
43
+ relid oid not null,
44
+ session_user_name text,
45
+ app_user_id integer,
46
+ app_user_type text,
47
+ app_ip_address inet,
48
+ action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
49
+ action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
50
+ action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
51
+ transaction_id bigint,
52
+ application_name text,
53
+ client_addr inet,
54
+ client_port integer,
55
+ client_query text,
56
+ action TEXT NOT NULL CHECK (action IN ('I','D','U', 'T')),
57
+ row_id bigint,
58
+ row_data hstore,
59
+ changed_fields hstore,
60
+ statement_only boolean not null
61
+ );
62
+
63
+ REVOKE ALL ON SELECTED_SCHEMA_NAME.logged_actions FROM public;
64
+
65
+ COMMENT ON TABLE SELECTED_SCHEMA_NAME.logged_actions IS 'History of auditable actions on audited tables, from SELECTED_SCHEMA_NAME.if_modified_func()';
66
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.event_id IS 'Unique identifier for each auditable event';
67
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.schema_name IS 'Database schema audited table for this event is in';
68
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.table_name IS 'Non-schema-qualified table name of table event occured in';
69
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.relid IS 'Table OID. Changes with drop/create. Get with ''tablename''::regclass';
70
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.session_user_name IS 'Login / session user whose statement caused the audited event';
71
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_user_id IS 'Application-provided polymorphic user id';
72
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_user_type IS 'Application-provided polymorphic user type';
73
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_ip_address IS 'Application-provided ip address of user whose statement caused the audited event';
74
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_tx IS 'Transaction start timestamp for tx in which audited event occurred';
75
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_stm IS 'Statement start timestamp for tx in which audited event occurred';
76
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_clk IS 'Wall clock time at which audited event''s trigger call occurred';
77
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.transaction_id IS 'Identifier of transaction that made the change. May wrap, but unique paired with action_tstamp_tx.';
78
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.client_addr IS 'IP address of client that issued query. Null for unix domain socket.';
79
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.client_port IS 'Remote peer IP port address of client that issued query. Undefined for unix socket.';
80
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.client_query IS 'Top-level query that caused this auditable event. May be more than one statement.';
81
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.application_name IS 'Application name set when this audit event occurred. Can be changed in-session by client.';
82
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action IS 'Action type; I = insert, D = delete, U = update, T = truncate';
83
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.row_id IS 'Record primary_key. Null for statement-level trigger. Prefers NEW.id if exists';
84
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.row_data IS 'Record value. Null for statement-level trigger. For INSERT this is the new tuple. For DELETE and UPDATE it is the old tuple.';
85
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.changed_fields IS 'New values of fields changed by UPDATE. Null except for row-level UPDATE events.';
86
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.statement_only IS '''t'' if audit event is from an FOR EACH STATEMENT trigger, ''f'' for FOR EACH ROW';
87
+
88
+ CREATE INDEX logged_actions_relid_idx ON SELECTED_SCHEMA_NAME.logged_actions(relid);
89
+ CREATE INDEX logged_actions_action_tstamp_tx_stm_idx ON SELECTED_SCHEMA_NAME.logged_actions(action_tstamp_stm);
90
+ CREATE INDEX logged_actions_action_idx ON SELECTED_SCHEMA_NAME.logged_actions(action);
91
+ CREATE INDEX logged_actions_row_id_idx ON SELECTED_SCHEMA_NAME.logged_actions(row_id);
92
+
93
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.if_modified_func()
94
+ RETURNS TRIGGER AS
95
+ $$
96
+ DECLARE
97
+ audit_row SELECTED_SCHEMA_NAME.logged_actions;
98
+ include_values boolean;
99
+ log_diffs boolean;
100
+ h_old hstore;
101
+ h_new hstore;
102
+ user_row record;
103
+ excluded_cols text[] = ARRAY[]::text[];
104
+ pk_val_query text;
105
+ BEGIN
106
+ IF TG_WHEN <> 'AFTER' THEN
107
+ RAISE EXCEPTION 'SELECTED_SCHEMA_NAME.if_modified_func() may only run as an AFTER trigger';
108
+ END IF;
109
+
110
+ audit_row = ROW(
111
+ nextval('SELECTED_SCHEMA_NAME.logged_actions_event_id_seq'), -- event_id
112
+ TG_TABLE_SCHEMA::text, -- schema_name
113
+ TG_TABLE_NAME::text, -- table_name
114
+ TG_RELID, -- relation OID for much quicker searches
115
+ session_user::text, -- session_user_name
116
+ NULL, NULL, NULL, -- app_user_id, app_user_type, app_ip_address
117
+ current_timestamp, -- action_tstamp_tx
118
+ statement_timestamp(), -- action_tstamp_stm
119
+ clock_timestamp(), -- action_tstamp_clk
120
+ txid_current(), -- transaction ID
121
+ current_setting('application_name'), -- client application
122
+ inet_client_addr(), -- client_addr
123
+ inet_client_port(), -- client_port
124
+ current_query(), -- top-level query or queries (if multistatement) from client
125
+ substring(TG_OP,1,1), -- action
126
+ NULL, NULL, NULL, -- row_id, row_data, changed_fields
127
+ 'f' -- statement_only
128
+ );
129
+
130
+ IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
131
+ audit_row.client_query = NULL;
132
+ END IF;
133
+
134
+ IF ((TG_ARGV[1] IS NOT NULL) AND (TG_LEVEL = 'ROW')) THEN
135
+ pk_val_query = 'SELECT $1.' || quote_ident(TG_ARGV[1]::text);
136
+
137
+ IF (TG_OP IS DISTINCT FROM 'DELETE') THEN
138
+ EXECUTE pk_val_query INTO audit_row.row_id USING NEW;
139
+ END IF;
140
+
141
+ IF audit_row.row_id IS NULL THEN
142
+ EXECUTE pk_val_query INTO audit_row.row_id USING OLD;
143
+ END IF;
144
+ END IF;
145
+
146
+ IF TG_ARGV[2] IS NOT NULL THEN
147
+ excluded_cols = TG_ARGV[2]::text[];
148
+ END IF;
149
+
150
+
151
+
152
+ IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
153
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
154
+ audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
155
+ IF audit_row.changed_fields = hstore('') THEN
156
+ -- All changed fields are ignored. Skip this update.
157
+ RETURN NULL;
158
+ END IF;
159
+ ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
160
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
161
+ ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
162
+ audit_row.row_data = hstore(NEW.*) - excluded_cols;
163
+ ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
164
+ audit_row.statement_only = 't';
165
+ ELSE
166
+ RAISE EXCEPTION '[SELECTED_SCHEMA_NAME.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
167
+ RETURN NULL;
168
+ END IF;
169
+
170
+ -- inject app_user data into audit
171
+ BEGIN
172
+ PERFORM
173
+ n.nspname, c.relname
174
+ FROM
175
+ pg_catalog.pg_class c
176
+ LEFT JOIN
177
+ pg_catalog.pg_namespace n
178
+ ON n.oid = c.relnamespace
179
+ WHERE
180
+ n.nspname like 'pg_temp_%'
181
+ AND
182
+ c.relname = '_app_user';
183
+
184
+ IF FOUND THEN
185
+ FOR user_row IN SELECT * FROM _app_user LIMIT 1 LOOP
186
+ audit_row.app_user_id = user_row.user_id;
187
+ audit_row.app_user_type = user_row.user_type;
188
+ audit_row.app_ip_address = user_row.ip_address;
189
+ END LOOP;
190
+ END IF;
191
+ END;
192
+ -- end app_user data
193
+
194
+ INSERT INTO SELECTED_SCHEMA_NAME.logged_actions VALUES (audit_row.*);
195
+ RETURN NULL;
196
+ END;
197
+ $$
198
+ LANGUAGE plpgsql
199
+ SECURITY DEFINER
200
+ SET search_path = pg_catalog, public;
201
+
202
+
203
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.if_modified_func() IS
204
+ $$
205
+ Track changes to a table at the statement and/or row level.
206
+
207
+ Optional parameters to trigger in CREATE TRIGGER call:
208
+
209
+ param 0: boolean, whether to log the query text. Default 't'.
210
+
211
+ param 1: text, primary_key_column of audited table if bigint.
212
+
213
+ param 2: text[], columns to ignore in updates. Default [].
214
+
215
+ Updates to ignored cols are omitted from changed_fields.
216
+
217
+ Updates with only ignored cols changed are not inserted
218
+ into the audit log.
219
+
220
+ Almost all the processing work is still done for updates
221
+ that ignored. If you need to save the load, you need to use
222
+ WHEN clause on the trigger instead.
223
+
224
+ No warning or error is issued if ignored_cols contains columns
225
+ that do not exist in the target table. This lets you specify
226
+ a standard set of ignored columns.
227
+
228
+ There is no parameter to disable logging of values. Add this trigger as
229
+ a 'FOR EACH STATEMENT' rather than 'FOR EACH ROW' trigger if you do not
230
+ want to log row values.
231
+
232
+ Note that the user name logged is the login role for the session. The audit trigger
233
+ cannot obtain the active role because it is reset by the SECURITY DEFINER invocation
234
+ of the audit trigger its self.
235
+ $$;
236
+
237
+
238
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(target_table text)
239
+ RETURNS text AS
240
+ $$
241
+ DECLARE
242
+ _pk_query_text text;
243
+ _pk_column_name text;
244
+ BEGIN
245
+ _pk_query_text = 'SELECT a.attname ' ||
246
+ 'FROM pg_index i ' ||
247
+ 'JOIN pg_attribute a ON a.attrelid = i.indrelid ' ||
248
+ ' AND a.attnum = ANY(i.indkey) ' ||
249
+ 'WHERE i.indrelid = ' || quote_literal(target_table::TEXT) || '::regclass ' ||
250
+ 'AND i.indisprimary ' ||
251
+ 'AND format_type(a.atttypid, a.atttypmod) = ' || quote_literal('bigint'::TEXT) ||
252
+ 'LIMIT 1';
253
+
254
+ EXECUTE _pk_query_text INTO _pk_column_name;
255
+ raise notice 'Value %', _pk_column_name;
256
+ return _pk_column_name;
257
+ END;
258
+ $$
259
+ LANGUAGE plpgsql;
260
+
261
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(text) IS
262
+ $$
263
+ Get primary key column name if single PK and type bigint.
264
+
265
+ Arguments:
266
+ target_table: Table name, schema qualified if not on search_path
267
+ $$;
268
+
269
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean, ignored_cols text[])
270
+ RETURNS void AS
271
+ $$
272
+ DECLARE
273
+ stm_targets text = 'INSERT OR UPDATE OR DELETE OR TRUNCATE';
274
+ _q_txt text;
275
+ _pk_column_name text;
276
+ _pk_column_snip text;
277
+ _ignored_cols_snip text = '';
278
+ BEGIN
279
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_row ON ' || quote_ident(target_table::TEXT);
280
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_stm ON ' || quote_ident(target_table::TEXT);
281
+
282
+ IF audit_rows THEN
283
+ _pk_column_name = SELECTED_SCHEMA_NAME.get_primary_key_column(target_table::TEXT);
284
+
285
+ IF _pk_column_name IS NOT NULL THEN
286
+ _pk_column_snip = ', ' || quote_literal(_pk_column_name);
287
+ ELSE
288
+ _pk_column_snip = ', NULL';
289
+ END IF;
290
+
291
+ IF array_length(ignored_cols,1) > 0 THEN
292
+ _ignored_cols_snip = ', ' || quote_literal(ignored_cols);
293
+ END IF;
294
+ _q_txt = 'CREATE TRIGGER audit_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
295
+ quote_ident(target_table::TEXT) ||
296
+ ' FOR EACH ROW EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func(' ||
297
+ quote_literal(audit_query_text) || _pk_column_snip || _ignored_cols_snip || ');';
298
+ RAISE NOTICE '%',_q_txt;
299
+ EXECUTE _q_txt;
300
+ stm_targets = 'TRUNCATE';
301
+ ELSE
302
+ END IF;
303
+
304
+ _q_txt = 'CREATE TRIGGER audit_trigger_stm AFTER ' || stm_targets || ' ON ' ||
305
+ target_table ||
306
+ ' FOR EACH STATEMENT EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func('||
307
+ quote_literal(audit_query_text) || ');';
308
+ RAISE NOTICE '%',_q_txt;
309
+ EXECUTE _q_txt;
310
+
311
+ END;
312
+ $$
313
+ LANGUAGE plpgsql;
314
+
315
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass, boolean, boolean, text[]) IS
316
+ $$
317
+ Add auditing support to a table.
318
+
319
+ Arguments:
320
+ target_table: Table name, schema qualified if not on search_path
321
+ audit_rows: Record each row change, or only audit at a statement level
322
+ audit_query_text: Record the text of the client query that triggered the audit event?
323
+ ignored_cols: Columns to exclude from update diffs, ignore updates that change only ignored cols.
324
+ $$;
325
+
326
+ -- Pg doesn't allow variadic calls with 0 params, so provide a wrapper
327
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean)
328
+ RETURNS void AS
329
+ $$
330
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, $2, $3, ARRAY[]::text[]);
331
+ $$
332
+ LANGUAGE SQL;
333
+
334
+ -- And provide a convenience call wrapper for the simplest case
335
+ -- of row-level logging with no excluded cols and query logging enabled.
336
+ --
337
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass)
338
+ RETURNS void AS
339
+ $$
340
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, BOOLEAN 't', BOOLEAN 't');
341
+ $$
342
+ LANGUAGE SQL;
343
+
344
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass) IS
345
+ $$
346
+ Add auditing support to the given table. Row-level changes will be logged with full client query text. No cols are ignored.
347
+ $$;