logidze 0.9.0 → 1.0.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +101 -5
  3. data/LICENSE.txt +1 -1
  4. data/README.md +309 -89
  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_compact_history.sql +38 -0
  9. data/lib/generators/logidze/install/functions/logidze_filter_keys.sql +27 -0
  10. data/lib/generators/logidze/install/functions/logidze_logger.sql +150 -0
  11. data/lib/generators/logidze/install/functions/logidze_snapshot.sql +24 -0
  12. data/lib/generators/logidze/install/functions/logidze_version.sql +20 -0
  13. data/lib/generators/logidze/install/install_generator.rb +61 -3
  14. data/lib/generators/logidze/install/templates/hstore.rb.erb +1 -1
  15. data/lib/generators/logidze/install/templates/migration.rb.erb +19 -232
  16. data/lib/generators/logidze/install/templates/migration_fx.rb.erb +41 -0
  17. data/lib/generators/logidze/model/model_generator.rb +60 -20
  18. data/lib/generators/logidze/model/templates/migration.rb.erb +57 -36
  19. data/lib/generators/logidze/model/triggers/logidze.sql +6 -0
  20. data/lib/logidze.rb +43 -21
  21. data/lib/logidze/engine.rb +4 -1
  22. data/lib/logidze/has_logidze.rb +12 -4
  23. data/lib/logidze/history.rb +7 -15
  24. data/lib/logidze/history/type.rb +1 -1
  25. data/lib/logidze/history/version.rb +6 -5
  26. data/lib/logidze/ignore_log_data.rb +11 -19
  27. data/lib/logidze/meta.rb +44 -17
  28. data/lib/logidze/model.rb +63 -46
  29. data/lib/logidze/version.rb +2 -1
  30. data/lib/logidze/versioned_association.rb +0 -1
  31. metadata +43 -103
  32. data/.gitignore +0 -40
  33. data/.hound.yml +0 -3
  34. data/.rubocop.yml +0 -94
  35. data/.travis.yml +0 -39
  36. data/Gemfile +0 -13
  37. data/Rakefile +0 -28
  38. data/bench/performance/README.md +0 -109
  39. data/bench/performance/diff_bench.rb +0 -36
  40. data/bench/performance/insert_bench.rb +0 -20
  41. data/bench/performance/memory_profile.rb +0 -53
  42. data/bench/performance/setup.rb +0 -308
  43. data/bench/performance/update_bench.rb +0 -36
  44. data/bench/triggers/Makefile +0 -56
  45. data/bench/triggers/Readme.md +0 -58
  46. data/bench/triggers/bench.sql +0 -6
  47. data/bench/triggers/hstore_trigger_setup.sql +0 -38
  48. data/bench/triggers/jsonb_minus_2_setup.sql +0 -47
  49. data/bench/triggers/jsonb_minus_setup.sql +0 -49
  50. data/bench/triggers/keys2_trigger_setup.sql +0 -44
  51. data/bench/triggers/keys_trigger_setup.sql +0 -50
  52. data/bin/console +0 -8
  53. data/bin/setup +0 -9
  54. data/gemfiles/rails42.gemfile +0 -5
  55. data/gemfiles/rails5.gemfile +0 -6
  56. data/gemfiles/rails52.gemfile +0 -6
  57. data/gemfiles/railsmaster.gemfile +0 -7
  58. data/lib/logidze/ignore_log_data/ignored_columns.rb +0 -46
  59. data/lib/logidze/ignore_log_data/missing_attribute_patch.rb +0 -16
  60. data/lib/logidze/migration.rb +0 -19
  61. data/logidze.gemspec +0 -33
@@ -0,0 +1,41 @@
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
2
+ def change
3
+ <%- if update? -%>
4
+ reversible do |dir|
5
+ dir.up do
6
+ # Drop legacy functions (<1.0)
7
+ execute <<~SQL
8
+ DROP FUNCTION IF EXISTS logidze_version(bigint, jsonb);
9
+ DROP FUNCTION IF EXISTS logidze_snapshot(jsonb);
10
+ DROP FUNCTION IF EXISTS logidze_version(bigint, jsonb, text[]);
11
+ DROP FUNCTION IF EXISTS logidze_snapshot(jsonb, text[]);
12
+ DROP FUNCTION IF EXISTS logidze_version(bigint, jsonb, timestamp with time zone, text[]);
13
+ DROP FUNCTION IF EXISTS logidze_snapshot(jsonb, text, text[]);
14
+ DROP FUNCTION IF EXISTS logidze_exclude_keys(jsonb, VARIADIC text[]);
15
+ DROP FUNCTION IF EXISTS logidze_compact_history(jsonb);
16
+ SQL
17
+ end
18
+ end
19
+
20
+ <%- end -%>
21
+ <%- function_definitions.each do |f| -%>
22
+ <%- previous_version = previous_version_for(f.name) -%>
23
+ <%- if previous_version -%>
24
+ <%- if previous_version != f.version -%>
25
+ update_function :<%= f.name %>, version: <%= f.version %>, revert_to_version: <%= previous_version %>
26
+ <%- end -%>
27
+ <%- else -%>
28
+ reversible do |dir|
29
+ dir.up do
30
+ create_function :<%= f.name %>, version: <%= f.version %>
31
+ end
32
+
33
+ dir.down do
34
+ execute "DROP FUNCTION IF EXISTS <%= f.name %>(<%= f.signature %>) CASCADE"
35
+ end
36
+ end
37
+
38
+ <%- end -%>
39
+ <%- end -%>
40
+ end
41
+ end
@@ -1,12 +1,20 @@
1
- # rubocop:disable Metrics/BlockLength
2
1
  # frozen_string_literal: true
2
+
3
3
  require "rails/generators"
4
4
  require "rails/generators/active_record/migration/migration_generator"
5
+ require_relative "../inject_sql"
6
+ require_relative "../fx_helper"
7
+
8
+ using RubyNext
5
9
 
6
10
  module Logidze
7
11
  module Generators
8
12
  class ModelGenerator < ::ActiveRecord::Generators::Base # :nodoc:
9
- source_root File.expand_path('templates', __dir__)
13
+ include InjectSql
14
+ include FxHelper
15
+
16
+ source_root File.expand_path("templates", __dir__)
17
+ source_paths << File.expand_path("triggers", __dir__)
10
18
 
11
19
  class_option :limit, type: :numeric, optional: true, desc: "Specify history size limit"
12
20
 
@@ -21,23 +29,31 @@ module Logidze
21
29
 
22
30
  class_option :path, type: :string, optional: true, desc: "Specify path to the model file"
23
31
 
24
- class_option :blacklist, type: :array, optional: true
25
- class_option :whitelist, type: :array, optional: true
32
+ class_option :except, type: :array, optional: true
33
+ class_option :only, type: :array, optional: true
26
34
 
27
35
  class_option :timestamp_column, type: :string, optional: true,
28
36
  desc: "Specify timestamp column"
29
37
 
38
+ class_option :name, type: :string, optional: true, desc: "Migration name"
39
+
30
40
  class_option :update, type: :boolean, optional: true,
31
41
  desc: "Define whether this is an update migration"
32
42
 
33
43
  def generate_migration
34
- if options[:blacklist] && options[:whitelist]
35
- warn "Use only one: --whitelist or --blacklist"
44
+ if options[:except] && options[:only]
45
+ warn "Use only one: --only or --except"
36
46
  exit(1)
37
47
  end
38
48
  migration_template "migration.rb.erb", "db/migrate/#{migration_file_name}"
39
49
  end
40
50
 
51
+ def generate_fx_trigger
52
+ return unless fx?
53
+
54
+ template "logidze.sql", "db/triggers/logidze_on_#{table_name}_v#{next_version.to_s.rjust(2, "0")}.sql"
55
+ end
56
+
41
57
  def inject_logidze_to_model
42
58
  return if update?
43
59
 
@@ -48,6 +64,8 @@ module Logidze
48
64
 
49
65
  no_tasks do
50
66
  def migration_name
67
+ return options[:name] if options[:name].present?
68
+
51
69
  if update?
52
70
  "update_logidze_for_#{plural_table_name}"
53
71
  else
@@ -75,19 +93,18 @@ module Logidze
75
93
  options[:update]
76
94
  end
77
95
 
78
- def columns_blacklist
79
- array = if !options[:whitelist]
80
- options[:blacklist]
81
- else
82
- class_name.constantize.column_names - options[:whitelist]
83
- end
96
+ def filtered_columns
97
+ format_pgsql_array(options[:only] || options[:except])
98
+ end
84
99
 
85
- format_pgsql_array(array)
100
+ def include_columns
101
+ return unless options[:only] || options[:except]
102
+ options[:only].present?
86
103
  end
87
104
 
88
105
  def timestamp_column
89
- value = options[:timestamp_column] || 'updated_at'
90
- return if %w(nil null false).include?(value)
106
+ value = options[:timestamp_column] || "updated_at"
107
+ return if %w[nil null false].include?(value)
91
108
 
92
109
  escape_pgsql_string(value)
93
110
  end
@@ -96,18 +113,41 @@ module Logidze
96
113
  options[:debounce_time]
97
114
  end
98
115
 
116
+ def previous_version
117
+ @previous_version ||= all_triggers.filter_map { |path| Regexp.last_match[1].to_i if path =~ %r{logidze_on_#{table_name}_v(\d+).sql} }.max
118
+ end
119
+
120
+ def next_version
121
+ previous_version&.next || 1
122
+ end
123
+
124
+ def all_triggers
125
+ @all_triggers ||=
126
+ begin
127
+ res = nil
128
+ in_root do
129
+ res = if File.directory?("db/triggers")
130
+ Dir.entries("db/triggers")
131
+ else
132
+ []
133
+ end
134
+ end
135
+ res
136
+ end
137
+ end
138
+
99
139
  def logidze_logger_parameters
100
- format_pgsql_args(limit, timestamp_column, columns_blacklist, debounce_time)
140
+ format_pgsql_args(limit, timestamp_column, filtered_columns, include_columns, debounce_time)
101
141
  end
102
142
 
103
143
  def logidze_snapshot_parameters
104
- format_pgsql_args('to_jsonb(t)', timestamp_column, columns_blacklist)
144
+ format_pgsql_args("to_jsonb(t)", timestamp_column, filtered_columns, include_columns)
105
145
  end
106
146
 
107
147
  def format_pgsql_array(ruby_array)
108
148
  return if ruby_array.blank?
109
149
 
110
- "'{" + ruby_array.join(', ') + "}'"
150
+ "'{" + ruby_array.join(", ") + "}'"
111
151
  end
112
152
 
113
153
  def escape_pgsql_string(string)
@@ -124,10 +164,10 @@ module Logidze
124
164
  def format_pgsql_args(*values)
125
165
  args = []
126
166
  values.reverse_each do |value|
127
- formatted_value = value.presence || (args.any? && 'null')
167
+ formatted_value = value.presence || (args.any? && "null")
128
168
  args << formatted_value if formatted_value
129
169
  end
130
- args.compact.reverse.join(', ')
170
+ args.compact.reverse.join(", ")
131
171
  end
132
172
  end
133
173
 
@@ -1,43 +1,64 @@
1
- class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VERSION::MAJOR < 5 ? '' : '[5.0]' %>
2
- require 'logidze/migration'
3
- include Logidze::Migration
4
-
5
- def up
6
- <% if update? %>
7
- execute "DROP TRIGGER logidze_on_<%= table_name %> on <%= table_name %>;"
8
- <% elsif !only_trigger? %>
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
2
+ def change
3
+ <%- unless update? || only_trigger? -%>
9
4
  add_column :<%= table_name %>, :log_data, :jsonb
10
- <% end %>
5
+ <%- end -%>
11
6
 
12
- execute <<-SQL
13
- CREATE TRIGGER logidze_on_<%= table_name %>
14
- BEFORE UPDATE OR INSERT ON <%= table_name %> FOR EACH ROW
15
- WHEN (coalesce(#{current_setting('logidze.disabled')}, '') <> 'on')
16
- EXECUTE PROCEDURE logidze_logger(<%= logidze_logger_parameters %>);
17
- SQL
7
+ <%- if fx? -%>
8
+ <%- if previous_version -%>
9
+ update_trigger :logidze_on_<%= table_name %>, on: :<%= table_name %>, version: <%= next_version %>, revert_to_version: <%= previous_version %>
10
+ <%- else -%>
11
+ reversible do |dir|
12
+ dir.up do
13
+ <%- if update? -%>
14
+ # Drop legacy trigger if any (<1.0)
15
+ execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
18
16
 
19
- <% if backfill? %>
20
- execute <<-SQL
21
- UPDATE <%= table_name %> as t
22
- SET log_data = logidze_snapshot(<%= logidze_snapshot_parameters %>);
23
- SQL
24
- <% end %>
25
- end
17
+ <%- end -%>
18
+ create_trigger :logidze_on_<%= table_name %>, on: :<%= table_name %>
19
+ end
20
+
21
+ dir.down do
22
+ execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
23
+ end
24
+ end
25
+ <%- end -%>
26
+ <%- else -%>
27
+ reversible do |dir|
28
+ dir.up do
29
+ <%- if update? -%>
30
+ execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
31
+
32
+ <%- end -%>
33
+ execute <<~SQL
34
+ <%= inject_sql("logidze.sql", indent: 10) %>
35
+ SQL
36
+ end
26
37
 
27
- def down
28
- <% if update? %>
29
- # NOTE: We have no idea on how to revert the migration
30
- # ('cause we don't know the previous trigger params),
31
- # but you can do that on your own.
32
- #
33
- # Uncomment this line if you want to raise an error.
34
- # raise ActiveRecord::IrreversibleMigration
35
- <% else %>
36
- execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
38
+ dir.down do
39
+ <%- if update? -%>
40
+ # NOTE: We have no idea on how to revert the migration
41
+ # ('cause we don't know the previous trigger params),
42
+ # but you can do that on your own.
43
+ #
44
+ # Uncomment this line if you want to raise an error.
45
+ # raise ActiveRecord::IrreversibleMigration
46
+ <%- else -%>
47
+ execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
48
+ <%- end -%>
49
+ end
50
+ end
51
+ <%- end -%>
52
+ <%- if backfill? -%>
37
53
 
38
- <% if !only_trigger? %>
39
- remove_column :<%= table_name %>, :log_data
40
- <% end %>
41
- <% end %>
54
+ reversible do |dir|
55
+ dir.up do
56
+ execute <<~SQL
57
+ UPDATE <%= table_name %> as t
58
+ SET log_data = logidze_snapshot(<%= logidze_snapshot_parameters %>);
59
+ SQL
60
+ end
61
+ end
62
+ <%- end -%>
42
63
  end
43
64
  end
@@ -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 %>);
@@ -1,40 +1,62 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "logidze/version"
3
4
 
4
5
  # Logidze provides tools for adding in-table JSON-based audit to DB tables
5
6
  # and ActiveRecord extensions to work with changes history.
6
7
  module Logidze
7
- require 'logidze/history'
8
- require 'logidze/model'
9
- require 'logidze/versioned_association'
10
- require 'logidze/ignore_log_data'
11
- require 'logidze/has_logidze'
12
- require 'logidze/meta'
8
+ require "ruby-next"
9
+ require "logidze/history"
10
+ require "logidze/model"
11
+ require "logidze/versioned_association"
12
+ require "logidze/ignore_log_data"
13
+ require "logidze/has_logidze"
14
+ require "logidze/meta"
13
15
 
14
16
  extend Logidze::Meta
15
17
 
16
- require 'logidze/engine' if defined?(Rails)
18
+ require "logidze/engine" if defined?(Rails)
17
19
 
18
20
  class << self
19
21
  # Determines if Logidze should append a version to the log after updating an old version.
20
22
  attr_accessor :append_on_undo
21
- attr_writer :associations_versioning
23
+ # Determines whether associations versioning is enabled or not
24
+ attr_accessor :associations_versioning
25
+ # Determines if Logidze should exclude log data from SELECT statements
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
22
29
 
23
- def associations_versioning
24
- @associations_versioning || false
30
+ # Temporary disable DB triggers.
31
+ #
32
+ # @example
33
+ # Logidze.without_logging { Post.update_all(active: true) }
34
+ def without_logging
35
+ with_logidze_setting("logidze.disabled", "on") { yield }
25
36
  end
26
- end
27
37
 
28
- # Temporary disable DB triggers.
29
- #
30
- # @example
31
- # Logidze.without_logging { Post.update_all(active: true) }
32
- def self.without_logging
33
- ActiveRecord::Base.transaction do
34
- ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled TO on;"
35
- res = yield
36
- ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled TO DEFAULT;"
37
- res
38
+ # Instructure Logidze to create a full snapshot for the new versions, not a diff
39
+ #
40
+ # @example
41
+ # Logidze.with_full_snapshot { post.touch }
42
+ def with_full_snapshot
43
+ with_logidze_setting("logidze.full_snapshot", "on") { yield }
44
+ end
45
+
46
+ private
47
+
48
+ def with_logidze_setting(name, value)
49
+ ActiveRecord::Base.transaction do
50
+ ActiveRecord::Base.connection.execute "SET LOCAL #{name} TO #{value};"
51
+ res = yield
52
+ ActiveRecord::Base.connection.execute "SET LOCAL #{name} TO DEFAULT;"
53
+ res
54
+ end
38
55
  end
39
56
  end
57
+
58
+ self.append_on_undo = false
59
+ self.associations_versioning = false
60
+ self.ignore_log_data_by_default = false
61
+ self.return_self_if_log_data_is_empty = true
40
62
  end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
- require 'logidze'
2
+
3
+ require "logidze"
3
4
 
4
5
  module Logidze
5
6
  class Engine < Rails::Engine # :nodoc:
7
+ config.logidze = Logidze
8
+
6
9
  initializer "extend ActiveRecord with Logidze" do |_app|
7
10
  ActiveSupport.on_load(:active_record) do
8
11
  ActiveRecord::Base.send :include, Logidze::HasLogidze
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'active_support'
2
+
3
+ require "active_support"
3
4
 
4
5
  module Logidze
5
6
  # Add `has_logidze` method to AR::Base
@@ -9,10 +10,17 @@ module Logidze
9
10
  module ClassMethods # :nodoc:
10
11
  # Include methods to work with history.
11
12
  #
12
- # rubocop:disable Naming/PredicateName
13
- def has_logidze(ignore_log_data: false)
13
+ def has_logidze(ignore_log_data: Logidze.ignore_log_data_by_default)
14
+ include Logidze::IgnoreLogData
14
15
  include Logidze::Model
15
- include Logidze::IgnoreLogData if ignore_log_data
16
+
17
+ @ignore_log_data = ignore_log_data
18
+
19
+ self.ignored_columns += ["log_data"] if @ignore_log_data
20
+ end
21
+
22
+ def ignores_log_data?
23
+ @ignore_log_data
16
24
  end
17
25
  end
18
26
  end
@@ -1,30 +1,22 @@
1
1
  # frozen_string_literal: true
2
- require 'active_support/core_ext/module/delegation'
2
+
3
+ require "active_support/core_ext/module/delegation"
3
4
 
4
5
  module Logidze
5
6
  # Log data wrapper
6
7
  class History
7
- require 'logidze/history/version'
8
+ require "logidze/history/version"
8
9
 
9
10
  # History key
10
- HISTORY = 'h'
11
+ HISTORY = "h"
11
12
  # Version key
12
- VERSION = 'v'
13
+ VERSION = "v"
13
14
 
14
15
  attr_reader :data
15
16
 
16
17
  delegate :size, to: :versions
17
18
  delegate :responsible_id, :meta, to: :current_version
18
19
 
19
- ### Rails 4 ###
20
- def self.dump(object)
21
- ActiveSupport::JSON.encode(object)
22
- end
23
-
24
- def self.load(json)
25
- new(json) if json.present?
26
- end
27
-
28
20
  def initialize(data)
29
21
  @data = data
30
22
  end
@@ -107,7 +99,7 @@ module Logidze
107
99
 
108
100
  # Return nearest (from the bottom) version to the specified time
109
101
  def find_by_time(time)
110
- versions.reverse.find { |v| v.time <= time }
102
+ versions.reverse_each.find { |v| v.time <= time }
111
103
  end
112
104
 
113
105
  def dup
@@ -128,7 +120,7 @@ module Logidze
128
120
 
129
121
  def build_changes(a, b)
130
122
  b.each_with_object({}) do |(k, v), acc|
131
- acc[k] = { "old" => a[k], "new" => v } unless v == a[k]
123
+ acc[k] = {"old" => a[k], "new" => v} unless v == a[k]
132
124
  end
133
125
  end
134
126