diffit 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +17 -0
- data/lib/diffit.rb +24 -0
- data/lib/diffit/changes.rb +98 -0
- data/lib/diffit/record.rb +27 -0
- data/lib/diffit/trackable.rb +21 -0
- data/lib/diffit/tracker.rb +205 -0
- data/lib/diffit/version.rb +3 -0
- data/lib/generators/diffit/base_generator.rb +35 -0
- data/lib/generators/diffit/init/init_generator.rb +32 -0
- data/lib/generators/diffit/init/migrations/create_function.erb +102 -0
- data/lib/generators/diffit/init/migrations/create_table.erb +24 -0
- data/lib/generators/diffit/init/templates/diffit.erb +12 -0
- data/lib/generators/diffit/triggers/migrations/create_triggers.erb +32 -0
- data/lib/generators/diffit/triggers/triggers_generator.rb +33 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 51a22c289600fbd9b443344c8d478ae2ab8c170b
|
4
|
+
data.tar.gz: 8b41bc0c64837641b34c3afeee1fb61fcf928a92
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f348d9f2c135baf8f25cfec6b9331b07691a4e36b038393e1043aef05fb734ba0df718a433cbdc9d1c000e87dd1351170c353ffa4c002ef65061509e211edf11
|
7
|
+
data.tar.gz: 6d02351691c2fc6ef90bfa15567db530717b3d691935de86d3413006b35879890b02d8cbff0a69010e1ce16e08eb1363c4ff5264319ebd4edb17908ba0daeac1
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Denis Lifanov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Diffit'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
Bundler::GemHelper.install_tasks
|
data/lib/diffit.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'diffit/version'
|
2
|
+
|
3
|
+
module Diffit
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
|
6
|
+
autoload :Changes
|
7
|
+
autoload :Record
|
8
|
+
autoload :Tracker
|
9
|
+
autoload :Trackable
|
10
|
+
|
11
|
+
mattr_accessor :function_name
|
12
|
+
@@function_name = :diffit_function
|
13
|
+
|
14
|
+
mattr_accessor :table_name
|
15
|
+
@@table_name = :diffit_column_diffs
|
16
|
+
|
17
|
+
mattr_accessor :strategy
|
18
|
+
@@strategy = :join
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
yield self
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Diffit
|
2
|
+
class Changes
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :timestamp
|
7
|
+
attr_reader :records
|
8
|
+
|
9
|
+
# Instantiates a Diffit::Changes with provided timestamp.
|
10
|
+
#
|
11
|
+
# @param timestamp [Time, DateTime, Date, Fixnum] date, time or timestamp.
|
12
|
+
# @return [Diffit::Changes] Diffit::Changes
|
13
|
+
def initialize(timestamp)
|
14
|
+
@timestamp = timestamp
|
15
|
+
@records = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Appends provided data to `self`.
|
19
|
+
#
|
20
|
+
# @param model [String] model name.
|
21
|
+
# @param data [Array(Hash)] data to append.
|
22
|
+
# @return [self] self
|
23
|
+
def append(model, data)
|
24
|
+
data.group_by { |row| row[:record_id] }.each do |record_id, changes|
|
25
|
+
@records << Record.new(model, record_id, changes.map { |c| c.slice(:column_name, :value, :changed_at)})
|
26
|
+
end
|
27
|
+
@length = nil
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Are there any changes?
|
32
|
+
#
|
33
|
+
# @return [Boolean] existence of changes.
|
34
|
+
def empty?
|
35
|
+
@records.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Number of changes.
|
39
|
+
#
|
40
|
+
# @return [Fixnum] number of changes
|
41
|
+
def length
|
42
|
+
@length ||= @records.inject(0) { |v,r| v += r.changes.length }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calls the given block once for each record in the collection.
|
46
|
+
#
|
47
|
+
# @return [Enumerator] if no block is given.
|
48
|
+
# @return [Array(Diffit::Record)] otherwise
|
49
|
+
def each
|
50
|
+
if block_given?
|
51
|
+
@records.each { |c| yield c }
|
52
|
+
else
|
53
|
+
@records.enum_for(:each)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# A short `String` representation of `self`.
|
58
|
+
#
|
59
|
+
# @return [String] the object converted to string.
|
60
|
+
def to_s
|
61
|
+
sprintf '#<%s:%#0x @timestamp: %s @changes: {%d}>',
|
62
|
+
self.class.to_s,
|
63
|
+
self.object_id,
|
64
|
+
@timestamp.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
65
|
+
length
|
66
|
+
end
|
67
|
+
|
68
|
+
alias :to_str :to_s
|
69
|
+
|
70
|
+
# A `Hash` representation of `self`.
|
71
|
+
#
|
72
|
+
# @return [Hash] the object converted to hash.
|
73
|
+
def to_h
|
74
|
+
{timestamp: timestamp.to_i, changes: @records.map(&:to_h)}
|
75
|
+
end
|
76
|
+
|
77
|
+
alias :to_hash :to_h
|
78
|
+
|
79
|
+
# A JSON representation of `self`.
|
80
|
+
#
|
81
|
+
# @return [String] the object converted to JSON.
|
82
|
+
def to_json
|
83
|
+
to_h.to_json
|
84
|
+
end
|
85
|
+
|
86
|
+
def prepare!
|
87
|
+
@records.sort_by! { |record| record.last_changed_at }
|
88
|
+
@records.uniq! { |record| [record.model, record.record_id] }
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def cleanup!
|
93
|
+
@records.clear
|
94
|
+
@length = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Diffit
|
2
|
+
class Record
|
3
|
+
|
4
|
+
attr_reader :model, :record_id, :changes
|
5
|
+
|
6
|
+
def initialize(model, record_id, changes)
|
7
|
+
@model = model
|
8
|
+
@record_id = record_id
|
9
|
+
@changes = changes
|
10
|
+
end
|
11
|
+
|
12
|
+
# Timestamp of the latest change.
|
13
|
+
#
|
14
|
+
# @return [Time] latest change timestamp.
|
15
|
+
def last_changed_at
|
16
|
+
@last_changed_at ||= @changes.map { |c| c[:changed_at] }.max
|
17
|
+
end
|
18
|
+
|
19
|
+
# A `Hash` representation of `self`.
|
20
|
+
#
|
21
|
+
# @return [Hash] the object converted to hash.
|
22
|
+
def to_h
|
23
|
+
{model: model, record_id: record_id, changes: changes}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Diffit
|
2
|
+
module Trackable
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def changes_since(timestamp)
|
9
|
+
Diffit::Tracker.new(timestamp).append(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
alias :diff_from :changes_since
|
13
|
+
|
14
|
+
def changes_since_midnight
|
15
|
+
Diffit::Tracker.new(Time.now.beginning_of_day).append(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
alias :diff_from_midnight :changes_since_midnight
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
module Diffit
|
2
|
+
class Tracker
|
3
|
+
|
4
|
+
attr_reader :timestamp
|
5
|
+
|
6
|
+
delegate :to_h, :to_hash, :to_json, :each, :length, :size, to: :changes
|
7
|
+
|
8
|
+
# Instantiates a Diffit::Tracker with provided timestamp.
|
9
|
+
#
|
10
|
+
# @param timestamp [Time, DateTime, Date, Fixnum] date, time or timestamp.
|
11
|
+
# @return [Diffit::Tracker] Diffit::Tracker
|
12
|
+
def initialize(timestamp)
|
13
|
+
if timestamp.respond_to?(:to_datetime)
|
14
|
+
@timestamp = timestamp.to_datetime
|
15
|
+
elsif timestamp.respond_to?(:to_i)
|
16
|
+
@timestamp = Time.at(timestamp.to_i).to_datetime
|
17
|
+
else
|
18
|
+
raise ArgumentError, "#{timestamp.inspect} is not a timestamp!"
|
19
|
+
end
|
20
|
+
|
21
|
+
@tracked = []
|
22
|
+
@changes = Diffit::Changes.new(self.timestamp)
|
23
|
+
@fetched = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize_clone(other)
|
27
|
+
@tracked = Array.new(@tracked)
|
28
|
+
@changes = Diffit::Changes.new(self.timestamp)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Appends provided objects.
|
32
|
+
#
|
33
|
+
# @param object [ActiveRecord::Relation, ActiveRecord::Base, Array(ActiveRecord::Base), Array(ActiveRecord::Relation)]
|
34
|
+
# @return [Diffit::Tracker] new instance of Diffit::Tracker.
|
35
|
+
def append(*objects)
|
36
|
+
copy = self.clone
|
37
|
+
copy.append!(*objects)
|
38
|
+
copy
|
39
|
+
end
|
40
|
+
|
41
|
+
# Appends provided objects to `self`.
|
42
|
+
#
|
43
|
+
# @param object [ActiveRecord::Relation, ActiveRecord::Base, Array(ActiveRecord::Base), Array(ActiveRecord::Relation)]
|
44
|
+
# @return [self] self
|
45
|
+
def append!(*objects)
|
46
|
+
objects.flatten!
|
47
|
+
objects.each do |object|
|
48
|
+
if accepts?(object)
|
49
|
+
@tracked << object
|
50
|
+
else
|
51
|
+
raise ArgumentError, 'Expected ActiveRecord::Base or ActiveRecord::Relation'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@changes.cleanup!
|
56
|
+
@fetched = false
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Appends all changes.
|
61
|
+
#
|
62
|
+
# @return [Diffit::Tracker] a new instance of Diffit::Tracker.
|
63
|
+
def all
|
64
|
+
copy = self.clone
|
65
|
+
copy.all!
|
66
|
+
copy
|
67
|
+
end
|
68
|
+
|
69
|
+
# Appends all changes to `self`.
|
70
|
+
#
|
71
|
+
# @return [self] self.
|
72
|
+
def all!
|
73
|
+
@changes.cleanup!
|
74
|
+
handle_all.group_by { |row| row[:table_name] }.each do |t, records|
|
75
|
+
@changes.append t.classify, records
|
76
|
+
end
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def changes
|
81
|
+
return @changes if @fetched
|
82
|
+
|
83
|
+
@tracked.each do |object|
|
84
|
+
if record?(object)
|
85
|
+
@changes.append object.model_name.name, handle_one(object)
|
86
|
+
elsif relation?(object)
|
87
|
+
model = object.respond_to?(:model) ? object.model : object.class
|
88
|
+
changes = case Diffit.strategy
|
89
|
+
when :join then handle_relation_with_join(object)
|
90
|
+
when :subquery then handle_relation_with_subquery(object)
|
91
|
+
end
|
92
|
+
|
93
|
+
@changes.append model.name, changes
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
@fetched = true
|
98
|
+
@changes.prepare!
|
99
|
+
@changes
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def accepts?(object)
|
105
|
+
record?(object) || relation?(object)
|
106
|
+
end
|
107
|
+
|
108
|
+
def relation?(object)
|
109
|
+
object.is_a?(ActiveRecord::Relation) || ActiveRecord::Base.descendants.include?(object)
|
110
|
+
end
|
111
|
+
|
112
|
+
def record?(object)
|
113
|
+
object.is_a?(ActiveRecord::Base)
|
114
|
+
end
|
115
|
+
|
116
|
+
def handle_relation_with_subquery(relation)
|
117
|
+
table = Arel::Table.new(Diffit.table_name)
|
118
|
+
|
119
|
+
sanitized = relation.except(:select, :order, :group, :having, :includes).select(:id)
|
120
|
+
|
121
|
+
query = table.
|
122
|
+
where(table[:changed_at].gteq(self.timestamp)).
|
123
|
+
where(table[:table_name].eq(relation.table_name)).
|
124
|
+
order(:table_name, :record_id)
|
125
|
+
|
126
|
+
if sanitized.where_values.present? || sanitized.joins_values.present?
|
127
|
+
query = query.where(table[:record_id].in(Arel.sql(sanitized.to_sql)))
|
128
|
+
end
|
129
|
+
|
130
|
+
query = query.project(table[:record_id], table[:column_name], table[:value], table[:changed_at])
|
131
|
+
|
132
|
+
execute_query(query)
|
133
|
+
end
|
134
|
+
|
135
|
+
def handle_relation_with_join(relation)
|
136
|
+
table = Arel::Table.new(Diffit.table_name)
|
137
|
+
|
138
|
+
sanitized = relation.except(:select, :order, :group, :having, :includes)
|
139
|
+
|
140
|
+
query = sanitized.
|
141
|
+
from(table).
|
142
|
+
where(table[:changed_at].gteq(self.timestamp)).
|
143
|
+
where(table[:table_name].eq(relation.table_name)).
|
144
|
+
select(table[:record_id], table[:column_name], table[:value], table[:changed_at])
|
145
|
+
|
146
|
+
if sanitized.where_values.present? || sanitized.joins_values.present?
|
147
|
+
|
148
|
+
join_cond = Arel::Nodes::On.new(sanitized.arel_table[:id].eq(table[:record_id]))
|
149
|
+
join_arel = Arel::Nodes::InnerJoin.new(sanitized.arel_table, join_cond)
|
150
|
+
|
151
|
+
query = query.joins(join_arel)
|
152
|
+
end
|
153
|
+
|
154
|
+
execute_query(query)
|
155
|
+
end
|
156
|
+
|
157
|
+
def handle_one(record)
|
158
|
+
table = Arel::Table.new(Diffit.table_name)
|
159
|
+
|
160
|
+
query = table.
|
161
|
+
where(table[:changed_at].gteq(self.timestamp)).
|
162
|
+
where(table[:table_name].eq(record.class.table_name)).
|
163
|
+
where(table[:record_id].eq(record.id)).
|
164
|
+
order(:table_name, :record_id).
|
165
|
+
project(table[:record_id], table[:column_name], table[:value], table[:changed_at])
|
166
|
+
|
167
|
+
execute_query(query)
|
168
|
+
end
|
169
|
+
|
170
|
+
def handle_all
|
171
|
+
table = Arel::Table.new(Diffit.table_name)
|
172
|
+
|
173
|
+
query = table.
|
174
|
+
where(table[:changed_at].gteq(self.timestamp)).
|
175
|
+
order(:table_name, :record_id).
|
176
|
+
project(table[:table_name], table[:record_id], table[:column_name], table[:value], table[:changed_at])
|
177
|
+
|
178
|
+
execute_query(query)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Executes raw SQL query.
|
182
|
+
# Uses ActiveRecord::Result, performs typecasting.
|
183
|
+
#
|
184
|
+
# @param query [ActiveRecord::Relation, Arel::SelectManager]
|
185
|
+
# @return [Array(Hash)] query result
|
186
|
+
def execute_query(query)
|
187
|
+
result = ActiveRecord::Base.connection.select_all(query.to_sql)
|
188
|
+
|
189
|
+
cols = result.columns.map { |c| c.to_sym }
|
190
|
+
rows = result.cast_values
|
191
|
+
|
192
|
+
rows.map do |row|
|
193
|
+
hash, i, l = {}, 0, cols.length
|
194
|
+
|
195
|
+
while i < l
|
196
|
+
hash[cols[i]] = row[i]
|
197
|
+
i += 1
|
198
|
+
end
|
199
|
+
|
200
|
+
hash
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module Diffit
|
5
|
+
class BaseGenerator < Rails::Generators::Base
|
6
|
+
|
7
|
+
hide!
|
8
|
+
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def migration_exists?(basename)
|
18
|
+
self.class.migration_exists?('db/migrate', basename).present?
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_diffit_migration(template, basename)
|
22
|
+
if migration_exists?(basename)
|
23
|
+
warning "Migration '#{basename}' already exists."
|
24
|
+
else
|
25
|
+
migration_template template, "db/migrate/#{basename}.rb"
|
26
|
+
sleep 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def warning(message)
|
31
|
+
say_status '!', message, :red
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'generators/diffit/base_generator'
|
2
|
+
|
3
|
+
module Diffit
|
4
|
+
class InitGenerator < Diffit::BaseGenerator
|
5
|
+
|
6
|
+
source_root File.dirname(__FILE__)
|
7
|
+
|
8
|
+
desc 'Creates a diffit initializer and migrations for tracking table and stored procedure.'
|
9
|
+
|
10
|
+
argument :table_name, type: :string, required: true, desc: "Name of tracking table"
|
11
|
+
|
12
|
+
def prepare
|
13
|
+
Diffit.table_name = self.table_name
|
14
|
+
Diffit.function_name = "#{self.table_name}_function"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_initializer
|
18
|
+
template 'templates/diffit.erb', 'config/initializers/diffit.rb'
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_table_migration
|
22
|
+
basename = "create_#{Diffit.table_name.to_s.underscore}"
|
23
|
+
create_diffit_migration 'migrations/create_table.erb', basename
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_function_migration
|
27
|
+
basename = "create_#{Diffit.table_name.to_s.underscore}_function"
|
28
|
+
create_diffit_migration 'migrations/create_function.erb', basename
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class Create<%= Diffit.function_name.to_s.camelize %> < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
execute <<-SQL
|
4
|
+
CREATE OR REPLACE FUNCTION <%= Diffit.function_name %>() RETURNS TRIGGER AS $$
|
5
|
+
DECLARE
|
6
|
+
|
7
|
+
n JSON; -- NEW as JSON
|
8
|
+
o JSON; -- OLD as JSON
|
9
|
+
t TIMESTAMP; -- current timestamp
|
10
|
+
a RECORD; -- attribute
|
11
|
+
|
12
|
+
BEGIN
|
13
|
+
|
14
|
+
t := current_timestamp;
|
15
|
+
n := row_to_json(NEW);
|
16
|
+
|
17
|
+
IF (TG_OP = 'UPDATE') THEN
|
18
|
+
o := row_to_json(OLD);
|
19
|
+
END IF;
|
20
|
+
|
21
|
+
-- select attributes (without pkeys)
|
22
|
+
|
23
|
+
FOR a IN SELECT
|
24
|
+
pga.attname AS name
|
25
|
+
FROM pg_attribute pga
|
26
|
+
LEFT JOIN pg_index pgi ON pgi.indrelid = pga.attrelid AND pga.attnum = ANY(pgi.indkey)
|
27
|
+
WHERE pga.attnum > 0 AND
|
28
|
+
pgi.indisprimary IS NOT true AND
|
29
|
+
pga.attrelid = TG_TABLE_NAME::regclass AND
|
30
|
+
NOT pga.attisdropped
|
31
|
+
ORDER BY pga.attnum
|
32
|
+
LOOP
|
33
|
+
|
34
|
+
IF (TG_OP = 'UPDATE' AND (n->(a.name))::text IS DISTINCT FROM (o->(a.name))::text) OR (TG_OP = 'INSERT') THEN
|
35
|
+
|
36
|
+
-- UPSERT
|
37
|
+
|
38
|
+
UPDATE <%= Diffit.table_name %>
|
39
|
+
SET
|
40
|
+
value = n->(a.name),
|
41
|
+
changed_at = t
|
42
|
+
WHERE
|
43
|
+
table_name = TG_TABLE_NAME AND
|
44
|
+
record_id = NEW.id AND
|
45
|
+
column_name = a.name;
|
46
|
+
|
47
|
+
IF NOT FOUND THEN
|
48
|
+
|
49
|
+
BEGIN
|
50
|
+
|
51
|
+
INSERT INTO <%= Diffit.table_name %>
|
52
|
+
(table_name, record_id, column_name, value, changed_at)
|
53
|
+
VALUES
|
54
|
+
(TG_TABLE_NAME, NEW.id, a.name, n->(a.name), t);
|
55
|
+
|
56
|
+
EXCEPTION
|
57
|
+
WHEN unique_violation THEN
|
58
|
+
|
59
|
+
-- handle unique violation (constraint: table_name, record_id, column_name)
|
60
|
+
-- TODO: notice
|
61
|
+
-- CHEKME
|
62
|
+
|
63
|
+
UPDATE <%= Diffit.table_name %>
|
64
|
+
SET
|
65
|
+
value = n->(a.name),
|
66
|
+
changed_at = t
|
67
|
+
WHERE
|
68
|
+
table_name = TG_TABLE_NAME AND
|
69
|
+
record_id = NEW.id AND
|
70
|
+
column_name = a.name;
|
71
|
+
|
72
|
+
WHEN OTHERS THEN
|
73
|
+
|
74
|
+
-- TODO: notice
|
75
|
+
|
76
|
+
UPDATE <%= Diffit.table_name %>
|
77
|
+
SET
|
78
|
+
value = n->(a.name),
|
79
|
+
changed_at = t
|
80
|
+
WHERE
|
81
|
+
table_name = TG_TABLE_NAME AND
|
82
|
+
record_id = NEW.id AND
|
83
|
+
column_name = a.name;
|
84
|
+
|
85
|
+
END;
|
86
|
+
END IF;
|
87
|
+
END IF;
|
88
|
+
END LOOP;
|
89
|
+
|
90
|
+
RETURN NEW;
|
91
|
+
|
92
|
+
END;
|
93
|
+
$$ LANGUAGE plpgsql;
|
94
|
+
SQL
|
95
|
+
end
|
96
|
+
|
97
|
+
def down
|
98
|
+
execute <<-SQL
|
99
|
+
DROP FUNCTION IF EXISTS <%= Diffit.function_name %>();
|
100
|
+
SQL
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Create<%= Diffit.table_name.to_s.camelize %> < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :<%= Diffit.table_name %> do |t|
|
4
|
+
t.string :table_name
|
5
|
+
t.integer :record_id
|
6
|
+
t.string :column_name
|
7
|
+
t.json :value
|
8
|
+
t.timestamp :changed_at
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :<%= Diffit.table_name %>, :table_name
|
12
|
+
add_index :<%= Diffit.table_name %>, :record_id
|
13
|
+
add_index :<%= Diffit.table_name %>, :changed_at
|
14
|
+
|
15
|
+
execute <<-SQL
|
16
|
+
ALTER TABLE <%= Diffit.table_name %>
|
17
|
+
ADD CONSTRAINT record_identifiers UNIQUE (table_name, record_id, column_name);
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
|
21
|
+
def down
|
22
|
+
drop_table :<%= Diffit.table_name %>
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Diffit.configure do |config|
|
2
|
+
|
3
|
+
# A name of the tracking table.
|
4
|
+
config.table_name = :<%= Diffit.table_name %>
|
5
|
+
|
6
|
+
# A name of the stored procedure.
|
7
|
+
config.function_name = :<%= Diffit.function_name %>
|
8
|
+
|
9
|
+
# A strategy to handle relations. Should be one of: [:join, :subquery].
|
10
|
+
config.strategy = :join
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Create<%= Diffit.function_name.to_s.camelize %>TriggersOn<%= table_name.camelize %> < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
|
4
|
+
execute <<-SQL
|
5
|
+
CREATE TRIGGER <%= Diffit.function_name %>_on_<%= table_name %>_insert_trigger
|
6
|
+
AFTER INSERT ON <%= table_name %>
|
7
|
+
FOR EACH ROW
|
8
|
+
EXECUTE PROCEDURE <%= Diffit.function_name %>();
|
9
|
+
SQL
|
10
|
+
|
11
|
+
execute <<-SQL
|
12
|
+
CREATE TRIGGER <%= Diffit.function_name %>_on_<%= table_name %>_update_trigger
|
13
|
+
AFTER UPDATE ON <%= table_name %>
|
14
|
+
FOR EACH ROW
|
15
|
+
WHEN (OLD.* IS DISTINCT FROM NEW.*)
|
16
|
+
EXECUTE PROCEDURE <%= Diffit.function_name %>();
|
17
|
+
SQL
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def down
|
22
|
+
|
23
|
+
execute <<-SQL
|
24
|
+
DROP TRIGGER <%= Diffit.function_name %>_on_<%= table_name %>_insert_trigger ON <%= table_name %>;
|
25
|
+
SQL
|
26
|
+
|
27
|
+
execute <<-SQL
|
28
|
+
DROP TRIGGER <%= Diffit.function_name %>_on_<%= table_name %>_update_trigger ON <%= table_name %>;
|
29
|
+
SQL
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'generators/diffit/base_generator'
|
2
|
+
|
3
|
+
module Diffit
|
4
|
+
class TriggersGenerator < Diffit::BaseGenerator
|
5
|
+
|
6
|
+
source_root File.dirname(__FILE__)
|
7
|
+
|
8
|
+
argument :table_name, type: :string, required: true, desc: "ModelName or table_name"
|
9
|
+
|
10
|
+
desc 'Creates diffit triggers using a ModelName or a table_name provided.'
|
11
|
+
|
12
|
+
def create_triggers_migration
|
13
|
+
detect_table_name!
|
14
|
+
basename = "create_#{Diffit.function_name.to_s.underscore}_triggers_on_#{table_name}"
|
15
|
+
create_diffit_migration 'migrations/create_triggers.erb', basename
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def detect_table_name!
|
21
|
+
return if table_name == table_name.tableize
|
22
|
+
|
23
|
+
begin
|
24
|
+
klass = table_name.classify.constantize
|
25
|
+
self.table_name = klass.table_name if klass.respond_to?(:table_name)
|
26
|
+
rescue NameError
|
27
|
+
end
|
28
|
+
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: diffit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Denis Lifanov
|
8
|
+
- Alexey Gaziev
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-08-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.2'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4.2'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: pg
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0.18'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.18'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rspec-rails
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '3.2'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.2'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: ammeter
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '1.1'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.1'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: simplecov
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0.10'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0.10'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: database_cleaner
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '1.4'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '1.4'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: yard
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 0.8.7
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 0.8.7
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: redcarpet
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '3.2'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '3.2'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: github-markup
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '1.3'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '1.3'
|
140
|
+
description: Track changes in your tables using PostgreSQL triggers..
|
141
|
+
email:
|
142
|
+
- inadsence@gmail.com
|
143
|
+
- alex.gaziev@gmail.com
|
144
|
+
executables: []
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files: []
|
147
|
+
files:
|
148
|
+
- MIT-LICENSE
|
149
|
+
- Rakefile
|
150
|
+
- lib/diffit.rb
|
151
|
+
- lib/diffit/changes.rb
|
152
|
+
- lib/diffit/record.rb
|
153
|
+
- lib/diffit/trackable.rb
|
154
|
+
- lib/diffit/tracker.rb
|
155
|
+
- lib/diffit/version.rb
|
156
|
+
- lib/generators/diffit/base_generator.rb
|
157
|
+
- lib/generators/diffit/init/init_generator.rb
|
158
|
+
- lib/generators/diffit/init/migrations/create_function.erb
|
159
|
+
- lib/generators/diffit/init/migrations/create_table.erb
|
160
|
+
- lib/generators/diffit/init/templates/diffit.erb
|
161
|
+
- lib/generators/diffit/triggers/migrations/create_triggers.erb
|
162
|
+
- lib/generators/diffit/triggers/triggers_generator.rb
|
163
|
+
homepage: https://github.com/gazay/diffit
|
164
|
+
licenses:
|
165
|
+
- MIT
|
166
|
+
metadata: {}
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 2.4.5
|
184
|
+
signing_key:
|
185
|
+
specification_version: 4
|
186
|
+
summary: A simple solution to track changes in your tables.
|
187
|
+
test_files: []
|