diffit 0.0.1
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 +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: []
|