better_record 0.17.9 → 0.18.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: 88614c5435066753d21aab6f1cdbf445362ce968c9832758c8286c308caa5660
4
- data.tar.gz: ef4e6cbee750ffbfb48baf1f33e97dd1fc9377d5d35585bc80f081dcc47a5ea1
3
+ metadata.gz: c07da71971172205aecfda94df6fb1ca1213d32569053fcd8892dda8a91076fa
4
+ data.tar.gz: a64508bffff376045804d34726d45bfa6a0ab80948f45198dac6a6c1a8a57d5f
5
5
  SHA512:
6
- metadata.gz: 7f1c870552dbc7a6fbd7b009a79c601b5241c5f1b7541ce0caf45298c9bd45ef5564b43c931548ea313ca52ffe49bc9428643fd32eae3775d49bb9e3c5c12fbd
7
- data.tar.gz: fbb33a161af131964a041994c02b127432aea7fc9b1e0df0c09399a2f27ad2abe6f867f837bf8fd3911de387f6cf0c28bca9b1b59a63a2d7b2b73b99e2edee56
6
+ metadata.gz: b47ff7753e4775cd1d95f4741833e4719e51d0c4624d5139b68828c84c51e40eaeece53dcd60ac5052b288f83d3f6ca8689c3a7b0ca7fcdbcf0cd8ee8259d5b9
7
+ data.tar.gz: 0ccc0c6aab836bba068ba45d014555b4bb08d8cf88b27a9d673489d0c36fcd6d3f5fa016525797e6ee57c3da2b150c36663f5a2b5b450410ecb06749b11a02fb
@@ -14,16 +14,25 @@ module BetterRecord
14
14
 
15
15
  # == Relationships ========================================================
16
16
  if (ha = BetterRecord.has_auditing_relation_by_default)
17
- has_many self.audit_relation_name,
18
- class_name: 'BetterRecord::LoggedAction',
19
- primary_type: :table_name,
20
- foreign_key: :row_id,
21
- foreign_type: :table_name,
22
- as: self.audit_relation_name
23
-
24
17
  class << self
25
18
  define_method BetterRecord.audit_relation_name do |*args, &block|
26
- base_q = BetterRecord::LoggedAction.where(table_name: self.table_name)
19
+ @logger_model ||=
20
+ begin
21
+ connection.execute(%Q(SELECT 1 FROM #{BetterRecord::LoggedAction.table_name}_#{self.table_name} LIMIT 1))
22
+
23
+ class self.to_s.constantize::LoggedAction < BetterRecord::LoggedAction
24
+ self.table_name = "#{BetterRecord::LoggedAction.table_name}_#{self.to_s.deconstantize.constantize.table_name}"
25
+ self
26
+ end
27
+ rescue ActiveRecord::StatementInvalid
28
+ class self.to_s.constantize::LoggedAction < BetterRecord::LoggedAction
29
+ self
30
+ end
31
+ end
32
+
33
+ return @logger_model if args.present? && args.first == 'SETTING_INHERITANCE'
34
+
35
+ base_q = @logger_model.where(table_name: self.table_name)
27
36
  base_q = base_q.where(*args) if args.present?
28
37
 
29
38
  if block
@@ -37,6 +46,16 @@ module BetterRecord
37
46
  end
38
47
  end
39
48
  end
49
+
50
+ def self.inherited(child)
51
+ super
52
+ TracePoint.trace(:end) do |t|
53
+ if child == t.self
54
+ child.set_audits_methods!
55
+ t.disable
56
+ end
57
+ end
58
+ end
40
59
  end
41
60
  # == Validations ==========================================================
42
61
 
@@ -47,6 +66,16 @@ module BetterRecord
47
66
  # == Boolean Class Methods ================================================
48
67
 
49
68
  # == Class Methods ========================================================
69
+ def self.set_audits_methods!
70
+ m = __send__ BetterRecord.audit_relation_name, 'SETTING_INHERITANCE'
71
+ self.has_many self.audit_relation_name,
72
+ class_name: m.to_s,
73
+ primary_type: :table_name,
74
+ foreign_key: :row_id,
75
+ foreign_type: :table_name,
76
+ as: self.audit_relation_name
77
+ end
78
+
50
79
  def self.gender_enum(col)
51
80
  enum col, BetterRecord::Gender::ENUM
52
81
  end
@@ -8,6 +8,7 @@ module BetterRecord
8
8
  I: 'INSERT',
9
9
  U: 'UPDATE',
10
10
  T: 'TRUNCATE',
11
+ A: 'ARCHIVE',
11
12
  }.with_indifferent_access
12
13
 
13
14
  # == Attributes ===========================================================
@@ -43,6 +44,15 @@ module BetterRecord
43
44
  ]
44
45
  end
45
46
 
47
+ # def self.set_audits_methods!
48
+ # self.has_many self.audit_relation_name,
49
+ # class_name: 'BetterRecord::LoggedAction',
50
+ # primary_type: :table_name,
51
+ # foreign_key: :row_id,
52
+ # foreign_type: :table_name,
53
+ # as: self.audit_relation_name
54
+ # end
55
+
46
56
  # == Boolean Methods ======================================================
47
57
 
48
58
  # == Instance Methods =====================================================
@@ -0,0 +1,103 @@
1
+ class UpdateBetterRecordAuditFunctions < ActiveRecord::Migration[5.2]
2
+ def up
3
+ last_event = BetterRecord::LoggedAction.count
4
+
5
+ execute <<-SQL
6
+ ALTER TABLE #{BetterRecord.db_audit_schema}.logged_actions
7
+ RENAME TO old_logged_actions
8
+ SQL
9
+
10
+ sql = ""
11
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-v2-table.psql'), "r")
12
+ while (line = source.gets)
13
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
14
+ end
15
+ source.close
16
+
17
+ execute sql
18
+
19
+ execute <<-SQL
20
+ ALTER SEQUENCE #{BetterRecord.db_audit_schema}.logged_actions_event_id_seq START WITH #{last_event}
21
+ SQL
22
+
23
+ sql = ""
24
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-v2-trigger.psql'), "r")
25
+ while (line = source.gets)
26
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
27
+ end
28
+ source.close
29
+
30
+ execute sql
31
+
32
+ rows = execute <<-SQL
33
+ select trg.tgname,
34
+ CASE trg.tgtype::integer & 66
35
+ WHEN 2 THEN 'BEFORE'
36
+ WHEN 64 THEN 'INSTEAD OF'
37
+ ELSE 'AFTER'
38
+ end as trigger_type,
39
+ case trg.tgtype::integer & cast(28 as int2)
40
+ when 16 then 'UPDATE'
41
+ when 8 then 'DELETE'
42
+ when 4 then 'INSERT'
43
+ when 20 then 'INSERT, UPDATE'
44
+ when 28 then 'INSERT, UPDATE, DELETE'
45
+ when 24 then 'UPDATE, DELETE'
46
+ when 12 then 'INSERT, DELETE'
47
+ end as trigger_event,
48
+ tbl.relname as table_name,
49
+ obj_description(trg.oid) as remarks,
50
+ case
51
+ when trg.tgenabled='O' then 'ENABLED'
52
+ else 'DISABLED'
53
+ end as status,
54
+ case trg.tgtype::integer & 1
55
+ when 1 then 'ROW'::text
56
+ else 'STATEMENT'::text
57
+ end as trigger_level
58
+ FROM pg_trigger trg
59
+ JOIN pg_class tbl on trg.tgrelid = tbl.oid
60
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
61
+ WHERE trg.tgname not like 'RI_ConstraintTrigger%'
62
+ AND trg.tgname not like 'pg_sync_pg%'
63
+ SQL
64
+
65
+ rows.each do |r|
66
+ if r['tgname'].to_s =~ /audit_trigger/
67
+ execute <<-SQL
68
+ CREATE TABLE IF NOT EXISTS #{BetterRecord.db_audit_schema}.logged_actions_#{r['table_name']} (
69
+ CHECK (table_name = '#{r['table_name']}')
70
+ ) INHERITS (#{BetterRecord.db_audit_schema}.logged_actions);
71
+ SQL
72
+
73
+ puts "\n\nPLEASE RECREATE ALL AUDIT TRIGGERS FOR #{r['table_name']}\n\n#{r}\n\n"
74
+ end
75
+ end
76
+
77
+ puts "\n\nTo insert old audits back into logged_actions run:\n\n"
78
+
79
+ puts <<-RUBY
80
+ class BetterRecord::OldLoggedAction < BetterRecord::LoggedAction
81
+ self.table_name = "#{BetterRecord.db_audit_schema}.old_logged_actions"
82
+ end
83
+
84
+ while BetterRecord::OldLoggedAction.count > 0
85
+ p BetterRecord::OldLoggedAction.count
86
+ BetterRecord::OldLoggedAction.order(:event_id).limit(100).each do |r|
87
+ klass = nil
88
+ begin
89
+ BetterRecord::LoggedAction.connection.execute(%Q(SELECT 1 FROM #{BetterRecord.db_audit_schema}.logged_actions_#\{r.table_name}))
90
+
91
+ klass = Class.new(BetterRecord::LoggedAction)
92
+ klass.table_name = "#{BetterRecord.db_audit_schema}.logged_actions_#\{r.table_name}"
93
+ rescue ActiveRecord::StatementInvalid
94
+ klass = BetterRecord::LoggedAction
95
+ end
96
+ klass.create!(r.attributes)
97
+ p klass.count
98
+ r.delete
99
+ end
100
+ end
101
+ RUBY
102
+ end
103
+ end
@@ -0,0 +1,77 @@
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);
@@ -0,0 +1,278 @@
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.if_modified_func()
16
+ RETURNS TRIGGER AS
17
+ $$
18
+ DECLARE
19
+ audit_row SELECTED_SCHEMA_NAME.logged_actions;
20
+ include_values boolean;
21
+ log_diffs boolean;
22
+ h_old hstore;
23
+ h_new hstore;
24
+ user_row record;
25
+ excluded_cols text[] = ARRAY[]::text[];
26
+ pk_val_query text;
27
+ BEGIN
28
+ IF TG_WHEN <> 'AFTER' THEN
29
+ RAISE EXCEPTION 'SELECTED_SCHEMA_NAME.if_modified_func() may only run as an AFTER trigger';
30
+ END IF;
31
+
32
+ audit_row = ROW(
33
+ nextval('SELECTED_SCHEMA_NAME.logged_actions_event_id_seq'), -- event_id
34
+ TG_TABLE_SCHEMA::text, -- schema_name
35
+ TG_TABLE_NAME::text, -- table_name
36
+ TG_RELID, -- relation OID for much quicker searches
37
+ session_user::text, -- session_user_name
38
+ NULL, NULL, NULL, -- app_user_id, app_user_type, app_ip_address
39
+ current_timestamp, -- action_tstamp_tx
40
+ statement_timestamp(), -- action_tstamp_stm
41
+ clock_timestamp(), -- action_tstamp_clk
42
+ txid_current(), -- transaction ID
43
+ current_setting('application_name'), -- client application
44
+ inet_client_addr(), -- client_addr
45
+ inet_client_port(), -- client_port
46
+ current_query(), -- top-level query or queries (if multistatement) from client
47
+ substring(TG_OP,1,1), -- action
48
+ NULL, NULL, NULL, -- row_id, row_data, changed_fields
49
+ 'f' -- statement_only
50
+ );
51
+
52
+ IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
53
+ audit_row.client_query = NULL;
54
+ END IF;
55
+
56
+ IF ((TG_ARGV[1] IS NOT NULL) AND (TG_LEVEL = 'ROW')) THEN
57
+ pk_val_query = 'SELECT $1.' || quote_ident(TG_ARGV[1]::text);
58
+
59
+ IF (TG_OP IS DISTINCT FROM 'DELETE') THEN
60
+ EXECUTE pk_val_query INTO audit_row.row_id USING NEW;
61
+ END IF;
62
+
63
+ IF audit_row.row_id IS NULL THEN
64
+ EXECUTE pk_val_query INTO audit_row.row_id USING OLD;
65
+ END IF;
66
+ END IF;
67
+
68
+ IF TG_ARGV[2] IS NOT NULL THEN
69
+ excluded_cols = TG_ARGV[2]::text[];
70
+ END IF;
71
+
72
+ CREATE TEMP TABLE IF NOT EXISTS
73
+ "_app_user" (user_id integer, user_type text, ip_address inet);
74
+
75
+ IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
76
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
77
+ audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
78
+ IF audit_row.changed_fields = hstore('') THEN
79
+ -- All changed fields are ignored. Skip this update.
80
+ RETURN NULL;
81
+ END IF;
82
+ ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
83
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
84
+ ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
85
+ audit_row.row_data = hstore(NEW.*) - excluded_cols;
86
+ ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
87
+ audit_row.statement_only = 't';
88
+ ELSE
89
+ RAISE EXCEPTION '[SELECTED_SCHEMA_NAME.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
90
+ RETURN NULL;
91
+ END IF;
92
+
93
+ -- inject app_user data into audit
94
+ BEGIN
95
+ PERFORM
96
+ n.nspname, c.relname
97
+ FROM
98
+ pg_catalog.pg_class c
99
+ LEFT JOIN
100
+ pg_catalog.pg_namespace n
101
+ ON n.oid = c.relnamespace
102
+ WHERE
103
+ n.nspname like 'pg_temp_%'
104
+ AND
105
+ c.relname = '_app_user';
106
+
107
+ IF FOUND THEN
108
+ FOR user_row IN SELECT * FROM _app_user LIMIT 1 LOOP
109
+ audit_row.app_user_id = user_row.user_id;
110
+ audit_row.app_user_type = user_row.user_type;
111
+ audit_row.app_ip_address = user_row.ip_address;
112
+ END LOOP;
113
+ END IF;
114
+ END;
115
+ -- end app_user data
116
+
117
+ EXECUTE 'CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(TG_TABLE_NAME::TEXT) || '(
118
+ CHECK (table_name = ' || quote_literal(TG_TABLE_NAME::TEXT) || ')
119
+ ) INHERITS (SELECTED_SCHEMA_NAME.logged_actions)';
120
+
121
+ EXECUTE 'INSERT INTO SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(TG_TABLE_NAME::TEXT) || ' VALUES ($1.*)' USING audit_row;
122
+ RETURN NULL;
123
+ END;
124
+ $$
125
+ LANGUAGE plpgsql
126
+ SECURITY DEFINER
127
+ SET search_path = pg_catalog, public;
128
+
129
+
130
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.if_modified_func() IS
131
+ $$
132
+ Track changes to a table at the statement and/or row level.
133
+
134
+ Optional parameters to trigger in CREATE TRIGGER call:
135
+
136
+ param 0: boolean, whether to log the query text. Default 't'.
137
+
138
+ param 1: text, primary_key_column of audited table if bigint.
139
+
140
+ param 2: text[], columns to ignore in updates. Default [].
141
+
142
+ Updates to ignored cols are omitted from changed_fields.
143
+
144
+ Updates with only ignored cols changed are not inserted
145
+ into the audit log.
146
+
147
+ Almost all the processing work is still done for updates
148
+ that ignored. If you need to save the load, you need to use
149
+ WHEN clause on the trigger instead.
150
+
151
+ No warning or error is issued if ignored_cols contains columns
152
+ that do not exist in the target table. This lets you specify
153
+ a standard set of ignored columns.
154
+
155
+ There is no parameter to disable logging of values. Add this trigger as
156
+ a 'FOR EACH STATEMENT' rather than 'FOR EACH ROW' trigger if you do not
157
+ want to log row values.
158
+
159
+ Note that the user name logged is the login role for the session. The audit trigger
160
+ cannot obtain the active role because it is reset by the SECURITY DEFINER invocation
161
+ of the audit trigger its self.
162
+ $$;
163
+
164
+
165
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(target_table text)
166
+ RETURNS text AS
167
+ $$
168
+ DECLARE
169
+ _pk_query_text text;
170
+ _pk_column_name text;
171
+ BEGIN
172
+ _pk_query_text = 'SELECT a.attname ' ||
173
+ 'FROM pg_index i ' ||
174
+ 'JOIN pg_attribute a ON a.attrelid = i.indrelid ' ||
175
+ ' AND a.attnum = ANY(i.indkey) ' ||
176
+ 'WHERE i.indrelid = ' || quote_literal(target_table::TEXT) || '::regclass ' ||
177
+ 'AND i.indisprimary ' ||
178
+ 'AND format_type(a.atttypid, a.atttypmod) = ' || quote_literal('bigint'::TEXT) ||
179
+ 'LIMIT 1';
180
+
181
+ EXECUTE _pk_query_text INTO _pk_column_name;
182
+ raise notice 'Value %', _pk_column_name;
183
+ return _pk_column_name;
184
+ END;
185
+ $$
186
+ LANGUAGE plpgsql;
187
+
188
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(text) IS
189
+ $$
190
+ Get primary key column name if single PK and type bigint.
191
+
192
+ Arguments:
193
+ target_table: Table name, schema qualified if not on search_path
194
+ $$;
195
+
196
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean, ignored_cols text[])
197
+ RETURNS void AS
198
+ $$
199
+ DECLARE
200
+ stm_targets text = 'INSERT OR UPDATE OR DELETE OR TRUNCATE';
201
+ _q_txt text;
202
+ _pk_column_name text;
203
+ _pk_column_snip text;
204
+ _ignored_cols_snip text = '';
205
+ BEGIN
206
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_row ON ' || quote_ident(target_table::TEXT);
207
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_stm ON ' || quote_ident(target_table::TEXT);
208
+
209
+ EXECUTE 'CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(target_table::TEXT) || '(
210
+ CHECK (table_name = ' || quote_literal(target_table::TEXT) || ')
211
+ ) INHERITS (SELECTED_SCHEMA_NAME.logged_actions)';
212
+
213
+ IF audit_rows THEN
214
+ _pk_column_name = SELECTED_SCHEMA_NAME.get_primary_key_column(target_table::TEXT);
215
+
216
+ IF _pk_column_name IS NOT NULL THEN
217
+ _pk_column_snip = ', ' || quote_literal(_pk_column_name);
218
+ ELSE
219
+ _pk_column_snip = ', NULL';
220
+ END IF;
221
+
222
+ IF array_length(ignored_cols,1) > 0 THEN
223
+ _ignored_cols_snip = ', ' || quote_literal(ignored_cols);
224
+ END IF;
225
+ _q_txt = 'CREATE TRIGGER audit_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
226
+ quote_ident(target_table::TEXT) ||
227
+ ' FOR EACH ROW EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func(' ||
228
+ quote_literal(audit_query_text) || _pk_column_snip || _ignored_cols_snip || ');';
229
+ RAISE NOTICE '%',_q_txt;
230
+ EXECUTE _q_txt;
231
+ stm_targets = 'TRUNCATE';
232
+ ELSE
233
+ END IF;
234
+
235
+ _q_txt = 'CREATE TRIGGER audit_trigger_stm AFTER ' || stm_targets || ' ON ' ||
236
+ target_table ||
237
+ ' FOR EACH STATEMENT EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func('||
238
+ quote_literal(audit_query_text) || ');';
239
+ RAISE NOTICE '%',_q_txt;
240
+ EXECUTE _q_txt;
241
+
242
+ END;
243
+ $$
244
+ LANGUAGE plpgsql;
245
+
246
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass, boolean, boolean, text[]) IS
247
+ $$
248
+ Add auditing support to a table.
249
+
250
+ Arguments:
251
+ target_table: Table name, schema qualified if not on search_path
252
+ audit_rows: Record each row change, or only audit at a statement level
253
+ audit_query_text: Record the text of the client query that triggered the audit event?
254
+ ignored_cols: Columns to exclude from update diffs, ignore updates that change only ignored cols.
255
+ $$;
256
+
257
+ -- Pg doesn't allow variadic calls with 0 params, so provide a wrapper
258
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean)
259
+ RETURNS void AS
260
+ $$
261
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, $2, $3, ARRAY[]::text[]);
262
+ $$
263
+ LANGUAGE SQL;
264
+
265
+ -- And provide a convenience call wrapper for the simplest case
266
+ -- of row-level logging with no excluded cols and query logging enabled.
267
+ --
268
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass)
269
+ RETURNS void AS
270
+ $$
271
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, BOOLEAN 't', BOOLEAN 't');
272
+ $$
273
+ LANGUAGE SQL;
274
+
275
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass) IS
276
+ $$
277
+ Add auditing support to the given table. Row-level changes will be logged with full client query text. No cols are ignored.
278
+ $$;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterRecord
4
- VERSION = '0.17.9'
4
+ VERSION = '0.18.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.9
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampson Crowley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-31 00:00:00.000000000 Z
11
+ date: 2019-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -270,7 +270,10 @@ files:
270
270
  - db/migrate/20181228204403_create_better_record_attachment_validations.rb
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
+ - db/migrate/20190209033946_update_better_record_audit_functions.rb
273
274
  - db/postgres-audit-trigger.psql
275
+ - db/postgres-audit-v2-table.psql
276
+ - db/postgres-audit-v2-trigger.psql
274
277
  - lib/better_record.rb
275
278
  - lib/better_record/batches.rb
276
279
  - lib/better_record/concerns/active_record_extensions/associations_extensions/association_scope_extensions.rb