table_differ 0.5.0
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/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +149 -0
- data/Rakefile +8 -0
- data/TODO +2 -0
- data/lib/table_differ/version.rb +3 -0
- data/lib/table_differ.rb +89 -0
- data/spec/diff_spec.rb +177 -0
- data/spec/snapshot_spec.rb +50 -0
- data/spec/spec_helper.rb +64 -0
- data/tablediffer.gemspec +27 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 753fc1ecd3ac45fb1a11785069a8236127cde352
|
4
|
+
data.tar.gz: 10d8fd04834d8cf542e86717332f8ad831f48cf8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 383647ef5b7608af4b895548868e33459bc752ec9ba5c82700d1678229c166d2b219b858e35fdb6aead81a72c3c50aec80acf0cc7f4134c76222a6f39707c806
|
7
|
+
data.tar.gz: 809cf0d2b6519daf343f5f1d8e01f7b9f7739bdd71b7c811ff19b7702bc2997ad74f23fae0f09e85653b29921181c7f00e19155d376bbe3d51a1247eefe52505
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Scott Bronson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# Table Differ
|
2
|
+
|
3
|
+
Take snapshots of database tables and compute the differences between two snapshots.
|
4
|
+
|
5
|
+
[](http://travis-ci.org/bronson/table_differ)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
The usual, add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'table_differ'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Synopsis
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
Attachment.create_snapshot
|
19
|
+
=> "attachments_20140626_233336"
|
20
|
+
Attachment.first.touch # set updated_at to right now
|
21
|
+
=> true
|
22
|
+
added,removed,changed = Attachment.diff_snapshot
|
23
|
+
=> [[], [], [<Attachment 1>]]
|
24
|
+
changed.first.original_attributes # returns the fields that have changed
|
25
|
+
=> {"updated_at"=>Fri, 27 Jun 2014 05:45:56 UTC +00:00}
|
26
|
+
Attachment.delete_snapshot "attachments_20140626_233336"
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Include TableDiffer in models that will be snapshotted:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Property < ActiveRecord::Base
|
35
|
+
include TableDiffer
|
36
|
+
...
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
### Snapshot a Table
|
41
|
+
|
42
|
+
Any time you want to snapshot a table (say, before a new data import),
|
43
|
+
call `create_snapshot`.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
Property.create_snapshot
|
47
|
+
Property.create_snapshot 'import_0012'
|
48
|
+
```
|
49
|
+
|
50
|
+
If you don't specify a name then a numeric name based on the current
|
51
|
+
date will be used (something like `property_20140606_124722`)
|
52
|
+
Whatever naming scheme you use, the names need to sort alphabetically so
|
53
|
+
Table Differ can know which one is most recent.
|
54
|
+
|
55
|
+
Use the snapshots method to return all the snapshots that exist now:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Property.snapshots
|
59
|
+
=> ['property_import_0011', 'property_import_0012']
|
60
|
+
```
|
61
|
+
|
62
|
+
### Compute Differences
|
63
|
+
|
64
|
+
Now, to retrieve a list of the differences, call diff_snapshot:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
added,removed,changed = Property.diff_snapshot
|
68
|
+
```
|
69
|
+
|
70
|
+
This computes the difference between the current table and the most recent
|
71
|
+
snapshot (determined alphabetically). Each value is an array of ActiveRecord
|
72
|
+
objects. `added` contains the records that have been added since the snapshot
|
73
|
+
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. Tablediffer
|
75
|
+
doesn't follow foreign keys so, if you want that, you'll need to do it manually.
|
76
|
+
|
77
|
+
Records in `added` and `changed` are regular ActiveRecord objects -- you can modify
|
78
|
+
their attributes and save them. Records in `removed`, however, aren't backed by
|
79
|
+
a database object and should be treated read-only.
|
80
|
+
|
81
|
+
Changed records include a hash of the original attributes before the change was
|
82
|
+
made. For example, if you changed the name column from 'Nexus' to 'Nexii':
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
record.attributes
|
86
|
+
=> { 'id' => 1, 'name' => 'Nexus' }
|
87
|
+
record.original_attributes
|
88
|
+
=> { 'name' => 'Nexii' } # id didn't change so it's not included
|
89
|
+
```
|
90
|
+
|
91
|
+
Single-Table Inheritance (STI) appears to work correctly (TODO: add this to tests!)
|
92
|
+
|
93
|
+
|
94
|
+
#### Columns to Ignore
|
95
|
+
|
96
|
+
By default, every column will be considered in the diff.
|
97
|
+
You can pass columns to ignore like this:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
Property.diff_snapshot ignore: %w[ id created_at updated_at ]
|
101
|
+
```
|
102
|
+
|
103
|
+
Note that if you ignore the primary key, Table Differ can no longer compute which
|
104
|
+
columns have changed. Changed records will appear as a remove followed by an add,
|
105
|
+
so you can ignore the empty third array.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
added,removed = Attachment.diff_snapshot(ignore: 'id')
|
109
|
+
```
|
110
|
+
|
111
|
+
Also, if you ignore the ID, you won't be able to update or save any models directly.
|
112
|
+
You must copy the attributes to another model, one that was loaded from the database
|
113
|
+
normally and still knows its ID.
|
114
|
+
|
115
|
+
#### Specifying the Snapshot
|
116
|
+
|
117
|
+
You can name the tables you want to diff explicitly:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
a,r,c = Property.diff_snapshot(old: 'import_0012') # changes between the named snapshot and now
|
121
|
+
a,r,c = Property.diff_snapshot('cc', 'cd') # difference between the two snapshots named cc and cd
|
122
|
+
```
|
123
|
+
|
124
|
+
### Delete Snapshots
|
125
|
+
|
126
|
+
delete_snapshot gets rid of unwanted snapshots.
|
127
|
+
Either pass a name or a proc to specify which snapshots should be deleted.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Property.delete_snapshot 'import_0012'
|
131
|
+
|
132
|
+
week_old_name = Property.snapshot_name(1.week.ago)
|
133
|
+
old_snapshots = Property.snapshots.select { |name| name < week_old_name }
|
134
|
+
Property.delete_snapshots(old_snapshots)
|
135
|
+
```
|
136
|
+
|
137
|
+
## Internals
|
138
|
+
|
139
|
+
Table Differ creates a full copy of the table whenever Snapshot is called.
|
140
|
+
If your table is large enough that it would cause problems if it suddenly
|
141
|
+
doubled in size, then this is not the gem for you.
|
142
|
+
|
143
|
+
Table Differ diffs the tables server-side using only two SELECT queries.
|
144
|
+
This should be plenty fast for any normal usage.
|
145
|
+
|
146
|
+
|
147
|
+
## Contributing
|
148
|
+
|
149
|
+
Send issues and pull requests to [Table Differ's Github](github.com/bronson/table_differ).
|
data/Rakefile
ADDED
data/TODO
ADDED
data/lib/table_differ.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
require "active_record"
|
3
|
+
|
4
|
+
|
5
|
+
module TableDiffer
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
attr_accessor :original_attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# pass a date or name fragment, receive the full snapshot name.
|
14
|
+
# it's ok to pass a snapshot name; it will be returned unchaged.
|
15
|
+
def snapshot_name name
|
16
|
+
return nil if name.nil?
|
17
|
+
|
18
|
+
if name.kind_of?(Date) || name.kind_of?(Time)
|
19
|
+
name = name.strftime("%Y%m%d_%H%M%S")
|
20
|
+
end
|
21
|
+
|
22
|
+
unless name.index(table_name) == 0
|
23
|
+
name = "#{table_name}_#{name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
name
|
27
|
+
end
|
28
|
+
|
29
|
+
# returns an array of the snapshot names that currently exist
|
30
|
+
def snapshots
|
31
|
+
connection.tables.grep(/^#{table_name}_/).sort
|
32
|
+
end
|
33
|
+
|
34
|
+
# creates a new snapshot
|
35
|
+
def create_snapshot suggestion=Time.now
|
36
|
+
name = snapshot_name(suggestion)
|
37
|
+
connection.execute("CREATE TABLE #{name} AS SELECT * FROM #{table_name}")
|
38
|
+
name
|
39
|
+
end
|
40
|
+
|
41
|
+
# deletes the named snapshot
|
42
|
+
def delete_snapshot name
|
43
|
+
connection.execute("DROP TABLE #{snapshot_name(name)}")
|
44
|
+
end
|
45
|
+
|
46
|
+
# deletes every snapshot named in the array
|
47
|
+
def delete_snapshots snapshots
|
48
|
+
snapshots.each { |name| delete_snapshot(name) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# ignore: %w[ created_at updated_at id ]
|
52
|
+
def diff_snapshot options={}
|
53
|
+
oldtable = snapshot_name(options[:old]) || snapshots.last
|
54
|
+
newtable = snapshot_name(options[:new]) || table_name
|
55
|
+
|
56
|
+
ignore = []
|
57
|
+
if options[:ignore]
|
58
|
+
ignore = Array(options[:ignore]).map(&:to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
columns = column_names - ignore
|
62
|
+
cols = columns.map { |c| "#{c} as #{c}" }.join(", ")
|
63
|
+
|
64
|
+
added = find_by_sql("SELECT #{cols} FROM #{newtable} EXCEPT SELECT #{cols} FROM #{oldtable}")
|
65
|
+
removed = find_by_sql("SELECT #{cols} from #{oldtable} EXCEPT SELECT #{cols} FROM #{newtable}")
|
66
|
+
|
67
|
+
# hm, none of this seems to matter... TODO: mark appropriate objects read-only: obj.readonly!
|
68
|
+
# AR always thinks the record is persisted in the db, even when it obviously isn't
|
69
|
+
# added.each { |o| o.instance_variable_set("@new_record", true) } unless table_name == oldtable
|
70
|
+
# removed.each { |o| o.instance_variable_set("@new_record", true) } unless table_name == newtable
|
71
|
+
# actually, it's probably more reliable just to use the presence of an id to determine if the record can be saved
|
72
|
+
# [*added, *removed].select { |o| !o.id }.each { |o| o.instance_variable_set("@new_record", true) }
|
73
|
+
|
74
|
+
changed = added & removed
|
75
|
+
changed.each do |obj|
|
76
|
+
orig = removed.find { |r| r == obj }
|
77
|
+
raise "this is impossible" if orig.nil?
|
78
|
+
|
79
|
+
nattrs = obj.attributes
|
80
|
+
# remove all unchanged elements -- original_attributes only contains changed values
|
81
|
+
oattrs = orig.attributes.reject { |k,v| nattrs.include?(k) && nattrs[k] == v }
|
82
|
+
|
83
|
+
obj.original_attributes = HashWithIndifferentAccess.new(oattrs)
|
84
|
+
end
|
85
|
+
|
86
|
+
[added - changed, removed - changed, changed]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/diff_spec.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
describe "diffing a model" do
|
2
|
+
include_context "model"
|
3
|
+
|
4
|
+
before(:each) do
|
5
|
+
Model.create!(name: 'one')
|
6
|
+
Model.create!(name: 'two')
|
7
|
+
Model.create_snapshot('original')
|
8
|
+
end
|
9
|
+
|
10
|
+
# around(:all) do |group|
|
11
|
+
# # puts 'before'
|
12
|
+
# # group.run_examples
|
13
|
+
# # puts 'after'
|
14
|
+
# end
|
15
|
+
|
16
|
+
|
17
|
+
describe "with IDs" do
|
18
|
+
it "detects no changes" do
|
19
|
+
added,removed,changed = Model.diff_snapshot
|
20
|
+
|
21
|
+
expect(added).to eq []
|
22
|
+
expect(removed).to eq []
|
23
|
+
expect(changed).to eq []
|
24
|
+
end
|
25
|
+
|
26
|
+
it "detects an added record" do
|
27
|
+
three = Model.create!(name: 'three')
|
28
|
+
added,removed,changed = Model.diff_snapshot
|
29
|
+
|
30
|
+
expect(added).to eq [three]
|
31
|
+
expect(added.first.new_record?).to eq false
|
32
|
+
expect(removed).to eq []
|
33
|
+
expect(changed).to eq []
|
34
|
+
|
35
|
+
# added records are normal AR objects, try using it
|
36
|
+
added.first.update_attributes!(name: 'trois')
|
37
|
+
expect(Model.find(added.first.id).name).to eq 'trois'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "detects a removed record" do
|
41
|
+
two = Model.where(name: 'two').first.destroy
|
42
|
+
added,removed,changed = Model.diff_snapshot
|
43
|
+
|
44
|
+
expect(added).to eq []
|
45
|
+
expect(removed).to eq [two]
|
46
|
+
expect(removed.first.new_record?).to eq false
|
47
|
+
expect(changed).to eq []
|
48
|
+
|
49
|
+
# calling save on the returned record should do nothing
|
50
|
+
expect(Model.count).to eq 1
|
51
|
+
removed.first.save!
|
52
|
+
expect(Model.count).to eq 1
|
53
|
+
end
|
54
|
+
|
55
|
+
it "detects a changed field" do
|
56
|
+
one = Model.where(name: 'one').first
|
57
|
+
one.update_attributes!(name: 'uno')
|
58
|
+
added,removed,changed = Model.diff_snapshot
|
59
|
+
|
60
|
+
expect(added).to eq []
|
61
|
+
expect(removed).to eq []
|
62
|
+
expect(changed).to eq [one]
|
63
|
+
expect(changed.first.name).to eq 'uno'
|
64
|
+
|
65
|
+
# ensure we can access the previous value, with indifferent access
|
66
|
+
expect(changed.first.original_attributes[:name]).to eq 'one'
|
67
|
+
expect(changed.first.original_attributes).to eq({'name' => 'one'})
|
68
|
+
|
69
|
+
# changed records are normal AR objects, try using it
|
70
|
+
changed.first.update_attributes!(name: 'nuevo')
|
71
|
+
expect(Model.find(changed.first.id).name).to eq 'nuevo'
|
72
|
+
end
|
73
|
+
|
74
|
+
it "resurrects a removed record" do
|
75
|
+
Model.where(name: 'two').first.destroy
|
76
|
+
_,removed,_ = Model.diff_snapshot
|
77
|
+
|
78
|
+
expect(Model.count).to eq 1
|
79
|
+
# we're expicitly setting the ID to the previous ID, that might not be ok?
|
80
|
+
Model.create!(removed.first.attributes)
|
81
|
+
expect(Model.count).to eq 2
|
82
|
+
|
83
|
+
# and now there are no differences
|
84
|
+
differences = Model.diff_snapshot
|
85
|
+
expect(differences).to eq [[], [], []]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# if we can't trust the model's primary key, we can't tell if anything
|
91
|
+
# changed. we can only see what's new and what's been deleted.
|
92
|
+
describe "ignoring IDs" do
|
93
|
+
it "detects no changes" do
|
94
|
+
added,removed,changed = Model.diff_snapshot(ignore: :id)
|
95
|
+
|
96
|
+
expect(added).to eq []
|
97
|
+
expect(removed).to eq []
|
98
|
+
expect(changed).to eq []
|
99
|
+
end
|
100
|
+
|
101
|
+
it "detects an added record" do
|
102
|
+
Model.create!(name: 'three')
|
103
|
+
added,removed = Model.diff_snapshot(ignore: :id)
|
104
|
+
|
105
|
+
expect(added.map(&:attributes)).to eq [{"id" => nil, "name" => "three"}]
|
106
|
+
expect(added.first.new_record?).to eq false # oh well
|
107
|
+
expect(removed).to eq []
|
108
|
+
|
109
|
+
# wthout an ID, updating attributes should do nothing
|
110
|
+
added.first.update_attributes!(name: 'trois')
|
111
|
+
expect(Model.count).to eq 3
|
112
|
+
expect(Model.pluck(:name).sort).to eq ['one', 'three', 'two'] # no trois
|
113
|
+
end
|
114
|
+
|
115
|
+
it "detects a removed record" do
|
116
|
+
Model.where(name: 'two').first.destroy
|
117
|
+
added,removed,changed = Model.diff_snapshot(ignore: :id)
|
118
|
+
|
119
|
+
expect(added).to eq []
|
120
|
+
expect(removed.map(&:attributes)).to eq [{"id" => nil, "name" => "two"}]
|
121
|
+
expect(removed.first.new_record?).to eq false # oh well
|
122
|
+
expect(changed).to eq []
|
123
|
+
end
|
124
|
+
|
125
|
+
# without an ID, we can't tell if anything changed
|
126
|
+
it "detects a changed field" do
|
127
|
+
one = Model.where(name: 'one').first
|
128
|
+
one.update_attributes!(name: 'uno')
|
129
|
+
added,removed,changed = Model.diff_snapshot(ignore: :id)
|
130
|
+
|
131
|
+
expect(added.map(&:attributes)).to eq [{"id" => nil, "name" => "uno"}]
|
132
|
+
expect(removed.map(&:attributes)).to eq [{"id" => nil, "name" => "one"}]
|
133
|
+
expect(changed).to eq []
|
134
|
+
end
|
135
|
+
|
136
|
+
it "resurrects a removed record" do
|
137
|
+
Model.where(name: 'two').first.destroy
|
138
|
+
_,removed,_ = Model.diff_snapshot(ignore: :id)
|
139
|
+
|
140
|
+
expect(Model.count).to eq 1
|
141
|
+
Model.create!(removed.first.attributes)
|
142
|
+
expect(Model.count).to eq 2
|
143
|
+
|
144
|
+
differences = Model.diff_snapshot(ignore: :id)
|
145
|
+
expect(differences).to eq [[], [], []]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# ensure we select the correct snapshots to diff between
|
150
|
+
it "uses the correct snapshot" do
|
151
|
+
insecond = Model.create!(name: 'only in second')
|
152
|
+
name = Model.create_snapshot('second')
|
153
|
+
expect(name).to eq "models_second"
|
154
|
+
main = Model.create!(name: 'only in main table')
|
155
|
+
|
156
|
+
|
157
|
+
# each of the following should be an individual test.
|
158
|
+
# not sure how I can make them all use the same db setup though...
|
159
|
+
# rspec really really needs a before(:all) { }.
|
160
|
+
|
161
|
+
# first make sure default diffs newer table
|
162
|
+
differences = Model.diff_snapshot
|
163
|
+
expect(differences).to eq [[main], [], []]
|
164
|
+
|
165
|
+
# now diff against older snapshot, ensure more changes
|
166
|
+
differences = Model.diff_snapshot old: 'original'
|
167
|
+
expect(differences).to eq [[insecond, main], [], []]
|
168
|
+
|
169
|
+
# specifying an older snapshot produces a reverse diff against the most recent snapshot
|
170
|
+
differences = Model.diff_snapshot new: 'models_original'
|
171
|
+
expect(differences).to eq [[], [insecond], []]
|
172
|
+
|
173
|
+
# finally, specify two named snapshots
|
174
|
+
differences = Model.diff_snapshot old: 'original', new: 'models_second'
|
175
|
+
expect(differences).to eq [[insecond], [], []]
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
describe TableDiffer do
|
2
|
+
include_context "model"
|
3
|
+
|
4
|
+
it "takes a snapshot" do
|
5
|
+
expect(Model.snapshots.size).to eq 0
|
6
|
+
Model.create_snapshot
|
7
|
+
expect(Model.snapshots.size).to eq 1
|
8
|
+
end
|
9
|
+
|
10
|
+
it "takes a name for a snapshot" do
|
11
|
+
expect(Model.snapshots.size).to eq 0
|
12
|
+
Model.create_snapshot('snapname')
|
13
|
+
expect(Model.snapshots).to eq ['models_snapname']
|
14
|
+
end
|
15
|
+
|
16
|
+
it "errors out if asked to create a duplicate snapshot" do
|
17
|
+
Model.create_snapshot('snapname')
|
18
|
+
expect {
|
19
|
+
Model.create_snapshot('snapname')
|
20
|
+
}.to raise_error(ActiveRecord::StatementInvalid, /already exists/)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns a list of snapshots" do
|
24
|
+
Model.create_snapshot('aiee')
|
25
|
+
Model.create_snapshot('bee')
|
26
|
+
Model.create_snapshot('cee')
|
27
|
+
expect(Model.snapshots.sort).to eq ['models_aiee', 'models_bee', 'models_cee']
|
28
|
+
end
|
29
|
+
|
30
|
+
it "deletes a named snapshot" do
|
31
|
+
Model.create_snapshot('snapname')
|
32
|
+
expect(Model.snapshots.size).to eq 1
|
33
|
+
Model.delete_snapshot('snapname')
|
34
|
+
expect(Model.snapshots.size).to eq 0
|
35
|
+
end
|
36
|
+
|
37
|
+
it "deletes a bunch of snapshots" do
|
38
|
+
Model.create_snapshot('21')
|
39
|
+
Model.create_snapshot('22')
|
40
|
+
Model.create_snapshot('33')
|
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']
|
49
|
+
end
|
50
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'table_differ'
|
2
|
+
require 'database_cleaner'
|
3
|
+
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.order = :random
|
7
|
+
|
8
|
+
config.expect_with :rspec do |expectations|
|
9
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
10
|
+
expectations.syntax = :expect
|
11
|
+
end
|
12
|
+
|
13
|
+
config.before(:suite) do
|
14
|
+
if false
|
15
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
16
|
+
ActiveRecord::Base.logger.level = Logger::ERROR
|
17
|
+
ActiveRecord::Base.logger.level = Logger::DEBUG
|
18
|
+
end
|
19
|
+
|
20
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
21
|
+
|
22
|
+
ActiveRecord::Schema.define do
|
23
|
+
create_table :models do |table|
|
24
|
+
table.column :name, :string
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# need to explicitly specify active_record since we don't have a database.yml?
|
29
|
+
DatabaseCleaner[:active_record].strategy = :transaction
|
30
|
+
DatabaseCleaner.clean_with(:truncation)
|
31
|
+
end
|
32
|
+
|
33
|
+
config.around(:each) do |example|
|
34
|
+
DatabaseCleaner.cleaning do
|
35
|
+
example.run
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
RSpec.shared_context "model" do
|
42
|
+
class Model < ActiveRecord::Base
|
43
|
+
include TableDiffer
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
=begin
|
48
|
+
maybe later...
|
49
|
+
|
50
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
51
|
+
# file, and it's useful to allow more verbose output when running an
|
52
|
+
# individual spec file.
|
53
|
+
if config.files_to_run.one?
|
54
|
+
# Use the documentation formatter for detailed output,
|
55
|
+
# unless a formatter has already been configured
|
56
|
+
# (e.g. via a command-line flag).
|
57
|
+
config.default_formatter = 'doc'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Print the 10 slowest examples and example groups at the
|
61
|
+
# end of the spec run, to help surface which specs are running
|
62
|
+
# particularly slow.
|
63
|
+
config.profile_examples = 10
|
64
|
+
=end
|
data/tablediffer.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'table_differ/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "table_differ"
|
7
|
+
spec.version = TableDiffer::VERSION
|
8
|
+
spec.authors = ["Scott Bronson"]
|
9
|
+
spec.email = ["brons_tablediffer@rinspin.com"]
|
10
|
+
spec.summary = %q{Take snapshots of database tables and compute the differences between two snapshots.}
|
11
|
+
# spec.description = %q{}
|
12
|
+
spec.homepage = "https://github.com/bronson/table_differ"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "activerecord"
|
21
|
+
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
# spec.add_development_dependency "rspec_around_all"
|
25
|
+
spec.add_development_dependency "database_cleaner"
|
26
|
+
spec.add_development_dependency "sqlite3"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: table_differ
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Scott Bronson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: database_cleaner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- brons_tablediffer@rinspin.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".travis.yml"
|
93
|
+
- CHANGELOG.md
|
94
|
+
- Gemfile
|
95
|
+
- Guardfile
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- TODO
|
100
|
+
- lib/table_differ.rb
|
101
|
+
- lib/table_differ/version.rb
|
102
|
+
- spec/diff_spec.rb
|
103
|
+
- spec/snapshot_spec.rb
|
104
|
+
- spec/spec_helper.rb
|
105
|
+
- tablediffer.gemspec
|
106
|
+
homepage: https://github.com/bronson/table_differ
|
107
|
+
licenses:
|
108
|
+
- MIT
|
109
|
+
metadata: {}
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 2.2.2
|
127
|
+
signing_key:
|
128
|
+
specification_version: 4
|
129
|
+
summary: Take snapshots of database tables and compute the differences between two
|
130
|
+
snapshots.
|
131
|
+
test_files:
|
132
|
+
- spec/diff_spec.rb
|
133
|
+
- spec/snapshot_spec.rb
|
134
|
+
- spec/spec_helper.rb
|