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.
@@ -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