better_record 0.22.9 → 0.23.3

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: 91f13fbd6a8dc65f36431f809f4abbee9e3efb4e1f5463dfe6bfc6da36e9b79b
4
- data.tar.gz: 8441806856eacd6c530680d9184c19363bf15b8060e4198ea1f260ebe893d3fc
3
+ metadata.gz: 60c961a739971ab307f28b0d8a9a79dd037bacf16ece71be8ba1d6359593a079
4
+ data.tar.gz: e9f0b3840fe6a3f33f284e32c08cfe4a212ffd923ea631f777a05bdf5678c98e
5
5
  SHA512:
6
- metadata.gz: e351916c382db5ca42772ea5ba8034d978bf069caa9338bb3c6785db209ce72722761e15afcbc6d8f0b5bab6ab4ab7e4cbf39f51d3e5cc73f25aa9113e5826bb
7
- data.tar.gz: 2c345b65982a8376b0f978ac899d1b8f392fcfecf12bd21aa07d1fef72053dfb196bba3f48b6b0b388e9d44016d1a06039aa4058659a3dc875c2faf5b6716d5e
6
+ metadata.gz: 3da0e6cfafcde379dd4a14fe3eb4fdc80f75ab17392325f6e0e137d3017116c5fc0551628ae6bb21e3c6e585c6c736f12e1c0ead6c0094930bf62bb200c02f88
7
+ data.tar.gz: 732e52e899620a71718b8d6b8b159f4b485fa43d7145b2e2d8dd0efd1ea43b98ed52502b083e04ababa81f461baab9791f41680b2784f3194e9cc003bce960b9
@@ -57,6 +57,54 @@ module BetterRecord
57
57
  select(cq).limit(1).first[:"hashed_cert_#{t}"]
58
58
  end
59
59
 
60
+ def self.schema_qualified
61
+ return @schema_qualified if @schema_qualified.present?
62
+ if table_name =~ /.+\..+/
63
+ tmp_name = table_name.split('.')
64
+ @schema_qualified = {
65
+ schema_name: tmp_name[0],
66
+ table_name: tmp_name[1]
67
+ }
68
+ else
69
+ paths = (connection.execute("show search_path").first || {})['search_path']
70
+ paths = (paths.presence || "public").to_s.split(",")
71
+ paths.each do |schema_path|
72
+ row = (
73
+ connection.execute <<-SQL.cleanup_production
74
+ SELECT c.oid,
75
+ n.nspname,
76
+ c.relname
77
+ FROM pg_catalog.pg_class c
78
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
79
+ WHERE c.relname = '#{table_name}'
80
+ AND n.nspname = '#{schema_path.strip}'
81
+ ORDER BY 2, 3;
82
+ SQL
83
+ ).first
84
+
85
+ if row.present?
86
+ @schema_name = {
87
+ schema_name: row['nspname'],
88
+ table_name: row['relname']
89
+ }
90
+ break
91
+ end
92
+ end
93
+ return @schema_name || {
94
+ schema_name: "public",
95
+ table_name: table_name
96
+ }
97
+ end
98
+ end
99
+
100
+ def self.full_table_name
101
+ "#{schema_qualified[:schema_name]}.#{schema_qualified[:table_name]}"
102
+ end
103
+
104
+ def self.table_name_only
105
+ schema_qualified[:table_name]
106
+ end
107
+
60
108
  # == Boolean Methods ======================================================
61
109
 
62
110
  # == Instance Methods =====================================================
@@ -82,7 +130,14 @@ module BetterRecord
82
130
  if (ha = BetterRecord.has_auditing_relation_by_default)
83
131
  has_many self.audit_relation_name,
84
132
  class_name: 'BetterRecord::LoggedAction',
85
- primary_type: :table_name,
133
+ primary_type: :full_table_name,
134
+ foreign_key: :row_id,
135
+ foreign_type: :full_name,
136
+ as: self.audit_relation_name
137
+
138
+ has_many :"#{self.audit_relation_name}_full_table",
139
+ class_name: 'BetterRecord::LoggedAction',
140
+ primary_type: :table_name_only,
86
141
  foreign_key: :row_id,
87
142
  foreign_type: :table_name,
88
143
  as: self.audit_relation_name
@@ -38,7 +38,7 @@ module BetterRecord
38
38
  [
39
39
  :event_id,
40
40
  :row_id,
41
- :table_name,
41
+ :full_name,
42
42
  :app_user_id,
43
43
  :app_user_type,
44
44
  :action_type,
@@ -6,7 +6,8 @@ class AuditTriggerV3 < ActiveRecord::Migration[5.2]
6
6
  SELECT pg_inherits.*, c.relname AS child, p.relname AS parent
7
7
  FROM
8
8
  pg_inherits JOIN pg_class AS c ON (inhrelid=c.oid)
9
- JOIN pg_class as p ON (inhparent=p.oid);
9
+ JOIN pg_class as p ON (inhparent=p.oid)
10
+ WHERE p.relnamespace::regnamespace::text = '#{BetterRecord.db_audit_schema}';
10
11
  SQL
11
12
 
12
13
  children.each do |child|
@@ -91,7 +92,7 @@ class AuditTriggerV3 < ActiveRecord::Migration[5.2]
91
92
 
92
93
  rows.each do |r|
93
94
  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
+ puts "\n\nPLEASE RECREATE ALL AUDIT TRIGGERS FOR #{r['schema_name']}.#{r['table_name']}\n\n#{r}\n\n"
95
96
  end
96
97
  end
97
98
 
@@ -0,0 +1,162 @@
1
+ class AuditTriggerV4 < ActiveRecord::Migration[5.2]
2
+ def up
3
+ execute <<-SQL
4
+ DROP TABLE IF EXISTS #{BetterRecord.db_audit_schema}.old_old_logged_actions CASCADE;
5
+ DROP TABLE IF EXISTS #{BetterRecord.db_audit_schema}.old_logged_actions CASCADE;
6
+ DROP SEQUENCE IF EXISTS #{BetterRecord.db_audit_schema}.old_logged_actions_event_id_seq;
7
+ SQL
8
+
9
+ children = execute <<-SQL
10
+ SELECT pg_inherits.*, c.relname AS child, p.relname AS parent
11
+ FROM
12
+ pg_inherits JOIN pg_class AS c ON (inhrelid=c.oid)
13
+ JOIN pg_class as p ON (inhparent=p.oid)
14
+ WHERE p.relnamespace::regnamespace::text = '#{BetterRecord.db_audit_schema}';
15
+ SQL
16
+
17
+ children.each do |child|
18
+ execute <<-SQL
19
+ ALTER TABLE #{BetterRecord.db_audit_schema}.#{child['child']}
20
+ RENAME TO #{'old_' * child['parent'].split('old_').size}#{child['child']}
21
+ SQL
22
+ end
23
+
24
+ # seq = execute <<-SQL
25
+ # SELECT table_name, column_name, column_default from
26
+ # information_schema.columns where table_name='old_old_logged_actions' AND column_name = 'event_id';
27
+ # SQL
28
+ #
29
+ # seq = seq.first
30
+ #
31
+ # val = "nextval('#{BetterRecord.db_audit_schema}.logged_actions_event_id_seq1'::regclass)"
32
+
33
+ rows = execute <<-SQL
34
+ SELECT trg.tgname,
35
+ CASE trg.tgtype::integer & 66
36
+ WHEN 2 THEN 'BEFORE'
37
+ WHEN 64 THEN 'INSTEAD OF'
38
+ ELSE 'AFTER'
39
+ END AS trigger_type,
40
+ CASE trg.tgtype::integer & cast(28 as int2)
41
+ WHEN 16 THEN 'UPDATE'
42
+ WHEN 8 THEN 'DELETE'
43
+ WHEN 4 THEN 'INSERT'
44
+ WHEN 20 THEN 'INSERT, UPDATE'
45
+ WHEN 28 THEN 'INSERT, UPDATE, DELETE'
46
+ WHEN 24 THEN 'UPDATE, DELETE'
47
+ WHEN 12 THEN 'INSERT, DELETE'
48
+ END AS trigger_event,
49
+ tbl.relname AS table_name,
50
+ tbl.relnamespace::regnamespace AS schema_name,
51
+ obj_description(trg.oid) AS remarks,
52
+ CASE
53
+ WHEN trg.tgenabled='O' THEN 'ENABLED'
54
+ ELSE 'DISABLED'
55
+ END AS status,
56
+ CASE trg.tgtype::integer & 1
57
+ WHEN 1 THEN 'ROW'::text
58
+ ELSE 'STATEMENT'::text
59
+ END AS trigger_level
60
+ FROM pg_trigger trg
61
+ JOIN pg_class tbl on trg.tgrelid = tbl.oid
62
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
63
+ WHERE trg.tgname not like 'RI_ConstraintTrigger%'
64
+ AND trg.tgname not like 'pg_sync_pg%'
65
+ SQL
66
+
67
+ rows.each do |r|
68
+ if r['tgname'].to_s =~ /audit_trigger/
69
+ execute "DROP TRIGGER IF EXISTS #{r['tgname']} ON #{r['schema_name']}.#{r['table_name']} CASCADE;"
70
+ end
71
+ end
72
+
73
+
74
+ execute <<-SQL
75
+ ALTER SEQUENCE IF EXISTS #{BetterRecord.db_audit_schema}.logged_actions_event_id_seq RENAME TO old_logged_actions_event_id_seq;
76
+ ALTER TABLE #{BetterRecord.db_audit_schema}.logged_actions RENAME TO old_logged_actions
77
+ SQL
78
+
79
+ sql = ""
80
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-v4-table.psql'), "r")
81
+ while (line = source.gets)
82
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
83
+ end
84
+ source.close
85
+
86
+ execute sql
87
+
88
+ sql = ""
89
+ source = File.new(BetterRecord::Engine.root.join('db', 'postgres-audit-v4-trigger.psql'), "r")
90
+ while (line = source.gets)
91
+ sql << line.gsub(/SELECTED_SCHEMA_NAME/, BetterRecord.db_audit_schema)
92
+ end
93
+ source.close
94
+
95
+ execute sql
96
+
97
+ puts ''
98
+
99
+ rows.each do |r|
100
+ if r['tgname'].to_s =~ /audit_trigger/
101
+ puts <<-TEXT
102
+ PLEASE RECREATE ALL AUDIT TRIGGERS FOR #{r['table_name']}
103
+ #{r}
104
+
105
+ TEXT
106
+ end
107
+ end
108
+
109
+ puts "\n\nTo insert old audits back into logged_actions run:\n\n"
110
+
111
+ puts <<-RUBY
112
+ INSERT INTO #{BetterRecord.db_audit_schema}.logged_actions_view
113
+ (
114
+ schema_name,
115
+ table_name,
116
+ full_name,
117
+ relid,
118
+ session_user_name,
119
+ app_user_id,
120
+ app_user_type,
121
+ app_ip_address,
122
+ action_tstamp_tx,
123
+ action_tstamp_stm,
124
+ action_tstamp_clk,
125
+ transaction_id,
126
+ application_name,
127
+ client_addr,
128
+ client_port,
129
+ client_query,
130
+ action,
131
+ row_id,
132
+ row_data,
133
+ changed_fields,
134
+ statement_only
135
+ )
136
+ SELECT
137
+ schema_name,
138
+ table_name,
139
+ schema_name || '.' || table_name,
140
+ relid,
141
+ session_user_name,
142
+ app_user_id,
143
+ app_user_type,
144
+ app_ip_address,
145
+ action_tstamp_tx,
146
+ action_tstamp_stm,
147
+ action_tstamp_clk,
148
+ transaction_id,
149
+ application_name,
150
+ client_addr,
151
+ client_port,
152
+ client_query,
153
+ action,
154
+ row_id,
155
+ row_data,
156
+ changed_fields,
157
+ statement_only
158
+ FROM #{BetterRecord.db_audit_schema}.old_logged_actions
159
+ ORDER BY old_logged_actions.event_id;
160
+ RUBY
161
+ end
162
+ end
@@ -0,0 +1,85 @@
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
+ full_name text not null,
30
+ relid oid not null,
31
+ session_user_name text,
32
+ app_user_id integer,
33
+ app_user_type text,
34
+ app_ip_address inet,
35
+ action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
36
+ action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
37
+ action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
38
+ transaction_id bigint,
39
+ application_name text,
40
+ client_addr inet,
41
+ client_port integer,
42
+ client_query text,
43
+ action TEXT NOT NULL CHECK (action IN ('I','D','U', 'T', 'A')),
44
+ row_id bigint,
45
+ row_data hstore,
46
+ changed_fields hstore,
47
+ statement_only boolean not null
48
+ );
49
+
50
+ REVOKE ALL ON SELECTED_SCHEMA_NAME.logged_actions FROM public;
51
+
52
+ COMMENT ON TABLE SELECTED_SCHEMA_NAME.logged_actions IS 'History of auditable actions on audited tables, from SELECTED_SCHEMA_NAME.if_modified_func()';
53
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.event_id IS 'Unique identifier for each auditable event';
54
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.schema_name IS 'Database schema audited table for this event is in';
55
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.table_name IS 'Non-schema-qualified table name of table event occured in';
56
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.full_name IS 'schema-qualified table name of table event occured in';
57
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.relid IS 'Table OID. Changes with drop/create. Get with ''tablename''::regclass';
58
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.session_user_name IS 'Login / session user whose statement caused the audited event';
59
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_user_id IS 'Application-provided polymorphic user id';
60
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_user_type IS 'Application-provided polymorphic user type';
61
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.app_ip_address IS 'Application-provided ip address of user whose statement caused the audited event';
62
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_tx IS 'Transaction start timestamp for tx in which audited event occurred';
63
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_stm IS 'Statement start timestamp for tx in which audited event occurred';
64
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action_tstamp_clk IS 'Wall clock time at which audited event''s trigger call occurred';
65
+ 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.';
66
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.client_addr IS 'IP address of client that issued query. Null for unix domain socket.';
67
+ 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.';
68
+ 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.';
69
+ 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.';
70
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.action IS 'Action type; I = insert, D = delete, U = update, T = truncate, A = archive';
71
+ COMMENT ON COLUMN SELECTED_SCHEMA_NAME.logged_actions.row_id IS 'Record primary_key. Null for statement-level trigger. Prefers NEW.id if exists';
72
+ 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.';
73
+ 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.';
74
+ 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';
75
+
76
+ CREATE INDEX IF NOT EXISTS logged_actions_relid_idx ON SELECTED_SCHEMA_NAME.logged_actions(relid);
77
+ CREATE INDEX IF NOT EXISTS logged_actions_table_name_idx ON SELECTED_SCHEMA_NAME.logged_actions(table_name);
78
+ CREATE INDEX IF NOT EXISTS logged_actions_full_name_idx ON SELECTED_SCHEMA_NAME.logged_actions(full_name);
79
+ CREATE INDEX IF NOT EXISTS logged_actions_action_tstamp_tx_stm_idx ON SELECTED_SCHEMA_NAME.logged_actions(action_tstamp_stm);
80
+ CREATE INDEX IF NOT EXISTS logged_actions_action_idx ON SELECTED_SCHEMA_NAME.logged_actions(action);
81
+ CREATE INDEX IF NOT EXISTS logged_actions_row_id_idx ON SELECTED_SCHEMA_NAME.logged_actions(row_id);
82
+
83
+ DROP VIEW IF EXISTS SELECTED_SCHEMA_NAME.logged_actions_view CASCADE;
84
+ CREATE OR REPLACE VIEW SELECTED_SCHEMA_NAME.logged_actions_view AS SELECT * FROM SELECTED_SCHEMA_NAME.logged_actions;
85
+ 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,358 @@
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
+ DO $$ BEGIN
16
+ CREATE TYPE temp_table_info AS (schema_name TEXT, table_name TEXT);
17
+ EXCEPTION
18
+ WHEN duplicate_object THEN null;
19
+ END $$;
20
+
21
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.get_table_information(target_table regclass)
22
+ RETURNS temp_table_info AS
23
+ $$
24
+ DECLARE
25
+ table_row record;
26
+ info_row temp_table_info;
27
+ BEGIN
28
+
29
+ FOR table_row IN SELECT * FROM pg_catalog.pg_class WHERE oid = target_table LOOP
30
+ info_row.schema_name = table_row.relnamespace::regnamespace::TEXT;
31
+ info_row.table_name = table_row.relname::TEXT;
32
+ END LOOP;
33
+ return info_row;
34
+ END;
35
+ $$
36
+ LANGUAGE plpgsql;
37
+
38
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.get_table_information(regclass) IS
39
+ $$
40
+ Get unqualified table name and schema name from a table regclass.
41
+
42
+ Arguments:
43
+ target_table: Table name, schema qualified if not on search_path
44
+ $$;
45
+
46
+
47
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(target_table text)
48
+ RETURNS text AS
49
+ $$
50
+ DECLARE
51
+ _pk_query_text text;
52
+ _pk_column_name text;
53
+ BEGIN
54
+ _pk_query_text = 'SELECT a.attname ' ||
55
+ 'FROM pg_index i ' ||
56
+ 'JOIN pg_attribute a ON a.attrelid = i.indrelid ' ||
57
+ ' AND a.attnum = ANY(i.indkey) ' ||
58
+ 'WHERE i.indrelid = ' || quote_literal(target_table::TEXT) || '::regclass ' ||
59
+ 'AND i.indisprimary ' ||
60
+ 'AND format_type(a.atttypid, a.atttypmod) = ' || quote_literal('bigint'::TEXT) ||
61
+ 'LIMIT 1';
62
+
63
+ EXECUTE _pk_query_text INTO _pk_column_name;
64
+ return _pk_column_name;
65
+ END;
66
+ $$
67
+ LANGUAGE plpgsql;
68
+
69
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.get_primary_key_column(text) IS
70
+ $$
71
+ Get primary key column name if single PK and type bigint.
72
+
73
+ Arguments:
74
+ target_table: Table name, schema qualified if not on search_path
75
+ $$;
76
+
77
+
78
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.skip_logged_actions_main()
79
+ RETURNS TRIGGER
80
+ AS $$
81
+ BEGIN
82
+ raise exception 'insert on wrong table';
83
+ RETURN NULL;
84
+ END;
85
+ $$
86
+ LANGUAGE plpgsql
87
+ SECURITY DEFINER
88
+ SET search_path = pg_catalog, public;
89
+
90
+ DROP TRIGGER IF EXISTS logged_actions_skip_direct ON SELECTED_SCHEMA_NAME.logged_actions;
91
+ CREATE TRIGGER logged_actions_skip_direct BEFORE INSERT ON SELECTED_SCHEMA_NAME.logged_actions EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.skip_logged_actions_main();
92
+
93
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.logged_actions_partition()
94
+ RETURNS TRIGGER AS
95
+ $$
96
+ DECLARE
97
+ table_name text;
98
+ table_info temp_table_info;
99
+ BEGIN
100
+ table_info = SELECTED_SCHEMA_NAME.get_table_information(NEW.table_name::regclass);
101
+
102
+ table_name = table_info.table_name::TEXT;
103
+
104
+ EXECUTE 'CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(table_name) || '(
105
+ CHECK (table_name = ' || quote_literal(table_info.table_name) || '),
106
+ LIKE SELECTED_SCHEMA_NAME.logged_actions INCLUDING ALL
107
+ ) INHERITS (SELECTED_SCHEMA_NAME.logged_actions)';
108
+
109
+ EXECUTE 'INSERT INTO SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(table_name) || ' VALUES ($1.*)' USING NEW;
110
+
111
+ RETURN NEW;
112
+ END;
113
+ $$
114
+ LANGUAGE plpgsql
115
+ SECURITY DEFINER
116
+ SET search_path = pg_catalog, public;
117
+
118
+ DROP TRIGGER IF EXISTS logged_actions_partition_by_table ON SELECTED_SCHEMA_NAME.logged_actions_view;
119
+ CREATE TRIGGER logged_actions_partition_by_table INSTEAD OF INSERT ON SELECTED_SCHEMA_NAME.logged_actions_view
120
+ FOR each ROW EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.logged_actions_partition();
121
+
122
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.if_modified_func()
123
+ RETURNS TRIGGER AS
124
+ $$
125
+ DECLARE
126
+ audit_row SELECTED_SCHEMA_NAME.logged_actions;
127
+ include_values boolean;
128
+ log_diffs boolean;
129
+ h_old hstore;
130
+ h_new hstore;
131
+ user_row record;
132
+ excluded_cols text[] = ARRAY[]::text[];
133
+ pk_val_query text;
134
+ BEGIN
135
+ IF TG_WHEN <> 'AFTER' THEN
136
+ RAISE EXCEPTION 'SELECTED_SCHEMA_NAME.if_modified_func() may only run as an AFTER trigger';
137
+ END IF;
138
+
139
+ audit_row = ROW(
140
+ nextval('SELECTED_SCHEMA_NAME.logged_actions_event_id_seq'), -- event_id
141
+ TG_TABLE_SCHEMA::text, -- schema_name
142
+ TG_TABLE_NAME::text, -- table_name
143
+ TG_TABLE_SCHEMA::text || '.' || TG_TABLE_NAME::text, -- full_name
144
+ TG_RELID, -- relation OID for much quicker searches
145
+ session_user::text, -- session_user_name
146
+ NULL, NULL, NULL, -- app_user_id, app_user_type, app_ip_address
147
+ current_timestamp, -- action_tstamp_tx
148
+ statement_timestamp(), -- action_tstamp_stm
149
+ clock_timestamp(), -- action_tstamp_clk
150
+ txid_current(), -- transaction ID
151
+ current_setting('application_name'), -- client application
152
+ inet_client_addr(), -- client_addr
153
+ inet_client_port(), -- client_port
154
+ current_query(), -- top-level query or queries (if multistatement) from client
155
+ substring(TG_OP,1,1), -- action
156
+ NULL, NULL, NULL, -- row_id, row_data, changed_fields
157
+ 'f' -- statement_only
158
+ );
159
+
160
+ IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
161
+ audit_row.client_query = NULL;
162
+ END IF;
163
+
164
+ IF ((TG_LEVEL = 'ROW') AND (TG_ARGV[1] IS NOT NULL) AND (TG_ARGV[1]::TEXT <> 'NULL') AND (TG_ARGV[1]::TEXT <> 'null') AND (TG_ARGV[1]::TEXT <> '')) THEN
165
+ pk_val_query = 'SELECT $1.' || quote_ident(TG_ARGV[1]::text);
166
+
167
+ IF (TG_OP IS DISTINCT FROM 'DELETE') THEN
168
+ EXECUTE pk_val_query INTO audit_row.row_id USING NEW;
169
+ END IF;
170
+
171
+ IF audit_row.row_id IS NULL THEN
172
+ EXECUTE pk_val_query INTO audit_row.row_id USING OLD;
173
+ END IF;
174
+ END IF;
175
+
176
+ IF TG_ARGV[2] IS NOT NULL THEN
177
+ excluded_cols = TG_ARGV[2]::text[];
178
+ END IF;
179
+
180
+ CREATE TEMP TABLE IF NOT EXISTS
181
+ "_app_user" (user_id integer, user_type text, ip_address inet);
182
+
183
+ IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
184
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
185
+ audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
186
+ IF audit_row.changed_fields = hstore('') THEN
187
+ -- All changed fields are ignored. Skip this update.
188
+ RETURN NULL;
189
+ END IF;
190
+ ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
191
+ audit_row.row_data = hstore(OLD.*) - excluded_cols;
192
+ ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
193
+ audit_row.row_data = hstore(NEW.*) - excluded_cols;
194
+ ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
195
+ audit_row.statement_only = 't';
196
+ ELSE
197
+ RAISE EXCEPTION '[SELECTED_SCHEMA_NAME.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
198
+ RETURN NULL;
199
+ END IF;
200
+
201
+ -- inject app_user data into audit
202
+ BEGIN
203
+ PERFORM
204
+ n.nspname, c.relname
205
+ FROM
206
+ pg_catalog.pg_class c
207
+ LEFT JOIN
208
+ pg_catalog.pg_namespace n
209
+ ON n.oid = c.relnamespace
210
+ WHERE
211
+ n.nspname like 'pg_temp_%'
212
+ AND
213
+ c.relname = '_app_user';
214
+
215
+ IF FOUND THEN
216
+ FOR user_row IN SELECT * FROM _app_user LIMIT 1 LOOP
217
+ audit_row.app_user_id = user_row.user_id;
218
+ audit_row.app_user_type = user_row.user_type;
219
+ audit_row.app_ip_address = user_row.ip_address;
220
+ END LOOP;
221
+ END IF;
222
+ END;
223
+ -- end app_user data
224
+
225
+ INSERT INTO SELECTED_SCHEMA_NAME.logged_actions_view VALUES (audit_row.*);
226
+ RETURN NULL;
227
+ END;
228
+ $$
229
+ LANGUAGE plpgsql
230
+ SECURITY DEFINER
231
+ SET search_path = pg_catalog, public;
232
+
233
+
234
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.if_modified_func() IS
235
+ $$
236
+ Track changes to a table at the statement and/or row level.
237
+
238
+ Optional parameters to trigger in CREATE TRIGGER call:
239
+
240
+ param 0: boolean, whether to log the query text. Default 't'.
241
+
242
+ param 1: text, primary_key_column of audited table if bigint.
243
+
244
+ param 2: text[], columns to ignore in updates. Default [].
245
+
246
+ Updates to ignored cols are omitted from changed_fields.
247
+
248
+ Updates with only ignored cols changed are not inserted
249
+ into the audit log.
250
+
251
+ Almost all the processing work is still done for updates
252
+ that ignored. If you need to save the load, you need to use
253
+ WHEN clause on the trigger instead.
254
+
255
+ No warning or error is issued if ignored_cols contains columns
256
+ that do not exist in the target table. This lets you specify
257
+ a standard set of ignored columns.
258
+
259
+ There is no parameter to disable logging of values. Add this trigger as
260
+ a 'FOR EACH STATEMENT' rather than 'FOR EACH ROW' trigger if you do not
261
+ want to log row values.
262
+
263
+ Note that the user name logged is the login role for the session. The audit trigger
264
+ cannot obtain the active role because it is reset by the SECURITY DEFINER invocation
265
+ of the audit trigger its self.
266
+ $$;
267
+
268
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean, ignored_cols text[])
269
+ RETURNS void AS
270
+ $$
271
+ DECLARE
272
+ stm_targets text = 'INSERT OR UPDATE OR DELETE OR TRUNCATE';
273
+ table_info temp_table_info;
274
+ _full_name regclass;
275
+ _q_txt text;
276
+ _pk_column_name text;
277
+ _pk_column_snip text;
278
+ _ignored_cols_snip text = '';
279
+ BEGIN
280
+ table_info = SELECTED_SCHEMA_NAME.get_table_information(target_table);
281
+ _full_name = quote_ident(table_info.schema_name) || '.' || quote_ident(table_info.table_name);
282
+
283
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_row ON ' || _full_name;
284
+ EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_stm ON ' || _full_name;
285
+
286
+
287
+ EXECUTE 'CREATE TABLE IF NOT EXISTS SELECTED_SCHEMA_NAME.logged_actions_' || quote_ident(table_info.table_name) || '(
288
+ CHECK (table_name = ' || quote_literal(table_info.table_name) || '),
289
+ LIKE SELECTED_SCHEMA_NAME.logged_actions INCLUDING ALL
290
+ ) INHERITS (SELECTED_SCHEMA_NAME.logged_actions)';
291
+
292
+ IF audit_rows THEN
293
+ _pk_column_name = SELECTED_SCHEMA_NAME.get_primary_key_column(_full_name::TEXT);
294
+
295
+ IF _pk_column_name IS NOT NULL THEN
296
+ _pk_column_snip = ', ' || quote_literal(_pk_column_name);
297
+ ELSE
298
+ _pk_column_snip = ', NULL';
299
+ END IF;
300
+
301
+ IF array_length(ignored_cols,1) > 0 THEN
302
+ _ignored_cols_snip = ', ' || quote_literal(ignored_cols);
303
+ END IF;
304
+ _q_txt = 'CREATE TRIGGER audit_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
305
+ _full_name ||
306
+ ' FOR EACH ROW EXECUTE PROCEDURE SELECTED_SCHEMA_NAME.if_modified_func(' ||
307
+ quote_literal(audit_query_text) || _pk_column_snip || _ignored_cols_snip || ');';
308
+ RAISE NOTICE '%',_q_txt;
309
+ EXECUTE _q_txt;
310
+ stm_targets = 'TRUNCATE';
311
+ ELSE
312
+ END IF;
313
+
314
+ _q_txt = '' ||
315
+ 'CREATE TRIGGER audit_trigger_stm AFTER ' || stm_targets ||
316
+ ' ON ' || _full_name ||
317
+ ' FOR EACH STATEMENT EXECUTE PROCEDURE ' ||
318
+ 'SELECTED_SCHEMA_NAME.if_modified_func(' || quote_literal(audit_query_text) || ');';
319
+ RAISE NOTICE '%',_q_txt;
320
+ EXECUTE _q_txt;
321
+
322
+ END;
323
+ $$
324
+ LANGUAGE plpgsql;
325
+
326
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass, boolean, boolean, text[]) IS
327
+ $$
328
+ Add auditing support to a table.
329
+
330
+ Arguments:
331
+ target_table: Table name, schema qualified if not on search_path
332
+ audit_rows: Record each row change, or only audit at a statement level
333
+ audit_query_text: Record the text of the client query that triggered the audit event?
334
+ ignored_cols: Columns to exclude from update diffs, ignore updates that change only ignored cols.
335
+ $$;
336
+
337
+ -- Pg doesn't allow variadic calls with 0 params, so provide a wrapper
338
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean)
339
+ RETURNS void AS
340
+ $$
341
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, $2, $3, ARRAY[]::text[]);
342
+ $$
343
+ LANGUAGE SQL;
344
+
345
+ -- And provide a convenience call wrapper for the simplest case
346
+ -- of row-level logging with no excluded cols and query logging enabled.
347
+ --
348
+ CREATE OR REPLACE FUNCTION SELECTED_SCHEMA_NAME.audit_table(target_table regclass)
349
+ RETURNS void AS
350
+ $$
351
+ SELECT SELECTED_SCHEMA_NAME.audit_table($1, BOOLEAN 't', BOOLEAN 't');
352
+ $$
353
+ LANGUAGE SQL;
354
+
355
+ COMMENT ON FUNCTION SELECTED_SCHEMA_NAME.audit_table(regclass) IS
356
+ $$
357
+ Add auditing support to the given table. Row-level changes will be logged with full client query text. No cols are ignored.
358
+ $$;
@@ -27,8 +27,8 @@ module BetterRecord
27
27
  end
28
28
 
29
29
  d.down do
30
- execute "DROP TRIGGER audit_trigger_row ON #{table_name};"
31
- execute "DROP TRIGGER audit_trigger_stm ON #{table_name};"
30
+ execute "DROP TRIGGER IF EXISTS audit_trigger_row ON #{table_name};"
31
+ execute "DROP TRIGGER IF EXISTS audit_trigger_stm ON #{table_name};"
32
32
  end
33
33
  end
34
34
  end
@@ -23,7 +23,7 @@ module BetterRecord
23
23
  $!.message, $!.backtrace if debug
24
24
 
25
25
  if type_val == :table_name_without_schema
26
- type_val = klass.table_name
26
+ type_val = klass.table_name.to_s.split('.').first
27
27
  else
28
28
  type_val = klass.polymorphic_name
29
29
  end
@@ -36,7 +36,7 @@ module BetterRecord
36
36
  end
37
37
 
38
38
  def self.all_types(klass)
39
- keys = [ :polymorphic_name, :table_name ]
39
+ keys = [ :polymorphic_name, :full_table_name, :table_name_only, :table_name ]
40
40
  keys |= [BetterRecord.default_polymorphic_method] if BetterRecord.default_polymorphic_method.present?
41
41
  p "Polymorphic methods:", keys if debug
42
42
  values = []
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterRecord
4
- VERSION = '0.22.9'
4
+ VERSION = '0.23.3'
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.22.9
4
+ version: 0.23.3
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-07-02 00:00:00.000000000 Z
11
+ date: 2019-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -275,11 +275,14 @@ files:
275
275
  - db/migrate/20190209033946_update_better_record_audit_functions.rb
276
276
  - db/migrate/20190209195134_audit_trigger_v3.rb
277
277
  - db/migrate/20190416215152_add_three_state_boolean_type.rb
278
+ - db/migrate/20190823220215_audit_trigger_v4.rb
278
279
  - db/postgres-audit-trigger.psql
279
280
  - db/postgres-audit-v2-table.psql
280
281
  - db/postgres-audit-v2-trigger.psql
281
282
  - db/postgres-audit-v3-table.psql
282
283
  - db/postgres-audit-v3-trigger.psql
284
+ - db/postgres-audit-v4-table.psql
285
+ - db/postgres-audit-v4-trigger.psql
283
286
  - lib/better_record.rb
284
287
  - lib/better_record/batches.rb
285
288
  - lib/better_record/concerns/active_record_extensions/associations_extensions/association_extension.rb
@@ -357,7 +360,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
357
360
  - !ruby/object:Gem::Version
358
361
  version: '0'
359
362
  requirements: []
360
- rubygems_version: 3.0.4
363
+ rubygems_version: 3.0.6
361
364
  signing_key:
362
365
  specification_version: 4
363
366
  summary: Fix functions that are lacking in Active record to be compatible with multi-app