logidze 1.3.1 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c8113ee63d992fde09019059c1cbd3a6d75ab8914ad365106e954a66c28b633
4
- data.tar.gz: e784d849dc188763ada793df28ad134226b2aa21416bbd818ac7e8c496ff1e2c
3
+ metadata.gz: 0cc10c417605b0f92991c201aeb958a44bc6130b64fd3b278780fb780818ccb3
4
+ data.tar.gz: 3c06efc69a95294bff75de4c8d91c1528c0cc67f5c0b5753097599a4c9e8fe5b
5
5
  SHA512:
6
- metadata.gz: d427303c2f7f07f3f0d6964469d124d54dfac17615c881192115a426e9cafe517ad95850753bb6b0676211ffb33b4ed850aa11475ca6753b19c99f07a31421d4
7
- data.tar.gz: 837353c8432d3a7c8e43f94949ad3ac45a0ca069d074d2db0e9acdad9abaabd03f04a5a0d9c3427f3ab710abd967a03d92f7eda02b6e15ce2c05fdb2b4e2b9fb
6
+ metadata.gz: 62d98e00caed7aa2a6f2ccd2be0ed536d2500ef0f195a391f17569c18cf7ce6051ef846abe45d167633100129d7e26555ae3ab4c728f73c05cfaec56dcd5df96
7
+ data.tar.gz: 56652129cef0c7ca97b2a87dd7a94ff6711b21a43680ba87a4af7f3b253fea7a171a5d2307bd81b8af2ca46a08f4b999929cfedf8dd46db2a75a72c61a8f551d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 1.4.0 (2025-05-09) 🎇
6
+
7
+ - Support Rails 7.2 ([@atomaka][])
8
+
9
+ - Add `--detached` option to store `log_data` in a separate `logidze_data` table to avoid table bloat.
10
+
5
11
  ## 1.3.1 (2024-10-23)
6
12
 
7
13
  - Fix `rails destroy logidze:model SomeModel` not deleting the `fx` trigger file file. ([@tylerhunt][])
data/README.md CHANGED
@@ -37,6 +37,7 @@ Other requirements:
37
37
  - [Logs timestamps](#logs-timestamps)
38
38
  - [Undoing a Generated Invocation](#undoing-a-generated-invocation)
39
39
  - [Using with partitioned tables](#using-with-partitioned-tables)
40
+ - [Storing history data in a separate table](#storing-history-data-in-a-separate-table)
40
41
  - [Usage](#usage)
41
42
  - [Basic API](#basic-api)
42
43
  - [Track meta information](#track-meta-information)
@@ -203,6 +204,25 @@ bundle exec rails generate logidze:model Post --after-trigger
203
204
 
204
205
  **IMPORTANT:** Using Logidze for partitioned tables in PostgreSQL 10 is not supported.
205
206
 
207
+ ### Storing history data in a separate table
208
+
209
+ By default, Logidze stores history data in the `log_data` column in the origin record table, which might lead to table bloat.
210
+ If it concerns you, you may configure Logidze to store history data in a separate table by providing `--detached` option to the migration:
211
+
212
+ ```sh
213
+ bundle exec rails logidze:model Post --detached
214
+ ```
215
+
216
+ You can also configure Logidze to always store history data in a separate table for all models:
217
+
218
+ ```ruby
219
+ # config/initializers/logidze.rb
220
+
221
+ Logidze.log_data_placement = :detached
222
+ ```
223
+
224
+ **IMPORTANT:** Using `--detached` mode for storing historic data slightly decreases performance. Check [bench results] for the details.
225
+
206
226
  ## Usage
207
227
 
208
228
  ### Basic API
@@ -645,3 +665,4 @@ Bug reports and pull requests are welcome on GitHub at [https://github.com/palka
645
665
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
646
666
 
647
667
  [fx]: https://github.com/teoljungberg/fx
668
+ [bench results]: https://github.com/palkan/logidze/blob/feat-log-data-separate-storage/bench/performance/README.md
@@ -1,5 +1,5 @@
1
1
  CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
2
- -- version: 4
2
+ -- version: 5
3
3
  DECLARE
4
4
  changes jsonb;
5
5
  version jsonb;
@@ -15,6 +15,15 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
15
15
  item record;
16
16
  columns text[];
17
17
  include_columns boolean;
18
+ detached_log_data jsonb;
19
+ -- We use `detached_loggable_type` for:
20
+ -- 1. Checking if current implementation is `--detached` (`log_data` is stored in a separated table)
21
+ -- 2. If implementation is `--detached` then we use detached_loggable_type to determine
22
+ -- to which table current `log_data` record belongs
23
+ detached_loggable_type text;
24
+ log_data_table_name text;
25
+ log_data_is_empty boolean;
26
+ log_data_ts_key_data text;
18
27
  ts timestamp with time zone;
19
28
  ts_column text;
20
29
  err_sqlstate text;
@@ -30,8 +39,30 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
30
39
  ts_column := NULLIF(TG_ARGV[1], 'null');
31
40
  columns := NULLIF(TG_ARGV[2], 'null');
32
41
  include_columns := NULLIF(TG_ARGV[3], 'null');
42
+ detached_loggable_type := NULLIF(TG_ARGV[5], 'null');
43
+ log_data_table_name := NULLIF(TG_ARGV[6], 'null');
33
44
 
34
- IF NEW.log_data is NULL OR NEW.log_data = '{}'::jsonb
45
+ -- getting previous log_data if it exists for detached `log_data` storage variant
46
+ IF detached_loggable_type IS NOT NULL
47
+ THEN
48
+ EXECUTE format(
49
+ 'SELECT ldtn.log_data ' ||
50
+ 'FROM %I ldtn ' ||
51
+ 'WHERE ldtn.loggable_type = $1 ' ||
52
+ 'AND ldtn.loggable_id = $2 ' ||
53
+ 'LIMIT 1',
54
+ log_data_table_name
55
+ ) USING detached_loggable_type, NEW.id INTO detached_log_data;
56
+ END IF;
57
+
58
+ IF detached_loggable_type IS NULL
59
+ THEN
60
+ log_data_is_empty = NEW.log_data is NULL OR NEW.log_data = '{}'::jsonb;
61
+ ELSE
62
+ log_data_is_empty = detached_log_data IS NULL OR detached_log_data = '{}'::jsonb;
63
+ END IF;
64
+
65
+ IF log_data_is_empty
35
66
  THEN
36
67
  IF columns IS NOT NULL THEN
37
68
  log_data = logidze_snapshot(to_jsonb(NEW.*), ts_column, columns, include_columns);
@@ -40,7 +71,16 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
40
71
  END IF;
41
72
 
42
73
  IF log_data#>>'{h, -1, c}' != '{}' THEN
43
- NEW.log_data := log_data;
74
+ IF detached_loggable_type IS NULL
75
+ THEN
76
+ NEW.log_data := log_data;
77
+ ELSE
78
+ EXECUTE format(
79
+ 'INSERT INTO %I(log_data, loggable_type, loggable_id) ' ||
80
+ 'VALUES ($1, $2, $3);',
81
+ log_data_table_name
82
+ ) USING log_data, detached_loggable_type, NEW.id;
83
+ END IF;
44
84
  END IF;
45
85
 
46
86
  ELSE
@@ -52,7 +92,12 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
52
92
  history_limit := NULLIF(TG_ARGV[0], 'null');
53
93
  debounce_time := NULLIF(TG_ARGV[4], 'null');
54
94
 
55
- log_data := NEW.log_data;
95
+ IF detached_loggable_type IS NULL
96
+ THEN
97
+ log_data := NEW.log_data;
98
+ ELSE
99
+ log_data := detached_log_data;
100
+ END IF;
56
101
 
57
102
  current_version := (log_data->>'v')::int;
58
103
 
@@ -65,8 +110,16 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
65
110
  END IF;
66
111
  ELSEIF TG_OP = 'INSERT' THEN
67
112
  ts := (to_jsonb(NEW.*) ->> ts_column)::timestamp with time zone;
68
- IF ts IS NULL OR (extract(epoch from ts) * 1000)::bigint = (NEW.log_data #>> '{h,-1,ts}')::bigint THEN
69
- ts := statement_timestamp();
113
+
114
+ IF detached_loggable_type IS NULL
115
+ THEN
116
+ log_data_ts_key_data = NEW.log_data #>> '{h,-1,ts}';
117
+ ELSE
118
+ log_data_ts_key_data = detached_log_data #>> '{h,-1,ts}';
119
+ END IF;
120
+
121
+ IF ts IS NULL OR (extract(epoch from ts) * 1000)::bigint = log_data_ts_key_data::bigint THEN
122
+ ts := statement_timestamp();
70
123
  END IF;
71
124
  END IF;
72
125
 
@@ -123,7 +176,12 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
123
176
  END;
124
177
  END IF;
125
178
 
126
- changes = changes - 'log_data';
179
+ -- We store `log_data` in a separate table for the `detached` mode
180
+ -- So we remove `log_data` only when we store historic data in the record's origin table
181
+ IF detached_loggable_type IS NULL
182
+ THEN
183
+ changes = changes - 'log_data';
184
+ END IF;
127
185
 
128
186
  IF columns IS NOT NULL THEN
129
187
  changes = logidze_filter_keys(changes, columns, include_columns);
@@ -170,7 +228,21 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
170
228
  log_data := logidze_compact_history(log_data, size - history_limit + 1);
171
229
  END IF;
172
230
 
173
- NEW.log_data := log_data;
231
+ IF detached_loggable_type IS NULL
232
+ THEN
233
+ NEW.log_data := log_data;
234
+ ELSE
235
+ detached_log_data = log_data;
236
+ EXECUTE format(
237
+ 'UPDATE %I ' ||
238
+ 'SET log_data = $1 ' ||
239
+ 'WHERE %I.loggable_type = $2 ' ||
240
+ 'AND %I.loggable_id = $3',
241
+ log_data_table_name,
242
+ log_data_table_name,
243
+ log_data_table_name
244
+ ) USING detached_log_data, detached_loggable_type, NEW.id;
245
+ END IF;
174
246
  END IF;
175
247
 
176
248
  RETURN NEW; -- result
@@ -1,3 +1,3 @@
1
1
  CREATE OR REPLACE FUNCTION logidze_logger_after() RETURNS TRIGGER AS $body$
2
- -- version: 4
2
+ -- version: 5
3
3
  <%= generate_logidze_logger_after %>
@@ -87,7 +87,17 @@ module Logidze
87
87
  source.sub!(/^CREATE OR REPLACE FUNCTION logidze_logger.*$/, "")
88
88
  source.sub!(/^ -- version.*$/, "")
89
89
  source.gsub!("RETURN NEW; -- pass", "RETURN NULL;")
90
- source.gsub!("RETURN NEW; -- result", " EXECUTE format('UPDATE %I.%I SET \"log_data\" = $1 WHERE ctid = %L', TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.CTID) USING NEW.log_data;\n RETURN NULL;")
90
+
91
+ return_condition = <<~SQL
92
+ IF detached_loggable_type IS NULL
93
+ THEN
94
+ EXECUTE format('UPDATE %I.%I SET "log_data" = $1 WHERE ctid = %L', TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.CTID) USING NEW.log_data;
95
+ END IF;
96
+
97
+ RETURN NULL;
98
+ SQL
99
+ source.gsub!("RETURN NEW; -- result", return_condition)
100
+
91
101
  source
92
102
  end
93
103
  end
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Generates the necessary migration for running Logidze with Logidze data stored in a separate table
3
+
4
+ Examples:
5
+ rails generate logidze:migration:logs
6
+
7
+ This will generate the migration to add separate table for storing Logidze data.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Logidze
7
+ module Generators
8
+ module Migration
9
+ class LogsGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def generate_migration
15
+ migration_template "migration.rb.erb", "db/migrate/create_logidze_data.rb"
16
+ end
17
+
18
+ def self.next_migration_number(dir)
19
+ ::ActiveRecord::Generators::Base.next_migration_number(dir)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ create_table :logidze_data do |t|
6
+ t.jsonb :log_data
7
+ t.belongs_to :loggable, polymorphic: true, index: {name: "index_logidze_loggable", unique: true}
8
+ end
9
+ end
10
+ end
@@ -11,3 +11,7 @@ Examples:
11
11
 
12
12
  This will generate the migration to update existing trigger (drop and create).
13
13
 
14
+ rails generate logidze:Model User --detached
15
+
16
+ This will generate migration to add trigger for the <User> model.
17
+ This will also add `has_logidze detached: true` to the model.
@@ -14,6 +14,9 @@ module Logidze
14
14
  source_root File.expand_path("templates", __dir__)
15
15
  source_paths << File.expand_path("triggers", __dir__)
16
16
 
17
+ class_option :detached, type: :boolean, optional: true,
18
+ desc: "Store history data in a separate table"
19
+
17
20
  class_option :limit, type: :numeric, optional: true, desc: "Specify history size limit"
18
21
 
19
22
  class_option :debounce_time, type: :numeric, optional: true,
@@ -60,8 +63,11 @@ module Logidze
60
63
  return if update?
61
64
 
62
65
  indents = " " * (class_name.scan("::").count + 1)
66
+ macros_name = detached? ? "has_logidze detached: true\n" : "has_logidze\n"
63
67
 
64
- inject_into_class(model_file_path, class_name.demodulize, "#{indents}has_logidze\n")
68
+ if File.readlines("#{destination_root}/#{model_file_path}").grep(/has_logidze/).empty?
69
+ inject_into_class(model_file_path, class_name.demodulize, indents + macros_name)
70
+ end
65
71
  end
66
72
 
67
73
  no_tasks do
@@ -80,6 +86,10 @@ module Logidze
80
86
  "#{config.table_name_prefix}#{table_name}#{config.table_name_suffix}"
81
87
  end
82
88
 
89
+ def detached_loggable_type
90
+ escape_pgsql_string(class_name) if detached?
91
+ end
92
+
83
93
  def limit
84
94
  options[:limit]
85
95
  end
@@ -88,6 +98,10 @@ module Logidze
88
98
  options[:backfill]
89
99
  end
90
100
 
101
+ def detached?
102
+ options[:detached] || Logidze.detached_log_placement?
103
+ end
104
+
91
105
  def only_trigger?
92
106
  options[:only_trigger]
93
107
  end
@@ -116,6 +130,10 @@ module Logidze
116
130
  escape_pgsql_string(value)
117
131
  end
118
132
 
133
+ def quoted_log_data_table_name
134
+ Logidze::LogidzeData.quoted_table_name if detached?
135
+ end
136
+
119
137
  def debounce_time
120
138
  options[:debounce_time]
121
139
  end
@@ -149,7 +167,8 @@ module Logidze
149
167
  end
150
168
 
151
169
  def logidze_logger_parameters
152
- format_pgsql_args(limit, timestamp_column, filtered_columns, include_columns, debounce_time)
170
+ format_pgsql_args(limit, timestamp_column, filtered_columns, include_columns, debounce_time,
171
+ detached_loggable_type, quoted_log_data_table_name)
153
172
  end
154
173
 
155
174
  def logidze_snapshot_parameters
@@ -1,6 +1,6 @@
1
1
  class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- <%- unless update? || only_trigger? -%>
3
+ <%- unless update? || only_trigger? || detached? -%>
4
4
  add_column :<%= table_name %>, :log_data, :jsonb
5
5
  <%- end -%>
6
6
 
@@ -58,13 +58,23 @@ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::M
58
58
  end
59
59
  <%- end -%>
60
60
  <%- if backfill? -%>
61
-
62
61
  reversible do |dir|
63
62
  dir.up do
64
- execute <<~SQL
65
- UPDATE "<%= full_table_name %>" as t
66
- SET log_data = logidze_snapshot(<%= logidze_snapshot_parameters %>);
67
- SQL
63
+ <%- if detached? %>
64
+ execute <<~SQL
65
+ INSERT INTO <%= quoted_log_data_table_name %> (log_data, loggable_type, loggable_id)
66
+ SELECT logidze_snapshot(<%= logidze_snapshot_parameters %>), <%= detached_loggable_type %>, t.id
67
+ FROM "<%= full_table_name %>" t
68
+ ON CONFLICT (loggable_type, loggable_id)
69
+ DO UPDATE
70
+ SET log_data = EXCLUDED.log_data;
71
+ SQL
72
+ <%- else %>
73
+ execute <<~SQL
74
+ UPDATE "<%= full_table_name %>" as t
75
+ SET log_data = logidze_snapshot(<%= logidze_snapshot_parameters %>);
76
+ SQL
77
+ <%- end %>
68
78
  end
69
79
  end
70
80
  <%- end -%>
@@ -2,5 +2,5 @@ CREATE TRIGGER <%= %Q("logidze_on_#{full_table_name}") %>
2
2
  BEFORE UPDATE OR INSERT ON <%= %Q("#{full_table_name}") %> FOR EACH ROW
3
3
  WHEN (coalesce(current_setting('logidze.disabled', true), '') <> 'on')
4
4
  -- Parameters: history_size_limit (integer), timestamp_column (text), filtered_columns (text[]),
5
- -- include_columns (boolean), debounce_time_ms (integer)
5
+ -- include_columns (boolean), debounce_time_ms (integer), detached_loggable_type(text), log_data_table_name(text)
6
6
  EXECUTE PROCEDURE logidze_logger(<%= logidze_logger_parameters %>);
@@ -2,5 +2,5 @@ CREATE TRIGGER <%= %Q("logidze_on_#{full_table_name}") %>
2
2
  AFTER UPDATE OR INSERT ON <%= %Q("#{full_table_name}") %> FOR EACH ROW
3
3
  WHEN (coalesce(current_setting('logidze.disabled', true), '') <> 'on' AND pg_trigger_depth() < 1)
4
4
  -- Parameters: history_size_limit (integer), timestamp_column (text), filtered_columns (text[]),
5
- -- include_columns (boolean), debounce_time_ms (integer)
5
+ -- include_columns (boolean), debounce_time_ms (integer), detached_loggable_type(text), log_data_table_name(text)
6
6
  EXECUTE PROCEDURE logidze_logger_after(<%= logidze_logger_parameters %>);
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logidze
4
+ module Detachable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_one :logidze_data, as: :loggable, class_name: "::Logidze::LogidzeData", dependent: :destroy, autosave: true
9
+
10
+ delegate :log_data, to: :logidze_data, allow_nil: true
11
+ end
12
+
13
+ module ClassMethods # :nodoc:
14
+ # Nullify log_data column for a association
15
+ #
16
+ # @return [Integer] number of deleted +Logidze::LogidzeData+ records
17
+ def reset_log_data
18
+ Logidze::LogidzeData.where(loggable_id: ids, loggable_type: name).delete_all
19
+ end
20
+
21
+ # Initialize log_data with the current state if it's null
22
+ def create_logidze_snapshot(timestamp: nil, only: nil, except: nil, sql_filter: nil)
23
+ ActiveRecord::Base.connection.execute <<~SQL.squish
24
+ INSERT INTO #{Logidze::LogidzeData.quoted_table_name} (log_data, loggable_type, loggable_id)
25
+ SELECT logidze_snapshot(
26
+ to_jsonb(#{quoted_table_name}),
27
+ #{snapshot_query_args(timestamp: timestamp, only: only, except: except)}
28
+ ),
29
+ '#{name}',
30
+ #{quoted_table_name}.id
31
+ FROM #{quoted_table_name}
32
+ #{sql_filter}
33
+ ON CONFLICT (loggable_type, loggable_id)
34
+ DO UPDATE
35
+ SET log_data = EXCLUDED.log_data;
36
+ SQL
37
+ end
38
+
39
+ private
40
+
41
+ def initial_scope
42
+ includes(:logidze_data)
43
+ end
44
+ end
45
+
46
+ # Loads log_data field from the database, stores to the attributes hash and returns it
47
+ def reload_log_data
48
+ reload_logidze_data.log_data
49
+ end
50
+
51
+ # Nullify log_data column for a single record
52
+ def reset_log_data
53
+ tap { logidze_data.delete }.reload_logidze_data
54
+ end
55
+
56
+ # Initialize log_data with the current state if it's null for a single record
57
+ def create_logidze_snapshot!(timestamp: nil, only: nil, except: nil)
58
+ id_filter = "WHERE #{self.class.quoted_table_name}.id = #{id}"
59
+ self.class.create_logidze_snapshot(timestamp: timestamp, only: only, except: except, sql_filter: id_filter)
60
+
61
+ reload_log_data
62
+ end
63
+
64
+ def raw_log_data
65
+ logidze_data&.read_attribute_before_type_cast(:log_data)
66
+ end
67
+
68
+ def log_data=(v)
69
+ logidze_data&.assign_attributes(log_data: v) || build_logidze_data(log_data: v)
70
+ v # rubocop:disable Lint/Void
71
+ end
72
+
73
+ def dup
74
+ super.tap { _1.logidze_data = logidze_data.dup }
75
+ end
76
+
77
+ protected
78
+
79
+ # rubocop: disable Lint/ShadowedArgument
80
+ def build_dup(log_entry, requested_ts = log_entry.time, object_at: nil)
81
+ object_at = dup
82
+ object_at.logidze_data = logidze_data.dup
83
+ super
84
+ end
85
+ # rubocop: enable Lint/ShadowedArgument
86
+ end
87
+ end
@@ -9,10 +9,14 @@ module Logidze
9
9
 
10
10
  module ClassMethods # :nodoc:
11
11
  # Include methods to work with history.
12
- #
13
- def has_logidze(ignore_log_data: Logidze.ignore_log_data_by_default)
12
+ def has_logidze(ignore_log_data: Logidze.ignore_log_data_by_default, detached: Logidze.detached_log_placement?)
14
13
  include Logidze::IgnoreLogData
15
14
  include Logidze::Model
15
+ if detached && !Logidze.inline_log_placement?
16
+ # Adds needed behavior to models and alters behavior of some methods from +Logidze::Model+ to
17
+ # work with detached table for `log_data`
18
+ include Logidze::Detachable
19
+ end
16
20
 
17
21
  @ignore_log_data = ignore_log_data
18
22
 
@@ -36,6 +36,10 @@ module Logidze
36
36
  def changed_in_place?(raw_old_value, new_value)
37
37
  cast_value(raw_old_value) != new_value
38
38
  end
39
+
40
+ def mutable?
41
+ true
42
+ end
39
43
  end
40
44
  end
41
45
  end
data/lib/logidze/model.rb CHANGED
@@ -18,12 +18,12 @@ module Logidze
18
18
  module ClassMethods # :nodoc:
19
19
  # Return records reverted to specified time
20
20
  def at(time: nil, version: nil)
21
- all.to_a.filter_map { |record| record.at(time: time, version: version) }
21
+ initial_scope.all.to_a.filter_map { |record| record.at(time: time, version: version) }
22
22
  end
23
23
 
24
24
  # Return changes made to records since specified time
25
25
  def diff_from(time: nil, version: nil)
26
- all.map { |record| record.diff_from(time: time, version: version) }
26
+ initial_scope.all.map { |record| record.diff_from(time: time, version: version) }
27
27
  end
28
28
 
29
29
  # Alias for Logidze.without_logging
@@ -44,6 +44,26 @@ module Logidze
44
44
 
45
45
  # Initialize log_data with the current state if it's null
46
46
  def create_logidze_snapshot(timestamp: nil, only: nil, except: nil)
47
+ without_logging do
48
+ where(log_data: nil).update_all(
49
+ <<~SQL.squish
50
+ log_data = logidze_snapshot(
51
+ to_jsonb(#{quoted_table_name}),
52
+ #{snapshot_query_args(timestamp: timestamp, only: only, except: except)}
53
+ )
54
+ SQL
55
+ )
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def initial_scope
62
+ all
63
+ end
64
+
65
+ # Computes args for creating initializing snapshots in +.create_logidze_snapshot+ and +#create_logidze_snapshot!+
66
+ def snapshot_query_args(timestamp: nil, only: nil, except: nil)
47
67
  args = ["'null'"]
48
68
 
49
69
  args[0] = "'#{timestamp}'" if timestamp
@@ -55,13 +75,7 @@ module Logidze
55
75
  args[2] = only ? "true" : "false"
56
76
  end
57
77
 
58
- without_logging do
59
- where(log_data: nil).update_all(
60
- <<~SQL
61
- log_data = logidze_snapshot(to_jsonb(#{quoted_table_name}), #{args.join(", ")})
62
- SQL
63
- )
64
- end
78
+ args.join(", ")
65
79
  end
66
80
  end
67
81
 
@@ -244,6 +258,10 @@ module Logidze
244
258
  reload_log_data
245
259
  end
246
260
 
261
+ def raw_log_data
262
+ read_attribute_before_type_cast(:log_data)
263
+ end
264
+
247
265
  protected
248
266
 
249
267
  def apply_diff(version, diff)
@@ -261,14 +279,16 @@ module Logidze
261
279
  write_attribute column, deserialize_value(column, value)
262
280
  end
263
281
 
264
- def build_dup(log_entry, requested_ts = log_entry.time)
265
- object_at = dup
282
+ # rubocop: disable Lint/ShadowedArgument
283
+ def build_dup(log_entry, requested_ts = log_entry.time, object_at: nil)
284
+ object_at ||= dup
266
285
  object_at.apply_diff(log_entry.version, log_data.changes_to(version: log_entry.version))
267
286
  object_at.id = id
268
287
  object_at.logidze_requested_ts = requested_ts
269
288
 
270
289
  object_at
271
290
  end
291
+ # rubocop: enable Lint/ShadowedArgument
272
292
 
273
293
  def deserialize_value(column, value)
274
294
  @attributes[column].type.deserialize(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logidze
4
- VERSION = "1.3.1"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/logidze.rb CHANGED
@@ -11,6 +11,7 @@ module Logidze
11
11
  require "logidze/ignore_log_data"
12
12
  require "logidze/has_logidze"
13
13
  require "logidze/meta"
14
+ require "logidze/detachable"
14
15
 
15
16
  extend Logidze::Meta
16
17
 
@@ -29,6 +30,12 @@ module Logidze
29
30
  attr_accessor :sort_triggers_by_name
30
31
  # Determines what Logidze should do when upgrade is needed (:raise | :warn | :ignore)
31
32
  attr_reader :on_pending_upgrade
33
+ # Determines where to store +log_data+:
34
+ # - +:inline+ - force Logidze to store it in the origin table in the +log_data+ column
35
+ # - +:detached+ - force Logidze to store it in the +logidze_data+ table in the +log_data+ column
36
+ #
37
+ # By default we do not set +log_data_placement+ value and rely on `has_logidze` macros
38
+ attr_accessor :log_data_placement
32
39
 
33
40
  # Temporary disable DB triggers.
34
41
  #
@@ -53,6 +60,14 @@ module Logidze
53
60
  @on_pending_upgrade = mode
54
61
  end
55
62
 
63
+ def detached_log_placement?
64
+ @log_data_placement == :detached
65
+ end
66
+
67
+ def inline_log_placement?
68
+ @log_data_placement == :inline
69
+ end
70
+
56
71
  private
57
72
 
58
73
  def with_logidze_setting(name, value)
@@ -71,4 +86,5 @@ module Logidze
71
86
  self.return_self_if_log_data_is_empty = true
72
87
  self.on_pending_upgrade = :ignore
73
88
  self.sort_triggers_by_name = false
89
+ self.log_data_placement = nil
74
90
  end
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: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-23 00:00:00.000000000 Z
11
+ date: 2025-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0.8'
139
+ - !ruby/object:Gem::Dependency
140
+ name: concurrent-ruby
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '='
144
+ - !ruby/object:Gem::Version
145
+ version: 1.3.4
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '='
151
+ - !ruby/object:Gem::Version
152
+ version: 1.3.4
139
153
  description: PostgreSQL JSONB-based model changes tracking
140
154
  email:
141
155
  - dementiev.vm@gmail.com
@@ -160,12 +174,16 @@ files:
160
174
  - lib/generators/logidze/install/templates/hstore.rb.erb
161
175
  - lib/generators/logidze/install/templates/migration.rb.erb
162
176
  - lib/generators/logidze/install/templates/migration_fx.rb.erb
177
+ - lib/generators/logidze/migration/USAGE
178
+ - lib/generators/logidze/migration/logs_generator.rb
179
+ - lib/generators/logidze/migration/templates/migration.rb.erb
163
180
  - lib/generators/logidze/model/USAGE
164
181
  - lib/generators/logidze/model/model_generator.rb
165
182
  - lib/generators/logidze/model/templates/migration.rb.erb
166
183
  - lib/generators/logidze/model/triggers/logidze.sql
167
184
  - lib/generators/logidze/model/triggers/logidze_after.sql
168
185
  - lib/logidze.rb
186
+ - lib/logidze/detachable.rb
169
187
  - lib/logidze/engine.rb
170
188
  - lib/logidze/has_logidze.rb
171
189
  - lib/logidze/history.rb
@@ -188,7 +206,7 @@ metadata:
188
206
  documentation_uri: http://github.com/palkan/logidze
189
207
  homepage_uri: http://github.com/palkan/logidze
190
208
  source_code_uri: http://github.com/palkan/logidze
191
- post_install_message:
209
+ post_install_message:
192
210
  rdoc_options: []
193
211
  require_paths:
194
212
  - lib
@@ -204,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
222
  version: '0'
205
223
  requirements: []
206
224
  rubygems_version: 3.4.19
207
- signing_key:
225
+ signing_key:
208
226
  specification_version: 4
209
227
  summary: PostgreSQL JSONB-based model changes tracking
210
228
  test_files: []