pgdice 0.1.0
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/.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
|