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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +21 -0
- data/lib/generators/logidze/install/functions/logidze_logger.sql +80 -8
- data/lib/generators/logidze/install/functions/logidze_logger_after.sql +1 -1
- data/lib/generators/logidze/install/install_generator.rb +11 -1
- data/lib/generators/logidze/migration/USAGE +7 -0
- data/lib/generators/logidze/migration/logs_generator.rb +24 -0
- data/lib/generators/logidze/migration/templates/migration.rb.erb +10 -0
- data/lib/generators/logidze/model/USAGE +4 -0
- data/lib/generators/logidze/model/model_generator.rb +21 -2
- data/lib/generators/logidze/model/templates/migration.rb.erb +16 -6
- data/lib/generators/logidze/model/triggers/logidze.sql +1 -1
- data/lib/generators/logidze/model/triggers/logidze_after.sql +1 -1
- data/lib/logidze/detachable.rb +87 -0
- data/lib/logidze/has_logidze.rb +6 -2
- data/lib/logidze/history/type.rb +4 -0
- data/lib/logidze/model.rb +31 -11
- data/lib/logidze/version.rb +1 -1
- data/lib/logidze.rb +16 -0
- metadata +23 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cc10c417605b0f92991c201aeb958a44bc6130b64fd3b278780fb780818ccb3
|
4
|
+
data.tar.gz: 3c06efc69a95294bff75de4c8d91c1528c0cc67f5c0b5753097599a4c9e8fe5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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,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
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
data/lib/logidze/has_logidze.rb
CHANGED
@@ -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
|
|
data/lib/logidze/history/type.rb
CHANGED
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
|
-
|
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
|
-
|
265
|
-
|
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)
|
data/lib/logidze/version.rb
CHANGED
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.
|
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:
|
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: []
|