pg_triggers 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/pg_triggers.rb +57 -0
- data/lib/pg_triggers/version.rb +1 -1
- data/pg_triggers.gemspec +1 -0
- data/spec/audit_table_spec.rb +220 -0
- data/spec/spec_helper.rb +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19f1fcd14466ad95643f91d2dc62934b7b6eef63
|
4
|
+
data.tar.gz: eaaea66c3afaa575234c0fb2be06c3b3f265df0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f25370dae7dfffba8e21b20c7072b979942c7542b12857166d8473b5307bbc108e897b5726560c22166962182e2d0720503467a86694f8e3c16be0df42ede480
|
7
|
+
data.tar.gz: 73b51af990c1a93f284ff1f1402378f5299e995ad39ae1452b7bbe22468b5cd772ac58f9fa4abe8fea9c6a232643ea4daaa7aa656fe85f6f09ae8c4d9e84ed74
|
data/CHANGELOG.md
CHANGED
data/lib/pg_triggers.rb
CHANGED
@@ -50,5 +50,62 @@ module PgTriggers
|
|
50
50
|
FOR EACH ROW EXECUTE PROCEDURE pg_triggers_counter_#{main_table}_#{counter_column}();
|
51
51
|
SQL
|
52
52
|
end
|
53
|
+
|
54
|
+
def create_audit_table
|
55
|
+
<<-SQL
|
56
|
+
CREATE TABLE audit_table(
|
57
|
+
id bigserial PRIMARY KEY,
|
58
|
+
table_name text NOT NULL,
|
59
|
+
changed_at timestamptz NOT NULL DEFAULT now(),
|
60
|
+
changes json NOT NULL
|
61
|
+
);
|
62
|
+
SQL
|
63
|
+
end
|
64
|
+
|
65
|
+
def audit_table(table_name, options = {})
|
66
|
+
incl = options[:include].map{|a| "'#{a}'"}.join(', ') if options[:include]
|
67
|
+
ignore = options[:ignore].map{|a| "'#{a}'"}.join(', ') if options[:ignore]
|
68
|
+
|
69
|
+
<<-SQL
|
70
|
+
CREATE OR REPLACE FUNCTION pg_triggers_audit_#{table_name}() RETURNS TRIGGER
|
71
|
+
AS $body$
|
72
|
+
DECLARE
|
73
|
+
changed_keys text[];
|
74
|
+
changes json;
|
75
|
+
BEGIN
|
76
|
+
IF (TG_OP = 'UPDATE') THEN
|
77
|
+
SELECT array_agg(o.key) INTO changed_keys
|
78
|
+
FROM json_each(row_to_json(OLD)) o
|
79
|
+
JOIN json_each(row_to_json(NEW)) n ON o.key = n.key
|
80
|
+
WHERE o.value::text <> n.value::text;
|
81
|
+
|
82
|
+
IF NOT (ARRAY[#{ignore}]::text[] @> changed_keys) THEN
|
83
|
+
SELECT ('{' || string_agg('"' || key || '":' || value, ',') || '}')::json INTO changes
|
84
|
+
FROM json_each(row_to_json(OLD))
|
85
|
+
WHERE (
|
86
|
+
key = ANY(changed_keys)
|
87
|
+
#{"AND key NOT IN (#{ignore})" if ignore}
|
88
|
+
)
|
89
|
+
#{"OR key IN (#{incl})" if incl};
|
90
|
+
|
91
|
+
INSERT INTO audit_table(table_name, changes) VALUES (TG_TABLE_NAME::TEXT, changes);
|
92
|
+
END IF;
|
93
|
+
|
94
|
+
RETURN OLD;
|
95
|
+
ELSIF (TG_OP = 'DELETE') THEN
|
96
|
+
INSERT INTO audit_table(table_name, changes) VALUES (TG_TABLE_NAME::TEXT, row_to_json(OLD));
|
97
|
+
RETURN OLD;
|
98
|
+
END IF;
|
99
|
+
END
|
100
|
+
$body$
|
101
|
+
LANGUAGE plpgsql;
|
102
|
+
|
103
|
+
DROP TRIGGER IF EXISTS pg_triggers_audit_#{table_name} ON #{table_name};
|
104
|
+
|
105
|
+
CREATE TRIGGER pg_triggers_audit_#{table_name}
|
106
|
+
AFTER UPDATE OR DELETE ON #{table_name}
|
107
|
+
FOR EACH ROW EXECUTE PROCEDURE pg_triggers_audit_#{table_name}();
|
108
|
+
SQL
|
109
|
+
end
|
53
110
|
end
|
54
111
|
end
|
data/lib/pg_triggers/version.rb
CHANGED
data/pg_triggers.gemspec
CHANGED
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
describe PgTriggers, 'auditing' do
|
5
|
+
before do
|
6
|
+
DB.drop_table?(:audit_table)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "create_audit_table" do
|
10
|
+
it "should create a table to hold auditing information" do
|
11
|
+
DB.run PgTriggers.create_audit_table
|
12
|
+
DB.table_exists?(:audit_table).should be true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "audit_table" do
|
17
|
+
before do
|
18
|
+
DB.run PgTriggers.create_audit_table
|
19
|
+
DB.drop_table?(:audited_table)
|
20
|
+
DB.create_table :audited_table do
|
21
|
+
primary_key :id
|
22
|
+
text :description
|
23
|
+
integer :item_count, null: false, default: 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should record old versions of rows when they are updated" do
|
28
|
+
DB.run PgTriggers.audit_table(:audited_table)
|
29
|
+
|
30
|
+
id = DB[:audited_table].insert
|
31
|
+
DB[:audited_table].where(id: id).update item_count: 1
|
32
|
+
DB[:audited_table].where(id: id).update description: 'blah'
|
33
|
+
DB[:audited_table].where(id: id).update description: nil
|
34
|
+
DB[:audited_table].where(id: id).update item_count: 2
|
35
|
+
|
36
|
+
DB[:audit_table].count.should == 4
|
37
|
+
r1, r2, r3, r4 = DB[:audit_table].order(:id).all
|
38
|
+
|
39
|
+
r1[:id].should == 1
|
40
|
+
r1[:table_name].should == 'audited_table'
|
41
|
+
r1[:changed_at].should be_within(3).of Time.now
|
42
|
+
JSON.parse(r1[:changes]).should == {'item_count' => 0}
|
43
|
+
|
44
|
+
r2[:id].should == 2
|
45
|
+
r2[:table_name].should == 'audited_table'
|
46
|
+
r2[:changed_at].should be_within(3).of Time.now
|
47
|
+
JSON.parse(r2[:changes]).should == {'description' => nil}
|
48
|
+
|
49
|
+
r3[:id].should == 3
|
50
|
+
r3[:table_name].should == 'audited_table'
|
51
|
+
r3[:changed_at].should be_within(3).of Time.now
|
52
|
+
JSON.parse(r3[:changes]).should == {'description' => 'blah'}
|
53
|
+
|
54
|
+
r4[:id].should == 4
|
55
|
+
r4[:table_name].should == 'audited_table'
|
56
|
+
r4[:changed_at].should be_within(3).of Time.now
|
57
|
+
JSON.parse(r4[:changes]).should == {'item_count' => 1}
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should always record the values of columns in the :always set" do
|
61
|
+
DB.run PgTriggers.audit_table(:audited_table, include: [:id, :item_count])
|
62
|
+
|
63
|
+
id = DB[:audited_table].insert
|
64
|
+
DB[:audited_table].where(id: id).update item_count: 1
|
65
|
+
DB[:audited_table].where(id: id).update description: 'blah'
|
66
|
+
|
67
|
+
DB[:audit_table].count.should == 2
|
68
|
+
r1, r2 = DB[:audit_table].order(:id).all
|
69
|
+
|
70
|
+
r1[:id].should == 1
|
71
|
+
r1[:table_name].should == 'audited_table'
|
72
|
+
r1[:changed_at].should be_within(3).of Time.now
|
73
|
+
JSON.parse(r1[:changes]).should == {'id' => 1, 'item_count' => 0}
|
74
|
+
|
75
|
+
r2[:id].should == 2
|
76
|
+
r2[:table_name].should == 'audited_table'
|
77
|
+
r2[:changed_at].should be_within(3).of Time.now
|
78
|
+
JSON.parse(r2[:changes]).should == {'id' => 1, 'item_count' => 1, 'description' => nil}
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not record UPDATEs when the only changed columns fall within the :ignore set" do
|
82
|
+
DB.run PgTriggers.audit_table(:audited_table, ignore: [:item_count])
|
83
|
+
|
84
|
+
id = DB[:audited_table].insert
|
85
|
+
DB[:audited_table].where(id: id).update item_count: 1
|
86
|
+
DB[:audited_table].where(id: id).update description: 'blah'
|
87
|
+
DB[:audited_table].where(id: id).update description: nil
|
88
|
+
DB[:audited_table].where(id: id).update item_count: 2
|
89
|
+
|
90
|
+
DB[:audit_table].count.should == 2
|
91
|
+
r1, r2 = DB[:audit_table].order(:id).all
|
92
|
+
|
93
|
+
r1[:id].should == 1
|
94
|
+
r1[:table_name].should == 'audited_table'
|
95
|
+
r1[:changed_at].should be_within(3).of Time.now
|
96
|
+
JSON.parse(r1[:changes]).should == {'description' => nil}
|
97
|
+
|
98
|
+
r2[:id].should == 2
|
99
|
+
r2[:table_name].should == 'audited_table'
|
100
|
+
r2[:changed_at].should be_within(3).of Time.now
|
101
|
+
JSON.parse(r2[:changes]).should == {'description' => 'blah'}
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should handle columns being in both the :include and :ignore sets properly" do
|
105
|
+
DB.run PgTriggers.audit_table(:audited_table, include: [:item_count], ignore: [:item_count])
|
106
|
+
|
107
|
+
id = DB[:audited_table].insert
|
108
|
+
DB[:audited_table].where(id: id).update item_count: 1
|
109
|
+
DB[:audited_table].where(id: id).update description: 'blah'
|
110
|
+
DB[:audited_table].where(id: id).update description: nil
|
111
|
+
DB[:audited_table].where(id: id).update item_count: 2
|
112
|
+
|
113
|
+
DB[:audit_table].count.should == 2
|
114
|
+
r1, r2 = DB[:audit_table].order(:id).all
|
115
|
+
|
116
|
+
r1[:id].should == 1
|
117
|
+
r1[:table_name].should == 'audited_table'
|
118
|
+
r1[:changed_at].should be_within(3).of Time.now
|
119
|
+
JSON.parse(r1[:changes]).should == {'description' => nil, 'item_count' => 1}
|
120
|
+
|
121
|
+
r2[:id].should == 2
|
122
|
+
r2[:table_name].should == 'audited_table'
|
123
|
+
r2[:changed_at].should be_within(3).of Time.now
|
124
|
+
JSON.parse(r2[:changes]).should == {'description' => 'blah', 'item_count' => 1}
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should ignore records that are not changed at all" do
|
128
|
+
DB.run PgTriggers.audit_table(:audited_table)
|
129
|
+
|
130
|
+
id = DB[:audited_table].insert
|
131
|
+
DB[:audited_table].where(id: id).update item_count: 0
|
132
|
+
DB[:audit_table].count.should == 0
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should not include changed columns if they are ignored" do
|
136
|
+
DB.run PgTriggers.audit_table(:audited_table, ignore: [:item_count])
|
137
|
+
|
138
|
+
id = DB[:audited_table].insert
|
139
|
+
DB[:audited_table].where(id: id).update description: 'blah', item_count: 1
|
140
|
+
DB[:audited_table].where(id: id).update description: 'blah', item_count: 2
|
141
|
+
DB[:audited_table].where(id: id).update description: nil, item_count: 3
|
142
|
+
|
143
|
+
DB[:audit_table].count.should == 2
|
144
|
+
r1, r2 = DB[:audit_table].order(:id).all
|
145
|
+
|
146
|
+
r1[:id].should == 1
|
147
|
+
r1[:table_name].should == 'audited_table'
|
148
|
+
r1[:changed_at].should be_within(3).of Time.now
|
149
|
+
JSON.parse(r1[:changes]).should == {'description' => nil}
|
150
|
+
|
151
|
+
r2[:id].should == 2
|
152
|
+
r2[:table_name].should == 'audited_table'
|
153
|
+
r2[:changed_at].should be_within(3).of Time.now
|
154
|
+
JSON.parse(r2[:changes]).should == {'description' => 'blah'}
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should record the entirety of the row when it is deleted" do
|
158
|
+
DB.run PgTriggers.audit_table(:audited_table)
|
159
|
+
|
160
|
+
id = DB[:audited_table].insert description: 'Go home and get your shinebox!', item_count: 5
|
161
|
+
DB[:audited_table].where(id: id).delete.should == 1
|
162
|
+
|
163
|
+
DB[:audit_table].count.should == 1
|
164
|
+
record = DB[:audit_table].first
|
165
|
+
record[:id].should == 1
|
166
|
+
record[:table_name].should == 'audited_table'
|
167
|
+
record[:changed_at].should be_within(3).of Time.now
|
168
|
+
JSON.parse(record[:changes]).should == {'id' => 1, 'description' => 'Go home and get your shinebox!', 'item_count' => 5}
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should seamlessly replace an existing audit trigger on the same table" do
|
172
|
+
id = DB[:audited_table].insert description: 'Go home and get your shinebox!', item_count: 5
|
173
|
+
|
174
|
+
DB.run PgTriggers.audit_table(:audited_table)
|
175
|
+
DB[:audited_table].where(id: id).update(item_count: 6)
|
176
|
+
DB[:audit_table].count.should == 1
|
177
|
+
|
178
|
+
DB.run PgTriggers.audit_table(:audited_table, ignore: [:item_count])
|
179
|
+
DB[:audited_table].where(id: id).update(item_count: 7)
|
180
|
+
DB[:audit_table].count.should == 1
|
181
|
+
DB[:audited_table].where(id: id).update(description: 'blah')
|
182
|
+
DB[:audit_table].count.should == 2
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should properly handle rows of type JSON" do
|
186
|
+
DB.alter_table :audited_table do
|
187
|
+
add_column :data, :json, null: false, default: '{}'
|
188
|
+
end
|
189
|
+
|
190
|
+
id = DB[:audited_table].insert data: '{}'
|
191
|
+
|
192
|
+
DB.run PgTriggers.audit_table(:audited_table)
|
193
|
+
DB[:audited_table].where(id: id).update(data: '{"a":1}')
|
194
|
+
DB[:audited_table].where(id: id).update(data: '{"a":1,"b":2}')
|
195
|
+
DB[:audited_table].where(id: id).update(data: '{"a":1,"b":2}')
|
196
|
+
DB[:audited_table].where(id: id).update(data: '{"a":2,"b":2}')
|
197
|
+
|
198
|
+
DB.run PgTriggers.audit_table(:audited_table, ignore: [:data])
|
199
|
+
DB[:audited_table].where(id: id).update(data: '{"a":8}')
|
200
|
+
|
201
|
+
DB[:audit_table].count.should == 3
|
202
|
+
r1, r2, r3 = DB[:audit_table].all
|
203
|
+
|
204
|
+
r1[:id].should == 1
|
205
|
+
r1[:table_name].should == 'audited_table'
|
206
|
+
r1[:changed_at].should be_within(3).of Time.now
|
207
|
+
JSON.parse(r1[:changes]).should == {'data' => {}}
|
208
|
+
|
209
|
+
r2[:id].should == 2
|
210
|
+
r2[:table_name].should == 'audited_table'
|
211
|
+
r2[:changed_at].should be_within(3).of Time.now
|
212
|
+
JSON.parse(r2[:changes]).should == {'data' => {'a' => 1}}
|
213
|
+
|
214
|
+
r3[:id].should == 3
|
215
|
+
r3[:table_name].should == 'audited_table'
|
216
|
+
r3[:changed_at].should be_within(3).of Time.now
|
217
|
+
JSON.parse(r3[:changes]).should == {'data' => {'a' => 1, 'b' => 2}}
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_triggers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Hanks
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-08-
|
11
|
+
date: 2014-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
description: Handy helpers to create PostgreSQL Triggers
|
84
98
|
email:
|
85
99
|
- christopher.m.hanks@gmail.com
|
@@ -96,6 +110,7 @@ files:
|
|
96
110
|
- lib/pg_triggers.rb
|
97
111
|
- lib/pg_triggers/version.rb
|
98
112
|
- pg_triggers.gemspec
|
113
|
+
- spec/audit_table_spec.rb
|
99
114
|
- spec/counter_cache_spec.rb
|
100
115
|
- spec/spec_helper.rb
|
101
116
|
homepage: ''
|
@@ -123,5 +138,6 @@ signing_key:
|
|
123
138
|
specification_version: 4
|
124
139
|
summary: Helpers for Postgres Triggers
|
125
140
|
test_files:
|
141
|
+
- spec/audit_table_spec.rb
|
126
142
|
- spec/counter_cache_spec.rb
|
127
143
|
- spec/spec_helper.rb
|