logidze 0.8.1 → 1.0.0.rc1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -5
  3. data/LICENSE.txt +1 -1
  4. data/README.md +310 -80
  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 +56 -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 -20
  21. data/lib/logidze/engine.rb +4 -1
  22. data/lib/logidze/has_logidze.rb +12 -3
  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 +22 -0
  27. data/lib/logidze/ignore_log_data/cast_attribute_patch.rb +17 -0
  28. data/lib/logidze/meta.rb +44 -17
  29. data/lib/logidze/model.rb +65 -37
  30. data/lib/logidze/version.rb +2 -1
  31. data/lib/logidze/versioned_association.rb +0 -1
  32. metadata +47 -103
  33. data/.gitignore +0 -40
  34. data/.hound.yml +0 -3
  35. data/.rubocop.yml +0 -94
  36. data/.travis.yml +0 -39
  37. data/Gemfile +0 -13
  38. data/Rakefile +0 -28
  39. data/bench/performance/README.md +0 -109
  40. data/bench/performance/diff_bench.rb +0 -36
  41. data/bench/performance/insert_bench.rb +0 -20
  42. data/bench/performance/memory_profile.rb +0 -53
  43. data/bench/performance/setup.rb +0 -308
  44. data/bench/performance/update_bench.rb +0 -36
  45. data/bench/triggers/Makefile +0 -56
  46. data/bench/triggers/Readme.md +0 -58
  47. data/bench/triggers/bench.sql +0 -6
  48. data/bench/triggers/hstore_trigger_setup.sql +0 -38
  49. data/bench/triggers/jsonb_minus_2_setup.sql +0 -47
  50. data/bench/triggers/jsonb_minus_setup.sql +0 -49
  51. data/bench/triggers/keys2_trigger_setup.sql +0 -44
  52. data/bench/triggers/keys_trigger_setup.sql +0 -50
  53. data/bin/console +0 -8
  54. data/bin/setup +0 -9
  55. data/gemfiles/rails42.gemfile +0 -5
  56. data/gemfiles/rails5.gemfile +0 -6
  57. data/gemfiles/rails52.gemfile +0 -6
  58. data/gemfiles/railsmaster.gemfile +0 -7
  59. data/lib/logidze/migration.rb +0 -19
  60. 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,8 +29,8 @@ 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"
@@ -31,13 +39,19 @@ module Logidze
31
39
  desc: "Define whether this is an update migration"
32
40
 
33
41
  def generate_migration
34
- if options[:blacklist] && options[:whitelist]
35
- warn "Use only one: --whitelist or --blacklist"
42
+ if options[:except] && options[:only]
43
+ warn "Use only one: --only or --except"
36
44
  exit(1)
37
45
  end
38
46
  migration_template "migration.rb.erb", "db/migrate/#{migration_file_name}"
39
47
  end
40
48
 
49
+ def generate_fx_trigger
50
+ return unless fx?
51
+
52
+ template "logidze.sql", "db/triggers/logidze_on_#{table_name}_v#{next_version.to_s.rjust(2, "0")}.sql"
53
+ end
54
+
41
55
  def inject_logidze_to_model
42
56
  return if update?
43
57
 
@@ -75,19 +89,18 @@ module Logidze
75
89
  options[:update]
76
90
  end
77
91
 
78
- def columns_blacklist
79
- array = if !options[:whitelist]
80
- options[:blacklist]
81
- else
82
- class_name.constantize.column_names - options[:whitelist]
83
- end
92
+ def filtered_columns
93
+ format_pgsql_array(options[:only] || options[:except])
94
+ end
84
95
 
85
- format_pgsql_array(array)
96
+ def include_columns
97
+ return unless options[:only] || options[:except]
98
+ options[:only].present?
86
99
  end
87
100
 
88
101
  def timestamp_column
89
- value = options[:timestamp_column] || 'updated_at'
90
- return if %w(nil null false).include?(value)
102
+ value = options[:timestamp_column] || "updated_at"
103
+ return if %w[nil null false].include?(value)
91
104
 
92
105
  escape_pgsql_string(value)
93
106
  end
@@ -96,18 +109,41 @@ module Logidze
96
109
  options[:debounce_time]
97
110
  end
98
111
 
112
+ def previous_version
113
+ @previous_version ||= all_triggers.filter_map { |path| Regexp.last_match[1].to_i if path =~ %r{logidze_on_#{table_name}_v(\d+).sql} }.max
114
+ end
115
+
116
+ def next_version
117
+ previous_version&.next || 1
118
+ end
119
+
120
+ def all_triggers
121
+ @all_triggers ||=
122
+ begin
123
+ res = nil
124
+ in_root do
125
+ res = if File.directory?("db/triggers")
126
+ Dir.entries("db/triggers")
127
+ else
128
+ []
129
+ end
130
+ end
131
+ res
132
+ end
133
+ end
134
+
99
135
  def logidze_logger_parameters
100
- format_pgsql_args(limit, timestamp_column, columns_blacklist, debounce_time)
136
+ format_pgsql_args(limit, timestamp_column, filtered_columns, include_columns, debounce_time)
101
137
  end
102
138
 
103
139
  def logidze_snapshot_parameters
104
- format_pgsql_args('to_jsonb(t)', timestamp_column, columns_blacklist)
140
+ format_pgsql_args("to_jsonb(t)", timestamp_column, filtered_columns, include_columns)
105
141
  end
106
142
 
107
143
  def format_pgsql_array(ruby_array)
108
144
  return if ruby_array.blank?
109
145
 
110
- "'{" + ruby_array.join(', ') + "}'"
146
+ "'{" + ruby_array.join(", ") + "}'"
111
147
  end
112
148
 
113
149
  def escape_pgsql_string(string)
@@ -124,10 +160,10 @@ module Logidze
124
160
  def format_pgsql_args(*values)
125
161
  args = []
126
162
  values.reverse_each do |value|
127
- formatted_value = value.presence || (args.any? && 'null')
163
+ formatted_value = value.presence || (args.any? && "null")
128
164
  args << formatted_value if formatted_value
129
165
  end
130
- args.compact.reverse.join(', ')
166
+ args.compact.reverse.join(", ")
131
167
  end
132
168
  end
133
169
 
@@ -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,39 +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/has_logidze'
11
- 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"
12
15
 
13
16
  extend Logidze::Meta
14
17
 
15
- require 'logidze/engine' if defined?(Rails)
18
+ require "logidze/engine" if defined?(Rails)
16
19
 
17
20
  class << self
18
21
  # Determines if Logidze should append a version to the log after updating an old version.
19
22
  attr_accessor :append_on_undo
20
- 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
21
29
 
22
- def associations_versioning
23
- @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 }
24
36
  end
25
- end
26
37
 
27
- # Temporary disable DB triggers.
28
- #
29
- # @example
30
- # Logidze.without_logging { Post.update_all(active: true) }
31
- def self.without_logging
32
- ActiveRecord::Base.transaction do
33
- ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled TO on;"
34
- res = yield
35
- ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled TO DEFAULT;"
36
- 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
37
55
  end
38
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
39
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,9 +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
13
+ def has_logidze(ignore_log_data: Logidze.ignore_log_data_by_default)
14
+ include Logidze::IgnoreLogData
14
15
  include Logidze::Model
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
15
24
  end
16
25
  end
17
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