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.
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "logidze"
5
+
6
+ require "pry"
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ gem install bundler --conservative
6
+ bundle check || bundle install
7
+
8
+ bundle exec rake dummy:db:create
9
+ bundle exec rake dummy:db:test:prepare
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,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rails', "~>4.2"
4
+
5
+ gemspec path: '..'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rails', '5.0.0.rc1'
4
+
5
+ gemspec path: '..'
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Generates the necessary files to get you up and running with Logidze gem
3
+
4
+ Examples:
5
+ rails generate logidze:install
6
+
7
+ This will generate the core migration file with trigger function defined.
@@ -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,5 @@
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ enable_extension :hstore
4
+ end
5
+ 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,8 @@
1
+ Description:
2
+ Generates the necessary migration to enable logging for model
3
+
4
+ Examples:
5
+ rails generate logidze:model User
6
+
7
+ This will generate the migration to add log column and trigger.
8
+ This will also add `has_logidze` to model.
@@ -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