better_record 0.18.3 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e03815d2331b4c05a3705bffcca86203bfccaba79d7bfe8505774ca504b5fef7
4
- data.tar.gz: 8437469c63dd94551197cac03193b91c9f45406abec4ade03a70cf0a667df88f
3
+ metadata.gz: 3e89311a82ba55ea3a011faa5ad3c85770bc88d29dd402fbd5856167a2151ab8
4
+ data.tar.gz: '01396e71a81cc99478664cb762c30d4d663abe3e06c76ce4a82e71e4d676d65d'
5
5
  SHA512:
6
- metadata.gz: 2b3ec051f5ac0362bd6dc08c8dad67eead886a54ab7c4e1e260420e8cfe3199c4d9ca8118ea2cdb28615bb8f1a37c5d8cd582988c98f602cf748c2e4bcee7dbc
7
- data.tar.gz: 76611ca55399b221de2f395af93a0f295daa15131d19368b1760cc82d124ad47c263ce0d89aeee096f71163ebeee35be5494063ea8f70b7707afd38e9b64f324
6
+ metadata.gz: f527649a03436eeabdbddf11aa1d0ac1b2bf60a0db5c42831fd143c47a3f3aac9fd9f85d6f18cf2142290c03fedef48d0cd2987d826e56f32cbc1f7adfd9c72a
7
+ data.tar.gz: 015501e77bd844a80c679ef16b443f9e1f9f561162aaea06102c22f3c66b7f7706521c3869080ba2aa21434d3abe99a82f8377a3d7797635e0bbdef16c1de5aa
@@ -25,7 +25,7 @@ module BetterRecord
25
25
  # == Class Methods ========================================================
26
26
  def self.set_audit_methods!
27
27
  begin
28
- connection.execute(%Q(SELECT 1 FROM #{BetterRecord::LoggedAction.table_name}_#{self.table_name} LIMIT 1))
28
+ connection.execute(%Q(SELECT 1 FROM #{BetterRecord::LoggedAction.table_name.sub('_view', '')}_#{self.table_name} LIMIT 1))
29
29
 
30
30
  self.const_set(:LoggedAction, Class.new(ApplicationRecord))
31
31
  self.const_get(:LoggedAction).table_name = "#{BetterRecord::LoggedAction.table_name}_#{self.table_name}"
@@ -12,7 +12,7 @@ module BetterRecord
12
12
  }.with_indifferent_access
13
13
 
14
14
  # == Attributes ===========================================================
15
- self.table_name = "#{BetterRecord.db_audit_schema}.logged_actions"
15
+ self.table_name = "#{BetterRecord.db_audit_schema}.logged_actions_view"
16
16
 
17
17
  # == Extensions ===========================================================
18
18
 
@@ -0,0 +1,196 @@
1
+ class AuditTriggerV3 < ActiveRecord::Migration[5.2]
2
+ def up
3
+
4
+
5
+ children = execute <<-SQL
6
+ SELECT pg_inherits.*, c.relname AS child, p.relname AS parent
7
+ FROM
8
+ pg_inherits JOIN pg_class AS c ON (inhrelid=c.oid)
9
+ JOIN pg_class as p ON (inhparent=p.oid);
10
+ SQL
11
+
12
+ children.each do |child|
13
+ execute <<-SQL
14
+ ALTER TABLE #{BetterRecord.db_audit_schema}.#{child['child']}
15
+ RENAME TO #{'old_' * child['parent'].split('old_').size}#{child['child']}
16
+ SQL
17
+ end
18
+
19
+ execute <<-SQL
20
+ ALTER SEQUENCE IF EXISTS auditing.old_logged_actions_event_id_seq RENAME TO old_old_logged_actions_event_id_seq;
21
+ ALTER TABLE #{BetterRecord.db_audit_schema}.old_logged_actions
22
+ RENAME TO old_old_logged_actions
23
+ SQL
24
+
25
+ # seq = execute <<-SQL
26
+ # SELECT table_name, column_name, column_default from
27
+ # information_schema.columns where table_name='old_old_logged_actions' AND column_name = 'event_id';
28
+ # SQL
29
+ #
30
+ # seq = seq.first
31
+ #
32
+ # val = "nextval('auditing.logged_actions_event_id_seq1'::regclass)"
33
+
34
+
35
+ execute <<-SQL
36
+ ALTER SEQUENCE IF EXISTS auditing.logged_actions_event_id_seq RENAME TO old_logged_actions_event_id_seq;
37
+ ALTER TABLE #{BetterRecord.db_audit_schema}.logged_actions
38
+ RENAME TO old_logged_actions
39
+ SQL
40
+
41
+ sql = ""
42
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-v3-table.psql'), "r")
43
+ while (line = source.gets)
44
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
45
+ end
46
+ source.close
47
+
48
+ execute sql
49
+
50
+ sql = ""
51
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-v3-trigger.psql'), "r")
52
+ while (line = source.gets)
53
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
54
+ end
55
+ source.close
56
+
57
+ execute sql
58
+
59
+ rows = Developer.connection.execute <<-SQL
60
+ select trg.tgname,
61
+ CASE trg.tgtype::integer & 66
62
+ WHEN 2 THEN 'BEFORE'
63
+ WHEN 64 THEN 'INSTEAD OF'
64
+ ELSE 'AFTER'
65
+ end as trigger_type,
66
+ case trg.tgtype::integer & cast(28 as int2)
67
+ when 16 then 'UPDATE'
68
+ when 8 then 'DELETE'
69
+ when 4 then 'INSERT'
70
+ when 20 then 'INSERT, UPDATE'
71
+ when 28 then 'INSERT, UPDATE, DELETE'
72
+ when 24 then 'UPDATE, DELETE'
73
+ when 12 then 'INSERT, DELETE'
74
+ end as trigger_event,
75
+ tbl.relname as table_name,
76
+ obj_description(trg.oid) as remarks,
77
+ case
78
+ when trg.tgenabled='O' then 'ENABLED'
79
+ else 'DISABLED'
80
+ end as status,
81
+ case trg.tgtype::integer & 1
82
+ when 1 then 'ROW'::text
83
+ else 'STATEMENT'::text
84
+ end as trigger_level
85
+ FROM pg_trigger trg
86
+ JOIN pg_class tbl on trg.tgrelid = tbl.oid
87
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
88
+ WHERE trg.tgname not like 'RI_ConstraintTrigger%'
89
+ AND trg.tgname not like 'pg_sync_pg%'
90
+ SQL
91
+
92
+ rows.each do |r|
93
+ if r['tgname'].to_s =~ /audit_trigger/
94
+ puts "\n\nPLEASE RECREATE ALL AUDIT TRIGGERS FOR #{r['table_name']}\n\n#{r}\n\n"
95
+ end
96
+ end
97
+
98
+ puts "\n\nTo insert old audits back into logged_actions run:\n\n"
99
+
100
+ puts <<-RUBY
101
+ INSERT INTO #{BetterRecord.db_audit_schema}.logged_actions_view
102
+ (
103
+ schema_name,
104
+ table_name,
105
+ relid,
106
+ session_user_name,
107
+ app_user_id,
108
+ app_user_type,
109
+ app_ip_address,
110
+ action_tstamp_tx,
111
+ action_tstamp_stm,
112
+ action_tstamp_clk,
113
+ transaction_id,
114
+ application_name,
115
+ client_addr,
116
+ client_port,
117
+ client_query,
118
+ action,
119
+ row_id,
120
+ row_data,
121
+ changed_fields,
122
+ statement_only
123
+ )
124
+ SELECT
125
+ schema_name,
126
+ table_name,
127
+ relid,
128
+ session_user_name,
129
+ app_user_id,
130
+ app_user_type,
131
+ app_ip_address,
132
+ action_tstamp_tx,
133
+ action_tstamp_stm,
134
+ action_tstamp_clk,
135
+ transaction_id,
136
+ application_name,
137
+ client_addr,
138
+ client_port,
139
+ client_query,
140
+ action,
141
+ row_id,
142
+ row_data,
143
+ changed_fields,
144
+ statement_only
145
+ FROM #{BetterRecord.db_audit_schema}.old_logged_actions
146
+ ORDER BY old_logged_actions.event_id;
147
+
148
+ INSERT INTO #{BetterRecord.db_audit_schema}.logged_actions_view
149
+ (
150
+ schema_name,
151
+ table_name,
152
+ relid,
153
+ session_user_name,
154
+ app_user_id,
155
+ app_user_type,
156
+ app_ip_address,
157
+ action_tstamp_tx,
158
+ action_tstamp_stm,
159
+ action_tstamp_clk,
160
+ transaction_id,
161
+ application_name,
162
+ client_addr,
163
+ client_port,
164
+ client_query,
165
+ action,
166
+ row_id,
167
+ row_data,
168
+ changed_fields,
169
+ statement_only
170
+ )
171
+ SELECT
172
+ schema_name,
173
+ table_name,
174
+ relid,
175
+ session_user_name,
176
+ app_user_id,
177
+ app_user_type,
178
+ app_ip_address,
179
+ action_tstamp_tx,
180
+ action_tstamp_stm,
181
+ action_tstamp_clk,
182
+ transaction_id,
183
+ application_name,
184
+ client_addr,
185
+ client_port,
186
+ client_query,
187
+ action,
188
+ row_id,
189
+ row_data,
190
+ changed_fields,
191
+ statement_only
192
+ FROM #{BetterRecord.db_audit_schema}.old_old_logged_actions
193
+ ORDER BY old_old_logged_actions.event_id;
194
+ RUBY
195
+ end
196
+ end
@@ -0,0 +1,80 @@
1
+ CREATE EXTENSION IF NOT EXISTS hstore;
2
+
3
+ CREATE SCHEMA IF NOT EXISTS SELECTED_SCHEMA_NAME;
4
+ REVOKE ALL ON SCHEMA SELECTED_SCHEMA_NAME FROM public;
5
+
6
+ COMMENT ON SCHEMA SELECTED_SCHEMA_NAME IS 'Out-of-table audit/history logging tables and trigger functions';
7
+
8
+ --
9
+ -- Audited data. Lots of information is available, it's just a matter of how much
10
+ -- you really want to record. See:
11
+ --
12
+ -- http://www.postgresql.org/docs/9.1/static/functions-info.html
13
+ --
14
+ -- Remember, every column you add takes up more audit table space and slows audit
15
+ -- inserts.
16
+ --
17
+ -- Every index you add has a big impact too, so avoid adding indexes to the
18
+ -- audit table unless you REALLY need them. The hstore GIST indexes are
19
+ -- particularly expensive.
20
+ --
21
+ -- It is sometimes worth copying the audit table, or a coarse subset of it that
22
+ -- you're interested in, into a temporary table where you CREATE any useful
23
+ -- indexes and do your analysis.
24
+ --
25
+ CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions (
26
+ event_id bigserial primary key,
27
+ schema_name text not null,
28
+ table_name text not null,
29
+ relid oid not null,
30
+ session_user_name text,
31
+ app_user_id integer,
32
+ app_user_type text,
33
+ app_ip_address inet,
34
+ action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
35
+ action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
36
+ action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
37
+ transaction_id bigint,
38
+ application_name text,
39
+ client_addr inet,
40
+ client_port integer,
41
+ client_query text,
42
+ action TEXT NOT NULL CHECK (action IN ('I','D','U', 'T', 'A')),
43
+ row_id bigint,
44
+ row_data hstore,
45
+ changed_fields hstore,
46
+ statement_only boolean not null
47
+ );
48
+
49
+ REVOKE ALL ON SELECTED_SCHEMA_NAME.logged_actions FROM public;
50
+
51
+ COMMENT ON TABLE SELECTED_SCHEMA_NAME.logged_actions IS 'History of auditable actions on audited tables, from SELECTED_SCHEMA_NAME.if_modified_func()';
52
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.event_id IS 'Unique identifier for each auditable event';
53
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.schema_name IS 'Database schema audited table for this event is in';
54
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.table_name IS 'Non-schema-qualified table name of table event occured in';
55
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.relid IS 'Table OID. Changes with drop/create. Get with ''tablename''::regclass';
56
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.session_user_name IS 'Login / session user whose statement caused the audited event';
57
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_user_id IS 'Application-provided polymorphic user id';
58
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_user_type IS 'Application-provided polymorphic user type';
59
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_ip_address IS 'Application-provided ip address of user whose statement caused the audited event';
60
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_tx IS 'Transaction start timestamp for tx in which audited event occurred';
61
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_stm IS 'Statement start timestamp for tx in which audited event occurred';
62
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_clk IS 'Wall clock time at which audited event''s trigger call occurred';
63
+ 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.';
64
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.client_addr IS 'IP address of client that issued query. Null for unix domain socket.';
65
+ 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.';
66
+ 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.';
67
+ 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.';
68
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action IS 'Action type; I = insert, D = delete, U = update, T = truncate, A = archive';
69
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.row_id IS 'Record primary_key. Null for statement-level trigger. Prefers NEW.id if exists';
70
+ 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.';
71
+ 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.';
72
+ 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';
73
+
74
+ CREATE INDEX IF NOT EXISTS logged_actions_relid_idx ON SELECTED_SCHEMA_NAME.logged_actions(relid);
75
+ CREATE INDEX IF NOT EXISTS logged_actions_action_tstamp_tx_stm_idx ON SELECTED_SCHEMA_NAME.logged_actions(action_tstamp_stm);
76
+ CREATE INDEX IF NOT EXISTS logged_actions_action_idx ON SELECTED_SCHEMA_NAME.logged_actions(action);
77
+ CREATE INDEX IF NOT EXISTS logged_actions_row_id_idx ON SELECTED_SCHEMA_NAME.logged_actions(row_id);
78
+
79
+ CREATE OR REPLACE VIEW SELECTED_SCHEMA_NAME.logged_actions_view AS SELECT * FROM SELECTED_SCHEMA_NAME.logged_actions;
80
+ ALTER VIEW SELECTED_SCHEMA_NAME.logged_actions_view ALTER COLUMN event_id SET DEFAULT NEXTVAL('"SELECTED_SCHEMA_NAME"."logged_actions_event_id_seq"');
@@ -0,0 +1,315 @@
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 OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.skip_logged_actions_main()
16
+ RETURNS TRIGGER
17
+ AS $$
18
+ BEGIN
19
+ raise exception 'insert on wrong table';
20
+ RETURN NULL;
21
+ END;
22
+ $$
23
+ LANGUAGE plpgsql
24
+ SECURITY DEFINER
25
+ SET search_path = pg_catalog, public;
26
+
27
+ DROP TRIGGER IF EXISTS logged_actions_skip_direct ON SELECTED_SCHEMA_NAME.logged_actions;
28
+ CREATE TRIGGER logged_actions_skip_direct BEFORE INSERT ON SELECTED_SCHEMA_NAME.logged_actions EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.skip_logged_actions_main();
29
+
30
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.logged_actions_partition()
31
+ RETURNS TRIGGER AS
32
+ $$
33
+ DECLARE
34
+ table_name text;
35
+ BEGIN
36
+
37
+ table_name = NEW.table_name::TEXT;
38
+
39
+ EXECUTE 'CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(table_name::TEXT) || '(
40
+ CHECK (table_name = ' || quote_literal(table_name::TEXT) || ')
41
+ ) INHERITS (SELECTED_SCHEMA_NAME.logged_actions)';
42
+
43
+ EXECUTE 'INSERT INTO SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(table_name::TEXT) || ' VALUES ($1.*)' USING NEW;
44
+
45
+ RETURN NEW;
46
+ END;
47
+ $$
48
+ LANGUAGE plpgsql
49
+ SECURITY DEFINER
50
+ SET search_path = pg_catalog, public;
51
+
52
+ DROP TRIGGER IF EXISTS logged_actions_partition_by_table ON SELECTED_SCHEMA_NAME.logged_actions_view;
53
+ CREATE TRIGGER logged_actions_partition_by_table instead OF INSERT ON SELECTED_SCHEMA_NAME.logged_actions_view
54
+ FOR each ROW EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.logged_actions_partition();
55
+
56
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.if_modified_func()
57
+ RETURNS TRIGGER AS
58
+ $$
59
+ DECLARE
60
+ audit_row SELECTED_SCHEMA_NAME.logged_actions;
61
+ include_values boolean;
62
+ log_diffs boolean;
63
+ h_old hstore;
64
+ h_new hstore;
65
+ user_row record;
66
+ excluded_cols text[] = ARRAY[]::text[];
67
+ pk_val_query text;
68
+ BEGIN
69
+ IF TG_WHEN <> 'AFTER' THEN
70
+ RAISE EXCEPTION 'SELECTED_SCHEMA_NAME.if_modified_func() may only run as an AFTER trigger';
71
+ END IF;
72
+
73
+ audit_row = ROW(
74
+ nextval('SELECTED_SCHEMA_NAME.logged_actions_event_id_seq'), -- event_id
75
+ TG_TABLE_SCHEMA::text, -- schema_name
76
+ TG_TABLE_NAME::text, -- table_name
77
+ TG_RELID, -- relation OID for much quicker searches
78
+ session_user::text, -- session_user_name
79
+ NULL, NULL, NULL, -- app_user_id, app_user_type, app_ip_address
80
+ current_timestamp, -- action_tstamp_tx
81
+ statement_timestamp(), -- action_tstamp_stm
82
+ clock_timestamp(), -- action_tstamp_clk
83
+ txid_current(), -- transaction ID
84
+ current_setting('application_name'), -- client application
85
+ inet_client_addr(), -- client_addr
86
+ inet_client_port(), -- client_port
87
+ current_query(), -- top-level query or queries (if multistatement) from client
88
+ substring(TG_OP,1,1), -- action
89
+ NULL, NULL, NULL, -- row_id, row_data, changed_fields
90
+ 'f' -- statement_only
91
+ );
92
+
93
+ IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
94
+ audit_row.client_query = NULL;
95
+ END IF;
96
+
97
+ IF ((TG_ARGV[1] IS NOT NULL) AND (TG_LEVEL = 'ROW')) THEN
98
+ pk_val_query = 'SELECT $1.' || quote_ident(TG_ARGV[1]::text);
99
+
100
+ IF (TG_OP IS DISTINCT FROM 'DELETE') THEN
101
+ EXECUTE pk_val_query INTO audit_row.row_id USING NEW;
102
+ END IF;
103
+
104
+ IF audit_row.row_id IS NULL THEN
105
+ EXECUTE pk_val_query INTO audit_row.row_id USING OLD;
106
+ END IF;
107
+ END IF;
108
+
109
+ IF TG_ARGV[2] IS NOT NULL THEN
110
+ excluded_cols = TG_ARGV[2]::text[];
111
+ END IF;
112
+
113
+ CREATE TEMP TABLE IF NOT EXISTS
114
+ "_app_user" (user_id integer, user_type text, ip_address inet);
115
+
116
+ IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
117
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
118
+ audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
119
+ IF audit_row.changed_fields = hstore('') THEN
120
+ -- All changed fields are ignored. Skip this update.
121
+ RETURN NULL;
122
+ END IF;
123
+ ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
124
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
125
+ ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
126
+ audit_row.row_data = hstore(NEW.*) - excluded_cols;
127
+ ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
128
+ audit_row.statement_only = 't';
129
+ ELSE
130
+ RAISE EXCEPTION '[SELECTED_SCHEMA_NAME.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
131
+ RETURN NULL;
132
+ END IF;
133
+
134
+ -- inject app_user data into audit
135
+ BEGIN
136
+ PERFORM
137
+ n.nspname, c.relname
138
+ FROM
139
+ pg_catalog.pg_class c
140
+ LEFT JOIN
141
+ pg_catalog.pg_namespace n
142
+ ON n.oid = c.relnamespace
143
+ WHERE
144
+ n.nspname like 'pg_temp_%'
145
+ AND
146
+ c.relname = '_app_user';
147
+
148
+ IF FOUND THEN
149
+ FOR user_row IN SELECT * FROM _app_user LIMIT 1 LOOP
150
+ audit_row.app_user_id = user_row.user_id;
151
+ audit_row.app_user_type = user_row.user_type;
152
+ audit_row.app_ip_address = user_row.ip_address;
153
+ END LOOP;
154
+ END IF;
155
+ END;
156
+ -- end app_user data
157
+
158
+ INSERT INTO SELECTED_SCHEMA_NAME.logged_actions_view VALUES (audit_row.*);
159
+ RETURN NULL;
160
+ END;
161
+ $$
162
+ LANGUAGE plpgsql
163
+ SECURITY DEFINER
164
+ SET search_path = pg_catalog, public;
165
+
166
+
167
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.if_modified_func() IS
168
+ $$
169
+ Track changes to a table at the statement and/or row level.
170
+
171
+ Optional parameters to trigger in CREATE TRIGGER call:
172
+
173
+ param 0: boolean, whether to log the query text. Default 't'.
174
+
175
+ param 1: text, primary_key_column of audited table if bigint.
176
+
177
+ param 2: text[], columns to ignore in updates. Default [].
178
+
179
+ Updates to ignored cols are omitted from changed_fields.
180
+
181
+ Updates with only ignored cols changed are not inserted
182
+ into the audit log.
183
+
184
+ Almost all the processing work is still done for updates
185
+ that ignored. If you need to save the load, you need to use
186
+ WHEN clause on the trigger instead.
187
+
188
+ No warning or error is issued if ignored_cols contains columns
189
+ that do not exist in the target table. This lets you specify
190
+ a standard set of ignored columns.
191
+
192
+ There is no parameter to disable logging of values. Add this trigger as
193
+ a 'FOR EACH STATEMENT' rather than 'FOR EACH ROW' trigger if you do not
194
+ want to log row values.
195
+
196
+ Note that the user name logged is the login role for the session. The audit trigger
197
+ cannot obtain the active role because it is reset by the SECURITY DEFINER invocation
198
+ of the audit trigger its self.
199
+ $$;
200
+
201
+
202
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(target_table text)
203
+ RETURNS text AS
204
+ $$
205
+ DECLARE
206
+ _pk_query_text text;
207
+ _pk_column_name text;
208
+ BEGIN
209
+ _pk_query_text = 'SELECT a.attname ' ||
210
+ 'FROM pg_index i ' ||
211
+ 'JOIN pg_attribute a ON a.attrelid = i.indrelid ' ||
212
+ ' AND a.attnum = ANY(i.indkey) ' ||
213
+ 'WHERE i.indrelid = ' || quote_literal(target_table::TEXT) || '::regclass ' ||
214
+ 'AND i.indisprimary ' ||
215
+ 'AND format_type(a.atttypid, a.atttypmod) = ' || quote_literal('bigint'::TEXT) ||
216
+ 'LIMIT 1';
217
+
218
+ EXECUTE _pk_query_text INTO _pk_column_name;
219
+ raise notice 'Value %', _pk_column_name;
220
+ return _pk_column_name;
221
+ END;
222
+ $$
223
+ LANGUAGE plpgsql;
224
+
225
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(text) IS
226
+ $$
227
+ Get primary key column name if single PK and type bigint.
228
+
229
+ Arguments:
230
+ target_table: Table name, schema qualified if not on search_path
231
+ $$;
232
+
233
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean, ignored_cols text[])
234
+ RETURNS void AS
235
+ $$
236
+ DECLARE
237
+ stm_targets text = 'INSERT OR UPDATE OR DELETE OR TRUNCATE';
238
+ _q_txt text;
239
+ _pk_column_name text;
240
+ _pk_column_snip text;
241
+ _ignored_cols_snip text = '';
242
+ BEGIN
243
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_row ON ' || quote_ident(target_table::TEXT);
244
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_stm ON ' || quote_ident(target_table::TEXT);
245
+
246
+ EXECUTE 'CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(target_table::TEXT) || '(
247
+ CHECK (table_name = ' || quote_literal(target_table::TEXT) || ')
248
+ ) INHERITS (SELECTED_SCHEMA_NAME.logged_actions)';
249
+
250
+ IF audit_rows THEN
251
+ _pk_column_name = SELECTED_SCHEMA_NAME.get_primary_key_column(target_table::TEXT);
252
+
253
+ IF _pk_column_name IS NOT NULL THEN
254
+ _pk_column_snip = ', ' || quote_literal(_pk_column_name);
255
+ ELSE
256
+ _pk_column_snip = ', NULL';
257
+ END IF;
258
+
259
+ IF array_length(ignored_cols,1) > 0 THEN
260
+ _ignored_cols_snip = ', ' || quote_literal(ignored_cols);
261
+ END IF;
262
+ _q_txt = 'CREATE TRIGGER audit_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
263
+ quote_ident(target_table::TEXT) ||
264
+ ' FOR EACH ROW EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func(' ||
265
+ quote_literal(audit_query_text) || _pk_column_snip || _ignored_cols_snip || ');';
266
+ RAISE NOTICE '%',_q_txt;
267
+ EXECUTE _q_txt;
268
+ stm_targets = 'TRUNCATE';
269
+ ELSE
270
+ END IF;
271
+
272
+ _q_txt = 'CREATE TRIGGER audit_trigger_stm AFTER ' || stm_targets || ' ON ' ||
273
+ target_table ||
274
+ ' FOR EACH STATEMENT EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func('||
275
+ quote_literal(audit_query_text) || ');';
276
+ RAISE NOTICE '%',_q_txt;
277
+ EXECUTE _q_txt;
278
+
279
+ END;
280
+ $$
281
+ LANGUAGE plpgsql;
282
+
283
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass, boolean, boolean, text[]) IS
284
+ $$
285
+ Add auditing support to a table.
286
+
287
+ Arguments:
288
+ target_table: Table name, schema qualified if not on search_path
289
+ audit_rows: Record each row change, or only audit at a statement level
290
+ audit_query_text: Record the text of the client query that triggered the audit event?
291
+ ignored_cols: Columns to exclude from update diffs, ignore updates that change only ignored cols.
292
+ $$;
293
+
294
+ -- Pg doesn't allow variadic calls with 0 params, so provide a wrapper
295
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean)
296
+ RETURNS void AS
297
+ $$
298
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, $2, $3, ARRAY[]::text[]);
299
+ $$
300
+ LANGUAGE SQL;
301
+
302
+ -- And provide a convenience call wrapper for the simplest case
303
+ -- of row-level logging with no excluded cols and query logging enabled.
304
+ --
305
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass)
306
+ RETURNS void AS
307
+ $$
308
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, BOOLEAN 't', BOOLEAN 't');
309
+ $$
310
+ LANGUAGE SQL;
311
+
312
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass) IS
313
+ $$
314
+ Add auditing support to the given table. Row-level changes will be logged with full client query text. No cols are ignored.
315
+ $$;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterRecord
4
- VERSION = '0.18.3'
4
+ VERSION = '0.19.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.3
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampson Crowley
@@ -271,9 +271,12 @@ files:
271
271
  - db/migrate/20190107202602_add_updated_at_to_better_record_table_sizes.rb
272
272
  - db/migrate/20190123225641_add_exchange_rate_integer_type.rb
273
273
  - db/migrate/20190209033946_update_better_record_audit_functions.rb
274
+ - db/migrate/20190209195134_audit_trigger_v3.rb
274
275
  - db/postgres-audit-trigger.psql
275
276
  - db/postgres-audit-v2-table.psql
276
277
  - db/postgres-audit-v2-trigger.psql
278
+ - db/postgres-audit-v3-table.psql
279
+ - db/postgres-audit-v3-trigger.psql
277
280
  - lib/better_record.rb
278
281
  - lib/better_record/batches.rb
279
282
  - lib/better_record/concerns/active_record_extensions/associations_extensions/association_scope_extensions.rb