pg_triggers 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|