logidze 0.0.1 → 0.1.0

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