logidze 0.0.1 → 0.1.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/.gitignore +40 -9
- data/.rubocop.yml +3 -1
- data/.travis.yml +36 -3
- data/CHANGELOG.md +0 -0
- data/Gemfile +0 -1
- data/README.md +144 -9
- data/Rakefile +20 -1
- data/bench/Makefile +37 -9
- data/bench/Readme.md +58 -0
- data/bench/jsonb_minus_2_setup.sql +47 -0
- data/bench/jsonb_minus_setup.sql +49 -0
- data/bench/keys2_trigger_setup.sql +44 -0
- data/bin/console +7 -0
- data/bin/setup +9 -0
- data/circle.yml +17 -0
- data/gemfiles/rails42.gemfile +5 -0
- data/gemfiles/rails5.gemfile +5 -0
- data/lib/generators/logidze/install/USAGE +7 -0
- data/lib/generators/logidze/install/install_generator.rb +25 -0
- data/lib/generators/logidze/install/templates/hstore.rb.erb +5 -0
- data/lib/generators/logidze/install/templates/migration.rb.erb +131 -0
- data/lib/generators/logidze/model/USAGE +8 -0
- data/lib/generators/logidze/model/model_generator.rb +43 -0
- data/lib/generators/logidze/model/templates/migration.rb.erb +18 -0
- data/lib/logidze.rb +21 -1
- data/lib/logidze/engine.rb +12 -0
- data/lib/logidze/has_logidze.rb +18 -0
- data/lib/logidze/history.rb +156 -0
- data/lib/logidze/model.rb +128 -0
- data/lib/logidze/version.rb +2 -1
- data/logidze.gemspec +7 -2
- metadata +101 -11
@@ -0,0 +1,47 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION jsonb_minus(arg1 jsonb, arg2 jsonb) RETURNS jsonb
|
2
|
+
AS $$
|
3
|
+
|
4
|
+
SELECT
|
5
|
+
COALESCE(json_object_agg(key, value), '{}')::jsonb
|
6
|
+
FROM
|
7
|
+
jsonb_each(arg1)
|
8
|
+
WHERE NOT jsonb_build_object(key, value) <@ arg2;
|
9
|
+
|
10
|
+
$$ LANGUAGE SQL;
|
11
|
+
|
12
|
+
CREATE OR REPLACE FUNCTION jsonb_minus_2_logger() RETURNS TRIGGER AS $body$
|
13
|
+
DECLARE
|
14
|
+
changes_h jsonb;
|
15
|
+
size integer;
|
16
|
+
buffer jsonb;
|
17
|
+
BEGIN
|
18
|
+
size := jsonb_array_length(NEW.log);
|
19
|
+
|
20
|
+
changes_h := jsonb_minus(row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb);
|
21
|
+
|
22
|
+
NEW.log := jsonb_set(
|
23
|
+
NEW.log,
|
24
|
+
ARRAY[size::text],
|
25
|
+
jsonb_build_object(
|
26
|
+
'ts',
|
27
|
+
extract(epoch from now())::int,
|
28
|
+
'i',
|
29
|
+
(NEW.log#>>ARRAY[(size - 1)::text, 'i'])::int + 1,
|
30
|
+
'd',
|
31
|
+
changes_h
|
32
|
+
),
|
33
|
+
true
|
34
|
+
);
|
35
|
+
return NEW;
|
36
|
+
END;
|
37
|
+
$body$
|
38
|
+
LANGUAGE plpgsql;
|
39
|
+
|
40
|
+
|
41
|
+
ALTER TABLE pgbench_accounts ADD COLUMN log jsonb DEFAULT '[]' NOT NULL;
|
42
|
+
|
43
|
+
UPDATE pgbench_accounts SET log = to_jsonb(ARRAY[json_build_object('i', 0)])::jsonb;
|
44
|
+
|
45
|
+
CREATE TRIGGER hstore_log_accounts
|
46
|
+
BEFORE UPDATE ON pgbench_accounts FOR EACH ROW
|
47
|
+
EXECUTE PROCEDURE jsonb_minus_2_logger();
|
@@ -0,0 +1,49 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION jsonb_minus(arg1 jsonb, arg2 jsonb) RETURNS jsonb
|
2
|
+
AS $$
|
3
|
+
|
4
|
+
SELECT
|
5
|
+
COALESCE(json_object_agg(key, value), '{}')::jsonb
|
6
|
+
FROM
|
7
|
+
jsonb_each(arg1)
|
8
|
+
WHERE
|
9
|
+
arg1 -> key <> arg2 -> key
|
10
|
+
OR arg2 -> key IS NULL;
|
11
|
+
|
12
|
+
$$ LANGUAGE SQL;
|
13
|
+
|
14
|
+
CREATE OR REPLACE FUNCTION jsonb_minus_logger() RETURNS TRIGGER AS $body$
|
15
|
+
DECLARE
|
16
|
+
changes_h jsonb;
|
17
|
+
size integer;
|
18
|
+
buffer jsonb;
|
19
|
+
BEGIN
|
20
|
+
size := jsonb_array_length(NEW.log);
|
21
|
+
|
22
|
+
changes_h := jsonb_minus(row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb);
|
23
|
+
|
24
|
+
NEW.log := jsonb_set(
|
25
|
+
NEW.log,
|
26
|
+
ARRAY[size::text],
|
27
|
+
jsonb_build_object(
|
28
|
+
'ts',
|
29
|
+
extract(epoch from now())::int,
|
30
|
+
'i',
|
31
|
+
(NEW.log#>>ARRAY[(size - 1)::text, 'i'])::int + 1,
|
32
|
+
'd',
|
33
|
+
changes_h
|
34
|
+
),
|
35
|
+
true
|
36
|
+
);
|
37
|
+
return NEW;
|
38
|
+
END;
|
39
|
+
$body$
|
40
|
+
LANGUAGE plpgsql;
|
41
|
+
|
42
|
+
|
43
|
+
ALTER TABLE pgbench_accounts ADD COLUMN log jsonb DEFAULT '[]' NOT NULL;
|
44
|
+
|
45
|
+
UPDATE pgbench_accounts SET log = to_jsonb(ARRAY[json_build_object('i', 0)])::jsonb;
|
46
|
+
|
47
|
+
CREATE TRIGGER hstore_log_accounts
|
48
|
+
BEFORE UPDATE ON pgbench_accounts FOR EACH ROW
|
49
|
+
EXECUTE PROCEDURE jsonb_minus_logger();
|
@@ -0,0 +1,44 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION keys_2_logger() RETURNS TRIGGER AS $body$
|
2
|
+
DECLARE
|
3
|
+
size integer;
|
4
|
+
old_j jsonb;
|
5
|
+
changes_j jsonb;
|
6
|
+
item record;
|
7
|
+
BEGIN
|
8
|
+
size := jsonb_array_length(NEW.log);
|
9
|
+
old_j := to_jsonb(OLD);
|
10
|
+
changes_j := to_jsonb(NEW);
|
11
|
+
|
12
|
+
FOR item in SELECT key as k, value as v FROM jsonb_each(old_j)
|
13
|
+
LOOP
|
14
|
+
IF changes_j->item.k = item.v THEN
|
15
|
+
changes_j := changes_j - item.k;
|
16
|
+
END IF;
|
17
|
+
END LOOP;
|
18
|
+
|
19
|
+
NEW.log := jsonb_set(
|
20
|
+
NEW.log,
|
21
|
+
ARRAY[size::text],
|
22
|
+
jsonb_build_object(
|
23
|
+
'ts',
|
24
|
+
extract(epoch from now())::int,
|
25
|
+
'i',
|
26
|
+
(NEW.log#>>ARRAY[(size - 1)::text, 'i'])::int + 1,
|
27
|
+
'd',
|
28
|
+
changes_j
|
29
|
+
),
|
30
|
+
true
|
31
|
+
);
|
32
|
+
return NEW;
|
33
|
+
END;
|
34
|
+
$body$
|
35
|
+
LANGUAGE plpgsql;
|
36
|
+
|
37
|
+
|
38
|
+
ALTER TABLE pgbench_accounts ADD COLUMN log jsonb DEFAULT '[]' NOT NULL;
|
39
|
+
|
40
|
+
UPDATE pgbench_accounts SET log = to_jsonb(ARRAY[json_build_object('i', 0)])::jsonb;
|
41
|
+
|
42
|
+
CREATE TRIGGER keys_log_accounts
|
43
|
+
BEFORE UPDATE ON pgbench_accounts FOR EACH ROW
|
44
|
+
EXECUTE PROCEDURE keys_2_logger();
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/circle.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
machine:
|
2
|
+
ruby:
|
3
|
+
version: 2.3.0
|
4
|
+
|
5
|
+
environment:
|
6
|
+
DATABASE_URL: postgres://postgres@127.0.0.1:5433/test_database
|
7
|
+
|
8
|
+
dependencies:
|
9
|
+
pre:
|
10
|
+
- gem install bundler -v 1.11.2
|
11
|
+
- sudo service postgresql stop 9.4
|
12
|
+
- sudo cp -v /etc/postgresql/9.{4,5}/main/pg_hba.conf && sudo service postgresql restart 9.5
|
13
|
+
|
14
|
+
database:
|
15
|
+
override:
|
16
|
+
- bundle exec rake dummy:db:test:prepare
|
17
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
require "rails/generators/active_record"
|
4
|
+
|
5
|
+
module Logidze
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base # :nodoc:
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
|
10
|
+
source_root File.expand_path('../templates', __FILE__)
|
11
|
+
|
12
|
+
def generate_migration
|
13
|
+
migration_template "migration.rb.erb", "db/migrate/logidze_install.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_hstore_migration
|
17
|
+
migration_template "hstore.rb.erb", "db/migrate/enable_hstore.rb"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.next_migration_number(dir)
|
21
|
+
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
class <%= @migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
execute <<~SQL
|
4
|
+
DO $$
|
5
|
+
BEGIN
|
6
|
+
EXECUTE 'ALTER DATABASE ' || current_database() || ' SET logidze.disabled TO off';
|
7
|
+
END;
|
8
|
+
$$
|
9
|
+
LANGUAGE plpgsql;
|
10
|
+
SQL
|
11
|
+
|
12
|
+
execute <<~SQL
|
13
|
+
CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
|
14
|
+
DECLARE
|
15
|
+
changes jsonb;
|
16
|
+
new_v integer;
|
17
|
+
ts bigint;
|
18
|
+
size integer;
|
19
|
+
history_limit integer;
|
20
|
+
current_version integer;
|
21
|
+
merged jsonb;
|
22
|
+
iterator integer;
|
23
|
+
item record;
|
24
|
+
BEGIN
|
25
|
+
ts := (extract(epoch from now()) * 1000)::bigint;
|
26
|
+
|
27
|
+
IF TG_OP = 'INSERT' THEN
|
28
|
+
changes := to_jsonb(NEW.*) - 'log_data';
|
29
|
+
new_v := 1;
|
30
|
+
|
31
|
+
NEW.log_data := json_build_object(
|
32
|
+
'v',
|
33
|
+
1,
|
34
|
+
'h',
|
35
|
+
jsonb_build_array(
|
36
|
+
jsonb_build_object(
|
37
|
+
'ts',
|
38
|
+
ts,
|
39
|
+
'v',
|
40
|
+
new_v,
|
41
|
+
'c',
|
42
|
+
changes
|
43
|
+
)
|
44
|
+
)
|
45
|
+
);
|
46
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
47
|
+
history_limit := TG_ARGV[0];
|
48
|
+
current_version := (NEW.log_data->>'v')::int;
|
49
|
+
|
50
|
+
IF NEW = OLD THEN
|
51
|
+
RETURN NEW;
|
52
|
+
END IF;
|
53
|
+
|
54
|
+
IF current_version < (NEW.log_data#>>'{h,-1,v}')::int THEN
|
55
|
+
iterator := 0;
|
56
|
+
FOR item in SELECT * FROM jsonb_array_elements(NEW.log_data->'h')
|
57
|
+
LOOP
|
58
|
+
IF (item.value->>'v')::int > current_version THEN
|
59
|
+
NEW.log_data := jsonb_set(
|
60
|
+
NEW.log_data,
|
61
|
+
'{h}',
|
62
|
+
(NEW.log_data->'h') - iterator
|
63
|
+
);
|
64
|
+
END IF;
|
65
|
+
iterator := iterator + 1;
|
66
|
+
END LOOP;
|
67
|
+
END IF;
|
68
|
+
|
69
|
+
changes := hstore_to_jsonb_loose(
|
70
|
+
hstore(NEW.*) - hstore(OLD.*)
|
71
|
+
) - 'log_data';
|
72
|
+
|
73
|
+
new_v := (NEW.log_data#>>'{h,-1,v}')::int + 1;
|
74
|
+
|
75
|
+
size := jsonb_array_length(NEW.log_data->'h');
|
76
|
+
|
77
|
+
NEW.log_data := jsonb_set(
|
78
|
+
NEW.log_data,
|
79
|
+
ARRAY['h', size::text],
|
80
|
+
jsonb_build_object(
|
81
|
+
'ts',
|
82
|
+
ts,
|
83
|
+
'v',
|
84
|
+
new_v,
|
85
|
+
'c',
|
86
|
+
changes
|
87
|
+
),
|
88
|
+
true
|
89
|
+
);
|
90
|
+
|
91
|
+
NEW.log_data := jsonb_set(
|
92
|
+
NEW.log_data,
|
93
|
+
'{v}',
|
94
|
+
to_jsonb(new_v)
|
95
|
+
);
|
96
|
+
|
97
|
+
IF history_limit IS NOT NULL AND history_limit = size THEN
|
98
|
+
merged := jsonb_build_object(
|
99
|
+
'ts',
|
100
|
+
NEW.log_data#>'{h,1,ts}',
|
101
|
+
'v',
|
102
|
+
NEW.log_data#>'{h,1,v}',
|
103
|
+
'c',
|
104
|
+
(NEW.log_data#>'{h,0,c}') || (NEW.log_data#>'{h,1,c}')
|
105
|
+
);
|
106
|
+
|
107
|
+
NEW.log_data := jsonb_set(
|
108
|
+
NEW.log_data,
|
109
|
+
'{h}',
|
110
|
+
jsonb_set(
|
111
|
+
NEW.log_data->'h',
|
112
|
+
'{1}',
|
113
|
+
merged
|
114
|
+
) - 0
|
115
|
+
);
|
116
|
+
END IF;
|
117
|
+
END IF;
|
118
|
+
|
119
|
+
return NEW;
|
120
|
+
END;
|
121
|
+
$body$
|
122
|
+
LANGUAGE plpgsql;
|
123
|
+
SQL
|
124
|
+
end
|
125
|
+
|
126
|
+
def down
|
127
|
+
execute <<~SQL
|
128
|
+
DROP FUNCTION logidze_logger() CASCADE;
|
129
|
+
SQL
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
require "rails/generators/active_record/migration/migration_generator"
|
4
|
+
|
5
|
+
module Logidze
|
6
|
+
module Generators
|
7
|
+
class ModelGenerator < ::ActiveRecord::Generators::Base # :nodoc:
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
class_option :limit, type: :numeric, optional: true, desc: "Specify history size limit"
|
11
|
+
|
12
|
+
def generate_migration
|
13
|
+
migration_template "migration.rb.erb", "db/migrate/#{migration_file_name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def inject_logidze_to_model
|
17
|
+
indents = " " * (class_name.scan("::").count + 1)
|
18
|
+
|
19
|
+
inject_into_class(model_file_path, class_name.demodulize, "#{indents}has_logidze\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
no_tasks do
|
23
|
+
def migration_name
|
24
|
+
"add_logidze_to_#{plural_table_name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def migration_file_name
|
28
|
+
"#{migration_name}.rb"
|
29
|
+
end
|
30
|
+
|
31
|
+
def limit
|
32
|
+
options[:limit]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def model_file_path
|
39
|
+
File.join("app", "models", "#{file_path}.rb")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class <%= @migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
add_column :<%= table_name %>, :log_data, :jsonb, default: '{}', null: false
|
4
|
+
|
5
|
+
execute <<-SQL
|
6
|
+
CREATE TRIGGER logidze_on_<%= table_name %>
|
7
|
+
BEFORE UPDATE OR INSERT ON <%= table_name %> FOR EACH ROW
|
8
|
+
WHEN (current_setting('logidze.disabled') <> 'on')
|
9
|
+
EXECUTE PROCEDURE logidze_logger(<%= limit || '' %>);
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
|
13
|
+
def down
|
14
|
+
execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
|
15
|
+
|
16
|
+
remove_column :<%= table_name %>, :log_data
|
17
|
+
end
|
18
|
+
end
|