logidze 0.1.0 → 0.2.1

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
  SHA1:
3
- metadata.gz: 8df5f1ac5aa84c04a0874f222011418bb4ef0fb7
4
- data.tar.gz: f354e24b771387fddfc094dd5ea902827d1f4deb
3
+ metadata.gz: 437f0707fbb215fbfb5655156b0f2f8450f2aa20
4
+ data.tar.gz: 533a010cbc9d240c5c70d565621ace20d571579a
5
5
  SHA512:
6
- metadata.gz: 971a0acde6848091d4944632b579a37ab71335a9d7cab813aa43eae054fa4ae5359b8d654403d1ed8d7145c53afb87917366404a616f73fc71bb6af84536e0e4
7
- data.tar.gz: 61d8ad229bb3d56bb5468b005e7e4907af9195bae3d187934a2acb7906b381628fd188ce99d2fb0af013bff8e22f750ae3c00f3fb7496725605b754ef9bad5bb
6
+ metadata.gz: 419c21e03ba9196ff236bb7d8ea1bc897eaa1e7b8573bff1e1196008113641975341712e2f55d0ea8abb887820c18e0f32b654ee673ea379ef263cdae63ce875
7
+ data.tar.gz: 8a885b62f97e4492001ef11a1961556faa0810d976f95fdb44dcb3071a5506622edc798f87aa6dd8fb27e40b46d605dd6adced7aa16e19278d2feb40f20beeb8
data/.hound.yml ADDED
@@ -0,0 +1,3 @@
1
+ ruby:
2
+ enabled: true
3
+ config_file: .rubocop.yml
data/.travis.yml CHANGED
@@ -29,9 +29,7 @@ before_script:
29
29
 
30
30
  matrix:
31
31
  include:
32
- - rvm: 2.3.0
32
+ - rvm: 2.3.1
33
33
  gemfile: gemfiles/rails5.gemfile
34
- - rvm: 2.3.0
34
+ - rvm: 2.3.1
35
35
  gemfile: gemfiles/rails42.gemfile
36
- allow_failures:
37
- - gemfile: gemfiles/rails5.gemfile
data/CHANGELOG.md CHANGED
@@ -0,0 +1,6 @@
1
+ # 0.2.1
2
+ - Support both Rails 4 and 5
3
+
4
+ # 0.2.0 (**Incompatible with 0.1.0**)
5
+
6
+ - Rails 5 support
data/Gemfile CHANGED
@@ -3,10 +3,10 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in logidze.gemspec
4
4
  gemspec
5
5
 
6
- local_gemfile = 'Gemfile.local'
6
+ local_gemfile = "#{File.dirname(__FILE__)}/Gemfile.local"
7
7
 
8
8
  if File.exist?(local_gemfile)
9
9
  eval(File.read(local_gemfile)) # rubocop:disable Lint/Eval
10
10
  else
11
- gem 'activerecord', '~>4.2'
11
+ gem 'activerecord', '~> 5.0.0'
12
12
  end
data/README.md CHANGED
@@ -2,23 +2,24 @@
2
2
 
3
3
  # Logidze
4
4
 
5
- Logidze provides tools for logging DB records changes.
6
- **This is not [audited](https://github.com/collectiveidea/audited) or [paper_trail](https://github.com/airblade/paper_trail) alternative!**
5
+ Logidze provides tools for logging DB records changes. Just like [audited](https://github.com/collectiveidea/audited) and [paper_trail](https://github.com/airblade/paper_trail) do (but [faster](bench/performance)).
7
6
 
8
- Logidze allows you to create DB-level log (using triggers) and gives you an API to browse this log.
9
- Log is stored with the record itself in JSONB column. No additional tables required.
7
+ Logidze allows you to create a DB-level log (using triggers) and gives you an API to browse this log.
8
+ The log is stored with the record itself in JSONB column. No additional tables required.
10
9
  Currently, only PostgreSQL 9.5+ is supported.
11
10
 
11
+ [Read the story behind Logidze](https://evilmartians.com/chronicles/introducing-logidze?utm_source=logidze)
12
+
12
13
  Other requirements:
13
- - Ruby ~>2.3;
14
- - Rails ~>4.2;
14
+ - Ruby ~> 2.3;
15
+ - Rails >= 4.2 (Yes, Rails 5 is supported!).
15
16
 
16
17
  <a href="https://evilmartians.com/">
17
18
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
18
19
 
19
20
  ## Installation
20
21
 
21
- 1. Add Logidze your application's Gemfile:
22
+ 1. Add Logidze to your application's Gemfile:
22
23
 
23
24
  ```ruby
24
25
  gem 'logidze'
@@ -48,7 +49,7 @@ rake db:migrate
48
49
  You can provide `limit` option to `generate` to limit the size of the log (by default it's unlimited):
49
50
 
50
51
  ```ruby
51
- rails generate logidze:mode Post --limit=10
52
+ rails generate logidze:model Post --limit=10
52
53
  ```
53
54
 
54
55
  This also adds `has_logidze` line to your model, which adds methods for working with logs.
@@ -68,7 +69,7 @@ post.log_version #=> 3
68
69
  # Show log size (number of versions)
69
70
  post.log_size #=> 3
70
71
 
71
- # Get copy of a record at a given time
72
+ # Get copy of a record at a given time
72
73
  old_post = post.at(2.days.ago)
73
74
 
74
75
  # or revert the record itself to the previous state (without committing to DB)
@@ -134,12 +135,12 @@ The `log_data` column has the following format:
134
135
  ```js
135
136
  {
136
137
  "v": 2, // current record version,
137
- "h": // list of changes
138
+ "h": // list of changes
138
139
  [
139
140
  {
140
141
  "v": 1, // change number
141
142
  "ts": 1460805759352, // change timestamp in milliseconds
142
- "c": {
143
+ "c": {
143
144
  "attr": "new value", // updated fields with new values
144
145
  "attr2": "new value"
145
146
  }
@@ -148,7 +149,7 @@ The `log_data` column has the following format:
148
149
  }
149
150
  ```
150
151
 
151
- If you specified the limit in you trigger definition then log size would not exceed the specified size. When a new change occurs, and there is no more room for it, the two oldest changes get merged.
152
+ If you specify the limit in the trigger definition then log size will not exceed the specified size. When a new change occurs, and there is no more room for it, the two oldest changes will be merged.
152
153
 
153
154
  ## Development
154
155
 
@@ -161,9 +162,9 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/palkan
161
162
 
162
163
  ## TODO
163
164
 
164
- - Exclude columns from log
165
- - Enhance `update_all` to support mass-logging
166
- - Other DB adapters
165
+ - Exclude columns from the log.
166
+ - Enhance update_all to support mass-logging.
167
+ - Other DB adapters.
167
168
 
168
169
  ## License
169
170
 
@@ -0,0 +1,109 @@
1
+ # Performance benchmarks: PaperTail vs. Logidze
2
+
3
+ We want to compare Logidze with the most popular versioning library for Rails – PaperTrail.
4
+
5
+
6
+ ## Insert ([source](insert_bench.rb))
7
+
8
+ ```
9
+ PaperTrail INSERT 213.148 (± 8.9%) i/s - 1.060k in 5.018504s
10
+ Logidze INSERT 613.387 (±16.3%) i/s - 2.970k in 5.036127s
11
+ ```
12
+
13
+
14
+ ## Update ([source](update_bench.rb))
15
+
16
+ When changeset has 2 fields:
17
+
18
+ ```
19
+ PaperTrail UPDATE #1 256.651 (±26.5%) i/s - 1.206k in 5.002300s
20
+ Logidze UPDATE #1 356.932 (±12.6%) i/s - 1.764k in 5.030560s
21
+ ```
22
+
23
+ When changeset has 5 fields:
24
+
25
+ ```
26
+ PaperTrail UPDATE #2 246.281 (±24.0%) i/s - 1.168k in 5.008234s
27
+ Logidze UPDATE #2 331.942 (±16.6%) i/s - 1.593k in 5.028135s
28
+ ```
29
+
30
+ ## Getting diff ([source](diff_bench.rb))
31
+
32
+ PaperTrail doesn't have built-in method to calculate diff between not adjacent versions.
33
+ We add `diff_from(ts)` and `diff_from_joined(ts)` (which uses SQL JOIN) methods to calculate diff from specified version using changesets.
34
+
35
+ When each record has 10 versions:
36
+
37
+ ```
38
+ PT DIFF 20.874 (± 4.8%) i/s - 106.000 in 5.091402s
39
+ PT (join) DIFF 20.619 (± 4.8%) i/s - 104.000 in 5.070160s
40
+ Logidze DIFF 109.482 (±24.7%) i/s - 500.000 in 5.103534s
41
+ ```
42
+
43
+ When each record has 100 versions:
44
+
45
+ ```
46
+ PT DIFF 2.998 (± 0.0%) i/s - 15.000 in 5.019494s
47
+ PT (join) DIFF 3.193 (± 0.0%) i/s - 16.000 in 5.030155s
48
+ Logidze DIFF 19.627 (±25.5%) i/s - 88.000 in 5.035555s
49
+ ```
50
+
51
+ And, finally, when each record has 1000 versions:
52
+
53
+ ```
54
+ PT DIFF 0.270 (± 0.0%) i/s - 17.000 in 63.038374s
55
+ PT (join) DIFF 0.235 (± 0.0%) i/s - 14.000 in 60.350886s
56
+ Logidze DIFF 2.022 (± 0.0%) i/s - 120.000 in 60.142965s
57
+ ```
58
+
59
+ ## Select memory usage ([source](memory_profile.rb))
60
+
61
+ Logidze loads more data (because it stores log in-place). But how much more?
62
+ We consider two cases for PaperTrail: when we want to calculate diff (and thus loading versions) and when we don't need any history related data.
63
+
64
+ When each record has 10 versions:
65
+
66
+ ```
67
+ PT records
68
+ Total Allocated: 27.8 KB
69
+ Total Retained: 16.59 KB
70
+ Retained memory (per record): 2.14 KB
71
+
72
+ PT with versions
73
+ Total Allocated: 228.01 KB
74
+ Total Retained: 170.78 KB
75
+ Retained memory (per record): 143.13 KB
76
+
77
+ Logidze records
78
+ Total Allocated: 46.45 KB
79
+ Total Retained: 34.73 KB
80
+ Retained memory (per record): 4.11 KB
81
+ ```
82
+
83
+ When each record has 100 versions:
84
+
85
+ ```
86
+ PT with versions
87
+ Total Allocated: 1.92 MB
88
+ Total Retained: 1.56 MB
89
+ Retained memory (per record): 1.53 MB
90
+
91
+ Logidze records
92
+ Total Allocated: 162.48 KB
93
+ Total Retained: 150.76 KB
94
+ Retained memory (per record): 15.4 KB
95
+ ```
96
+
97
+ When each record has 1000 versions:
98
+
99
+ ```
100
+ PT with versions
101
+ Total Allocated: 18.23 MB
102
+ Total Retained: 14.86 MB
103
+ Retained memory (per record): 14.83 MB
104
+
105
+ Logidze records
106
+ Total Allocated: 1.32 MB
107
+ Total Retained: 1.31 MB
108
+ Retained memory (per record): 131.59 KB
109
+ ```
@@ -0,0 +1,36 @@
1
+ require 'benchmark/ips'
2
+ require './setup'
3
+
4
+ # How many records do you want?
5
+ N = (ENV['N'] || '100').to_i
6
+
7
+ # How many version each record has?
8
+ V = (ENV['V'] || '10').to_i
9
+
10
+ # Benchmark run time
11
+ BM_TIME = (ENV['BM_TIME'] || 5).to_i
12
+
13
+ BM_WARMUP = [(BM_TIME / 10), 2].max
14
+
15
+ LogidzeBench.cleanup
16
+ LogidzeBench.populate(N)
17
+
18
+ ts1 = LogidzeBench.generate_versions(V/2)
19
+
20
+ LogidzeBench.generate_versions(V/2)
21
+
22
+ Benchmark.ips do |x|
23
+ x.config(time: BM_TIME, warmup: BM_WARMUP)
24
+
25
+ x.report('PT DIFF') do
26
+ User.random(N/2).diff_from(ts1)
27
+ end
28
+
29
+ x.report('PT (join) DIFF') do
30
+ User.random(N/2).diff_from_joined(ts1)
31
+ end
32
+
33
+ x.report('Logidze DIFF') do
34
+ LogidzeUser.random(N/2).diff_from(ts1)
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ require 'benchmark/ips'
2
+ require './setup'
3
+
4
+ params = {
5
+ email: Faker::Internet.email,
6
+ position: Faker::Number.number(3),
7
+ name: Faker::Name.name,
8
+ age: Faker::Number.number(2),
9
+ bio: Faker::Lorem.paragraph
10
+ }
11
+
12
+ Benchmark.ips do |x|
13
+ x.report('PaperTrail INSERT') do
14
+ User.create!(params)
15
+ end
16
+
17
+ x.report('Logidze INSERT') do
18
+ LogidzeUser.create!(params)
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ require './setup'
2
+ require 'active_support/core_ext'
3
+ require 'memory_profiler'
4
+
5
+ # How many records do you want?
6
+ N = (ENV['N'] || '10').to_i
7
+
8
+ # How many version each record has?
9
+ V = (ENV['V'] || '10').to_i
10
+
11
+ LogidzeBench.cleanup
12
+ LogidzeBench.populate(N)
13
+ LogidzeBench.generate_versions(V)
14
+
15
+ module MemoryReport
16
+ KILO_BYTE = 1024
17
+ MEGA_BYTE = 1024 * 1024
18
+
19
+ module_function
20
+ def call(msg, relation)
21
+ buffer = nil
22
+ delta = N / 10
23
+ r0 = MemoryProfiler.report do
24
+ buffer = relation.random(N - delta).to_a
25
+ end
26
+
27
+ buffer = nil
28
+ r1 = MemoryProfiler.report do
29
+ buffer = relation.to_a
30
+ end
31
+
32
+ $stdout.puts msg
33
+ $stdout.puts "Total Allocated:\t\t\t\t#{to_human_size(r1.total_allocated_memsize)}"
34
+ $stdout.puts "Total Retained:\t\t\t\t\t#{to_human_size(r1.total_retained_memsize)}"
35
+ $stdout.puts "Retained_memsize memory (per record):\t\t#{to_human_size((r1.total_retained_memsize - r0.total_retained_memsize) / delta)}"
36
+ end
37
+
38
+ module_function
39
+ def to_human_size(size)
40
+ if size > MEGA_BYTE
41
+ "#{(size.to_f / MEGA_BYTE).round(2)} MB"
42
+ elsif size > KILO_BYTE
43
+ "#{(size.to_f / KILO_BYTE).round(2)} KB"
44
+ else
45
+ "#{size} B"
46
+ end
47
+ end
48
+ end
49
+
50
+
51
+ MemoryReport.("PT records", User.all)
52
+ MemoryReport.("PT with versions", User.joins(:versions).all)
53
+ MemoryReport.("Logidze records", LogidzeUser.all)
@@ -0,0 +1,308 @@
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')
@@ -0,0 +1,36 @@
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
File without changes
@@ -11,7 +11,7 @@ Create database:
11
11
  make setup
12
12
  ```
13
13
 
14
- You can provide database name through `DB` variable (by default "logidze_bench").
14
+ You can provide database name by `DB` variable (defaults to "logidze_bench").
15
15
 
16
16
  Run all benchmarks:
17
17
 
@@ -32,7 +32,7 @@ make keys
32
32
 
33
33
  make keys2
34
34
 
35
- # raw update, no triggers
35
+ # Raw update, no triggers
36
36
  make plain
37
37
  ```
38
38
 
@@ -44,15 +44,15 @@ make T=1000000
44
44
 
45
45
  # Results
46
46
 
47
- The benchmark shows that hstore and jsonb variants are of the same efficiency (running on MacPro 2013, 2.4 GHz Core i5, 4GB, SSD, 1 million transactions per test):
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
48
 
49
49
  |Mode | TPS | Statement latency (ms) |
50
50
  |--------|------|------------------------|
51
- | plain | 3805 | 0.106 |
52
- | hstore | 3061 | 0.165 |
53
- | jsonb | 3079 | 0.165 |
54
- | jsonb2 | 3057 | 0.166 |
55
- | keys | 2606 | 0.209 |
56
- | keys2 | 2610 | 0.216 |
57
-
58
- _Logidze_ uses jsonb variant.
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.
File without changes
data/circle.yml CHANGED
@@ -3,13 +3,11 @@ machine:
3
3
  version: 2.3.0
4
4
 
5
5
  environment:
6
- DATABASE_URL: postgres://postgres@127.0.0.1:5433/test_database
6
+ DATABASE_URL: postgres://postgres@127.0.0.1:5432/test_database
7
7
 
8
8
  dependencies:
9
9
  pre:
10
10
  - gem install bundler -v 1.11.2
11
- - sudo service postgresql stop 9.4
12
- - sudo cp -v /etc/postgresql/9.{4,5}/main/pg_hba.conf && sudo service postgresql restart 9.5
13
11
 
14
12
  database:
15
13
  override:
@@ -1,5 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'rails', "~>4.2"
3
+ gem 'rails', '~> 4.2'
4
4
 
5
5
  gemspec path: '..'
@@ -1,5 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'rails', '5.0.0.rc1'
3
+ gem 'rails', '~> 5.0.0'
4
+ gem 'rspec-rails', '~> 3.5.0'
4
5
 
5
6
  gemspec path: '..'
@@ -1,38 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  module Logidze
3
- # Log data coder used for attribute serialization
3
+ # Log data wrapper
4
4
  class History
5
+ require 'logidze/history/version'
6
+
5
7
  # History key
6
8
  HISTORY = 'h'
7
9
  # Version key
8
10
  VERSION = 'v'
9
11
 
10
- # Represents one log item
11
- class Version
12
- # Timestamp key
13
- TS = 'ts'
14
- # Changes key
15
- CHANGES = 'c'
16
-
17
- attr_reader :data
18
-
19
- def initialize(data)
20
- @data = data
21
- end
22
-
23
- def version
24
- data.fetch(VERSION)
25
- end
26
-
27
- def changes
28
- data.fetch(CHANGES)
29
- end
12
+ attr_reader :data
30
13
 
31
- def time
32
- data.fetch(TS)
33
- end
34
- end
14
+ delegate :size, to: :versions
35
15
 
16
+ ### Rails 4 ###
36
17
  def self.dump(object)
37
18
  ActiveSupport::JSON.encode(object)
38
19
  end
@@ -41,9 +22,7 @@ module Logidze
41
22
  new(json) unless json.nil?
42
23
  end
43
24
 
44
- attr_reader :data
45
-
46
- delegate :size, to: :versions
25
+ ### Rails 4 ###
47
26
 
48
27
  def initialize(data)
49
28
  @data = data
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require 'active_model/type/value'
3
+
4
+ module Logidze
5
+ class History
6
+ # Type for converting JSONB to and from History
7
+ class Type < ActiveModel::Type::Value
8
+ def type
9
+ :jsonb
10
+ end
11
+
12
+ # rubocop:disable Style/RescueModifier
13
+ def cast_value(value)
14
+ case value
15
+ when String
16
+ decoded = ::ActiveSupport::JSON.decode(value) rescue nil
17
+ History.new(decoded) if decoded.present?
18
+ when Hash
19
+ History.new(value)
20
+ when History
21
+ value
22
+ end
23
+ end
24
+ # rubocop:enable Style/RescueModifier
25
+
26
+ def serialize(value)
27
+ case value
28
+ when Hash, History
29
+ ::ActiveSupport::JSON.encode(value)
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def changed_in_place?(raw_old_value, new_value)
36
+ cast_value(raw_old_value) != new_value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module Logidze
3
+ class History
4
+ # Represents one log item
5
+ class Version
6
+ # Timestamp key
7
+ TS = 'ts'
8
+ # Changes key
9
+ CHANGES = 'c'
10
+
11
+ attr_reader :data
12
+
13
+ def initialize(data)
14
+ @data = data
15
+ end
16
+
17
+ def version
18
+ data.fetch(VERSION)
19
+ end
20
+
21
+ def changes
22
+ data.fetch(CHANGES)
23
+ end
24
+
25
+ def time
26
+ data.fetch(TS)
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/logidze/model.rb CHANGED
@@ -4,10 +4,16 @@ require 'active_support'
4
4
  module Logidze
5
5
  # Extends model with methods to browse history
6
6
  module Model
7
+ require 'logidze/history/type' if Rails::VERSION::MAJOR >= 5
8
+
7
9
  extend ActiveSupport::Concern
8
10
 
9
11
  included do
10
- serialize :log_data, Logidze::History
12
+ if Rails::VERSION::MAJOR < 5
13
+ serialize :log_data, Logidze::History
14
+ else
15
+ attribute :log_data, Logidze::History::Type.new
16
+ end
11
17
 
12
18
  delegate :version, :size, to: :log_data, prefix: "log"
13
19
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Logidze
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.1"
4
4
  end
data/logidze.gemspec CHANGED
@@ -17,12 +17,11 @@ Gem::Specification.new do |spec|
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_dependency "rails", ">= 4.2.6"
20
+ spec.add_dependency "rails", ">= 4.2"
21
21
 
22
22
  spec.add_development_dependency "pg", "~>0.18"
23
23
  spec.add_development_dependency "bundler", "~> 1.11"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "rspec", ">= 3.4"
26
25
  spec.add_development_dependency "rspec-rails", ">= 3.4"
27
26
  spec.add_development_dependency "database_cleaner", "~> 1.5"
28
27
  spec.add_development_dependency "simplecov", ">= 0.3.8"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logidze
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-12 00:00:00.000000000 Z
11
+ date: 2016-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.2.6
19
+ version: '4.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.2.6
26
+ version: '4.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '10.0'
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '3.4'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '3.4'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rspec-rails
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -158,6 +144,7 @@ extensions: []
158
144
  extra_rdoc_files: []
159
145
  files:
160
146
  - ".gitignore"
147
+ - ".hound.yml"
161
148
  - ".rspec"
162
149
  - ".rubocop.yml"
163
150
  - ".travis.yml"
@@ -166,14 +153,20 @@ files:
166
153
  - LICENSE.txt
167
154
  - README.md
168
155
  - Rakefile
169
- - bench/Makefile
170
- - bench/Readme.md
171
- - bench/bench.sql
172
- - bench/hstore_trigger_setup.sql
173
- - bench/jsonb_minus_2_setup.sql
174
- - bench/jsonb_minus_setup.sql
175
- - bench/keys2_trigger_setup.sql
176
- - bench/keys_trigger_setup.sql
156
+ - bench/performance/README.md
157
+ - bench/performance/diff_bench.rb
158
+ - bench/performance/insert_bench.rb
159
+ - bench/performance/memory_profile.rb
160
+ - bench/performance/setup.rb
161
+ - bench/performance/update_bench.rb
162
+ - bench/triggers/Makefile
163
+ - bench/triggers/Readme.md
164
+ - bench/triggers/bench.sql
165
+ - bench/triggers/hstore_trigger_setup.sql
166
+ - bench/triggers/jsonb_minus_2_setup.sql
167
+ - bench/triggers/jsonb_minus_setup.sql
168
+ - bench/triggers/keys2_trigger_setup.sql
169
+ - bench/triggers/keys_trigger_setup.sql
177
170
  - bin/console
178
171
  - bin/setup
179
172
  - circle.yml
@@ -190,6 +183,8 @@ files:
190
183
  - lib/logidze/engine.rb
191
184
  - lib/logidze/has_logidze.rb
192
185
  - lib/logidze/history.rb
186
+ - lib/logidze/history/type.rb
187
+ - lib/logidze/history/version.rb
193
188
  - lib/logidze/model.rb
194
189
  - lib/logidze/version.rb
195
190
  - logidze.gemspec