pgdice 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +66 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +6 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Guardfile +31 -0
- data/LICENSE +21 -0
- data/README.md +262 -0
- data/Rakefile +12 -0
- data/bin/_guard-core +29 -0
- data/bin/console +15 -0
- data/bin/guard +29 -0
- data/bin/setup +8 -0
- data/lib/pgdice/configuration.rb +122 -0
- data/lib/pgdice/database_connection.rb +30 -0
- data/lib/pgdice/partition_helper.rb +69 -0
- data/lib/pgdice/partition_manager.rb +86 -0
- data/lib/pgdice/pg_slice_manager.rb +119 -0
- data/lib/pgdice/table_dropper.rb +26 -0
- data/lib/pgdice/validation.rb +132 -0
- data/lib/pgdice/version.rb +5 -0
- data/lib/pgdice.rb +109 -0
- data/pgdice.gemspec +44 -0
- metadata +358 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for configuration
|
4
|
+
module PgDice
|
5
|
+
class << self
|
6
|
+
attr_accessor :configuration
|
7
|
+
|
8
|
+
def configure
|
9
|
+
self.configuration ||= PgDice::Configuration.new
|
10
|
+
yield(configuration)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Configuration class which holds all configurable values
|
15
|
+
class Configuration
|
16
|
+
def self.days_ago(days)
|
17
|
+
Time.now.utc - days * 24 * 60 * 60
|
18
|
+
end
|
19
|
+
|
20
|
+
VALUES = { logger: Logger.new(STDOUT),
|
21
|
+
database_url: nil,
|
22
|
+
additional_validators: [],
|
23
|
+
approved_tables: [],
|
24
|
+
older_than: PgDice::Configuration.days_ago(90),
|
25
|
+
dry_run: false,
|
26
|
+
table_drop_batch_size: 7 }.freeze
|
27
|
+
|
28
|
+
attr_writer :logger,
|
29
|
+
:database_url,
|
30
|
+
:additional_validators,
|
31
|
+
:approved_tables,
|
32
|
+
:older_than,
|
33
|
+
:dry_run,
|
34
|
+
:table_drop_batch_size,
|
35
|
+
:database_connection,
|
36
|
+
:pg_connection
|
37
|
+
|
38
|
+
attr_accessor :table_dropper,
|
39
|
+
:pg_slice_manager,
|
40
|
+
:partition_manager,
|
41
|
+
:partition_helper
|
42
|
+
|
43
|
+
def initialize(existing_configuration = nil)
|
44
|
+
VALUES.each do |key, value|
|
45
|
+
initialize_value(key, value, existing_configuration)
|
46
|
+
end
|
47
|
+
initialize_objects
|
48
|
+
end
|
49
|
+
|
50
|
+
def logger
|
51
|
+
return @logger unless @logger.nil?
|
52
|
+
|
53
|
+
raise PgDice::InvalidConfigurationError, 'logger must be present!'
|
54
|
+
end
|
55
|
+
|
56
|
+
def database_url
|
57
|
+
return @database_url unless @database_url.nil?
|
58
|
+
|
59
|
+
raise PgDice::InvalidConfigurationError, 'database_url must be present!'
|
60
|
+
end
|
61
|
+
|
62
|
+
def database_connection
|
63
|
+
return @database_connection unless @database_connection.nil?
|
64
|
+
|
65
|
+
raise PgDice::InvalidConfigurationError, 'database_connection must be present!'
|
66
|
+
end
|
67
|
+
|
68
|
+
def additional_validators
|
69
|
+
return @additional_validators if @additional_validators.is_a?(Array)
|
70
|
+
|
71
|
+
raise PgDice::InvalidConfigurationError, 'additional_validators must be an Array!'
|
72
|
+
end
|
73
|
+
|
74
|
+
def approved_tables
|
75
|
+
return @approved_tables if @approved_tables.is_a?(Array)
|
76
|
+
|
77
|
+
raise PgDice::InvalidConfigurationError, 'approved_tables must be an Array of strings!'
|
78
|
+
end
|
79
|
+
|
80
|
+
def older_than
|
81
|
+
return @older_than if @older_than.is_a?(Time)
|
82
|
+
|
83
|
+
raise PgDice::InvalidConfigurationError, 'older_than must be a Time!'
|
84
|
+
end
|
85
|
+
|
86
|
+
def dry_run
|
87
|
+
return @dry_run if [true, false].include?(@dry_run)
|
88
|
+
|
89
|
+
raise PgDice::InvalidConfigurationError, 'dry_run must be either true or false!'
|
90
|
+
end
|
91
|
+
|
92
|
+
def table_drop_batch_size
|
93
|
+
return @table_drop_batch_size.to_i if @table_drop_batch_size.to_i >= 0
|
94
|
+
|
95
|
+
raise PgDice::InvalidConfigurationError, 'table_drop_batch_size must be a non-negative Integer!'
|
96
|
+
end
|
97
|
+
|
98
|
+
# Lazily initialized
|
99
|
+
def pg_connection
|
100
|
+
@pg_connection ||= PG::Connection.new(database_url)
|
101
|
+
return @pg_connection if @pg_connection.respond_to?(:exec)
|
102
|
+
|
103
|
+
raise PgDice::InvalidConfigurationError, 'pg_connection must be present!'
|
104
|
+
end
|
105
|
+
|
106
|
+
def deep_clone
|
107
|
+
PgDice::Configuration.new(self)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def initialize_value(variable_name, default_value, existing_configuration)
|
113
|
+
instance_variable_set("@#{variable_name}", existing_configuration&.send(variable_name)&.clone || default_value)
|
114
|
+
end
|
115
|
+
|
116
|
+
def initialize_objects
|
117
|
+
@database_connection = PgDice::DatabaseConnection.new(self)
|
118
|
+
@partition_manager = PgDice::PartitionManager.new(self)
|
119
|
+
@table_dropper = PgDice::TableDropper.new(self)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for DatabaseConnection
|
4
|
+
module PgDice
|
5
|
+
# Wrapper class around database connection handlers
|
6
|
+
class DatabaseConnection
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@configuration, :logger, :dry_run, :pg_connection
|
9
|
+
|
10
|
+
def initialize(configuration = PgDice::Configuration.new)
|
11
|
+
@configuration = configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(query)
|
15
|
+
logger.debug { "DatabaseConnection to execute query: #{query}" }
|
16
|
+
if dry_run
|
17
|
+
PgDicePgResponse.new
|
18
|
+
else
|
19
|
+
pg_connection.exec(query)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Null-object pattern for PG::Result since that object isn't straightforward to initialize
|
25
|
+
class PgDicePgResponse
|
26
|
+
def values
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for PartitionHelper
|
4
|
+
module PgDice
|
5
|
+
# Helps do high-level tasks like getting tables partitioned
|
6
|
+
class PartitionHelper
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@configuration, :logger
|
9
|
+
|
10
|
+
attr_reader :pg_slice_manager, :validation_helper
|
11
|
+
|
12
|
+
def initialize(configuration = PgDice::Configuration.new)
|
13
|
+
@configuration = configuration
|
14
|
+
@pg_slice_manager = PgDice::PgSliceManager.new(configuration)
|
15
|
+
@validation_helper = PgDice::Validation.new(configuration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def partition_table!(params = {})
|
19
|
+
params[:column_name] ||= 'created_at'
|
20
|
+
params[:period] ||= :day
|
21
|
+
|
22
|
+
validation_helper.validate_parameters(params)
|
23
|
+
|
24
|
+
logger.info { "Preparing database with params: #{params}" }
|
25
|
+
|
26
|
+
prep_and_fill(params)
|
27
|
+
swap_and_fill(params)
|
28
|
+
end
|
29
|
+
|
30
|
+
def undo_partitioning!(params = {})
|
31
|
+
table_name = params.fetch(:table_name)
|
32
|
+
|
33
|
+
validation_helper.validate_parameters(params)
|
34
|
+
logger.info { "Cleaning up database with params: #{table_name}" }
|
35
|
+
|
36
|
+
pg_slice_manager.analyze(table_name: table_name, swapped: true)
|
37
|
+
pg_slice_manager.unswap!(table_name: table_name)
|
38
|
+
pg_slice_manager.unprep!(table_name: table_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def partition_table(params = {})
|
42
|
+
partition_table!(params)
|
43
|
+
rescue PgDice::PgSliceError => error
|
44
|
+
logger.error { "Rescued PgSliceError: #{error}" }
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def undo_partitioning(params = {})
|
49
|
+
undo_partitioning!(params)
|
50
|
+
rescue PgDice::PgSliceError => error
|
51
|
+
logger.error { "Rescued PgSliceError: #{error}" }
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def prep_and_fill(params)
|
58
|
+
pg_slice_manager.prep(params)
|
59
|
+
pg_slice_manager.add_partitions(params.merge!(intermediate: true))
|
60
|
+
pg_slice_manager.fill(params) if params[:fill]
|
61
|
+
end
|
62
|
+
|
63
|
+
def swap_and_fill(params)
|
64
|
+
pg_slice_manager.analyze(params)
|
65
|
+
pg_slice_manager.swap(params)
|
66
|
+
pg_slice_manager.fill(params.merge!(swapped: true)) if params[:fill]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for PartitionManager
|
4
|
+
module PgDice
|
5
|
+
# PartitionManager is a class used to fulfill high-level tasks for partitioning
|
6
|
+
class PartitionManager
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@configuration, :logger, :older_than, :table_drop_batch_size
|
9
|
+
|
10
|
+
attr_reader :validation, :pg_slice_manager, :database_connection
|
11
|
+
|
12
|
+
def initialize(configuration = PgDice::Configuration.new)
|
13
|
+
@configuration = configuration
|
14
|
+
@validation = PgDice::Validation.new(configuration)
|
15
|
+
@pg_slice_manager = PgDice::PgSliceManager.new(configuration)
|
16
|
+
@database_connection = PgDice::DatabaseConnection.new(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_new_partitions(params = {})
|
20
|
+
logger.info { "add_new_partitions has been called with params: #{params}" }
|
21
|
+
|
22
|
+
validation.validate_parameters(params)
|
23
|
+
pg_slice_manager.add_partitions(params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def drop_old_partitions(params = {})
|
27
|
+
logger.info { "drop_old_partitions has been called with params: #{params}" }
|
28
|
+
|
29
|
+
validation.validate_parameters(params)
|
30
|
+
old_partitions = list_old_partitions(params)
|
31
|
+
logger.warn { "Partitions to be deleted are: #{old_partitions}" }
|
32
|
+
|
33
|
+
old_partitions.each do |old_partition|
|
34
|
+
@configuration.table_dropper.call(old_partition, logger)
|
35
|
+
end
|
36
|
+
old_partitions
|
37
|
+
end
|
38
|
+
|
39
|
+
def list_old_partitions(params = {})
|
40
|
+
params[:older_than] ||= older_than
|
41
|
+
logger.info { "Listing old partitions with params: #{params}" }
|
42
|
+
|
43
|
+
validation.validate_parameters(params)
|
44
|
+
|
45
|
+
partition_tables = fetch_partition_tables(params)
|
46
|
+
|
47
|
+
filter_partitions(partition_tables, params[:table_name], params[:older_than])
|
48
|
+
end
|
49
|
+
|
50
|
+
# Grabs only tables that start with the base_table_name and end in numbers
|
51
|
+
def fetch_partition_tables(params = {})
|
52
|
+
schema = params[:schema] ||= 'public'
|
53
|
+
logger.info { "Fetching partition tables with params: #{params}" }
|
54
|
+
|
55
|
+
sql = build_partition_table_fetch_sql(params)
|
56
|
+
|
57
|
+
partition_tables = database_connection.execute(sql).values.flatten
|
58
|
+
logger.debug { "Table: #{schema}.#{params[:table_name]} has partition_tables: #{partition_tables}" }
|
59
|
+
partition_tables
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def filter_partitions(partition_tables, base_table_name, partitions_older_than_time)
|
65
|
+
partition_tables.select do |partition_name|
|
66
|
+
partition_created_at_date = Date.parse(partition_name.gsub(/#{base_table_name}_/, '')).to_time
|
67
|
+
partition_created_at_date < partitions_older_than_time
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_partition_table_fetch_sql(params = {})
|
72
|
+
schema = params.fetch(:schema)
|
73
|
+
base_table_name = params.fetch(:table_name)
|
74
|
+
limit = params.fetch(:limit, table_drop_batch_size)
|
75
|
+
|
76
|
+
<<~SQL
|
77
|
+
SELECT tablename
|
78
|
+
FROM pg_tables
|
79
|
+
WHERE schemaname = '#{schema}'
|
80
|
+
AND tablename ~ '^#{base_table_name}_\\d+$'
|
81
|
+
ORDER BY tablename
|
82
|
+
LIMIT #{limit}
|
83
|
+
SQL
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for PgSliceManager
|
4
|
+
module PgDice
|
5
|
+
# PgSliceManager is a wrapper around PgSlice
|
6
|
+
class PgSliceManager
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@configuration, :logger, :database_url, :dry_run
|
9
|
+
|
10
|
+
def initialize(configuration = PgDice::Configuration.new)
|
11
|
+
@configuration = configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def prep(params = {})
|
15
|
+
table_name = params.fetch(:table_name)
|
16
|
+
column_name = params.fetch(:column_name)
|
17
|
+
period = params.fetch(:period)
|
18
|
+
run_pgslice("prep #{table_name} #{column_name} #{period}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def fill(params = {})
|
22
|
+
table_name = params.fetch(:table_name)
|
23
|
+
swapped = params.fetch(:swapped, '')
|
24
|
+
swapped = '--swapped' if swapped.to_s.casecmp('true').zero?
|
25
|
+
|
26
|
+
run_pgslice("fill #{table_name} #{swapped}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def analyze(params = {})
|
30
|
+
table_name = params.fetch(:table_name)
|
31
|
+
swapped = params.fetch(:swapped, '')
|
32
|
+
swapped = '--swapped' if swapped.to_s.casecmp('true').zero?
|
33
|
+
|
34
|
+
run_pgslice("analyze #{table_name} #{swapped}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def swap(params = {})
|
38
|
+
table_name = params.fetch(:table_name)
|
39
|
+
run_pgslice("swap #{table_name}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_partitions(params = {})
|
43
|
+
table_name = params.fetch(:table_name)
|
44
|
+
future_tables = params.fetch(:future, nil)
|
45
|
+
future_tables = "--future #{Integer(future_tables)}" if future_tables
|
46
|
+
|
47
|
+
past_tables = params.fetch(:past, nil)
|
48
|
+
past_tables = "--past #{Integer(past_tables)}" if past_tables
|
49
|
+
|
50
|
+
intermediate = params.fetch(:intermediate, nil)
|
51
|
+
intermediate = '--intermediate' if intermediate.to_s.casecmp('true').zero?
|
52
|
+
|
53
|
+
run_pgslice("add_partitions #{table_name} #{intermediate} #{future_tables} #{past_tables}")
|
54
|
+
end
|
55
|
+
|
56
|
+
def unprep!(params = {})
|
57
|
+
table_name = params.fetch(:table_name)
|
58
|
+
|
59
|
+
run_pgslice("unprep #{table_name}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def unswap!(params = {})
|
63
|
+
table_name = params.fetch(:table_name)
|
64
|
+
|
65
|
+
run_pgslice("unswap #{table_name}")
|
66
|
+
end
|
67
|
+
|
68
|
+
def unprep(params = {})
|
69
|
+
table_name = params.fetch(:table_name)
|
70
|
+
|
71
|
+
run_pgslice("unprep #{table_name}")
|
72
|
+
rescue PgSliceError => error
|
73
|
+
logger.error { "Rescued PgSliceError: #{error}" }
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def unswap(params = {})
|
78
|
+
table_name = params.fetch(:table_name)
|
79
|
+
|
80
|
+
run_pgslice("unswap #{table_name}")
|
81
|
+
rescue PgSliceError => error
|
82
|
+
logger.error { "Rescued PgSliceError: #{error}" }
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def run_pgslice(argument_string)
|
89
|
+
parameters = build_pg_slice_command(argument_string)
|
90
|
+
|
91
|
+
stdout, stderr, status = Open3.capture3(parameters)
|
92
|
+
log_result(stdout, stderr, status)
|
93
|
+
|
94
|
+
if status.exitstatus.to_i.positive?
|
95
|
+
raise PgDice::PgSliceError,
|
96
|
+
"pgslice with arguments: '#{argument_string}' failed with status: '#{status.exitstatus}' "\
|
97
|
+
"STDOUT: '#{stdout}' STDERR: '#{stderr}'"
|
98
|
+
end
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def build_pg_slice_command(argument_string)
|
103
|
+
argument_string = argument_string.strip
|
104
|
+
logger.info { "Running pgslice command: '#{argument_string}'" }
|
105
|
+
$stdout.flush
|
106
|
+
$stderr.flush
|
107
|
+
command = "pgslice #{argument_string} "
|
108
|
+
command += '--dry-run true ' if dry_run
|
109
|
+
command += "--url #{database_url}"
|
110
|
+
command
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_result(stdout, stderr, status)
|
114
|
+
logger.debug { "pgslice STDERR: #{stderr}" } if stderr
|
115
|
+
logger.debug { "pgslice STDOUT: #{stdout}" } if stdout
|
116
|
+
logger.debug { "pgslice exit status: #{status.exitstatus}" } if status
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for TableDropperHelper
|
4
|
+
module PgDice
|
5
|
+
# Simple class used to provide a mechanism that users can hook into if they want to override this
|
6
|
+
# default behavior for dropping a table.
|
7
|
+
class TableDropper
|
8
|
+
def initialize(configuration = PgDice::Configuration.new)
|
9
|
+
@configuration = configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(table_to_drop, _logger)
|
13
|
+
@configuration.database_connection.execute(drop_partition(table_to_drop))
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def drop_partition(table_name)
|
19
|
+
<<~SQL
|
20
|
+
BEGIN;
|
21
|
+
DROP TABLE IF EXISTS #{table_name} CASCADE;
|
22
|
+
COMMIT;
|
23
|
+
SQL
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgDice
|
4
|
+
# Collection of utilities that provide ways for users to ensure things are working properly
|
5
|
+
class Validation
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@configuration, :logger, :additional_validators, :database_connection, :approved_tables
|
8
|
+
|
9
|
+
def initialize(configuration = PgDice::Configuration.new)
|
10
|
+
@configuration = configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
def assert_tables(params = {})
|
14
|
+
unless params[:future] || params[:past]
|
15
|
+
raise ArgumentError, 'You must provide either a future or past number of tables to assert on.'
|
16
|
+
end
|
17
|
+
|
18
|
+
table_name = validate_table_name(params)
|
19
|
+
period = resolve_period(params)
|
20
|
+
|
21
|
+
assert_future_tables(table_name, params[:future], period) if params[:future]
|
22
|
+
assert_past_tables(table_name, params[:past], period) if params[:past]
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate_parameters(params)
|
27
|
+
validate_table_name(params)
|
28
|
+
validate_period(params)
|
29
|
+
|
30
|
+
run_additional_validators(params)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def resolve_period(params)
|
37
|
+
period = validate_period(params) || fetch_period_from_table_comment(params.fetch(:table_name))
|
38
|
+
|
39
|
+
# If the user doesn't supply a period and we fail to find one on the table then it's a pretty good bet
|
40
|
+
# this table is not partitioned at all.
|
41
|
+
unless period
|
42
|
+
raise TableNotPartitionedError,
|
43
|
+
"Table: #{params.fetch(:table_name)} is not partitioned! Cannot validate partitions that don't exist!"
|
44
|
+
end
|
45
|
+
period
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_additional_validators(params)
|
49
|
+
return true if additional_validators.all? { |validator| validator.call(params, logger) }
|
50
|
+
|
51
|
+
raise PgDice::CustomValidationError.new(params, additional_validators)
|
52
|
+
rescue StandardError => error
|
53
|
+
raise PgDice::CustomValidationError.new(params, additional_validators, error)
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_table_name(params)
|
57
|
+
table_name = params.fetch(:table_name)
|
58
|
+
unless approved_tables.include?(table_name)
|
59
|
+
raise PgDice::IllegalTableError, "Table: #{table_name} is not in the list of approved tables!"
|
60
|
+
end
|
61
|
+
|
62
|
+
table_name
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_period(params)
|
66
|
+
return unless params[:period]
|
67
|
+
|
68
|
+
unless PgDice::SUPPORTED_PERIODS.include?(params[:period])
|
69
|
+
raise ArgumentError,
|
70
|
+
"Period must be one of: #{PgDice::SUPPORTED_PERIODS.keys}. Value: #{params[:period]} is not valid."
|
71
|
+
end
|
72
|
+
|
73
|
+
params[:period].to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
def assert_future_tables(table_name, future, period)
|
77
|
+
sql = build_assert_sql(table_name, future, period, :future)
|
78
|
+
|
79
|
+
response = database_connection.execute(sql)
|
80
|
+
|
81
|
+
return true if response.values.size == 1
|
82
|
+
|
83
|
+
raise PgDice::InsufficientFutureTablesError.new(table_name, future, period)
|
84
|
+
end
|
85
|
+
|
86
|
+
def assert_past_tables(table_name, past, period)
|
87
|
+
sql = build_assert_sql(table_name, past, period, :past)
|
88
|
+
|
89
|
+
response = database_connection.execute(sql)
|
90
|
+
|
91
|
+
return true if response.values.size == 1
|
92
|
+
|
93
|
+
raise PgDice::InsufficientPastTablesError.new(table_name, past, period)
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_assert_sql(table_name, table_count, period, direction)
|
97
|
+
add_or_subtract = { future: '+', past: '-' }.fetch(direction, '+')
|
98
|
+
<<~SQL
|
99
|
+
SELECT 1
|
100
|
+
FROM pg_catalog.pg_class pg_class
|
101
|
+
INNER JOIN pg_catalog.pg_namespace pg_namespace ON pg_namespace.oid = pg_class.relnamespace
|
102
|
+
WHERE pg_class.relkind = 'r'
|
103
|
+
AND pg_namespace.nspname = 'public'
|
104
|
+
AND pg_class.relname = '#{table_name}_' || to_char(NOW()
|
105
|
+
#{add_or_subtract} INTERVAL '#{table_count} #{period}', '#{SUPPORTED_PERIODS[period.to_sym]}')
|
106
|
+
SQL
|
107
|
+
end
|
108
|
+
|
109
|
+
def fetch_period_from_table_comment(table_name)
|
110
|
+
sql = build_table_comment_sql(table_name, 'public')
|
111
|
+
values = database_connection.execute(sql).values.flatten.compact
|
112
|
+
convert_comment_to_hash(values.first)[:period]
|
113
|
+
end
|
114
|
+
|
115
|
+
def convert_comment_to_hash(comment)
|
116
|
+
return {} unless comment
|
117
|
+
|
118
|
+
partition_template = {}
|
119
|
+
comment.split(',').each do |key_value_pair|
|
120
|
+
key, value = key_value_pair.split(':')
|
121
|
+
partition_template[key.to_sym] = value
|
122
|
+
end
|
123
|
+
partition_template
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_table_comment_sql(table_name, schema)
|
127
|
+
<<~SQL
|
128
|
+
SELECT obj_description('#{schema}.#{table_name}'::REGCLASS) AS comment
|
129
|
+
SQL
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/pgdice.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pg'
|
4
|
+
require 'open3'
|
5
|
+
require 'logger'
|
6
|
+
require 'pgslice'
|
7
|
+
require 'pgdice/version'
|
8
|
+
require 'pgdice/validation'
|
9
|
+
require 'pgdice/table_dropper'
|
10
|
+
require 'pgdice/configuration'
|
11
|
+
require 'pgdice/pg_slice_manager'
|
12
|
+
require 'pgdice/partition_manager'
|
13
|
+
require 'pgdice/partition_helper'
|
14
|
+
require 'pgdice/database_connection'
|
15
|
+
|
16
|
+
# This is a stupid comment
|
17
|
+
module PgDice
|
18
|
+
class Error < StandardError
|
19
|
+
end
|
20
|
+
class PgSliceError < Error
|
21
|
+
end
|
22
|
+
class ValidationError < Error
|
23
|
+
end
|
24
|
+
class IllegalTableError < ValidationError
|
25
|
+
end
|
26
|
+
class TableNotPartitionedError < Error
|
27
|
+
end
|
28
|
+
|
29
|
+
# Rubocop is stupid
|
30
|
+
class InsufficientTablesError < Error
|
31
|
+
def initialize(direction, table_name, table_count, period)
|
32
|
+
super("Insufficient #{direction} tables exist for table: #{table_name}. "\
|
33
|
+
"Expected: #{table_count} having period of: #{period}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Rubocop is stupid
|
38
|
+
class InsufficientFutureTablesError < InsufficientTablesError
|
39
|
+
def initialize(table_name, table_count, period)
|
40
|
+
super('future', table_name, table_count, period)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Rubocop is stupid
|
45
|
+
class InsufficientPastTablesError < InsufficientTablesError
|
46
|
+
def initialize(table_name, table_count, period)
|
47
|
+
super('past', table_name, table_count, period)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Rubocop is stupid
|
52
|
+
class CustomValidationError < ValidationError
|
53
|
+
def initialize(params, validators, error = nil)
|
54
|
+
error_message = "Custom validation failed with params: #{params}. "
|
55
|
+
error_message += "Caused by error: #{error} " if error
|
56
|
+
error_message += "Validators: #{validators.map { |validator| source_location(validator) }.flatten}"
|
57
|
+
super(error_message)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Helps users know what went wrong in their custom validators
|
63
|
+
def source_location(proc)
|
64
|
+
return proc.source_location if proc.respond_to?(:source_location)
|
65
|
+
|
66
|
+
proc.to_s
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ConfigurationError < Error
|
71
|
+
end
|
72
|
+
|
73
|
+
# Rubocop is stupid
|
74
|
+
class NotConfiguredError < ConfigurationError
|
75
|
+
def initialize(method_name)
|
76
|
+
super("Cannot use #{method_name} before PgDice has been configured! "\
|
77
|
+
'See README.md for configuration help.')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Rubocop is stupid
|
82
|
+
class InvalidConfigurationError < ConfigurationError
|
83
|
+
def initialize(message)
|
84
|
+
super("PgDice is not configured properly. #{message}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
SUPPORTED_PERIODS = { day: 'YYYYMMDD', month: 'YYYYMM', year: 'YYYY' }.freeze
|
89
|
+
|
90
|
+
class << self
|
91
|
+
def partition_manager
|
92
|
+
raise PgDice::NotConfiguredError, 'partition_manager' unless configuration
|
93
|
+
|
94
|
+
@partition_manager ||= PgDice::PartitionManager.new(configuration)
|
95
|
+
end
|
96
|
+
|
97
|
+
def partition_helper
|
98
|
+
raise PgDice::NotConfiguredError, 'partition_helper' unless configuration
|
99
|
+
|
100
|
+
@partition_helper ||= PgDice::PartitionHelper.new(configuration)
|
101
|
+
end
|
102
|
+
|
103
|
+
def validation
|
104
|
+
raise PgDice::NotConfiguredError, 'validation' unless configuration
|
105
|
+
|
106
|
+
@validation ||= PgDice::Validation.new(configuration)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|