logidze 0.8.1 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +124 -5
- data/LICENSE.txt +1 -1
- data/README.md +310 -80
- data/lib/generators/logidze/fx_helper.rb +17 -0
- data/lib/generators/logidze/inject_sql.rb +18 -0
- data/lib/generators/logidze/install/USAGE +6 -1
- data/lib/generators/logidze/install/functions/logidze_compact_history.sql +38 -0
- data/lib/generators/logidze/install/functions/logidze_filter_keys.sql +27 -0
- data/lib/generators/logidze/install/functions/logidze_logger.sql +150 -0
- data/lib/generators/logidze/install/functions/logidze_snapshot.sql +24 -0
- data/lib/generators/logidze/install/functions/logidze_version.sql +20 -0
- data/lib/generators/logidze/install/install_generator.rb +61 -3
- data/lib/generators/logidze/install/templates/hstore.rb.erb +1 -1
- data/lib/generators/logidze/install/templates/migration.rb.erb +19 -232
- data/lib/generators/logidze/install/templates/migration_fx.rb.erb +41 -0
- data/lib/generators/logidze/model/model_generator.rb +56 -20
- data/lib/generators/logidze/model/templates/migration.rb.erb +57 -36
- data/lib/generators/logidze/model/triggers/logidze.sql +6 -0
- data/lib/logidze.rb +43 -20
- data/lib/logidze/engine.rb +4 -1
- data/lib/logidze/has_logidze.rb +12 -3
- data/lib/logidze/history.rb +7 -15
- data/lib/logidze/history/type.rb +1 -1
- data/lib/logidze/history/version.rb +6 -5
- data/lib/logidze/ignore_log_data.rb +22 -0
- data/lib/logidze/ignore_log_data/cast_attribute_patch.rb +17 -0
- data/lib/logidze/meta.rb +44 -17
- data/lib/logidze/model.rb +65 -37
- data/lib/logidze/version.rb +2 -1
- data/lib/logidze/versioned_association.rb +0 -1
- metadata +47 -103
- data/.gitignore +0 -40
- data/.hound.yml +0 -3
- data/.rubocop.yml +0 -94
- data/.travis.yml +0 -39
- data/Gemfile +0 -13
- data/Rakefile +0 -28
- data/bench/performance/README.md +0 -109
- data/bench/performance/diff_bench.rb +0 -36
- data/bench/performance/insert_bench.rb +0 -20
- data/bench/performance/memory_profile.rb +0 -53
- data/bench/performance/setup.rb +0 -308
- data/bench/performance/update_bench.rb +0 -36
- data/bench/triggers/Makefile +0 -56
- data/bench/triggers/Readme.md +0 -58
- data/bench/triggers/bench.sql +0 -6
- data/bench/triggers/hstore_trigger_setup.sql +0 -38
- data/bench/triggers/jsonb_minus_2_setup.sql +0 -47
- data/bench/triggers/jsonb_minus_setup.sql +0 -49
- data/bench/triggers/keys2_trigger_setup.sql +0 -44
- data/bench/triggers/keys_trigger_setup.sql +0 -50
- data/bin/console +0 -8
- data/bin/setup +0 -9
- data/gemfiles/rails42.gemfile +0 -5
- data/gemfiles/rails5.gemfile +0 -6
- data/gemfiles/rails52.gemfile +0 -6
- data/gemfiles/railsmaster.gemfile +0 -7
- data/lib/logidze/migration.rb +0 -19
- data/logidze.gemspec +0 -33
data/bench/performance/setup.rb
DELETED
@@ -1,308 +0,0 @@
|
|
1
|
-
begin
|
2
|
-
require 'bundler/inline'
|
3
|
-
rescue LoadError => e
|
4
|
-
$stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
|
5
|
-
raise e
|
6
|
-
end
|
7
|
-
|
8
|
-
gemfile(true) do
|
9
|
-
source 'https://rubygems.org'
|
10
|
-
gem 'activerecord', '~>4.2'
|
11
|
-
gem 'pg'
|
12
|
-
gem 'paper_trail', '~>4.2', require: false
|
13
|
-
gem 'pry-byebug'
|
14
|
-
gem 'faker'
|
15
|
-
gem 'benchmark-ips'
|
16
|
-
gem 'memory_profiler'
|
17
|
-
end
|
18
|
-
|
19
|
-
DB_NAME = ENV['DB_NAME'] || 'logidze_query_bench'
|
20
|
-
|
21
|
-
begin
|
22
|
-
system("createdb #{DB_NAME}")
|
23
|
-
rescue
|
24
|
-
$stdout.puts "DB already exists"
|
25
|
-
end
|
26
|
-
|
27
|
-
$LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
|
28
|
-
|
29
|
-
require 'active_record'
|
30
|
-
require 'logger'
|
31
|
-
require 'logidze'
|
32
|
-
|
33
|
-
ActiveRecord::Base.send :include, Logidze::HasLogidze
|
34
|
-
|
35
|
-
ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: DB_NAME)
|
36
|
-
|
37
|
-
at_exit do
|
38
|
-
ActiveRecord::Base.connection.disconnect!
|
39
|
-
end
|
40
|
-
|
41
|
-
require 'paper_trail'
|
42
|
-
|
43
|
-
module LogidzeBench
|
44
|
-
module_function
|
45
|
-
def setup_db
|
46
|
-
ActiveRecord::Schema.define do
|
47
|
-
# PaperTrail setup
|
48
|
-
create_table :versions, force: true do |t|
|
49
|
-
t.string :item_type, null: false
|
50
|
-
t.integer :item_id, null: false
|
51
|
-
t.string :event, null: false
|
52
|
-
t.string :whodunnit
|
53
|
-
t.text :object
|
54
|
-
t.jsonb :object_changes
|
55
|
-
|
56
|
-
t.datetime :created_at
|
57
|
-
end
|
58
|
-
|
59
|
-
add_index :versions, [:item_type, :item_id]
|
60
|
-
|
61
|
-
# Logidze setup
|
62
|
-
enable_extension :hstore
|
63
|
-
|
64
|
-
execute <<~SQL
|
65
|
-
DO $$
|
66
|
-
BEGIN
|
67
|
-
EXECUTE 'ALTER DATABASE ' || current_database() || ' SET logidze.disabled TO off';
|
68
|
-
END;
|
69
|
-
$$
|
70
|
-
LANGUAGE plpgsql;
|
71
|
-
SQL
|
72
|
-
|
73
|
-
execute <<~SQL
|
74
|
-
CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
|
75
|
-
DECLARE
|
76
|
-
changes jsonb;
|
77
|
-
new_v integer;
|
78
|
-
ts bigint;
|
79
|
-
size integer;
|
80
|
-
history_limit integer;
|
81
|
-
current_version integer;
|
82
|
-
merged jsonb;
|
83
|
-
iterator integer;
|
84
|
-
item record;
|
85
|
-
BEGIN
|
86
|
-
ts := (extract(epoch from now()) * 1000)::bigint;
|
87
|
-
|
88
|
-
IF TG_OP = 'INSERT' THEN
|
89
|
-
changes := to_jsonb(NEW.*) - 'log_data';
|
90
|
-
new_v := 1;
|
91
|
-
|
92
|
-
NEW.log_data := json_build_object(
|
93
|
-
'v',
|
94
|
-
1,
|
95
|
-
'h',
|
96
|
-
jsonb_build_array(
|
97
|
-
jsonb_build_object(
|
98
|
-
'ts',
|
99
|
-
ts,
|
100
|
-
'v',
|
101
|
-
new_v,
|
102
|
-
'c',
|
103
|
-
changes
|
104
|
-
)
|
105
|
-
)
|
106
|
-
);
|
107
|
-
ELSIF TG_OP = 'UPDATE' THEN
|
108
|
-
history_limit := TG_ARGV[0];
|
109
|
-
current_version := (NEW.log_data->>'v')::int;
|
110
|
-
|
111
|
-
IF NEW = OLD THEN
|
112
|
-
RETURN NEW;
|
113
|
-
END IF;
|
114
|
-
|
115
|
-
IF current_version < (NEW.log_data#>>'{h,-1,v}')::int THEN
|
116
|
-
iterator := 0;
|
117
|
-
FOR item in SELECT * FROM jsonb_array_elements(NEW.log_data->'h')
|
118
|
-
LOOP
|
119
|
-
IF (item.value->>'v')::int > current_version THEN
|
120
|
-
NEW.log_data := jsonb_set(
|
121
|
-
NEW.log_data,
|
122
|
-
'{h}',
|
123
|
-
(NEW.log_data->'h') - iterator
|
124
|
-
);
|
125
|
-
END IF;
|
126
|
-
iterator := iterator + 1;
|
127
|
-
END LOOP;
|
128
|
-
END IF;
|
129
|
-
|
130
|
-
changes := hstore_to_jsonb_loose(
|
131
|
-
hstore(NEW.*) - hstore(OLD.*)
|
132
|
-
) - 'log_data';
|
133
|
-
|
134
|
-
new_v := (NEW.log_data#>>'{h,-1,v}')::int + 1;
|
135
|
-
|
136
|
-
size := jsonb_array_length(NEW.log_data->'h');
|
137
|
-
|
138
|
-
NEW.log_data := jsonb_set(
|
139
|
-
NEW.log_data,
|
140
|
-
ARRAY['h', size::text],
|
141
|
-
jsonb_build_object(
|
142
|
-
'ts',
|
143
|
-
ts,
|
144
|
-
'v',
|
145
|
-
new_v,
|
146
|
-
'c',
|
147
|
-
changes
|
148
|
-
),
|
149
|
-
true
|
150
|
-
);
|
151
|
-
|
152
|
-
NEW.log_data := jsonb_set(
|
153
|
-
NEW.log_data,
|
154
|
-
'{v}',
|
155
|
-
to_jsonb(new_v)
|
156
|
-
);
|
157
|
-
|
158
|
-
IF history_limit IS NOT NULL AND history_limit = size THEN
|
159
|
-
merged := jsonb_build_object(
|
160
|
-
'ts',
|
161
|
-
NEW.log_data#>'{h,1,ts}',
|
162
|
-
'v',
|
163
|
-
NEW.log_data#>'{h,1,v}',
|
164
|
-
'c',
|
165
|
-
(NEW.log_data#>'{h,0,c}') || (NEW.log_data#>'{h,1,c}')
|
166
|
-
);
|
167
|
-
|
168
|
-
NEW.log_data := jsonb_set(
|
169
|
-
NEW.log_data,
|
170
|
-
'{h}',
|
171
|
-
jsonb_set(
|
172
|
-
NEW.log_data->'h',
|
173
|
-
'{1}',
|
174
|
-
merged
|
175
|
-
) - 0
|
176
|
-
);
|
177
|
-
END IF;
|
178
|
-
END IF;
|
179
|
-
|
180
|
-
return NEW;
|
181
|
-
END;
|
182
|
-
$body$
|
183
|
-
LANGUAGE plpgsql;
|
184
|
-
SQL
|
185
|
-
|
186
|
-
create_table :users, force: true do |t|
|
187
|
-
t.string :email
|
188
|
-
t.integer :position
|
189
|
-
t.string :name
|
190
|
-
t.text :bio
|
191
|
-
t.integer :age
|
192
|
-
t.timestamps
|
193
|
-
end
|
194
|
-
|
195
|
-
create_table :logidze_users, force: true do |t|
|
196
|
-
t.string :email
|
197
|
-
t.integer :position
|
198
|
-
t.string :name
|
199
|
-
t.text :bio
|
200
|
-
t.integer :age
|
201
|
-
t.jsonb :log_data, default: '{}', null: false
|
202
|
-
t.timestamps
|
203
|
-
end
|
204
|
-
|
205
|
-
execute <<~SQL
|
206
|
-
CREATE TRIGGER logidze_on_logidze_users
|
207
|
-
BEFORE UPDATE OR INSERT ON logidze_users FOR EACH ROW
|
208
|
-
WHEN (current_setting('logidze.disabled') <> 'on')
|
209
|
-
EXECUTE PROCEDURE logidze_logger();
|
210
|
-
SQL
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
module_function
|
215
|
-
def populate(n = 1_000)
|
216
|
-
n.times do
|
217
|
-
params = fake_params
|
218
|
-
User.create!(params)
|
219
|
-
LogidzeUser.create!(params)
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
module_function
|
224
|
-
def cleanup
|
225
|
-
LogidzeUser.delete_all
|
226
|
-
User.delete_all
|
227
|
-
PaperTrail::Version.delete_all
|
228
|
-
end
|
229
|
-
|
230
|
-
module_function
|
231
|
-
def generate_versions(num = 1)
|
232
|
-
num.times do
|
233
|
-
User.find_each do |u|
|
234
|
-
u.update!(fake_params(sample: true))
|
235
|
-
end
|
236
|
-
|
237
|
-
LogidzeUser.find_each do |u|
|
238
|
-
u.update!(fake_params(sample: true))
|
239
|
-
end
|
240
|
-
|
241
|
-
# make at least 1 second between versions
|
242
|
-
sleep 1
|
243
|
-
end
|
244
|
-
Time.now
|
245
|
-
end
|
246
|
-
|
247
|
-
module_function
|
248
|
-
def fake_params(sample: false)
|
249
|
-
params = {
|
250
|
-
email: Faker::Internet.email,
|
251
|
-
position: Faker::Number.number(3),
|
252
|
-
name: Faker::Name.name,
|
253
|
-
age: Faker::Number.number(2),
|
254
|
-
bio: Faker::Lorem.paragraph
|
255
|
-
}
|
256
|
-
|
257
|
-
return params.slice(%i(email position name age bio).sample) if sample
|
258
|
-
params
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
module ARandom
|
263
|
-
def random(num = 1)
|
264
|
-
rel = order('random()')
|
265
|
-
num == 1 ? rel.first : rel.limit(num)
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
class User < ActiveRecord::Base
|
270
|
-
extend ARandom
|
271
|
-
has_paper_trail
|
272
|
-
|
273
|
-
def self.diff_from(ts)
|
274
|
-
includes(:versions).map { |u| { 'id' => u.id, 'changes' => u.diff_from(ts) } }
|
275
|
-
end
|
276
|
-
|
277
|
-
def self.diff_from_joined(ts)
|
278
|
-
eager_load(:versions).map { |u| { 'id' => u.id, 'changes' => u.diff_from(ts) } }
|
279
|
-
end
|
280
|
-
|
281
|
-
def diff_from(ts)
|
282
|
-
changes = {}
|
283
|
-
versions.each do |v|
|
284
|
-
next if v.created_at < ts
|
285
|
-
merge_changeset(changes, v.changeset)
|
286
|
-
end
|
287
|
-
changes
|
288
|
-
end
|
289
|
-
|
290
|
-
private
|
291
|
-
|
292
|
-
def merge_changeset(acc, data)
|
293
|
-
data.each do |k,v|
|
294
|
-
unless acc.key?(k)
|
295
|
-
acc[k] = { 'old' => v[0] }
|
296
|
-
end
|
297
|
-
acc[k]['new'] = v[1]
|
298
|
-
end
|
299
|
-
end
|
300
|
-
end
|
301
|
-
|
302
|
-
class LogidzeUser < ActiveRecord::Base
|
303
|
-
extend ARandom
|
304
|
-
has_logidze
|
305
|
-
end
|
306
|
-
|
307
|
-
# Run migration only if neccessary
|
308
|
-
LogidzeBench.setup_db if ENV['FORCE'].present? || !ActiveRecord::Base.connection.tables.include?('logidze_users')
|
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'benchmark/ips'
|
2
|
-
require './setup'
|
3
|
-
|
4
|
-
params = {
|
5
|
-
age: Faker::Number.number(2),
|
6
|
-
email: Faker::Internet.email
|
7
|
-
}
|
8
|
-
|
9
|
-
params2 = {
|
10
|
-
email: Faker::Internet.email,
|
11
|
-
position: Faker::Number.number(3),
|
12
|
-
name: Faker::Name.name,
|
13
|
-
age: Faker::Number.number(2),
|
14
|
-
bio: Faker::Lorem.paragraph
|
15
|
-
}
|
16
|
-
|
17
|
-
LogidzeBench.cleanup
|
18
|
-
LogidzeBench.populate
|
19
|
-
|
20
|
-
Benchmark.ips do |x|
|
21
|
-
x.report('PT UPDATE #1') do
|
22
|
-
User.random.update!(params)
|
23
|
-
end
|
24
|
-
|
25
|
-
x.report('Logidze UPDATE #1') do
|
26
|
-
LogidzeUser.random.update!(params)
|
27
|
-
end
|
28
|
-
|
29
|
-
x.report('PT UPDATE #2') do
|
30
|
-
User.random.update!(params2)
|
31
|
-
end
|
32
|
-
|
33
|
-
x.report('Logidze UPDATE #2') do
|
34
|
-
LogidzeUser.random.update!(params2)
|
35
|
-
end
|
36
|
-
end
|
data/bench/triggers/Makefile
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
# Database name
|
2
|
-
ifndef DB
|
3
|
-
DB = logidze_bench
|
4
|
-
endif
|
5
|
-
|
6
|
-
# Transactions count
|
7
|
-
ifndef T
|
8
|
-
T = 10000
|
9
|
-
endif
|
10
|
-
|
11
|
-
all: plain hstore jsonb jsonb2 keys keys2
|
12
|
-
|
13
|
-
setup:
|
14
|
-
createdb $(DB) -w
|
15
|
-
psql -q -d $(DB) -c 'CREATE EXTENSION IF NOT EXISTS hstore;'
|
16
|
-
|
17
|
-
plain:
|
18
|
-
$(info )
|
19
|
-
$(info ====== [START] Update without triggers ======)
|
20
|
-
pgbench -i -q $(DB)
|
21
|
-
pgbench -f bench.sql -t $(T) -r $(DB)
|
22
|
-
|
23
|
-
hstore:
|
24
|
-
$(info )
|
25
|
-
$(info ====== [START] Update with hstore-based triggers ======)
|
26
|
-
pgbench -i -q $(DB)
|
27
|
-
psql -q -d $(DB) -f hstore_trigger_setup.sql
|
28
|
-
pgbench -f bench.sql -t $(T) -r $(DB)
|
29
|
-
|
30
|
-
jsonb:
|
31
|
-
$(info )
|
32
|
-
$(info ====== [START] Update with jsonb-minus triggers ======)
|
33
|
-
pgbench -i -q $(DB)
|
34
|
-
psql -q -d $(DB) -f jsonb_minus_setup.sql
|
35
|
-
pgbench -f bench.sql -t $(T) -r $(DB)
|
36
|
-
|
37
|
-
jsonb2:
|
38
|
-
$(info )
|
39
|
-
$(info ====== [START] Update with jsonb-minus triggers ======)
|
40
|
-
pgbench -i -q $(DB)
|
41
|
-
psql -q -d $(DB) -f jsonb_minus_2_setup.sql
|
42
|
-
pgbench -f bench.sql -t $(T) -r $(DB)
|
43
|
-
|
44
|
-
keys:
|
45
|
-
$(info )
|
46
|
-
$(info ====== [START] Update with loop thru keys triggers (v1) ======)
|
47
|
-
pgbench -i -q $(DB)
|
48
|
-
psql -q -d $(DB) -f keys_trigger_setup.sql
|
49
|
-
pgbench -f bench.sql -t $(T) -r $(DB)
|
50
|
-
|
51
|
-
keys2:
|
52
|
-
$(info )
|
53
|
-
$(info ====== [START] Update with loop thru keys triggers (v2) ======)
|
54
|
-
pgbench -i -q $(DB)
|
55
|
-
psql -q -d $(DB) -f keys2_trigger_setup.sql
|
56
|
-
pgbench -f bench.sql -t $(T) -r $(DB)
|
data/bench/triggers/Readme.md
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
# Triggers benchmarks
|
2
|
-
|
3
|
-
This benchmark uses standard _pg\_bench_ table `pgbench_accounts`.
|
4
|
-
We consider several approaches for calculating records diff: one uses _hstore_ extension, two uses jsonb functions and two others iterate through record fields.
|
5
|
-
|
6
|
-
# Usage
|
7
|
-
|
8
|
-
Create database:
|
9
|
-
|
10
|
-
```sh
|
11
|
-
make setup
|
12
|
-
```
|
13
|
-
|
14
|
-
You can provide database name by `DB` variable (defaults to "logidze_bench").
|
15
|
-
|
16
|
-
Run all benchmarks:
|
17
|
-
|
18
|
-
```sh
|
19
|
-
make
|
20
|
-
```
|
21
|
-
|
22
|
-
or separate benchmark:
|
23
|
-
|
24
|
-
```sh
|
25
|
-
make hstore
|
26
|
-
|
27
|
-
make jsonb
|
28
|
-
|
29
|
-
make jsonb2
|
30
|
-
|
31
|
-
make keys
|
32
|
-
|
33
|
-
make keys2
|
34
|
-
|
35
|
-
# Raw update, no triggers
|
36
|
-
make plain
|
37
|
-
```
|
38
|
-
|
39
|
-
You can specify the number of transactions by `T` variable (defaults to 10000):
|
40
|
-
|
41
|
-
```sh
|
42
|
-
make T=1000000
|
43
|
-
```
|
44
|
-
|
45
|
-
# Results
|
46
|
-
|
47
|
-
The benchmark shows that hstore variant is the most efficient (running on MacPro 2013, 2.4 GHz Core i5, 4GB, SSD, 1 million transactions per test):
|
48
|
-
|
49
|
-
|Mode | TPS | Statement latency (ms) |
|
50
|
-
|--------|------|------------------------|
|
51
|
-
| plain | 3628 | 0.113 |
|
52
|
-
| hstore | 3015 | 0.168 |
|
53
|
-
| jsonb | 1647 | 0.363 |
|
54
|
-
| jsonb2 | 1674 | 0.354 |
|
55
|
-
| keys | 2355 | 0.219 |
|
56
|
-
| keys2 | 2542 | 0.210 |
|
57
|
-
|
58
|
-
_Logidze_ uses hstore variant.
|