table_differ 0.6.10 → 0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +6 -0
- data/Guardfile +1 -1
- data/README.md +71 -50
- data/TODO +1 -0
- data/lib/table_differ.rb +11 -3
- data/lib/table_differ/version.rb +1 -1
- data/spec/snapshot_spec.rb +39 -14
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 602a0b1ed4708321a8bee1cfc177a4c194df5208
|
4
|
+
data.tar.gz: 8ad8aa0badd13e031ce7a350bcc05fae945636b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87f01c2b832339b5fe798dbc15d5ac906632c77abdcd34cb6a4181dc69f1dc9c684175ee881a289445e68277c306f0723f99f3785e897a7296dcffd38d7aa3c8
|
7
|
+
data.tar.gz: d86fd94c247b7e7977ca003bb830cfb87dd8e167f9ab67cb3188f09260daa2c4cf2c51c04ade1cafbc90a7f94287ef87cf35364a48bc4fe62124057b62979e11
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Table Differ
|
2
2
|
|
3
|
-
|
3
|
+
Snapshot database tables, restore them, and compute the differences between two snapshots.
|
4
4
|
|
5
5
|
[![Build Status](https://api.travis-ci.org/bronson/table_differ.png?branch=master)](http://travis-ci.org/bronson/table_differ)
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/table_differ.svg)](http://badge.fury.io/rb/table_differ)
|
@@ -15,15 +15,21 @@ gem 'table_differ'
|
|
15
15
|
|
16
16
|
## Synopsis
|
17
17
|
|
18
|
+
To follow this, replace `Attachment` with any model from your own application.
|
19
|
+
Once you restore the snapshot, your database should appear unchanged.
|
20
|
+
|
18
21
|
```ruby
|
19
|
-
Attachment.create_snapshot
|
20
|
-
|
21
|
-
Attachment.
|
22
|
-
|
22
|
+
snapshot = Attachment.create_snapshot
|
23
|
+
Attachment.first.update_attributes!(name: 'newname') # make a change
|
24
|
+
# or run rake db:migrate, Attachment.delete_all, or anything.
|
25
|
+
|
26
|
+
# compute the changes
|
27
|
+
added,removed,changed = Attachment.diff_snapshot(snapshot)
|
23
28
|
=> [[], [], [<Attachment 1>]]
|
24
|
-
|
25
|
-
|
26
|
-
Attachment.delete_snapshot
|
29
|
+
|
30
|
+
Attachment.restore_snapshot(snapshot)
|
31
|
+
Attachment.delete_snapshot(snapshot)
|
32
|
+
# and we're right back where we started
|
27
33
|
```
|
28
34
|
|
29
35
|
## Usage
|
@@ -31,52 +37,75 @@ Attachment.delete_snapshot "attachments_20140626_233336"
|
|
31
37
|
Include TableDiffer in models that will be snapshotted:
|
32
38
|
|
33
39
|
```ruby
|
34
|
-
class
|
40
|
+
class Attachment < ActiveRecord::Base
|
35
41
|
include TableDiffer
|
36
42
|
...
|
37
43
|
end
|
38
44
|
```
|
39
45
|
|
40
|
-
### Snapshot
|
41
|
-
|
42
|
-
Any time you want to snapshot a table (say, before a new data import),
|
43
|
-
call `create_snapshot`.
|
46
|
+
### Create Snapshot
|
44
47
|
|
45
48
|
```ruby
|
46
49
|
Property.create_snapshot
|
47
50
|
Property.create_snapshot 'import_0012'
|
48
51
|
```
|
49
52
|
|
50
|
-
If you don't specify a name then
|
51
|
-
|
52
|
-
|
53
|
-
Table Differ can know which one is most recent.
|
53
|
+
If you don't specify a name then one will be specified for you.
|
54
|
+
Whatever naming scheme you use, the names should sort alphabetically.
|
55
|
+
Otherwise some Table Differ functions won't be able to default to the most recent snapshot.
|
54
56
|
|
55
|
-
|
57
|
+
### List Snapshots
|
56
58
|
|
57
59
|
```ruby
|
58
60
|
Property.snapshots
|
59
61
|
=> ['property_import_0011', 'property_import_0012']
|
60
62
|
```
|
61
63
|
|
64
|
+
### Restore Snapshot
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Property.restore_snapshot 'import_0012'
|
68
|
+
```
|
69
|
+
|
70
|
+
### Delete Snapshots
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
Property.delete_snapshot 'import_0012'
|
74
|
+
```
|
75
|
+
|
76
|
+
Or multiple snapshots:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
Property.delete_snapshots ['import_01', 'import_02']
|
80
|
+
Property.delete_snapshots # deletes all Property snapshots
|
81
|
+
|
82
|
+
# more complex: delete all snapshots more than one week old
|
83
|
+
week_old_name = Property.snapshot_name(1.week.ago)
|
84
|
+
Property.delete_snapshots { |name| name < week_old_name }
|
85
|
+
```
|
86
|
+
|
62
87
|
### Compute Differences
|
63
88
|
|
64
89
|
Now, to retrieve a list of the differences, call diff_snapshot:
|
65
90
|
|
66
91
|
```ruby
|
67
|
-
added,removed,changed =
|
92
|
+
added,removed,changed = Attachment.diff_snapshot # compute the change
|
93
|
+
=> [[], [], [<Attachment 1>]]
|
94
|
+
changed.first.original_attributes # returns original value for each field
|
95
|
+
=> {"name" => 'oldname'}
|
68
96
|
```
|
69
97
|
|
70
98
|
This computes the difference between the current table and the most recent
|
71
99
|
snapshot (determined alphabetically). Each value is an array of ActiveRecord
|
72
100
|
objects. `added` contains the records that have been added since the snapshot
|
73
101
|
was taken, `removed` contains the records that were removed, and `changed` contains
|
74
|
-
records where, of course, one or more of their columns have changed.
|
75
|
-
doesn't follow foreign keys
|
102
|
+
records where, of course, one or more of their columns have changed. Table Differ
|
103
|
+
doesn't follow foreign keys for that would be madness. If you want to discover
|
104
|
+
changes in related tables, you'll need to snapshot and diff them one by one.
|
76
105
|
|
77
106
|
Records in `added` and `changed` are regular ActiveRecord objects -- you can modify
|
78
107
|
their attributes and save them. Records in `removed`, however, aren't backed by
|
79
|
-
a database object and should be treated read-only.
|
108
|
+
a database object (obviously) and should be treated read-only.
|
80
109
|
|
81
110
|
Changed records include a hash of the original attributes before the change was
|
82
111
|
made. For example, if you changed the name column from 'Nexus' to 'Nexii':
|
@@ -101,25 +130,24 @@ Property.diff_snapshot ignore: %w[ id created_at updated_at ]
|
|
101
130
|
```
|
102
131
|
|
103
132
|
Note that if you ignore the primary key, Table Differ can no longer compute which
|
104
|
-
columns have changed.
|
105
|
-
|
133
|
+
columns have changed. This is no problem, but changed records will appear as a
|
134
|
+
remove followed by an add. The changed array will always be empty.
|
106
135
|
|
107
136
|
```ruby
|
108
137
|
added,removed = Attachment.diff_snapshot(ignore: 'id')
|
109
138
|
```
|
110
139
|
|
111
|
-
If there are other fields that
|
112
|
-
you can specify them in the unique_by option. This will
|
113
|
-
|
140
|
+
If there are other fields that uniquely identify the records,
|
141
|
+
you can specify them in the unique_by option. This will cause changes to
|
142
|
+
be computed, and the ActiveRecord objects returned are
|
114
143
|
complete with IDs. This requires one database lookup per returned object,
|
115
|
-
however so, if your results
|
144
|
+
however so, if your results are large, this might not be a good idea.
|
116
145
|
|
117
146
|
```ruby
|
118
147
|
# Normally ingoring the ID prevents diff from being able to compute the changed records.
|
119
|
-
# If we can
|
120
|
-
# then
|
148
|
+
# If we can use one or more fields to uniquely identify the object,
|
149
|
+
# then changesets can be computed and full ActiveRecord objects will be returned.
|
121
150
|
added,removed,changed = Contact.diff_snapshot(ignore: 'id', unique_by: [:property_id, :contact_id])
|
122
|
-
|
123
151
|
```
|
124
152
|
|
125
153
|
Also, if you ignore the ID, you won't be able to update or save any models directly.
|
@@ -131,23 +159,8 @@ normally and still knows its ID.
|
|
131
159
|
You can name the tables you want to diff explicitly:
|
132
160
|
|
133
161
|
```ruby
|
134
|
-
|
135
|
-
|
136
|
-
```
|
137
|
-
|
138
|
-
### Delete Snapshots
|
139
|
-
|
140
|
-
delete_snapshot gets rid of unwanted snapshots.
|
141
|
-
Pass an array of names or a proc to specify which snapshots should be deleted,
|
142
|
-
or `:all`.
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
Property.delete_snapshot 'import_0012'
|
146
|
-
Property.delete_snapshots :all
|
147
|
-
|
148
|
-
week_old_name = Property.snapshot_name(1.week.ago)
|
149
|
-
old_snapshots = Property.snapshots.select { |name| name < week_old_name }
|
150
|
-
Property.delete_snapshots(old_snapshots)
|
162
|
+
add,del,ch = Property.diff_snapshot(old: 'import_0012') # differences between the named snapshot and the table
|
163
|
+
add,del,ch = Property.diff_snapshot('cc', 'cd') # differences between the snapshots named cc and cd
|
151
164
|
```
|
152
165
|
|
153
166
|
## Internals
|
@@ -156,8 +169,16 @@ Table Differ creates a full copy of the table whenever Snapshot is called.
|
|
156
169
|
If your table is large enough that it would cause problems if it suddenly
|
157
170
|
doubled in size, then this is not the gem for you.
|
158
171
|
|
159
|
-
Table Differ
|
160
|
-
|
172
|
+
Table Differ creates and restores snapshots with a single CREATE/SELECT statement,
|
173
|
+
and it diffs the tables 100% server-side using two SELECTs. It should be fast
|
174
|
+
enough.
|
175
|
+
|
176
|
+
It doesn't touch indicies.
|
177
|
+
|
178
|
+
|
179
|
+
## Alternatives
|
180
|
+
|
181
|
+
* [Stellar](https://github.com/fastmonkeys/stellar) appears to do the same thing, written in Python.
|
161
182
|
|
162
183
|
|
163
184
|
## Contributing
|
data/TODO
CHANGED
data/lib/table_differ.rb
CHANGED
@@ -38,6 +38,14 @@ module TableDiffer
|
|
38
38
|
name
|
39
39
|
end
|
40
40
|
|
41
|
+
def restore_snapshot name
|
42
|
+
name = snapshot_name(name)
|
43
|
+
raise "#{name} doesn't exist" unless connection.tables.include?(name)
|
44
|
+
|
45
|
+
delete_all
|
46
|
+
connection.execute("INSERT INTO #{table_name} SELECT * FROM #{name}")
|
47
|
+
end
|
48
|
+
|
41
49
|
# deletes the named snapshot
|
42
50
|
def delete_snapshot name
|
43
51
|
connection.execute("DROP TABLE #{snapshot_name(name)}")
|
@@ -45,14 +53,15 @@ module TableDiffer
|
|
45
53
|
|
46
54
|
# deletes every snapshot named in the array
|
47
55
|
# Model.delete_snapshots(:all) deletes all snapshots
|
48
|
-
def delete_snapshots snaps
|
49
|
-
snaps =
|
56
|
+
def delete_snapshots snaps=self.snapshots, &block
|
57
|
+
snaps = snaps.select(&block) if block
|
50
58
|
snaps.each { |name| delete_snapshot(name) }
|
51
59
|
end
|
52
60
|
|
53
61
|
def table_differ_remap_objects params, records, table
|
54
62
|
model = self
|
55
63
|
if table != table_name
|
64
|
+
# create an exact copy of the model, but using a different table
|
56
65
|
model = Class.new(self)
|
57
66
|
model.table_name = table
|
58
67
|
end
|
@@ -75,7 +84,6 @@ module TableDiffer
|
|
75
84
|
end
|
76
85
|
end
|
77
86
|
|
78
|
-
# ignore: %w[ created_at updated_at id ]
|
79
87
|
def diff_snapshot options={}
|
80
88
|
oldtable = snapshot_name(options[:old]) || snapshots.last
|
81
89
|
newtable = snapshot_name(options[:new]) || table_name
|
data/lib/table_differ/version.rb
CHANGED
data/spec/snapshot_spec.rb
CHANGED
@@ -27,6 +27,27 @@ describe TableDiffer do
|
|
27
27
|
expect(Model.snapshots.sort).to eq ['models_aiee', 'models_bee', 'models_cee']
|
28
28
|
end
|
29
29
|
|
30
|
+
it "restores a snapshot" do
|
31
|
+
# TODO: test that it doesn't delete any indices
|
32
|
+
Model.create!(name: 'one')
|
33
|
+
first_id = Model.first.id # ensure the ID doesn't change
|
34
|
+
snapshot = Model.create_snapshot('snapname')
|
35
|
+
Model.create!(name: 'two')
|
36
|
+
|
37
|
+
Model.restore_snapshot(snapshot)
|
38
|
+
expect(Model.pluck(:id, :name).sort).to eq [[first_id, 'one']]
|
39
|
+
|
40
|
+
Model.delete_snapshot(snapshot)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "doesn't destroy the database if the snapshot can't be found" do
|
44
|
+
snapshot = Model.create_snapshot('snapname')
|
45
|
+
expect {
|
46
|
+
Model.restore_snapshot(snapshot+' ')
|
47
|
+
}.to raise_error(/doesn't exist/)
|
48
|
+
Model.delete_snapshot(snapshot)
|
49
|
+
end
|
50
|
+
|
30
51
|
it "deletes a named snapshot" do
|
31
52
|
Model.create_snapshot('snapname')
|
32
53
|
expect(Model.snapshots.size).to eq 1
|
@@ -34,24 +55,28 @@ describe TableDiffer do
|
|
34
55
|
expect(Model.snapshots.size).to eq 0
|
35
56
|
end
|
36
57
|
|
37
|
-
it "deletes
|
38
|
-
Model.create_snapshot('
|
39
|
-
Model.
|
40
|
-
Model.
|
41
|
-
|
42
|
-
to_delete = Model.snapshots.select do |snapname|
|
43
|
-
# return true for all tables with names divisble by 11
|
44
|
-
name = /(\d+)$/.match(snapname)[1]
|
45
|
-
(name.to_i % 11) == 0
|
46
|
-
end
|
47
|
-
Model.delete_snapshots(to_delete)
|
48
|
-
expect(Model.snapshots.sort).to eq ['models_21']
|
58
|
+
it "deletes all snapshots" do
|
59
|
+
Model.create_snapshot('snapname')
|
60
|
+
expect(Model.snapshots.size).to eq 1
|
61
|
+
Model.delete_snapshots
|
62
|
+
expect(Model.snapshots.size).to eq 0
|
49
63
|
end
|
50
64
|
|
51
|
-
it "
|
65
|
+
it "doesn't delete snapshots if none specified" do
|
52
66
|
Model.create_snapshot('snapname')
|
53
67
|
expect(Model.snapshots.size).to eq 1
|
54
|
-
Model.delete_snapshots
|
68
|
+
Model.delete_snapshots []
|
69
|
+
expect(Model.snapshots.size).to eq 1
|
70
|
+
Model.delete_snapshots(Model.snapshots)
|
55
71
|
expect(Model.snapshots.size).to eq 0
|
56
72
|
end
|
73
|
+
|
74
|
+
it "deletes a block of snapshots" do
|
75
|
+
Model.create_snapshot('21')
|
76
|
+
Model.create_snapshot('22')
|
77
|
+
Model.create_snapshot('33')
|
78
|
+
|
79
|
+
Model.delete_snapshots { |name| name == 'models_22' || name == 'models_33' }
|
80
|
+
expect(Model.snapshots.sort).to eq ['models_21']
|
81
|
+
end
|
57
82
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: table_differ
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.7'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Bronson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|