pg_shrink 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|