pg_shrink 0.0.2
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 +24 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/README.md +92 -0
- data/Rakefile +10 -0
- data/Shrinkfile.example +74 -0
- data/bin/pg_shrink +44 -0
- data/lib/pg_shrink/database/postgres.rb +91 -0
- data/lib/pg_shrink/database.rb +61 -0
- data/lib/pg_shrink/sub_table_filter.rb +21 -0
- data/lib/pg_shrink/sub_table_operator.rb +44 -0
- data/lib/pg_shrink/sub_table_sanitizer.rb +33 -0
- data/lib/pg_shrink/table.rb +159 -0
- data/lib/pg_shrink/table_filter.rb +14 -0
- data/lib/pg_shrink/table_sanitizer.rb +14 -0
- data/lib/pg_shrink/version.rb +3 -0
- data/lib/pg_shrink.rb +59 -0
- data/pg_shrink.gemspec +34 -0
- data/spec/Shrinkfile.basic +6 -0
- data/spec/pg_config.yml +6 -0
- data/spec/pg_shrink/database/postgres_spec.rb +86 -0
- data/spec/pg_shrink/database_spec.rb +26 -0
- data/spec/pg_shrink/table_spec.rb +158 -0
- data/spec/pg_shrink_spec.rb +459 -0
- data/spec/pg_spec_helper.rb +45 -0
- data/spec/spec_helper.rb +4 -0
- metadata +262 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
module PgShrink
|
2
|
+
class TableFilter
|
3
|
+
attr_accessor :table
|
4
|
+
def initialize(table, opts, &block)
|
5
|
+
self.table = table
|
6
|
+
@opts = opts # Currently not used, but who knows
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def apply(hash)
|
11
|
+
@block.call(hash)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module PgShrink
|
2
|
+
class TableSanitizer
|
3
|
+
attr_accessor :table
|
4
|
+
def initialize(table, opts, &block)
|
5
|
+
self.table = table
|
6
|
+
@opts = opts # Currently not used, but who knows
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def apply(hash)
|
11
|
+
@block.call(hash)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/pg_shrink.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "active_support"
|
3
|
+
require "active_support/inflector"
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'active_support/core_ext/hash'
|
6
|
+
require "pg_shrink/version"
|
7
|
+
require "pg_shrink/database"
|
8
|
+
require "pg_shrink/database/postgres"
|
9
|
+
require "pg_shrink/table_filter"
|
10
|
+
require "pg_shrink/table_sanitizer"
|
11
|
+
require "pg_shrink/sub_table_operator"
|
12
|
+
require "pg_shrink/sub_table_filter"
|
13
|
+
require "pg_shrink/sub_table_sanitizer"
|
14
|
+
require "pg_shrink/table"
|
15
|
+
module PgShrink
|
16
|
+
|
17
|
+
def self.blank_options
|
18
|
+
{
|
19
|
+
url: nil,
|
20
|
+
config: 'Shrinkfile',
|
21
|
+
force: false
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.valid_pg_url?(url)
|
26
|
+
uri = URI.parse(url)
|
27
|
+
uri.scheme == 'postgres' && !uri.user.blank? && uri.path != '/'
|
28
|
+
rescue => ex
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.run(options)
|
33
|
+
unless File.exists?(options[:config])
|
34
|
+
raise "Could not find file: #{options[:config]}"
|
35
|
+
end
|
36
|
+
|
37
|
+
unless valid_pg_url?(options[:url])
|
38
|
+
raise "Invalid postgres url: #{options[:url]}"
|
39
|
+
end
|
40
|
+
|
41
|
+
database = Database::Postgres.new(:postgres_url => options[:url])
|
42
|
+
|
43
|
+
database.instance_eval(File.read(options[:config]), options[:config], 1)
|
44
|
+
|
45
|
+
# TODO: Figure out how to write a spec for this.
|
46
|
+
unless options[:force] == true
|
47
|
+
puts 'WARNING: pg_shrink is destructive! It will change this database in place.'
|
48
|
+
puts 'Are you sure you want to continue? (y/N)'
|
49
|
+
cont = gets
|
50
|
+
cont = cont.strip
|
51
|
+
unless cont == 'y' || cont == 'Y'
|
52
|
+
puts 'Aborting!'
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
database.shrink!
|
58
|
+
end
|
59
|
+
end
|
data/pg_shrink.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pg_shrink/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pg_shrink"
|
8
|
+
spec.version = PgShrink::VERSION
|
9
|
+
spec.authors = ["Kevin Ball"]
|
10
|
+
spec.email = ["kmball11@gmail.com"]
|
11
|
+
spec.description = "pg_shrink makes it simple to shrink and sanitize a psql database"
|
12
|
+
spec.summary = ""
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = ""
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'pg'
|
22
|
+
spec.add_runtime_dependency 'activesupport'
|
23
|
+
spec.add_runtime_dependency 'sequel'
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "rspec-nc"
|
28
|
+
spec.add_development_dependency "rspec-mocks"
|
29
|
+
spec.add_development_dependency "guard"
|
30
|
+
spec.add_development_dependency "guard-rspec"
|
31
|
+
spec.add_development_dependency "pry"
|
32
|
+
spec.add_development_dependency "pry-remote"
|
33
|
+
spec.add_development_dependency "pry-nav"
|
34
|
+
end
|
data/spec/pg_config.yml
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pg_spec_helper'
|
3
|
+
|
4
|
+
describe PgShrink::Database::Postgres do
|
5
|
+
let(:db) do
|
6
|
+
PgShrink::Database::Postgres.new(PgSpecHelper.pg_config.merge(:batch_size => 5))
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
PgSpecHelper.reset_database
|
11
|
+
PgSpecHelper.create_table(db.connection, :test_table,
|
12
|
+
{'name' => 'character(128)', 'test' => 'integer'})
|
13
|
+
end
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
PgSpecHelper.clear_table(db.connection, :test_table)
|
17
|
+
end
|
18
|
+
|
19
|
+
context "A simple postgres database" do
|
20
|
+
it "sets up a Sequel connection" do
|
21
|
+
expect(db.connection.class).to eq(Sequel::Postgres::Database)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "with 20 simple records" do
|
25
|
+
before(:each) do
|
26
|
+
(1..20).each do |i|
|
27
|
+
db.connection.run(
|
28
|
+
"insert into test_table (name, test) values ('test', #{i})"
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "can fetch records in batches" do
|
34
|
+
batch_sizes = []
|
35
|
+
db.records_in_batches(:test_table) do |batch|
|
36
|
+
batch_sizes << batch.size
|
37
|
+
end
|
38
|
+
expect(batch_sizes).to eq([5, 5, 5, 5])
|
39
|
+
end
|
40
|
+
|
41
|
+
it "throws an error if records change their primary keys during update" do
|
42
|
+
old_records = db.connection["select * from test_table where test <= 5"].
|
43
|
+
all
|
44
|
+
new_records = old_records.map {|r| r.merge(:id => r[:id] * 10)}
|
45
|
+
expect {db.update_records(:test_table, old_records, new_records)}.
|
46
|
+
to raise_error
|
47
|
+
end
|
48
|
+
|
49
|
+
it "can delete records based on a condition" do
|
50
|
+
db.delete_records(:test_table, {:test => 1..5})
|
51
|
+
|
52
|
+
results = db.connection["select * from test_table"].all
|
53
|
+
expect(results.size).to eq(15)
|
54
|
+
expect(results.map {|r| r[:test]}).to match_array((6..20).to_a)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "can update records" do
|
58
|
+
old_records = db.connection["select * from test_table where test <= 5"].
|
59
|
+
all
|
60
|
+
new_records = old_records.map {|r| r.merge(:test => r[:test] * 10)}
|
61
|
+
db.update_records(:test_table, old_records, new_records)
|
62
|
+
expect(
|
63
|
+
db.connection["select * from test_table where test <= 5"].all.size
|
64
|
+
).to eq(0)
|
65
|
+
updated_records = db.connection[:test_table].
|
66
|
+
where(:id => old_records.map {|r| r[:id]}).all
|
67
|
+
expect(updated_records.size).to eq(old_records.size)
|
68
|
+
expect(updated_records).to eq(new_records)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "throws an error if you try to delete records in update" do
|
72
|
+
old_records = db.connection["select * from test_table where test <= 5"].
|
73
|
+
all
|
74
|
+
new_records = old_records.first(2)
|
75
|
+
expect {db.update_records(:test_table, old_records, new_records)}.
|
76
|
+
to raise_error
|
77
|
+
end
|
78
|
+
|
79
|
+
it "deletes the whole table" do
|
80
|
+
db.remove_table(:test_table)
|
81
|
+
db.filter!
|
82
|
+
expect(db.connection["select * from test_table"].all.size).to eq(0)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PgShrink::Database do
|
4
|
+
let(:db) {PgShrink::Database.new}
|
5
|
+
|
6
|
+
it "should yield a table to filter_table" do
|
7
|
+
db.filter_table(:test_table) do |tb|
|
8
|
+
expect(tb.class).to eq(PgShrink::Table)
|
9
|
+
expect(tb.database).to eq(db)
|
10
|
+
expect(tb.table_name).to eq(:test_table)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should allow options to set a different primary_key in filter_table" do
|
15
|
+
db.filter_table(:test_table, :primary_key => :foo) do |tb|
|
16
|
+
expect(tb.primary_key).to eq(:foo)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should retain options in later invocations" do
|
21
|
+
db.filter_table(:test_table, :primary_key => :foo) {}
|
22
|
+
db.filter_table(:test_table) do |tb|
|
23
|
+
expect(tb.primary_key).to eq(:foo)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PgShrink::Table do
|
4
|
+
context "when a filter is specified" do
|
5
|
+
let(:database) {PgShrink::Database.new}
|
6
|
+
let(:table) { PgShrink::Table.new(database, :test_table) }
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
table.filter_by {|test| test[:u] == 1 }
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should add filter to filters array" do
|
13
|
+
expect(table.filters.size).to eq(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should accept values that match the block" do
|
17
|
+
expect(table.filters.first.apply({:u => 1})).to eq(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should reject values that don't match the block" do
|
21
|
+
expect(table.filters.first.apply({:u => 2})).to eq(false)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when running filters" do
|
25
|
+
it "should return matching subset" do
|
26
|
+
test_data = [{:u => 1}, {:u => 2}]
|
27
|
+
expect(table).to receive(:records_in_batches).and_yield(test_data)
|
28
|
+
expect(table).to receive(:delete_records) do |old_batch, new_batch|
|
29
|
+
expect(old_batch.size).to eq(2)
|
30
|
+
expect(new_batch.size).to eq(1)
|
31
|
+
expect(new_batch.first).to eq({:u => 1})
|
32
|
+
end
|
33
|
+
table.filter!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when locked" do
|
38
|
+
before(:each) do
|
39
|
+
table.lock { |test| !!test[:lock] }
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not filter locked records" do
|
43
|
+
test_data = [{:u => 1, :lock => false},
|
44
|
+
{:u => 2, :lock => false},
|
45
|
+
{:u => 2, :lock => true}]
|
46
|
+
allow(table).to receive(:records_in_batches).and_yield(test_data)
|
47
|
+
allow(table).to receive(:delete_records) do |old_batch, new_batch|
|
48
|
+
expect(old_batch.size).to eq(3)
|
49
|
+
expect(new_batch.size).to eq(2)
|
50
|
+
expect(new_batch).
|
51
|
+
to eq([{:u => 1, :lock => false}, {:u => 2, :lock => true}])
|
52
|
+
end
|
53
|
+
table.filter!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when a sanitizer is specified" do
|
59
|
+
let(:database) {PgShrink::Database.new}
|
60
|
+
let(:table) { PgShrink::Table.new(database, :test_table) }
|
61
|
+
before(:each) do
|
62
|
+
table.sanitize do |test|
|
63
|
+
test[:u] = -test[:u]
|
64
|
+
test
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should add sanitizer to sanitizers array" do
|
69
|
+
expect(table.sanitizers.size).to eq(1)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should alter values based on the block" do
|
73
|
+
expect(table.sanitizers.first.apply({:u => 1})).to eq({:u => -1})
|
74
|
+
end
|
75
|
+
|
76
|
+
context "when running sanitizers" do
|
77
|
+
it "returns an altered set of records" do
|
78
|
+
test_data = [{:u => 1}, {:u => 2}]
|
79
|
+
expect(table).to receive(:records_in_batches).and_yield(test_data)
|
80
|
+
expect(table).to receive(:update_records) do |old_batch, new_batch|
|
81
|
+
expect(old_batch).to eq(test_data)
|
82
|
+
expect(new_batch).to eq([{:u => -1}, {:u => -2}])
|
83
|
+
end
|
84
|
+
table.sanitize!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when a subtable filter is specified" do
|
90
|
+
let(:database) {PgShrink::Database.new}
|
91
|
+
let(:table) { PgShrink::Table.new(database, :test_table) }
|
92
|
+
|
93
|
+
before(:each) do
|
94
|
+
table.filter_subtable(:subtable)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "yields back a table so additional manipulations can be made" do
|
98
|
+
table.filter_subtable(:subtable) do |f|
|
99
|
+
expect(f.class).to eq(PgShrink::Table)
|
100
|
+
expect(f.table_name).to eq(:subtable)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it "adds subtable_filter to subtable_filters array" do
|
105
|
+
expect(table.subtable_filters.size).to eq(1)
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "when running filters" do
|
109
|
+
before(:each) do
|
110
|
+
table.filter_by do |test|
|
111
|
+
!!test[:u]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
it "runs subtable filters with old and new batches" do
|
116
|
+
test_data = [{:u => true}, {:u => false}]
|
117
|
+
expect(table).to receive(:records_in_batches).and_yield(test_data)
|
118
|
+
expect(database).to receive(:delete_records)
|
119
|
+
expect(table).to receive(:filter_subtables) do |old_batch, new_batch|
|
120
|
+
expect(old_batch).to eq(test_data)
|
121
|
+
expect(new_batch).to eq([{:u => true}])
|
122
|
+
end
|
123
|
+
table.filter!
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
context "when a remove is specified" do
|
128
|
+
let(:database) {PgShrink::Database.new}
|
129
|
+
let(:table) { PgShrink::Table.new(database, :test_table) }
|
130
|
+
let(:test_data) {[{:u => 1}, {:u => 2}]}
|
131
|
+
|
132
|
+
before(:each) do
|
133
|
+
table.mark_for_removal!
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should by default remove all" do
|
137
|
+
expect(table).to receive(:records_in_batches).and_yield(test_data)
|
138
|
+
expect(table).to receive(:delete_records) do |old_batch, new_batch|
|
139
|
+
expect(old_batch).to eq(test_data)
|
140
|
+
expect(new_batch).to eq([])
|
141
|
+
end
|
142
|
+
table.shrink!
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should allow locking of records" do
|
146
|
+
table.lock do |u|
|
147
|
+
u[:u] == 1
|
148
|
+
end
|
149
|
+
expect(table).to receive(:records_in_batches).and_yield(test_data)
|
150
|
+
expect(table).to receive(:delete_records) do |old_batch, new_batch|
|
151
|
+
expect(old_batch).to eq(test_data)
|
152
|
+
expect(new_batch).to eq([{:u => 1}])
|
153
|
+
end
|
154
|
+
table.shrink!
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|