pg_shrink 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module PgShrink
2
+ VERSION = "0.0.2"
3
+ 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
@@ -0,0 +1,6 @@
1
+ filter_table :users do |f|
2
+ f.filter_by do |u|
3
+ u[:name] == "test 1"
4
+ end
5
+ f.filter_subtable(:user_preferences, :foreign_key => :user_id)
6
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ database: test_pg_shrink
3
+ user: postgres
4
+ password:
5
+ host: localhost
6
+ port:
@@ -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