logidze 0.11.0 → 1.2.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -4
  3. data/LICENSE.txt +1 -1
  4. data/README.md +305 -102
  5. data/lib/generators/logidze/fx_helper.rb +17 -0
  6. data/lib/generators/logidze/inject_sql.rb +18 -0
  7. data/lib/generators/logidze/install/USAGE +6 -1
  8. data/lib/generators/logidze/install/functions/logidze_capture_exception.sql +23 -0
  9. data/lib/generators/logidze/install/functions/logidze_compact_history.sql +38 -0
  10. data/lib/generators/logidze/install/functions/logidze_filter_keys.sql +27 -0
  11. data/lib/generators/logidze/install/functions/logidze_logger.sql +203 -0
  12. data/lib/generators/logidze/install/functions/logidze_snapshot.sql +33 -0
  13. data/lib/generators/logidze/install/functions/logidze_version.sql +21 -0
  14. data/lib/generators/logidze/install/install_generator.rb +43 -1
  15. data/lib/generators/logidze/install/templates/hstore.rb.erb +1 -1
  16. data/lib/generators/logidze/install/templates/migration.rb.erb +19 -232
  17. data/lib/generators/logidze/install/templates/migration_fx.rb.erb +41 -0
  18. data/lib/generators/logidze/model/model_generator.rb +53 -13
  19. data/lib/generators/logidze/model/templates/migration.rb.erb +57 -36
  20. data/lib/generators/logidze/model/triggers/logidze.sql +6 -0
  21. data/lib/logidze.rb +37 -14
  22. data/lib/logidze/engine.rb +9 -0
  23. data/lib/logidze/has_logidze.rb +1 -1
  24. data/lib/logidze/history.rb +2 -11
  25. data/lib/logidze/ignore_log_data.rb +1 -3
  26. data/lib/logidze/meta.rb +43 -16
  27. data/lib/logidze/model.rb +51 -44
  28. data/lib/logidze/utils/check_pending.rb +57 -0
  29. data/lib/logidze/utils/function_definitions.rb +49 -0
  30. data/lib/logidze/utils/pending_migration_error.rb +25 -0
  31. data/lib/logidze/version.rb +1 -1
  32. metadata +69 -77
  33. data/.gitattributes +0 -3
  34. data/.github/ISSUE_TEMPLATE.md +0 -20
  35. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  36. data/.gitignore +0 -40
  37. data/.rubocop.yml +0 -55
  38. data/.travis.yml +0 -42
  39. data/Gemfile +0 -15
  40. data/Rakefile +0 -28
  41. data/assets/pg_log_data_chart.png +0 -0
  42. data/bench/performance/README.md +0 -109
  43. data/bench/performance/diff_bench.rb +0 -38
  44. data/bench/performance/insert_bench.rb +0 -22
  45. data/bench/performance/memory_profile.rb +0 -56
  46. data/bench/performance/setup.rb +0 -315
  47. data/bench/performance/update_bench.rb +0 -38
  48. data/bench/triggers/Makefile +0 -56
  49. data/bench/triggers/Readme.md +0 -58
  50. data/bench/triggers/bench.sql +0 -6
  51. data/bench/triggers/hstore_trigger_setup.sql +0 -38
  52. data/bench/triggers/jsonb_minus_2_setup.sql +0 -47
  53. data/bench/triggers/jsonb_minus_setup.sql +0 -49
  54. data/bench/triggers/keys2_trigger_setup.sql +0 -44
  55. data/bench/triggers/keys_trigger_setup.sql +0 -50
  56. data/bin/console +0 -8
  57. data/bin/setup +0 -9
  58. data/gemfiles/rails42.gemfile +0 -6
  59. data/gemfiles/rails5.gemfile +0 -6
  60. data/gemfiles/rails52.gemfile +0 -6
  61. data/gemfiles/rails6.gemfile +0 -6
  62. data/gemfiles/railsmaster.gemfile +0 -7
  63. data/lib/logidze/ignore_log_data/association.rb +0 -11
  64. data/lib/logidze/ignore_log_data/ignored_columns.rb +0 -46
  65. data/lib/logidze/migration.rb +0 -20
  66. data/logidze.gemspec +0 -41
@@ -0,0 +1,6 @@
1
+ CREATE TRIGGER logidze_on_<%= table_name %>
2
+ BEFORE UPDATE OR INSERT ON <%= table_name %> FOR EACH ROW
3
+ WHEN (coalesce(current_setting('logidze.disabled', true), '') <> 'on')
4
+ -- Parameters: history_size_limit (integer), timestamp_column (text), filtered_columns (text[]),
5
+ -- include_columns (boolean), debounce_time_ms (integer)
6
+ EXECUTE PROCEDURE logidze_logger(<%= logidze_logger_parameters %>);
data/lib/logidze.rb CHANGED
@@ -5,6 +5,7 @@ require "logidze/version"
5
5
  # Logidze provides tools for adding in-table JSON-based audit to DB tables
6
6
  # and ActiveRecord extensions to work with changes history.
7
7
  module Logidze
8
+ require "ruby-next"
8
9
  require "logidze/history"
9
10
  require "logidze/model"
10
11
  require "logidze/versioned_association"
@@ -19,31 +20,53 @@ module Logidze
19
20
  class << self
20
21
  # Determines if Logidze should append a version to the log after updating an old version.
21
22
  attr_accessor :append_on_undo
22
-
23
- attr_writer :associations_versioning
24
-
25
- def associations_versioning
26
- @associations_versioning || false
27
- end
28
-
23
+ # Determines whether associations versioning is enabled or not
24
+ attr_accessor :associations_versioning
29
25
  # Determines if Logidze should exclude log data from SELECT statements
30
- attr_writer :ignore_log_data_by_default
31
-
32
- def ignore_log_data_by_default
33
- @ignore_log_data_by_default || false
34
- end
26
+ attr_accessor :ignore_log_data_by_default
27
+ # Whether #at should return self or nil when log_data is nil
28
+ attr_accessor :return_self_if_log_data_is_empty
29
+ # Determines what Logidze should do when upgrade is needed (:raise | :warn | :ignore)
30
+ attr_reader :on_pending_upgrade
35
31
 
36
32
  # Temporary disable DB triggers.
37
33
  #
38
34
  # @example
39
35
  # Logidze.without_logging { Post.update_all(active: true) }
40
36
  def without_logging
37
+ with_logidze_setting("logidze.disabled", "on") { yield }
38
+ end
39
+
40
+ # Instruct Logidze to create a full snapshot for the new versions, not a diff
41
+ #
42
+ # @example
43
+ # Logidze.with_full_snapshot { post.touch }
44
+ def with_full_snapshot
45
+ with_logidze_setting("logidze.full_snapshot", "on") { yield }
46
+ end
47
+
48
+ def on_pending_upgrade=(mode)
49
+ if %i[raise warn ignore].exclude? mode
50
+ raise ArgumentError, "Unknown on_pending_upgrade option `#{mode.inspect}`. Expecting :raise, :warn or :ignore"
51
+ end
52
+ @on_pending_upgrade = mode
53
+ end
54
+
55
+ private
56
+
57
+ def with_logidze_setting(name, value)
41
58
  ActiveRecord::Base.transaction do
42
- ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled TO on;"
59
+ ActiveRecord::Base.connection.execute "SET LOCAL #{name} TO #{value};"
43
60
  res = yield
44
- ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled TO DEFAULT;"
61
+ ActiveRecord::Base.connection.execute "SET LOCAL #{name} TO DEFAULT;"
45
62
  res
46
63
  end
47
64
  end
48
65
  end
66
+
67
+ self.append_on_undo = false
68
+ self.associations_versioning = false
69
+ self.ignore_log_data_by_default = false
70
+ self.return_self_if_log_data_is_empty = true
71
+ self.on_pending_upgrade = :ignore
49
72
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logidze"
4
+ require "logidze/utils/check_pending"
4
5
 
5
6
  module Logidze
6
7
  class Engine < Rails::Engine # :nodoc:
@@ -11,5 +12,13 @@ module Logidze
11
12
  ActiveRecord::Base.send :include, Logidze::HasLogidze
12
13
  end
13
14
  end
15
+
16
+ initializer "check Logidze function versions" do |app|
17
+ if config.logidze.on_pending_upgrade != :ignore
18
+ ActiveSupport.on_load(:active_record) do
19
+ app.config.app_middleware.use Logidze::Utils::CheckPending
20
+ end
21
+ end
22
+ end
14
23
  end
15
24
  end
@@ -11,8 +11,8 @@ module Logidze
11
11
  # Include methods to work with history.
12
12
  #
13
13
  def has_logidze(ignore_log_data: Logidze.ignore_log_data_by_default)
14
- include Logidze::Model
15
14
  include Logidze::IgnoreLogData
15
+ include Logidze::Model
16
16
 
17
17
  @ignore_log_data = ignore_log_data
18
18
 
@@ -17,15 +17,6 @@ module Logidze
17
17
  delegate :size, to: :versions
18
18
  delegate :responsible_id, :meta, to: :current_version
19
19
 
20
- ### Rails 4 ###
21
- def self.dump(object)
22
- ActiveSupport::JSON.encode(object)
23
- end
24
-
25
- def self.load(json)
26
- new(json) if json.present?
27
- end
28
-
29
20
  def initialize(data)
30
21
  @data = data
31
22
  end
@@ -57,7 +48,7 @@ module Logidze
57
48
  end
58
49
 
59
50
  # Return diff from the initial state to specified time or version.
60
- # Optional `data` paramater can be used as initial diff state.
51
+ # Optional `data` parameter can be used as initial diff state.
61
52
  def changes_to(time: nil, version: nil, data: {}, from: 0)
62
53
  raise ArgumentError, "Time or version must be specified" if time.nil? && version.nil?
63
54
 
@@ -108,7 +99,7 @@ module Logidze
108
99
 
109
100
  # Return nearest (from the bottom) version to the specified time
110
101
  def find_by_time(time)
111
- versions.reverse.find { |v| v.time <= time }
102
+ versions.reverse_each.find { |v| v.time <= time }
112
103
  end
113
104
 
114
105
  def dup
@@ -5,9 +5,7 @@ module Logidze
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- if Rails::VERSION::MAJOR == 4
9
- require "logidze/ignore_log_data/ignored_columns"
10
- elsif Rails::VERSION::MAJOR == 5
8
+ if Rails::VERSION::MAJOR == 5
11
9
  require "logidze/ignore_log_data/cast_attribute_patch"
12
10
  include CastAttributePatch
13
11
  end
data/lib/logidze/meta.rb CHANGED
@@ -3,18 +3,19 @@
3
3
  module Logidze # :nodoc:
4
4
  # Provide methods to attach meta information
5
5
  module Meta
6
- def with_meta(meta, &block)
7
- MetaTransaction.wrap_with(meta, &block)
6
+ def with_meta(meta, transactional: true, &block)
7
+ wrapper = transactional ? MetaWithTransaction : MetaWithoutTransaction
8
+ wrapper.wrap_with(meta, &block)
8
9
  end
9
10
 
10
- def with_responsible(responsible_id, &block)
11
+ def with_responsible(responsible_id, transactional: true, &block)
11
12
  return yield if responsible_id.nil?
12
13
 
13
14
  meta = {Logidze::History::Version::META_RESPONSIBLE => responsible_id}
14
- with_meta(meta, &block)
15
+ with_meta(meta, transactional: transactional, &block)
15
16
  end
16
17
 
17
- class MetaTransaction # :nodoc:
18
+ class MetaWrapper # :nodoc:
18
19
  def self.wrap_with(meta, &block)
19
20
  new(meta, &block).perform
20
21
  end
@@ -29,14 +30,12 @@ module Logidze # :nodoc:
29
30
  end
30
31
 
31
32
  def perform
32
- return if block.nil?
33
+ raise ArgumentError, "Block must be given" unless block
33
34
  return block.call if meta.nil?
34
35
 
35
- ActiveRecord::Base.transaction { call_block_in_meta_context }
36
+ call_block_in_meta_context
36
37
  end
37
38
 
38
- private
39
-
40
39
  def call_block_in_meta_context
41
40
  prev_meta = current_meta
42
41
 
@@ -44,10 +43,9 @@ module Logidze # :nodoc:
44
43
 
45
44
  pg_set_meta_param(current_meta)
46
45
  result = block.call
47
- pg_reset_meta_param(prev_meta)
48
-
49
46
  result
50
47
  ensure
48
+ pg_reset_meta_param(prev_meta)
51
49
  meta_stack.pop
52
50
  end
53
51
 
@@ -60,20 +58,49 @@ module Logidze # :nodoc:
60
58
  Thread.current[:meta]
61
59
  end
62
60
 
63
- def pg_set_meta_param(value)
64
- encoded_meta = connection.quote(ActiveSupport::JSON.encode(value))
65
- connection.execute("SET LOCAL logidze.meta = #{encoded_meta};")
61
+ def encode_meta(value)
62
+ connection.quote(ActiveSupport::JSON.encode(value))
66
63
  end
67
64
 
68
65
  def pg_reset_meta_param(prev_meta)
69
66
  if prev_meta.empty?
70
- connection.execute("SET LOCAL logidze.meta TO DEFAULT;")
67
+ pg_clear_meta_param
71
68
  else
72
69
  pg_set_meta_param(prev_meta)
73
70
  end
74
71
  end
75
72
  end
76
73
 
77
- private_constant :MetaTransaction
74
+ class MetaWithTransaction < MetaWrapper # :nodoc:
75
+ private
76
+
77
+ def call_block_in_meta_context
78
+ connection.transaction { super }
79
+ end
80
+
81
+ def pg_set_meta_param(value)
82
+ connection.execute("SET LOCAL logidze.meta = #{encode_meta(value)};")
83
+ end
84
+
85
+ def pg_clear_meta_param
86
+ connection.execute("SET LOCAL logidze.meta TO DEFAULT;")
87
+ end
88
+ end
89
+
90
+ class MetaWithoutTransaction < MetaWrapper # :nodoc:
91
+ private
92
+
93
+ def pg_set_meta_param(value)
94
+ connection.execute("SET logidze.meta = #{encode_meta(value)};")
95
+ end
96
+
97
+ def pg_clear_meta_param
98
+ connection.execute("SET logidze.meta TO DEFAULT;")
99
+ end
100
+ end
101
+
102
+ private_constant :MetaWrapper
103
+ private_constant :MetaWithTransaction
104
+ private_constant :MetaWithoutTransaction
78
105
  end
79
106
  end
data/lib/logidze/model.rb CHANGED
@@ -3,43 +3,28 @@
3
3
  require "active_support"
4
4
 
5
5
  module Logidze
6
- module Deprecations # :nodoc:
7
- def self.show_ts_deprecation_for(meth)
8
- warn(
9
- "[Deprecation] Usage of #{meth}(time) will be removed in the future releases, "\
10
- "use #{meth}(time: ts) instead"
11
- )
12
- end
13
- end
6
+ using RubyNext
14
7
 
15
8
  # Extends model with methods to browse history
16
9
  module Model
17
- require "logidze/history/type" if Rails::VERSION::MAJOR >= 5
10
+ require "logidze/history/type"
18
11
 
19
12
  extend ActiveSupport::Concern
20
13
 
21
14
  included do
22
- if Rails::VERSION::MAJOR < 5
23
- serialize :log_data, Logidze::History
24
- else
25
- attribute :log_data, Logidze::History::Type.new
26
- end
15
+ attribute :log_data, Logidze::History::Type.new
27
16
 
28
17
  delegate :version, to: :log_data, prefix: "log"
29
18
  end
30
19
 
31
20
  module ClassMethods # :nodoc:
32
21
  # Return records reverted to specified time
33
- def at(ts = nil, time: nil, version: nil)
34
- Deprecations.show_ts_deprecation_for(".at") if ts
35
- time ||= ts
36
- all.map { |record| record.at(time: time, version: version) }.compact
22
+ def at(time: nil, version: nil)
23
+ all.to_a.filter_map { |record| record.at(time: time, version: version) }
37
24
  end
38
25
 
39
26
  # Return changes made to records since specified time
40
- def diff_from(ts = nil, time: nil, version: nil)
41
- Deprecations.show_ts_deprecation_for(".diff_from") if ts
42
- time ||= ts
27
+ def diff_from(time: nil, version: nil)
43
28
  all.map { |record| record.diff_from(time: time, version: version) }
44
29
  end
45
30
 
@@ -58,6 +43,28 @@ module Logidze
58
43
  def reset_log_data
59
44
  without_logging { update_all(log_data: nil) }
60
45
  end
46
+
47
+ # Initialize log_data with the current state if it's null
48
+ def create_logidze_snapshot(timestamp: nil, only: nil, except: nil)
49
+ args = ["'null'"]
50
+
51
+ args[0] = "'#{timestamp}'" if timestamp
52
+
53
+ columns = only || except
54
+
55
+ if columns
56
+ args[1] = "'{#{columns.join(",")}}'"
57
+ args[2] = only ? "true" : "false"
58
+ end
59
+
60
+ without_logging do
61
+ where(log_data: nil).update_all(
62
+ <<~SQL
63
+ log_data = logidze_snapshot(to_jsonb(#{quoted_table_name}), #{args.join(", ")})
64
+ SQL
65
+ )
66
+ end
67
+ end
61
68
  end
62
69
 
63
70
  # Use this to convert Ruby time to milliseconds
@@ -69,14 +76,15 @@ module Logidze
69
76
  # If time/version is less then the first version, then return nil.
70
77
  # If time/version is greater then the last version, then return self.
71
78
  # rubocop: disable Metrics/MethodLength
72
- def at(ts = nil, time: nil, version: nil)
73
- Deprecations.show_ts_deprecation_for("#at") if ts
74
-
79
+ def at(time: nil, version: nil)
75
80
  return at_version(version) if version
76
81
 
77
- time ||= ts
78
82
  time = parse_time(time)
79
83
 
84
+ unless log_data
85
+ return Logidze.return_self_if_log_data_is_empty ? self : nil
86
+ end
87
+
80
88
  return nil unless log_data.exists_ts?(time)
81
89
 
82
90
  if log_data.current_ts?(time)
@@ -91,12 +99,11 @@ module Logidze
91
99
  # rubocop: enable Metrics/MethodLength
92
100
 
93
101
  # Revert record to the version at specified time (without saving to DB)
94
- def at!(ts = nil, time: nil, version: nil)
95
- Deprecations.show_ts_deprecation_for("#at!") if ts
96
-
102
+ def at!(time: nil, version: nil)
97
103
  return at_version!(version) if version
98
104
 
99
- time ||= ts
105
+ raise ArgumentError, "#log_data is empty" unless log_data
106
+
100
107
  time = parse_time(time)
101
108
 
102
109
  return self if log_data.current_ts?(time)
@@ -119,6 +126,8 @@ module Logidze
119
126
 
120
127
  # Revert record to the specified version (without saving to DB)
121
128
  def at_version!(version)
129
+ raise ArgumentError, "#log_data is empty" unless log_data
130
+
122
131
  return self if log_data.version == version
123
132
  return false unless log_data.find_by_version(version)
124
133
 
@@ -131,13 +140,11 @@ module Logidze
131
140
  #
132
141
  # post.diff_from(time: 2.days.ago) # or post.diff_from(version: 2)
133
142
  # #=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }
134
- def diff_from(ts = nil, version: nil, time: nil)
135
- Deprecations.show_ts_deprecation_for("#diff_from") if ts
136
- time ||= ts
143
+ def diff_from(version: nil, time: nil)
137
144
  time = parse_time(time) if time
138
- changes = log_data.diff_from(time: time, version: version).tap do |v|
145
+ changes = log_data&.diff_from(time: time, version: version)&.tap do |v|
139
146
  deserialize_changes!(v)
140
- end
147
+ end || {}
141
148
 
142
149
  changes.delete_if { |k, _v| deleted_column?(k) }
143
150
 
@@ -206,7 +213,7 @@ module Logidze
206
213
 
207
214
  # Loads log_data field from the database, stores to the attributes hash and returns it
208
215
  def reload_log_data
209
- self.log_data = self.class.where(self.class.primary_key => id).pluck(:log_data).first
216
+ self.log_data = self.class.where(self.class.primary_key => id).pluck("#{self.class.table_name}.log_data").first
210
217
  end
211
218
 
212
219
  # Nullify log_data column for a single record
@@ -214,6 +221,12 @@ module Logidze
214
221
  self.class.without_logging { update_column(:log_data, nil) }
215
222
  end
216
223
 
224
+ def create_logidze_snapshot!(**opts)
225
+ self.class.where(self.class.primary_key => id).create_logidze_snapshot(**opts)
226
+
227
+ reload_log_data
228
+ end
229
+
217
230
  protected
218
231
 
219
232
  def apply_diff(version, diff)
@@ -226,7 +239,7 @@ module Logidze
226
239
  end
227
240
 
228
241
  def apply_column_diff(column, value)
229
- return if deleted_column?(column)
242
+ return if deleted_column?(column) || column == "log_data"
230
243
 
231
244
  write_attribute column, deserialize_value(column, value)
232
245
  end
@@ -240,14 +253,8 @@ module Logidze
240
253
  object_at
241
254
  end
242
255
 
243
- if Rails::VERSION::MAJOR < 5
244
- def deserialize_value(column, value)
245
- @attributes[column].type.type_cast_from_database(value)
246
- end
247
- else
248
- def deserialize_value(column, value)
249
- @attributes[column].type.deserialize(value)
250
- end
256
+ def deserialize_value(column, value)
257
+ @attributes[column].type.deserialize(value)
251
258
  end
252
259
 
253
260
  def deleted_column?(column)