partition_gardener 0.3.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/CHANGELOG.md +36 -0
- data/LICENSE.md +21 -0
- data/README.md +203 -0
- data/SECURITY.md +27 -0
- data/docs/THIRD_PARTY_LICENSE_MANIFEST.tsv +60 -0
- data/docs/application_contract.md +82 -0
- data/docs/audit_reference.md +125 -0
- data/docs/background_job.md +161 -0
- data/docs/cli.md +71 -0
- data/docs/configuration.md +261 -0
- data/docs/cutover.md +180 -0
- data/docs/decision_flow.md +154 -0
- data/docs/host_testing.md +91 -0
- data/docs/monitoring.md +110 -0
- data/docs/naming.md +71 -0
- data/docs/operations.md +131 -0
- data/docs/partition_landscape.md +323 -0
- data/docs/pg_party_recipe.md +44 -0
- data/docs/related_postgres_tooling.md +195 -0
- data/docs/retention.md +65 -0
- data/docs/schemas/partition_garden.schema.json +114 -0
- data/docs/schemas/plan_report.schema.json +82 -0
- data/docs/tooling_split.md +79 -0
- data/exe/partition_gardener +6 -0
- data/lib/partition_gardener/active_record_run_record_store.rb +1 -0
- data/lib/partition_gardener/advisory_lock.rb +48 -0
- data/lib/partition_gardener/archive_retention.rb +88 -0
- data/lib/partition_gardener/audit.rb +93 -0
- data/lib/partition_gardener/blank.rb +13 -0
- data/lib/partition_gardener/cli.rb +190 -0
- data/lib/partition_gardener/config_document.rb +240 -0
- data/lib/partition_gardener/configuration.rb +102 -0
- data/lib/partition_gardener/connection.rb +260 -0
- data/lib/partition_gardener/date_bucket.rb +117 -0
- data/lib/partition_gardener/date_calendar.rb +65 -0
- data/lib/partition_gardener/date_range_maintenance.rb +297 -0
- data/lib/partition_gardener/default_partition.rb +54 -0
- data/lib/partition_gardener/executor.rb +324 -0
- data/lib/partition_gardener/gap_detection.rb +59 -0
- data/lib/partition_gardener/hash_routing.rb +70 -0
- data/lib/partition_gardener/layout/calendar_year.rb +28 -0
- data/lib/partition_gardener/layout/integer_window.rb +28 -0
- data/lib/partition_gardener/layout/sliding_window.rb +28 -0
- data/lib/partition_gardener/layout/three_area.rb +27 -0
- data/lib/partition_gardener/layout/zone_segments.rb +62 -0
- data/lib/partition_gardener/lock_not_acquired.rb +3 -0
- data/lib/partition_gardener/maintenance_backend.rb +73 -0
- data/lib/partition_gardener/memory_run_record_store.rb +19 -0
- data/lib/partition_gardener/migration/hot_switch_concern.rb +445 -0
- data/lib/partition_gardener/missing_conflict_index.rb +3 -0
- data/lib/partition_gardener/naming.rb +29 -0
- data/lib/partition_gardener/orphaned_rebalance_staging.rb +3 -0
- data/lib/partition_gardener/pg_connection.rb +94 -0
- data/lib/partition_gardener/plan.rb +49 -0
- data/lib/partition_gardener/plan_applier.rb +289 -0
- data/lib/partition_gardener/plan_diff.rb +51 -0
- data/lib/partition_gardener/plan_report.rb +95 -0
- data/lib/partition_gardener/planner.rb +21 -0
- data/lib/partition_gardener/predicate.rb +85 -0
- data/lib/partition_gardener/premake_monthly_maintenance.rb +44 -0
- data/lib/partition_gardener/rails.rb +12 -0
- data/lib/partition_gardener/registry.rb +84 -0
- data/lib/partition_gardener/run_failed.rb +10 -0
- data/lib/partition_gardener/run_metrics.rb +65 -0
- data/lib/partition_gardener/run_record.rb +76 -0
- data/lib/partition_gardener/sql_run_record_store.rb +106 -0
- data/lib/partition_gardener/stdlib_extensions.rb +15 -0
- data/lib/partition_gardener/strategy/composite.rb +27 -0
- data/lib/partition_gardener/strategy/cursor_columns.rb +18 -0
- data/lib/partition_gardener/strategy/date_range.rb +303 -0
- data/lib/partition_gardener/strategy/hash_branches.rb +161 -0
- data/lib/partition_gardener/strategy/integer_range.rb +261 -0
- data/lib/partition_gardener/strategy/list_split.rb +125 -0
- data/lib/partition_gardener/strategy/requires_default_partition.rb +19 -0
- data/lib/partition_gardener/strategy.rb +26 -0
- data/lib/partition_gardener/templates.rb +373 -0
- data/lib/partition_gardener/unmoved_rows_remaining.rb +15 -0
- data/lib/partition_gardener/version.rb +3 -0
- data/lib/partition_gardener.rb +215 -0
- data/sig/partition_gardener.rbs +19 -0
- metadata +367 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class RunMetrics
|
|
3
|
+
attr_reader :table_name, :started_at, :rows_moved, :finished_at, :skipped, :skip_reason
|
|
4
|
+
attr_accessor :plan_signature
|
|
5
|
+
|
|
6
|
+
def initialize(table_name)
|
|
7
|
+
@table_name = table_name
|
|
8
|
+
@started_at = monotonic_clock
|
|
9
|
+
@plan_signature = nil
|
|
10
|
+
@rows_moved = 0
|
|
11
|
+
@finished_at = nil
|
|
12
|
+
@skipped = false
|
|
13
|
+
@skip_reason = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_rows(count)
|
|
17
|
+
@rows_moved += count.to_i
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def mark_skipped!(reason)
|
|
21
|
+
@skipped = true
|
|
22
|
+
@skip_reason = reason
|
|
23
|
+
finish!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def finish!
|
|
27
|
+
@finished_at = monotonic_clock
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def duration_ms
|
|
31
|
+
return nil unless @finished_at
|
|
32
|
+
|
|
33
|
+
((@finished_at - @started_at) * 1000).round
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
table_name: table_name,
|
|
39
|
+
duration_ms: duration_ms,
|
|
40
|
+
plan_signature: plan_signature,
|
|
41
|
+
rows_moved: rows_moved,
|
|
42
|
+
skipped: skipped,
|
|
43
|
+
skip_reason: skip_reason
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def monotonic_clock
|
|
50
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
RunSummary = Data.define(:tables, :errors) do
|
|
55
|
+
SCHEMA_VERSION = "1.0"
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{
|
|
59
|
+
schema_version: SCHEMA_VERSION,
|
|
60
|
+
tables: tables.map(&:to_h),
|
|
61
|
+
errors: errors.map { |error| error.message }
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class RunRecord
|
|
3
|
+
PHASES = %w[detach segments rows cleanup complete].freeze
|
|
4
|
+
|
|
5
|
+
attr_reader :table_name, :phase, :plan_signature, :staging_row_count
|
|
6
|
+
|
|
7
|
+
def self.load(table_name)
|
|
8
|
+
attributes = PartitionGardener.configuration.run_record_store.load(table_name)
|
|
9
|
+
return unless attributes
|
|
10
|
+
|
|
11
|
+
from_h(attributes)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(attributes)
|
|
15
|
+
new(
|
|
16
|
+
table_name: fetch_attribute(attributes, :table_name),
|
|
17
|
+
phase: fetch_attribute(attributes, :phase),
|
|
18
|
+
plan_signature: fetch_attribute(attributes, :plan_signature),
|
|
19
|
+
staging_row_count: fetch_attribute(attributes, :staging_row_count) || 0
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.fetch_attribute(attributes, key)
|
|
24
|
+
attributes[key] || attributes[key.to_s]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.clear(table_name)
|
|
28
|
+
PartitionGardener.configuration.run_record_store.clear(table_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.start(table_name:, plan_signature:)
|
|
32
|
+
new(
|
|
33
|
+
table_name: table_name,
|
|
34
|
+
phase: "detach",
|
|
35
|
+
plan_signature: plan_signature,
|
|
36
|
+
staging_row_count: 0
|
|
37
|
+
).tap(&:save!)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(table_name:, phase:, plan_signature:, staging_row_count: 0)
|
|
41
|
+
@table_name = table_name
|
|
42
|
+
@phase = phase
|
|
43
|
+
@plan_signature = plan_signature
|
|
44
|
+
@staging_row_count = staging_row_count
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def incomplete?
|
|
48
|
+
Blank.present?(phase) && phase != "complete"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save!
|
|
52
|
+
PartitionGardener.configuration.run_record_store.save(
|
|
53
|
+
table_name,
|
|
54
|
+
{
|
|
55
|
+
table_name: table_name,
|
|
56
|
+
phase: phase,
|
|
57
|
+
plan_signature: plan_signature,
|
|
58
|
+
staging_row_count: staging_row_count
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def advance!(phase, staging_row_count: nil)
|
|
64
|
+
RunRecord.new(
|
|
65
|
+
table_name: table_name,
|
|
66
|
+
phase: phase,
|
|
67
|
+
plan_signature: plan_signature,
|
|
68
|
+
staging_row_count: staging_row_count.nil? ? self.staging_row_count : staging_row_count
|
|
69
|
+
).tap(&:save!)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def phase_at_least?(name)
|
|
73
|
+
PHASES.index(phase) >= PHASES.index(name)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class SqlRunRecordStore
|
|
3
|
+
TABLE_NAME = "partition_gardener_run_records"
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@schema_mutex = Mutex.new
|
|
7
|
+
@schema_ready = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def load(table_name)
|
|
11
|
+
ensure_schema!
|
|
12
|
+
sql = load_sql(table_name)
|
|
13
|
+
row = connection.execute(sql).first
|
|
14
|
+
|
|
15
|
+
return unless row
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
table_name: row["table_name"],
|
|
19
|
+
phase: row["phase"],
|
|
20
|
+
plan_signature: row["plan_signature"],
|
|
21
|
+
staging_row_count: row["staging_row_count"].to_i
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save(table_name, attributes)
|
|
26
|
+
ensure_schema!
|
|
27
|
+
sql = save_sql(table_name, attributes)
|
|
28
|
+
connection.execute(sql)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear(table_name)
|
|
32
|
+
ensure_schema!
|
|
33
|
+
sql = clear_sql(table_name)
|
|
34
|
+
connection.execute(sql)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ensure_schema!
|
|
38
|
+
return if @schema_ready
|
|
39
|
+
|
|
40
|
+
@schema_mutex.synchronize do
|
|
41
|
+
return if @schema_ready
|
|
42
|
+
|
|
43
|
+
connection.execute(create_table_sql)
|
|
44
|
+
@schema_ready = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def connection
|
|
51
|
+
PartitionGardener.configuration.connection
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def quoted_table(name)
|
|
55
|
+
connection.quote_table_name(name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def load_sql(table_name)
|
|
59
|
+
<<~SQL
|
|
60
|
+
SELECT table_name, phase, plan_signature, staging_row_count
|
|
61
|
+
FROM #{quoted_table(TABLE_NAME)}
|
|
62
|
+
WHERE table_name = #{connection.quote(table_name)}
|
|
63
|
+
SQL
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def save_sql(table_name, attributes)
|
|
67
|
+
<<~SQL
|
|
68
|
+
INSERT INTO #{quoted_table(TABLE_NAME)} (
|
|
69
|
+
table_name, phase, plan_signature, staging_row_count, updated_at
|
|
70
|
+
) VALUES (
|
|
71
|
+
#{connection.quote(table_name)},
|
|
72
|
+
#{connection.quote(attributes.fetch(:phase))},
|
|
73
|
+
#{connection.quote(attributes.fetch(:plan_signature))},
|
|
74
|
+
#{attributes.fetch(:staging_row_count, 0).to_i},
|
|
75
|
+
NOW()
|
|
76
|
+
)
|
|
77
|
+
ON CONFLICT (table_name) DO UPDATE SET
|
|
78
|
+
phase = EXCLUDED.phase,
|
|
79
|
+
plan_signature = EXCLUDED.plan_signature,
|
|
80
|
+
staging_row_count = EXCLUDED.staging_row_count,
|
|
81
|
+
updated_at = NOW()
|
|
82
|
+
SQL
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def clear_sql(table_name)
|
|
86
|
+
<<~SQL
|
|
87
|
+
DELETE FROM #{quoted_table(TABLE_NAME)}
|
|
88
|
+
WHERE table_name = #{connection.quote(table_name)}
|
|
89
|
+
SQL
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_table_sql
|
|
93
|
+
<<~SQL
|
|
94
|
+
CREATE TABLE IF NOT EXISTS #{quoted_table(TABLE_NAME)} (
|
|
95
|
+
table_name text PRIMARY KEY,
|
|
96
|
+
phase text NOT NULL,
|
|
97
|
+
plan_signature text NOT NULL,
|
|
98
|
+
staging_row_count integer NOT NULL DEFAULT 0,
|
|
99
|
+
updated_at timestamptz NOT NULL DEFAULT NOW()
|
|
100
|
+
)
|
|
101
|
+
SQL
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
ActiveRecordRunRecordStore = SqlRunRecordStore
|
|
106
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class CompositeMaintenance
|
|
3
|
+
def initialize(config, job_class_name: "PartitionGardener", executor: nil)
|
|
4
|
+
@config = config
|
|
5
|
+
@job_class_name = job_class_name
|
|
6
|
+
@executor = executor || Executor.for_config(config)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def run!
|
|
10
|
+
Templates.expand(@config).each do |branch_config|
|
|
11
|
+
ThreeAreaMaintenance.new(branch_config, job_class_name: @job_class_name, executor: @executor).run!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def split_future_month_from_current!(_identifier = nil)
|
|
16
|
+
run!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def split_pressured_future_month_partitions
|
|
20
|
+
run!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def collapse_low_volume_future_month_partitions
|
|
24
|
+
run!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
module CursorColumns
|
|
4
|
+
def cursor_columns
|
|
5
|
+
base_key = partition_key_base
|
|
6
|
+
conflict_columns = @config[:conflict_key] || [base_key, "id"]
|
|
7
|
+
([base_key] + conflict_columns).uniq
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def partition_key_base
|
|
11
|
+
column = @config[:partition_key_column]
|
|
12
|
+
return column.to_s if column.nil?
|
|
13
|
+
|
|
14
|
+
column.to_s.split("::").first.strip
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
class DateRange
|
|
4
|
+
include Naming
|
|
5
|
+
include RequiresDefaultPartition
|
|
6
|
+
include CursorColumns
|
|
7
|
+
|
|
8
|
+
DEFAULT_ACTIVE_MONTHS = 12
|
|
9
|
+
DEFAULT_ACTIVE_YEARS = 2
|
|
10
|
+
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def active_window
|
|
16
|
+
bucket = date_bucket
|
|
17
|
+
active_start = DateBucket.beginning_of_bucket(today, bucket)
|
|
18
|
+
active_span = active_span_for(bucket)
|
|
19
|
+
active_end = DateBucket.add_buckets(active_start, active_span, bucket)
|
|
20
|
+
|
|
21
|
+
{start: active_start, end: active_end}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_plan
|
|
25
|
+
window = active_window
|
|
26
|
+
heatmap = collect_heatmap(window)
|
|
27
|
+
hot_buckets = hot_buckets_in_window(heatmap, window)
|
|
28
|
+
|
|
29
|
+
segments = if year_bucket?
|
|
30
|
+
Layout::CalendarYear.build_segments(
|
|
31
|
+
config: @config,
|
|
32
|
+
active_start: window[:start],
|
|
33
|
+
active_end: window[:end],
|
|
34
|
+
hot_years: hot_buckets
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
Layout::SlidingWindow.build_segments(
|
|
38
|
+
config: @config,
|
|
39
|
+
active_start: window[:start],
|
|
40
|
+
active_end: window[:end],
|
|
41
|
+
hot_months: hot_buckets
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Plan::Result.new(segments: segments, hot_buckets: hot_buckets)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def attached_tail_segments
|
|
49
|
+
window = active_window
|
|
50
|
+
|
|
51
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
52
|
+
next if partition.default
|
|
53
|
+
next unless managed_tail_partition?(partition.name, window: window)
|
|
54
|
+
|
|
55
|
+
Plan::Segment.new(
|
|
56
|
+
name: partition.name,
|
|
57
|
+
range_start: partition.range_start,
|
|
58
|
+
range_end: partition.range_end,
|
|
59
|
+
kind: segment_kind_for(partition.name, window: window)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def collect_heatmap(window)
|
|
65
|
+
bucket_counts = Hash.new(0)
|
|
66
|
+
dedicated_partition_counts = {}
|
|
67
|
+
dedicated_partitions = hot_bucket_partitions_in_window(window).to_h
|
|
68
|
+
|
|
69
|
+
heatmap_source_partitions(window).each do |partition_name|
|
|
70
|
+
counts = counts_by_bucket_in_partition(partition_name)
|
|
71
|
+
counts.each { |bucket, count| bucket_counts[bucket] += count }
|
|
72
|
+
|
|
73
|
+
bucket = dedicated_partitions[partition_name]
|
|
74
|
+
dedicated_partition_counts[bucket] = counts.values.sum if bucket
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
bucket_counts: bucket_counts,
|
|
79
|
+
default_rows: default_row_count,
|
|
80
|
+
dedicated_partition_counts: dedicated_partition_counts
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def hot_buckets_in_window(heatmap, window)
|
|
85
|
+
return [] if rolling_current_layout?
|
|
86
|
+
|
|
87
|
+
hot_buckets = Set.new
|
|
88
|
+
|
|
89
|
+
heatmap[:bucket_counts].each do |bucket, row_count|
|
|
90
|
+
next unless bucket_in_window?(bucket, window)
|
|
91
|
+
next unless row_count >= split_row_threshold
|
|
92
|
+
|
|
93
|
+
hot_buckets << bucket
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
dedicated_counts = heatmap.fetch(:dedicated_partition_counts, {})
|
|
97
|
+
hot_bucket_partitions_in_window(window).each do |_partition_name, bucket|
|
|
98
|
+
row_count = dedicated_counts[bucket]
|
|
99
|
+
next if row_count.nil?
|
|
100
|
+
|
|
101
|
+
if row_count >= split_row_threshold
|
|
102
|
+
hot_buckets << bucket
|
|
103
|
+
else
|
|
104
|
+
hot_buckets.delete(bucket)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
hot_buckets.sort
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def managed_tail_partition_names(window: active_window)
|
|
112
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
113
|
+
next if partition.default
|
|
114
|
+
next unless managed_tail_partition?(partition.name, window: window)
|
|
115
|
+
|
|
116
|
+
partition.name
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def tail_slot_name?(partition_name)
|
|
121
|
+
partition_name == current_partition_name(table_name) ||
|
|
122
|
+
partition_name == open_partition_name(table_name) ||
|
|
123
|
+
partition_name == future_partition_name(table_name) ||
|
|
124
|
+
partition_name.match?(/^#{Regexp.escape(table_name)}_open_\d+$/)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def current_and_future_where_condition(window: active_window)
|
|
128
|
+
partition_key_column = @config[:partition_key_column]
|
|
129
|
+
"#{partition_key_column} >= #{connection.quote(window[:start])}::date"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def bucket_where_condition(bucket)
|
|
133
|
+
partition_key_column = @config[:partition_key_column]
|
|
134
|
+
start_range = beginning_of_bucket(bucket)
|
|
135
|
+
end_range = end_of_bucket(bucket)
|
|
136
|
+
"#{partition_key_column} >= #{connection.quote(start_range)}::date AND #{partition_key_column} < #{connection.quote(end_range)}::date"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def archive_bucket?(bucket)
|
|
140
|
+
beginning_of_bucket(bucket) < active_window[:start]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def future_bucket?(bucket)
|
|
144
|
+
beginning_of_bucket(bucket) > beginning_of_bucket(today)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def archive_bucket_from_partition_name(partition_name)
|
|
148
|
+
DateBucket.archive_bucket_from_partition_name(table_name, partition_name, date_bucket)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def segment_for_values_clause(segment)
|
|
152
|
+
if segment.range_end == :max
|
|
153
|
+
"FROM ('#{segment.range_start}') TO (MAXVALUE)"
|
|
154
|
+
elsif segment.hash_partition?
|
|
155
|
+
"WITH (modulus #{segment.range_start[:modulus]}, remainder #{segment.range_start[:remainder]})"
|
|
156
|
+
else
|
|
157
|
+
"FROM ('#{segment.range_start}') TO ('#{segment.range_end}')"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def bucket_counts_in_partition(partition_name)
|
|
162
|
+
counts_by_bucket_in_partition(partition_name)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def table_name
|
|
168
|
+
@config[:table_name]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def connection
|
|
172
|
+
Connection.connection
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def today
|
|
176
|
+
PartitionGardener.configuration.today
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def date_bucket
|
|
180
|
+
DateBucket.normalize(@config.fetch(:bucket, :month))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def year_bucket?
|
|
184
|
+
date_bucket == :year
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def rolling_current_layout?
|
|
188
|
+
@config.fetch(:layout, :sliding_window) == :rolling_current
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def active_span_for(bucket)
|
|
192
|
+
active_key = DateBucket.active_key(bucket)
|
|
193
|
+
default = case bucket
|
|
194
|
+
when :year then DEFAULT_ACTIVE_YEARS
|
|
195
|
+
when :month then DEFAULT_ACTIVE_MONTHS
|
|
196
|
+
else DateBucket.default_active_span(bucket)
|
|
197
|
+
end
|
|
198
|
+
@config.fetch(active_key, default)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def split_row_threshold
|
|
202
|
+
@config.fetch(:split_row_threshold, FUTURE_MONTH_PARTITION_ROW_THRESHOLD)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def beginning_of_bucket(bucket)
|
|
206
|
+
DateBucket.beginning_of_bucket(bucket.to_date, date_bucket)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def end_of_bucket(bucket)
|
|
210
|
+
DateBucket.end_of_bucket(bucket.to_date, date_bucket)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def each_bucket_in_range(range_start, range_end_exclusive)
|
|
214
|
+
buckets = []
|
|
215
|
+
bucket = beginning_of_bucket(range_start)
|
|
216
|
+
|
|
217
|
+
while bucket < range_end_exclusive
|
|
218
|
+
buckets << bucket
|
|
219
|
+
bucket = DateBucket.next_bucket(bucket, date_bucket)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
buckets
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def bucket_in_window?(bucket, window)
|
|
226
|
+
return false if window[:start].nil? || window[:end].nil?
|
|
227
|
+
|
|
228
|
+
bucket >= window[:start] && bucket < window[:end]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def managed_tail_partition?(partition_name, window:)
|
|
232
|
+
return true if tail_slot_name?(partition_name)
|
|
233
|
+
|
|
234
|
+
bucket = archive_bucket_from_partition_name(partition_name)
|
|
235
|
+
bucket && bucket_in_window?(bucket, window)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def segment_kind_for(partition_name, window:)
|
|
239
|
+
return :future if partition_name == future_partition_name(table_name)
|
|
240
|
+
return :filler if tail_slot_name?(partition_name)
|
|
241
|
+
|
|
242
|
+
:hot_bucket
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def heatmap_source_partitions(window)
|
|
246
|
+
names = tail_slot_names_for_heatmap.select { |name| Connection.partition_attached?(table_name, name) }
|
|
247
|
+
names.concat(hot_bucket_partitions_in_window(window).map(&:first))
|
|
248
|
+
names.uniq
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def tail_slot_names_for_heatmap
|
|
252
|
+
[
|
|
253
|
+
current_partition_name(table_name),
|
|
254
|
+
open_partition_name(table_name),
|
|
255
|
+
future_partition_name(table_name)
|
|
256
|
+
] + open_slot_partition_names
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def open_slot_partition_names
|
|
260
|
+
Connection.attached_partitions(table_name).map(&:name).select do |partition_name|
|
|
261
|
+
partition_name.match?(/^#{Regexp.escape(table_name)}_open_\d+$/)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def hot_bucket_partitions_in_window(window)
|
|
266
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
267
|
+
bucket = archive_bucket_from_partition_name(partition.name)
|
|
268
|
+
next unless bucket
|
|
269
|
+
next unless bucket_in_window?(bucket, window)
|
|
270
|
+
|
|
271
|
+
[partition.name, bucket]
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def counts_by_bucket_in_partition(partition_name)
|
|
276
|
+
partition_key_column = @config[:partition_key_column]
|
|
277
|
+
trunc_unit = DateBucket.date_trunc_unit(date_bucket)
|
|
278
|
+
bucket_expression = if partition_key_column.include?("::")
|
|
279
|
+
"date_trunc('#{trunc_unit}', #{partition_key_column})::date"
|
|
280
|
+
else
|
|
281
|
+
"date_trunc('#{trunc_unit}', #{connection.quote_column_name(partition_key_column)})::date"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
sql = <<~SQL
|
|
285
|
+
SELECT #{bucket_expression} AS bucket, COUNT(*)::int AS row_count
|
|
286
|
+
FROM #{Connection.quoted_table(partition_name)}
|
|
287
|
+
GROUP BY 1
|
|
288
|
+
SQL
|
|
289
|
+
|
|
290
|
+
connection.execute(sql).each_with_object({}) do |row, counts|
|
|
291
|
+
counts[Date.parse(row["bucket"].to_s)] = row["row_count"].to_i
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def default_row_count
|
|
296
|
+
default_name = default_partition_name(table_name)
|
|
297
|
+
return 0 unless Connection.partition_exists?(default_name)
|
|
298
|
+
|
|
299
|
+
Connection.count_rows_in_partition_table(default_name)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|