logidze 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|