pgdice 0.1.0 → 0.2.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 +4 -4
- data/.circleci/config.yml +2 -2
- data/.codeclimate.yml +5 -0
- data/.rubocop.yml +6 -1
- data/CHANGELOG.md +14 -0
- data/Guardfile +6 -6
- data/README.md +111 -76
- data/Rakefile +3 -0
- data/examples/config.yml +13 -0
- data/lib/pgdice/approved_tables.rb +63 -0
- data/lib/pgdice/configuration.rb +69 -54
- data/lib/pgdice/configuration_file_loader.rb +62 -0
- data/lib/pgdice/database_connection.rb +17 -9
- data/lib/pgdice/database_connection_factory.rb +20 -0
- data/lib/pgdice/date_helper.rb +39 -0
- data/lib/pgdice/error.rb +71 -0
- data/lib/pgdice/log_helper.rb +37 -0
- data/lib/pgdice/partition_dropper.rb +34 -0
- data/lib/pgdice/partition_dropper_factory.rb +20 -0
- data/lib/pgdice/partition_helper.rb +18 -30
- data/lib/pgdice/partition_helper_factory.rb +24 -0
- data/lib/pgdice/partition_lister.rb +33 -0
- data/lib/pgdice/partition_lister_factory.rb +22 -0
- data/lib/pgdice/partition_manager.rb +62 -58
- data/lib/pgdice/partition_manager_factory.rb +52 -0
- data/lib/pgdice/period_fetcher.rb +31 -0
- data/lib/pgdice/period_fetcher_factory.rb +19 -0
- data/lib/pgdice/pg_slice_manager.rb +44 -29
- data/lib/pgdice/pg_slice_manager_factory.rb +35 -0
- data/lib/pgdice/table.rb +87 -0
- data/lib/pgdice/table_finder.rb +41 -0
- data/lib/pgdice/validation.rb +52 -79
- data/lib/pgdice/validation_factory.rb +21 -0
- data/lib/pgdice/version.rb +1 -1
- data/lib/pgdice.rb +34 -72
- data/pgdice.gemspec +3 -4
- metadata +27 -27
- data/lib/pgdice/table_dropper.rb +0 -26
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for PartitionManager
|
4
|
+
module PgDice
|
5
|
+
# PartitionManagerFactory is a class used to build PartitionManagers
|
6
|
+
class PartitionManagerFactory
|
7
|
+
def initialize(configuration, opts = {})
|
8
|
+
@configuration = configuration
|
9
|
+
initialize_simple_factories(opts)
|
10
|
+
initialize_complex_factories(opts)
|
11
|
+
initialize_values(opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
PgDice::PartitionManager.new(logger: @logger_factory.call,
|
16
|
+
batch_size: @batch_size_factory.call,
|
17
|
+
approved_tables: @approved_tables_factory.call,
|
18
|
+
validation: @validation_factory.call,
|
19
|
+
partition_adder: @partition_adder_factory.call,
|
20
|
+
partition_lister: @partition_lister_factory.call,
|
21
|
+
partition_dropper: @partition_dropper_factory.call,
|
22
|
+
current_date_provider: @current_date_provider)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def initialize_simple_factories(opts)
|
28
|
+
@logger_factory = opts[:logger_factory] ||= proc { @configuration.logger }
|
29
|
+
@batch_size_factory = opts[:batch_size_factory] ||= proc { @configuration.batch_size }
|
30
|
+
@approved_tables_factory = opts[:approved_tables_factory] ||= proc { @configuration.approved_tables }
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize_complex_factories(opts)
|
34
|
+
@validation_factory = opts[:validation_factory] ||= PgDice::ValidationFactory.new(@configuration)
|
35
|
+
@partition_adder_factory = opts[:partition_adder_factory] ||= partition_adder_factory
|
36
|
+
@partition_lister_factory = opts[:partition_lister_factory] ||= PgDice::PartitionListerFactory.new(@configuration)
|
37
|
+
@partition_dropper_factory =
|
38
|
+
opts[:partition_dropper_factory] ||= PgDice::PartitionDropperFactory.new(@configuration)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize_values(opts)
|
42
|
+
@current_date_provider = opts[:current_date_provider] ||= proc { Time.now.utc.to_date }
|
43
|
+
end
|
44
|
+
|
45
|
+
def partition_adder_factory
|
46
|
+
proc do
|
47
|
+
pg_slice_manager = PgDice::PgSliceManagerFactory.new(@configuration).call
|
48
|
+
->(all_params) { pg_slice_manager.add_partitions(all_params) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgDice
|
4
|
+
# Used to find the period of a postgres table using the comment on the table created by pgslice
|
5
|
+
class PeriodFetcher
|
6
|
+
def initialize(query_executor:)
|
7
|
+
@query_executor = query_executor
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(params)
|
11
|
+
sql = build_table_comment_sql(params.fetch(:table_name), params.fetch(:schema))
|
12
|
+
values = @query_executor.call(sql)
|
13
|
+
convert_comment_to_hash(values.first)[:period]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def convert_comment_to_hash(comment)
|
19
|
+
return {} unless comment
|
20
|
+
|
21
|
+
comment.split(',').reduce({}) do |hash, key_value_pair|
|
22
|
+
key, value = key_value_pair.split(':')
|
23
|
+
hash.merge(key.to_sym => value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_table_comment_sql(table_name, schema)
|
28
|
+
"SELECT obj_description('#{schema}.#{table_name}'::REGCLASS) AS comment"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgDice
|
4
|
+
# Factory for PeriodFetcher
|
5
|
+
class PeriodFetcherFactory
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@configuration, :database_connection
|
9
|
+
|
10
|
+
def initialize(configuration, opts = {})
|
11
|
+
@configuration = configuration
|
12
|
+
@query_executor = opts[:query_executor] ||= ->(sql) { database_connection.execute(sql).values.flatten.compact }
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
PgDice::PeriodFetcher.new(query_executor: @query_executor)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -4,18 +4,22 @@
|
|
4
4
|
module PgDice
|
5
5
|
# PgSliceManager is a wrapper around PgSlice
|
6
6
|
class PgSliceManager
|
7
|
-
|
8
|
-
def_delegators :@configuration, :logger, :database_url, :dry_run
|
7
|
+
include PgDice::LogHelper
|
9
8
|
|
10
|
-
|
11
|
-
|
9
|
+
attr_reader :logger, :database_url
|
10
|
+
|
11
|
+
def initialize(logger:, database_url:, pg_slice_executor:, dry_run: false)
|
12
|
+
@logger = logger
|
13
|
+
@database_url = database_url
|
14
|
+
@dry_run = dry_run
|
15
|
+
@pg_slice_executor = pg_slice_executor
|
12
16
|
end
|
13
17
|
|
14
18
|
def prep(params = {})
|
15
19
|
table_name = params.fetch(:table_name)
|
16
20
|
column_name = params.fetch(:column_name)
|
17
21
|
period = params.fetch(:period)
|
18
|
-
run_pgslice("prep #{table_name} #{column_name} #{period}")
|
22
|
+
run_pgslice("prep #{table_name} #{column_name} #{period}", params[:dry_run])
|
19
23
|
end
|
20
24
|
|
21
25
|
def fill(params = {})
|
@@ -23,7 +27,7 @@ module PgDice
|
|
23
27
|
swapped = params.fetch(:swapped, '')
|
24
28
|
swapped = '--swapped' if swapped.to_s.casecmp('true').zero?
|
25
29
|
|
26
|
-
run_pgslice("fill #{table_name} #{swapped}")
|
30
|
+
run_pgslice("fill #{table_name} #{swapped}", params[:dry_run])
|
27
31
|
end
|
28
32
|
|
29
33
|
def analyze(params = {})
|
@@ -31,12 +35,12 @@ module PgDice
|
|
31
35
|
swapped = params.fetch(:swapped, '')
|
32
36
|
swapped = '--swapped' if swapped.to_s.casecmp('true').zero?
|
33
37
|
|
34
|
-
run_pgslice("analyze #{table_name} #{swapped}")
|
38
|
+
run_pgslice("analyze #{table_name} #{swapped}", params[:dry_run])
|
35
39
|
end
|
36
40
|
|
37
41
|
def swap(params = {})
|
38
42
|
table_name = params.fetch(:table_name)
|
39
|
-
run_pgslice("swap #{table_name}")
|
43
|
+
run_pgslice("swap #{table_name}", params[:dry_run])
|
40
44
|
end
|
41
45
|
|
42
46
|
def add_partitions(params = {})
|
@@ -50,25 +54,25 @@ module PgDice
|
|
50
54
|
intermediate = params.fetch(:intermediate, nil)
|
51
55
|
intermediate = '--intermediate' if intermediate.to_s.casecmp('true').zero?
|
52
56
|
|
53
|
-
run_pgslice("add_partitions #{table_name} #{intermediate} #{future_tables} #{past_tables}")
|
57
|
+
run_pgslice("add_partitions #{table_name} #{intermediate} #{future_tables} #{past_tables}", params[:dry_run])
|
54
58
|
end
|
55
59
|
|
56
60
|
def unprep!(params = {})
|
57
61
|
table_name = params.fetch(:table_name)
|
58
62
|
|
59
|
-
run_pgslice("unprep #{table_name}")
|
63
|
+
run_pgslice("unprep #{table_name}", params[:dry_run])
|
60
64
|
end
|
61
65
|
|
62
66
|
def unswap!(params = {})
|
63
67
|
table_name = params.fetch(:table_name)
|
64
68
|
|
65
|
-
run_pgslice("unswap #{table_name}")
|
69
|
+
run_pgslice("unswap #{table_name}", params[:dry_run])
|
66
70
|
end
|
67
71
|
|
68
72
|
def unprep(params = {})
|
69
73
|
table_name = params.fetch(:table_name)
|
70
74
|
|
71
|
-
run_pgslice("unprep #{table_name}")
|
75
|
+
run_pgslice("unprep #{table_name}", params[:dry_run])
|
72
76
|
rescue PgSliceError => error
|
73
77
|
logger.error { "Rescued PgSliceError: #{error}" }
|
74
78
|
false
|
@@ -77,7 +81,7 @@ module PgDice
|
|
77
81
|
def unswap(params = {})
|
78
82
|
table_name = params.fetch(:table_name)
|
79
83
|
|
80
|
-
run_pgslice("unswap #{table_name}")
|
84
|
+
run_pgslice("unswap #{table_name}", params[:dry_run])
|
81
85
|
rescue PgSliceError => error
|
82
86
|
logger.error { "Rescued PgSliceError: #{error}" }
|
83
87
|
false
|
@@ -85,35 +89,46 @@ module PgDice
|
|
85
89
|
|
86
90
|
private
|
87
91
|
|
88
|
-
def run_pgslice(argument_string)
|
89
|
-
|
92
|
+
def run_pgslice(argument_string, dry_run)
|
93
|
+
command = build_pg_slice_command(argument_string, dry_run)
|
90
94
|
|
91
|
-
stdout, stderr, status =
|
92
|
-
log_result(stdout, stderr, status)
|
95
|
+
stdout, stderr, status = run_and_log(command)
|
93
96
|
|
94
|
-
if status.
|
97
|
+
if status.to_i.positive?
|
95
98
|
raise PgDice::PgSliceError,
|
96
|
-
"pgslice with arguments: '#{argument_string}' failed with status: '#{status
|
99
|
+
"pgslice with arguments: '#{argument_string}' failed with status: '#{status}' "\
|
97
100
|
"STDOUT: '#{stdout}' STDERR: '#{stderr}'"
|
98
101
|
end
|
99
102
|
true
|
100
103
|
end
|
101
104
|
|
102
|
-
def build_pg_slice_command(argument_string)
|
105
|
+
def build_pg_slice_command(argument_string, dry_run)
|
103
106
|
argument_string = argument_string.strip
|
104
|
-
logger.info { "Running pgslice command: '#{argument_string}'" }
|
105
107
|
$stdout.flush
|
106
108
|
$stderr.flush
|
107
|
-
command = "pgslice #{argument_string}
|
108
|
-
command += '--dry-run true
|
109
|
-
command
|
110
|
-
command
|
109
|
+
command = "pgslice #{argument_string}"
|
110
|
+
command += ' --dry-run true' if @dry_run || dry_run
|
111
|
+
command = squish(command)
|
112
|
+
logger.info { "Running pgslice command: '#{command}'" }
|
113
|
+
command + " --url #{database_url}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def log_result(stdout, stderr)
|
117
|
+
logger.debug { "pgslice STDERR: #{stderr}" } unless blank?(stderr)
|
118
|
+
logger.debug { "pgslice STDOUT: #{stdout}" } unless blank?(stdout)
|
111
119
|
end
|
112
120
|
|
113
|
-
def
|
114
|
-
logger.debug { "pgslice
|
115
|
-
|
116
|
-
|
121
|
+
def log_status(status)
|
122
|
+
logger.debug { "pgslice exit status: #{status}" } unless blank?(status) || status.to_i.zero?
|
123
|
+
end
|
124
|
+
|
125
|
+
def run_and_log(command)
|
126
|
+
PgDice::LogHelper.log_duration('PgSlice', logger) do
|
127
|
+
stdout, stderr, status = @pg_slice_executor.call(command)
|
128
|
+
log_result(stdout, stderr)
|
129
|
+
log_status(status)
|
130
|
+
[stdout, stderr, status]
|
131
|
+
end
|
117
132
|
end
|
118
133
|
end
|
119
134
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for PartitionManager
|
4
|
+
module PgDice
|
5
|
+
# PartitionManagerFactory is a class used to build PartitionManagers
|
6
|
+
class PgSliceManagerFactory
|
7
|
+
extend Forwardable
|
8
|
+
include PgDice::LogHelper
|
9
|
+
|
10
|
+
def_delegators :@configuration, :logger, :database_url, :dry_run
|
11
|
+
|
12
|
+
def initialize(configuration, opts = {})
|
13
|
+
@configuration = configuration
|
14
|
+
@pg_slice_executor = opts[:pg_slice_executor] ||= executor
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
PgDice::PgSliceManager.new(logger: logger,
|
19
|
+
database_url: database_url,
|
20
|
+
pg_slice_executor: @pg_slice_executor,
|
21
|
+
dry_run: dry_run)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def executor
|
27
|
+
lambda do |command|
|
28
|
+
results = Open3.capture3(command)
|
29
|
+
stdout, stderr = results.first(2).map { |output| squish(output.to_s) }
|
30
|
+
status = results[2].exitstatus.to_s
|
31
|
+
[stdout, stderr, status]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/pgdice/table.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgDice
|
4
|
+
# Object to represent a table's configuration in the context of PgDice.
|
5
|
+
class Table
|
6
|
+
include Comparable
|
7
|
+
attr_reader :table_name
|
8
|
+
attr_accessor :past, :future, :column_name, :period, :schema
|
9
|
+
|
10
|
+
def initialize(table_name:, past: 90, future: 7, column_name: 'created_at', period: 'day', schema: 'public')
|
11
|
+
raise ArgumentError, 'table_name must be a string' unless table_name.is_a?(String)
|
12
|
+
|
13
|
+
@table_name = table_name
|
14
|
+
@past = past
|
15
|
+
@future = future
|
16
|
+
@column_name = column_name
|
17
|
+
@period = period
|
18
|
+
@schema = schema
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
check_type(:past, Integer)
|
23
|
+
check_type(:future, Integer)
|
24
|
+
check_type(:column_name, String)
|
25
|
+
check_type(:period, String)
|
26
|
+
check_type(:schema, String)
|
27
|
+
unless PgDice::SUPPORTED_PERIODS.include?(period)
|
28
|
+
raise ArgumentError,
|
29
|
+
"Period must be one of: #{PgDice::SUPPORTED_PERIODS.keys}. Value: #{period} is not valid."
|
30
|
+
end
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
table_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def full_name
|
39
|
+
"#{schema}.#{name}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_h
|
43
|
+
{ table_name: table_name,
|
44
|
+
past: past,
|
45
|
+
future: future,
|
46
|
+
column_name: column_name,
|
47
|
+
period: period,
|
48
|
+
schema: schema }
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"#{schema}.#{name}: <past: #{past}, future: #{future}, column_name: #{column_name}, period: #{period}>"
|
53
|
+
end
|
54
|
+
|
55
|
+
def smash(override_parameters)
|
56
|
+
to_h.merge!(override_parameters)
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
to_h == other.to_h
|
61
|
+
end
|
62
|
+
|
63
|
+
def <=>(other)
|
64
|
+
table_name <=> other.table_name
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get expected size of this configured table (past + present + future table counts)
|
68
|
+
def size
|
69
|
+
past + future + 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.from_hash(hash)
|
73
|
+
Table.new(**hash.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; })
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def check_type(field, expected_type)
|
79
|
+
unless send(field).is_a?(expected_type)
|
80
|
+
raise ArgumentError,
|
81
|
+
"PgDice::Table: #{name} failed validation on field: #{field}. "\
|
82
|
+
"Expected type of: #{expected_type} but found #{send(field).class}"
|
83
|
+
end
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgDice
|
4
|
+
# Module which is a collection of methods used by PartitionManager to find and list tables
|
5
|
+
module TableFinder
|
6
|
+
include PgDice::DateHelper
|
7
|
+
|
8
|
+
def find_droppable_partitions(all_tables, older_than, minimum_tables, period)
|
9
|
+
tables_older_than = tables_older_than(all_tables, older_than, period)
|
10
|
+
tables_to_grab = tables_to_grab(tables_older_than.size, minimum_tables)
|
11
|
+
tables_older_than.first(tables_to_grab)
|
12
|
+
end
|
13
|
+
|
14
|
+
def tables_to_grab(eligible_tables, minimum_tables)
|
15
|
+
tables_to_grab = eligible_tables - minimum_tables
|
16
|
+
tables_to_grab.positive? ? tables_to_grab : 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def batched_tables(tables, batch_size)
|
20
|
+
tables.first(batch_size)
|
21
|
+
end
|
22
|
+
|
23
|
+
def tables_older_than(tables, older_than, period)
|
24
|
+
table_tester(tables, lambda do |partition_created_at_time|
|
25
|
+
partition_created_at_time < truncate_date(older_than.to_date, period)
|
26
|
+
end)
|
27
|
+
end
|
28
|
+
|
29
|
+
def tables_newer_than(tables, newer_than, period)
|
30
|
+
table_tester(tables, lambda do |partition_created_at_time|
|
31
|
+
partition_created_at_time > truncate_date(newer_than.to_date, period)
|
32
|
+
end)
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_tester(tables, predicate)
|
36
|
+
tables.select do |partition_name|
|
37
|
+
predicate.call(safe_date_builder(partition_name))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/pgdice/validation.rb
CHANGED
@@ -3,23 +3,27 @@
|
|
3
3
|
module PgDice
|
4
4
|
# Collection of utilities that provide ways for users to ensure things are working properly
|
5
5
|
class Validation
|
6
|
-
|
7
|
-
def_delegators :@configuration, :logger, :additional_validators, :database_connection, :approved_tables
|
6
|
+
include PgDice::TableFinder
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
attr_reader :logger, :approved_tables
|
9
|
+
|
10
|
+
def initialize(logger:, partition_lister:, period_fetcher:, approved_tables:, current_date_provider:)
|
11
|
+
@logger = logger
|
12
|
+
@approved_tables = approved_tables
|
13
|
+
@partition_lister = partition_lister
|
14
|
+
@period_fetcher = period_fetcher
|
15
|
+
@current_date_provider = current_date_provider
|
11
16
|
end
|
12
17
|
|
13
|
-
def assert_tables(
|
14
|
-
|
15
|
-
|
16
|
-
|
18
|
+
def assert_tables(table_name, opts = nil)
|
19
|
+
table, period, all_params, params = filter_parameters(approved_tables.fetch(table_name), opts)
|
20
|
+
validate_parameters(all_params)
|
21
|
+
logger.debug { "Running asserts on table: #{table} with params: #{all_params}" }
|
17
22
|
|
18
|
-
|
19
|
-
period = resolve_period(params)
|
23
|
+
partitions = @partition_lister.call(all_params)
|
20
24
|
|
21
|
-
assert_future_tables(table_name, params[:future]
|
22
|
-
assert_past_tables(table_name, params[:past]
|
25
|
+
assert_future_tables(table_name, partitions, period, params[:future]) if params && params[:future]
|
26
|
+
assert_past_tables(table_name, partitions, period, params[:past]) if params && params[:past]
|
23
27
|
true
|
24
28
|
end
|
25
29
|
|
@@ -27,14 +31,46 @@ module PgDice
|
|
27
31
|
validate_table_name(params)
|
28
32
|
validate_period(params)
|
29
33
|
|
30
|
-
run_additional_validators(params)
|
31
34
|
true
|
32
35
|
end
|
33
36
|
|
34
37
|
private
|
35
38
|
|
39
|
+
def filter_parameters(table, params)
|
40
|
+
if params.nil?
|
41
|
+
params = {}
|
42
|
+
params[:future] = table.future
|
43
|
+
params[:past] = table.past
|
44
|
+
period = table.period
|
45
|
+
else
|
46
|
+
period = resolve_period(schema: table.schema, table_name: table.name, **params)
|
47
|
+
end
|
48
|
+
all_params = table.smash(params.merge!(period: period))
|
49
|
+
|
50
|
+
[table, period, all_params, params]
|
51
|
+
end
|
52
|
+
|
53
|
+
def assert_future_tables(table_name, partitions, period, expected)
|
54
|
+
newer_tables = tables_newer_than(partitions, @current_date_provider.call, period).size
|
55
|
+
if newer_tables < expected
|
56
|
+
raise PgDice::InsufficientFutureTablesError.new(table_name, expected, period, newer_tables)
|
57
|
+
end
|
58
|
+
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def assert_past_tables(table_name, partitions, period, expected)
|
63
|
+
older_tables = tables_older_than(partitions, @current_date_provider.call, period).size
|
64
|
+
if older_tables < expected
|
65
|
+
raise PgDice::InsufficientPastTablesError.new(table_name, expected, period, older_tables)
|
66
|
+
end
|
67
|
+
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
36
71
|
def resolve_period(params)
|
37
|
-
|
72
|
+
validate_period(params) if params[:period]
|
73
|
+
period = @period_fetcher.call(params)
|
38
74
|
|
39
75
|
# 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
76
|
# this table is not partitioned at all.
|
@@ -42,17 +78,10 @@ module PgDice
|
|
42
78
|
raise TableNotPartitionedError,
|
43
79
|
"Table: #{params.fetch(:table_name)} is not partitioned! Cannot validate partitions that don't exist!"
|
44
80
|
end
|
81
|
+
validate_period(period: period)
|
45
82
|
period
|
46
83
|
end
|
47
84
|
|
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
85
|
def validate_table_name(params)
|
57
86
|
table_name = params.fetch(:table_name)
|
58
87
|
unless approved_tables.include?(table_name)
|
@@ -65,68 +94,12 @@ module PgDice
|
|
65
94
|
def validate_period(params)
|
66
95
|
return unless params[:period]
|
67
96
|
|
68
|
-
unless PgDice::SUPPORTED_PERIODS.include?(params[:period])
|
97
|
+
unless PgDice::SUPPORTED_PERIODS.include?(params[:period].to_s)
|
69
98
|
raise ArgumentError,
|
70
99
|
"Period must be one of: #{PgDice::SUPPORTED_PERIODS.keys}. Value: #{params[:period]} is not valid."
|
71
100
|
end
|
72
101
|
|
73
102
|
params[:period].to_sym
|
74
103
|
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
104
|
end
|
132
105
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgDice
|
4
|
+
# Factory for PgDice::Validations
|
5
|
+
class ValidationFactory
|
6
|
+
def initialize(configuration, opts = {})
|
7
|
+
@configuration = configuration
|
8
|
+
@partition_lister_factory = opts[:partition_lister_factory] ||= PgDice::PartitionListerFactory.new(@configuration)
|
9
|
+
@period_fetcher_factory = opts[:period_fetcher_factory] ||= PgDice::PeriodFetcherFactory.new(@configuration)
|
10
|
+
@current_date_provider = opts[:current_date_provider] ||= proc { Time.now.utc.to_date }
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
PgDice::Validation.new(logger: @configuration.logger,
|
15
|
+
partition_lister: @partition_lister_factory.call,
|
16
|
+
period_fetcher: @period_fetcher_factory.call,
|
17
|
+
approved_tables: @configuration.approved_tables,
|
18
|
+
current_date_provider: @current_date_provider)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/pgdice/version.rb
CHANGED