logidze 0.11.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -4
  3. data/LICENSE.txt +1 -1
  4. data/README.md +305 -102
  5. data/lib/generators/logidze/fx_helper.rb +17 -0
  6. data/lib/generators/logidze/inject_sql.rb +18 -0
  7. data/lib/generators/logidze/install/USAGE +6 -1
  8. data/lib/generators/logidze/install/functions/logidze_capture_exception.sql +23 -0
  9. data/lib/generators/logidze/install/functions/logidze_compact_history.sql +38 -0
  10. data/lib/generators/logidze/install/functions/logidze_filter_keys.sql +27 -0
  11. data/lib/generators/logidze/install/functions/logidze_logger.sql +203 -0
  12. data/lib/generators/logidze/install/functions/logidze_snapshot.sql +33 -0
  13. data/lib/generators/logidze/install/functions/logidze_version.sql +21 -0
  14. data/lib/generators/logidze/install/install_generator.rb +43 -1
  15. data/lib/generators/logidze/install/templates/hstore.rb.erb +1 -1
  16. data/lib/generators/logidze/install/templates/migration.rb.erb +19 -232
  17. data/lib/generators/logidze/install/templates/migration_fx.rb.erb +41 -0
  18. data/lib/generators/logidze/model/model_generator.rb +53 -13
  19. data/lib/generators/logidze/model/templates/migration.rb.erb +57 -36
  20. data/lib/generators/logidze/model/triggers/logidze.sql +6 -0
  21. data/lib/logidze.rb +37 -14
  22. data/lib/logidze/engine.rb +9 -0
  23. data/lib/logidze/has_logidze.rb +1 -1
  24. data/lib/logidze/history.rb +2 -11
  25. data/lib/logidze/ignore_log_data.rb +1 -3
  26. data/lib/logidze/meta.rb +43 -16
  27. data/lib/logidze/model.rb +51 -44
  28. data/lib/logidze/utils/check_pending.rb +57 -0
  29. data/lib/logidze/utils/function_definitions.rb +49 -0
  30. data/lib/logidze/utils/pending_migration_error.rb +25 -0
  31. data/lib/logidze/version.rb +1 -1
  32. metadata +69 -77
  33. data/.gitattributes +0 -3
  34. data/.github/ISSUE_TEMPLATE.md +0 -20
  35. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  36. data/.gitignore +0 -40
  37. data/.rubocop.yml +0 -55
  38. data/.travis.yml +0 -42
  39. data/Gemfile +0 -15
  40. data/Rakefile +0 -28
  41. data/assets/pg_log_data_chart.png +0 -0
  42. data/bench/performance/README.md +0 -109
  43. data/bench/performance/diff_bench.rb +0 -38
  44. data/bench/performance/insert_bench.rb +0 -22
  45. data/bench/performance/memory_profile.rb +0 -56
  46. data/bench/performance/setup.rb +0 -315
  47. data/bench/performance/update_bench.rb +0 -38
  48. data/bench/triggers/Makefile +0 -56
  49. data/bench/triggers/Readme.md +0 -58
  50. data/bench/triggers/bench.sql +0 -6
  51. data/bench/triggers/hstore_trigger_setup.sql +0 -38
  52. data/bench/triggers/jsonb_minus_2_setup.sql +0 -47
  53. data/bench/triggers/jsonb_minus_setup.sql +0 -49
  54. data/bench/triggers/keys2_trigger_setup.sql +0 -44
  55. data/bench/triggers/keys_trigger_setup.sql +0 -50
  56. data/bin/console +0 -8
  57. data/bin/setup +0 -9
  58. data/gemfiles/rails42.gemfile +0 -6
  59. data/gemfiles/rails5.gemfile +0 -6
  60. data/gemfiles/rails52.gemfile +0 -6
  61. data/gemfiles/rails6.gemfile +0 -6
  62. data/gemfiles/railsmaster.gemfile +0 -7
  63. data/lib/logidze/ignore_log_data/association.rb +0 -11
  64. data/lib/logidze/ignore_log_data/ignored_columns.rb +0 -46
  65. data/lib/logidze/migration.rb +0 -20
  66. data/logidze.gemspec +0 -41
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logidze
4
+ module Generators
5
+ # Adds --fx option and provide #fx? method
6
+ module FxHelper
7
+ def self.included(base)
8
+ base.class_option :fx, type: :boolean, optional: true,
9
+ desc: "Define whether to use fx gem functionality"
10
+ end
11
+
12
+ def fx?
13
+ options[:fx] || (options[:fx] != false && defined?(::Fx::SchemaDumper))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logidze
4
+ module Generators
5
+ module InjectSql
6
+ def inject_sql(source, indent: 4)
7
+ source = ::File.expand_path(find_in_source_paths(source.to_s))
8
+
9
+ indent(
10
+ ERB.new(::File.binread(source)).tap do |erb|
11
+ erb.filename = source
12
+ end.result(instance_eval("binding")), # rubocop:disable Style/EvalWithLocation
13
+ indent
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,7 +1,12 @@
1
1
  Description:
2
2
  Generates the necessary files to get you up and running with Logidze gem
3
-
3
+
4
4
  Examples:
5
5
  rails generate logidze:install
6
6
 
7
7
  This will generate the core migration file with trigger function defined.
8
+
9
+ rails generate logidze:install --fx
10
+
11
+ This will generate schema.rb compatible migration with `create_function` definitions and separate SQL files.
12
+ The fx gem must be installed.
@@ -0,0 +1,23 @@
1
+ CREATE OR REPLACE FUNCTION logidze_capture_exception(error_data jsonb) RETURNS boolean AS $body$
2
+ -- version: 1
3
+ BEGIN
4
+ -- Feel free to change this function to change Logidze behavior on exception.
5
+ --
6
+ -- Return `false` to raise exception or `true` to commit record changes.
7
+ --
8
+ -- `error_data` contains:
9
+ -- - returned_sqlstate
10
+ -- - message_text
11
+ -- - pg_exception_detail
12
+ -- - pg_exception_hint
13
+ -- - pg_exception_context
14
+ -- - schema_name
15
+ -- - table_name
16
+ -- Learn more about available keys:
17
+ -- https://www.postgresql.org/docs/9.6/plpgsql-control-structures.html#PLPGSQL-EXCEPTION-DIAGNOSTICS-VALUES
18
+ --
19
+
20
+ return false;
21
+ END;
22
+ $body$
23
+ LANGUAGE plpgsql;
@@ -0,0 +1,38 @@
1
+ CREATE OR REPLACE FUNCTION logidze_compact_history(log_data jsonb, cutoff integer DEFAULT 1) RETURNS jsonb AS $body$
2
+ -- version: 1
3
+ DECLARE
4
+ merged jsonb;
5
+ BEGIN
6
+ LOOP
7
+ merged := jsonb_build_object(
8
+ 'ts',
9
+ log_data#>'{h,1,ts}',
10
+ 'v',
11
+ log_data#>'{h,1,v}',
12
+ 'c',
13
+ (log_data#>'{h,0,c}') || (log_data#>'{h,1,c}')
14
+ );
15
+
16
+ IF (log_data#>'{h,1}' ? 'm') THEN
17
+ merged := jsonb_set(merged, ARRAY['m'], log_data#>'{h,1,m}');
18
+ END IF;
19
+
20
+ log_data := jsonb_set(
21
+ log_data,
22
+ '{h}',
23
+ jsonb_set(
24
+ log_data->'h',
25
+ '{1}',
26
+ merged
27
+ ) - 0
28
+ );
29
+
30
+ cutoff := cutoff - 1;
31
+
32
+ EXIT WHEN cutoff <= 0;
33
+ END LOOP;
34
+
35
+ return log_data;
36
+ END;
37
+ $body$
38
+ LANGUAGE plpgsql;
@@ -0,0 +1,27 @@
1
+ CREATE OR REPLACE FUNCTION logidze_filter_keys(obj jsonb, keys text[], include_columns boolean DEFAULT false) RETURNS jsonb AS $body$
2
+ -- version: 1
3
+ DECLARE
4
+ res jsonb;
5
+ key text;
6
+ BEGIN
7
+ res := '{}';
8
+
9
+ IF include_columns THEN
10
+ FOREACH key IN ARRAY keys
11
+ LOOP
12
+ IF obj ? key THEN
13
+ res = jsonb_insert(res, ARRAY[key], obj->key);
14
+ END IF;
15
+ END LOOP;
16
+ ELSE
17
+ res = obj;
18
+ FOREACH key IN ARRAY keys
19
+ LOOP
20
+ res = res - key;
21
+ END LOOP;
22
+ END IF;
23
+
24
+ RETURN res;
25
+ END;
26
+ $body$
27
+ LANGUAGE plpgsql;
@@ -0,0 +1,203 @@
1
+ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
2
+ -- version: 2
3
+ DECLARE
4
+ changes jsonb;
5
+ version jsonb;
6
+ snapshot jsonb;
7
+ new_v integer;
8
+ size integer;
9
+ history_limit integer;
10
+ debounce_time integer;
11
+ current_version integer;
12
+ k text;
13
+ iterator integer;
14
+ item record;
15
+ columns text[];
16
+ include_columns boolean;
17
+ ts timestamp with time zone;
18
+ ts_column text;
19
+ err_sqlstate text;
20
+ err_message text;
21
+ err_detail text;
22
+ err_hint text;
23
+ err_context text;
24
+ err_table_name text;
25
+ err_schema_name text;
26
+ err_jsonb jsonb;
27
+ err_captured boolean;
28
+ BEGIN
29
+ ts_column := NULLIF(TG_ARGV[1], 'null');
30
+ columns := NULLIF(TG_ARGV[2], 'null');
31
+ include_columns := NULLIF(TG_ARGV[3], 'null');
32
+
33
+ IF TG_OP = 'INSERT' THEN
34
+ IF columns IS NOT NULL THEN
35
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column, columns, include_columns);
36
+ ELSE
37
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column);
38
+ END IF;
39
+
40
+ IF snapshot#>>'{h, -1, c}' != '{}' THEN
41
+ NEW.log_data := snapshot;
42
+ END IF;
43
+
44
+ ELSIF TG_OP = 'UPDATE' THEN
45
+
46
+ IF OLD.log_data is NULL OR OLD.log_data = '{}'::jsonb THEN
47
+ IF columns IS NOT NULL THEN
48
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column, columns, include_columns);
49
+ ELSE
50
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column);
51
+ END IF;
52
+
53
+ IF snapshot#>>'{h, -1, c}' != '{}' THEN
54
+ NEW.log_data := snapshot;
55
+ END IF;
56
+ RETURN NEW;
57
+ END IF;
58
+
59
+ history_limit := NULLIF(TG_ARGV[0], 'null');
60
+ debounce_time := NULLIF(TG_ARGV[4], 'null');
61
+
62
+ current_version := (NEW.log_data->>'v')::int;
63
+
64
+ IF ts_column IS NULL THEN
65
+ ts := statement_timestamp();
66
+ ELSE
67
+ ts := (to_jsonb(NEW.*)->>ts_column)::timestamp with time zone;
68
+ IF ts IS NULL OR ts = (to_jsonb(OLD.*)->>ts_column)::timestamp with time zone THEN
69
+ ts := statement_timestamp();
70
+ END IF;
71
+ END IF;
72
+
73
+ IF NEW = OLD THEN
74
+ RETURN NEW;
75
+ END IF;
76
+
77
+ IF current_version < (NEW.log_data#>>'{h,-1,v}')::int THEN
78
+ iterator := 0;
79
+ FOR item in SELECT * FROM jsonb_array_elements(NEW.log_data->'h')
80
+ LOOP
81
+ IF (item.value->>'v')::int > current_version THEN
82
+ NEW.log_data := jsonb_set(
83
+ NEW.log_data,
84
+ '{h}',
85
+ (NEW.log_data->'h') - iterator
86
+ );
87
+ END IF;
88
+ iterator := iterator + 1;
89
+ END LOOP;
90
+ END IF;
91
+
92
+ changes := '{}';
93
+
94
+ IF (coalesce(current_setting('logidze.full_snapshot', true), '') = 'on') THEN
95
+ BEGIN
96
+ changes = hstore_to_jsonb_loose(hstore(NEW.*));
97
+ EXCEPTION
98
+ WHEN NUMERIC_VALUE_OUT_OF_RANGE THEN
99
+ changes = row_to_json(NEW.*)::jsonb;
100
+ FOR k IN (SELECT key FROM jsonb_each(changes))
101
+ LOOP
102
+ IF jsonb_typeof(changes->k) = 'object' THEN
103
+ changes = jsonb_set(changes, ARRAY[k], to_jsonb(changes->>k));
104
+ END IF;
105
+ END LOOP;
106
+ END;
107
+ ELSE
108
+ BEGIN
109
+ changes = hstore_to_jsonb_loose(
110
+ hstore(NEW.*) - hstore(OLD.*)
111
+ );
112
+ EXCEPTION
113
+ WHEN NUMERIC_VALUE_OUT_OF_RANGE THEN
114
+ changes = (SELECT
115
+ COALESCE(json_object_agg(key, value), '{}')::jsonb
116
+ FROM
117
+ jsonb_each(row_to_json(NEW.*)::jsonb)
118
+ WHERE NOT jsonb_build_object(key, value) <@ row_to_json(OLD.*)::jsonb);
119
+ FOR k IN (SELECT key FROM jsonb_each(changes))
120
+ LOOP
121
+ IF jsonb_typeof(changes->k) = 'object' THEN
122
+ changes = jsonb_set(changes, ARRAY[k], to_jsonb(changes->>k));
123
+ END IF;
124
+ END LOOP;
125
+ END;
126
+ END IF;
127
+
128
+ changes = changes - 'log_data';
129
+
130
+ IF columns IS NOT NULL THEN
131
+ changes = logidze_filter_keys(changes, columns, include_columns);
132
+ END IF;
133
+
134
+ IF changes = '{}' THEN
135
+ RETURN NEW;
136
+ END IF;
137
+
138
+ new_v := (NEW.log_data#>>'{h,-1,v}')::int + 1;
139
+
140
+ size := jsonb_array_length(NEW.log_data->'h');
141
+ version := logidze_version(new_v, changes, ts);
142
+
143
+ IF (
144
+ debounce_time IS NOT NULL AND
145
+ (version->>'ts')::bigint - (NEW.log_data#>'{h,-1,ts}')::text::bigint <= debounce_time
146
+ ) THEN
147
+ -- merge new version with the previous one
148
+ new_v := (NEW.log_data#>>'{h,-1,v}')::int;
149
+ version := logidze_version(new_v, (NEW.log_data#>'{h,-1,c}')::jsonb || changes, ts);
150
+ -- remove the previous version from log
151
+ NEW.log_data := jsonb_set(
152
+ NEW.log_data,
153
+ '{h}',
154
+ (NEW.log_data->'h') - (size - 1)
155
+ );
156
+ END IF;
157
+
158
+ NEW.log_data := jsonb_set(
159
+ NEW.log_data,
160
+ ARRAY['h', size::text],
161
+ version,
162
+ true
163
+ );
164
+
165
+ NEW.log_data := jsonb_set(
166
+ NEW.log_data,
167
+ '{v}',
168
+ to_jsonb(new_v)
169
+ );
170
+
171
+ IF history_limit IS NOT NULL AND history_limit <= size THEN
172
+ NEW.log_data := logidze_compact_history(NEW.log_data, size - history_limit + 1);
173
+ END IF;
174
+ END IF;
175
+
176
+ return NEW;
177
+ EXCEPTION
178
+ WHEN OTHERS THEN
179
+ GET STACKED DIAGNOSTICS err_sqlstate = RETURNED_SQLSTATE,
180
+ err_message = MESSAGE_TEXT,
181
+ err_detail = PG_EXCEPTION_DETAIL,
182
+ err_hint = PG_EXCEPTION_HINT,
183
+ err_context = PG_EXCEPTION_CONTEXT,
184
+ err_schema_name = SCHEMA_NAME,
185
+ err_table_name = TABLE_NAME;
186
+ err_jsonb := jsonb_build_object(
187
+ 'returned_sqlstate', err_sqlstate,
188
+ 'message_text', err_message,
189
+ 'pg_exception_detail', err_detail,
190
+ 'pg_exception_hint', err_hint,
191
+ 'pg_exception_context', err_context,
192
+ 'schema_name', err_schema_name,
193
+ 'table_name', err_table_name
194
+ );
195
+ err_captured = logidze_capture_exception(err_jsonb);
196
+ IF err_captured THEN
197
+ return NEW;
198
+ ELSE
199
+ RAISE;
200
+ END IF;
201
+ END;
202
+ $body$
203
+ LANGUAGE plpgsql;
@@ -0,0 +1,33 @@
1
+ CREATE OR REPLACE FUNCTION logidze_snapshot(item jsonb, ts_column text DEFAULT NULL, columns text[] DEFAULT NULL, include_columns boolean DEFAULT false) RETURNS jsonb AS $body$
2
+ -- version: 3
3
+ DECLARE
4
+ ts timestamp with time zone;
5
+ k text;
6
+ BEGIN
7
+ item = item - 'log_data';
8
+ IF ts_column IS NULL THEN
9
+ ts := statement_timestamp();
10
+ ELSE
11
+ ts := coalesce((item->>ts_column)::timestamp with time zone, statement_timestamp());
12
+ END IF;
13
+
14
+ IF columns IS NOT NULL THEN
15
+ item := logidze_filter_keys(item, columns, include_columns);
16
+ END IF;
17
+
18
+ FOR k IN (SELECT key FROM jsonb_each(item))
19
+ LOOP
20
+ IF jsonb_typeof(item->k) = 'object' THEN
21
+ item := jsonb_set(item, ARRAY[k], to_jsonb(item->>k));
22
+ END IF;
23
+ END LOOP;
24
+
25
+ return json_build_object(
26
+ 'v', 1,
27
+ 'h', jsonb_build_array(
28
+ logidze_version(1, item, ts)
29
+ )
30
+ );
31
+ END;
32
+ $body$
33
+ LANGUAGE plpgsql;
@@ -0,0 +1,21 @@
1
+ CREATE OR REPLACE FUNCTION logidze_version(v bigint, data jsonb, ts timestamp with time zone) RETURNS jsonb AS $body$
2
+ -- version: 2
3
+ DECLARE
4
+ buf jsonb;
5
+ BEGIN
6
+ data = data - 'log_data';
7
+ buf := jsonb_build_object(
8
+ 'ts',
9
+ (extract(epoch from ts) * 1000)::bigint,
10
+ 'v',
11
+ v,
12
+ 'c',
13
+ data
14
+ );
15
+ IF coalesce(current_setting('logidze.meta', true), '') <> '' THEN
16
+ buf := jsonb_insert(buf, '{m}', current_setting('logidze.meta')::jsonb);
17
+ END IF;
18
+ RETURN buf;
19
+ END;
20
+ $body$
21
+ LANGUAGE plpgsql;
@@ -2,19 +2,28 @@
2
2
 
3
3
  require "rails/generators"
4
4
  require "rails/generators/active_record"
5
+ require "logidze/utils/function_definitions"
6
+ require_relative "../inject_sql"
7
+ require_relative "../fx_helper"
8
+
9
+ using RubyNext
5
10
 
6
11
  module Logidze
7
12
  module Generators
8
13
  class InstallGenerator < ::Rails::Generators::Base # :nodoc:
9
14
  include Rails::Generators::Migration
15
+ include InjectSql
16
+ include FxHelper
10
17
 
11
18
  source_root File.expand_path("templates", __dir__)
19
+ source_paths << File.expand_path("functions", __dir__)
12
20
 
13
21
  class_option :update, type: :boolean, optional: true,
14
22
  desc: "Define whether this is an update migration"
15
23
 
16
24
  def generate_migration
17
- migration_template "migration.rb.erb", "db/migrate/#{migration_name}.rb"
25
+ migration_template = fx? ? "migration_fx.rb.erb" : "migration.rb.erb"
26
+ migration_template migration_template, "db/migrate/#{migration_name}.rb"
18
27
  end
19
28
 
20
29
  def generate_hstore_migration
@@ -23,6 +32,16 @@ module Logidze
23
32
  migration_template "hstore.rb.erb", "db/migrate/enable_hstore.rb"
24
33
  end
25
34
 
35
+ def generate_fx_functions
36
+ return unless fx?
37
+
38
+ function_definitions.each do |fdef|
39
+ next if fdef.version == previous_version_for(fdef.name)
40
+
41
+ template "#{fdef.name}.sql", "db/functions/#{fdef.name}_v#{fdef.version.to_s.rjust(2, "0")}.sql"
42
+ end
43
+ end
44
+
26
45
  no_tasks do
27
46
  def migration_name
28
47
  if update?
@@ -39,6 +58,29 @@ module Logidze
39
58
  def update?
40
59
  options[:update]
41
60
  end
61
+
62
+ def previous_version_for(name)
63
+ all_functions.filter_map { |path| Regexp.last_match[1].to_i if path =~ %r{#{name}_v(\d+).sql} }.max
64
+ end
65
+
66
+ def all_functions
67
+ @all_functions ||=
68
+ begin
69
+ res = nil
70
+ in_root do
71
+ res = if File.directory?("db/functions")
72
+ Dir.entries("db/functions")
73
+ else
74
+ []
75
+ end
76
+ end
77
+ res
78
+ end
79
+ end
80
+
81
+ def function_definitions
82
+ @function_definitions ||= Logidze::Utils::FunctionDefinitions.from_fs
83
+ end
42
84
  end
43
85
 
44
86
  def self.next_migration_number(dir)