table_differ 0.6.10 → 0.7
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 +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
|
[](http://travis-ci.org/bronson/table_differ)
|
6
6
|
[](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
|