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,161 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
class HashBranches
|
|
4
|
+
include Naming
|
|
5
|
+
include RequiresDefaultPartition
|
|
6
|
+
include CursorColumns
|
|
7
|
+
|
|
8
|
+
DEFAULT_MODULUS = 32
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def active_window
|
|
15
|
+
{start: 0, end: modulus}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build_plan
|
|
19
|
+
heatmap = collect_heatmap(active_window)
|
|
20
|
+
hot_buckets = hot_buckets_in_window(heatmap, active_window)
|
|
21
|
+
|
|
22
|
+
segments = (0...modulus).map do |remainder|
|
|
23
|
+
hot = hot_buckets.include?(remainder)
|
|
24
|
+
Plan::Segment.new(
|
|
25
|
+
name: hash_partition_name(remainder, hot: hot),
|
|
26
|
+
range_start: {modulus: modulus, remainder: remainder},
|
|
27
|
+
range_end: nil,
|
|
28
|
+
kind: hot ? :hot_bucket : :archive
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Plan::Result.new(segments: segments, hot_buckets: hot_buckets)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attached_tail_segments
|
|
36
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
37
|
+
next if partition.default
|
|
38
|
+
next unless hash_partition_name?(partition.name)
|
|
39
|
+
|
|
40
|
+
remainder = hash_remainder_from_partition_name(partition.name)
|
|
41
|
+
next if remainder.nil?
|
|
42
|
+
|
|
43
|
+
hot = hot_bucket_partition_name?(partition.name)
|
|
44
|
+
Plan::Segment.new(
|
|
45
|
+
name: partition.name,
|
|
46
|
+
range_start: {modulus: modulus, remainder: remainder},
|
|
47
|
+
range_end: nil,
|
|
48
|
+
kind: hot ? :hot_bucket : :archive
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def collect_heatmap(_window)
|
|
54
|
+
bucket_counts = HashRouting.collect_bucket_counts(@config)
|
|
55
|
+
|
|
56
|
+
{bucket_counts: bucket_counts, default_rows: default_row_count}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def hot_buckets_in_window(heatmap, _window)
|
|
60
|
+
heatmap[:bucket_counts].filter_map do |remainder, row_count|
|
|
61
|
+
remainder if row_count >= split_row_threshold
|
|
62
|
+
end.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def managed_tail_partition_names(window: active_window)
|
|
66
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
67
|
+
next if partition.default
|
|
68
|
+
next unless hash_partition_name?(partition.name)
|
|
69
|
+
|
|
70
|
+
partition.name
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tail_slot_name?(_partition_name)
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def current_and_future_where_condition(window: active_window)
|
|
79
|
+
"FALSE"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def default_partition_required?
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rebalance_default_drain_where_condition(window: active_window)
|
|
87
|
+
"FALSE"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def default_partition_drain_where_condition(window: active_window)
|
|
91
|
+
"TRUE"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def bucket_where_condition(_bucket)
|
|
95
|
+
"TRUE"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def archive_bucket?(_bucket)
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def future_bucket?(_bucket)
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def archive_bucket_from_partition_name(partition_name)
|
|
107
|
+
hash_remainder_from_partition_name(partition_name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def segment_for_values_clause(segment)
|
|
111
|
+
"WITH (modulus #{segment.range_start[:modulus]}, remainder #{segment.range_start[:remainder]})"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def table_name
|
|
117
|
+
@config[:table_name]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def connection
|
|
121
|
+
Connection.connection
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def modulus
|
|
125
|
+
@config.fetch(:hash_modulus, DEFAULT_MODULUS)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def split_row_threshold
|
|
129
|
+
threshold = @config[:split_row_threshold]
|
|
130
|
+
threshold.nil? ? FUTURE_MONTH_PARTITION_ROW_THRESHOLD : threshold
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def hash_partition_name(remainder, hot:)
|
|
134
|
+
prefix = hot ? "h" : "a"
|
|
135
|
+
format("#{table_name}_#{prefix}_%02d", remainder)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def hash_partition_name?(partition_name)
|
|
139
|
+
partition_name.match?(/^#{Regexp.escape(table_name)}_[ha]_\d+$/)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def hot_bucket_partition_name?(partition_name)
|
|
143
|
+
partition_name.match?(/^#{Regexp.escape(table_name)}_h_\d+$/)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def hash_remainder_from_partition_name(partition_name)
|
|
147
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_[ha]_(\d+)$/)
|
|
148
|
+
return unless match
|
|
149
|
+
|
|
150
|
+
match[1].to_i
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def default_row_count
|
|
154
|
+
default_name = default_partition_name(table_name)
|
|
155
|
+
return 0 unless Connection.partition_exists?(default_name)
|
|
156
|
+
|
|
157
|
+
Connection.count_rows_in_partition_table(default_name)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
class IntegerRange
|
|
4
|
+
include Naming
|
|
5
|
+
include RequiresDefaultPartition
|
|
6
|
+
include CursorColumns
|
|
7
|
+
|
|
8
|
+
DEFAULT_ACTIVE_ID_WIDTH = 10_000_000
|
|
9
|
+
DEFAULT_CURRENT_BAND_SIZE = 1_000_000
|
|
10
|
+
DEFAULT_ARCHIVE_BAND_SIZE = 10_000_000
|
|
11
|
+
|
|
12
|
+
def initialize(config)
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def active_window
|
|
17
|
+
active_lo = @config.fetch(:active_id_lo) { 0 }
|
|
18
|
+
active_hi = active_lo + active_id_width
|
|
19
|
+
{start: active_lo, end: active_hi}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_plan
|
|
23
|
+
window = active_window
|
|
24
|
+
heatmap = collect_heatmap(window)
|
|
25
|
+
hot_buckets = hot_buckets_in_window(heatmap, window)
|
|
26
|
+
|
|
27
|
+
segments = Layout::ZoneSegments.build_filler_and_hot_segments(
|
|
28
|
+
table_name: table_name,
|
|
29
|
+
buckets: each_bucket_in_range(window[:start], window[:end]),
|
|
30
|
+
hot_buckets: hot_buckets,
|
|
31
|
+
active_start: window[:start],
|
|
32
|
+
active_end: window[:end],
|
|
33
|
+
hot_bucket_name: method(:hot_bucket_partition_name),
|
|
34
|
+
bucket_end: ->(bucket) { [bucket + current_band_size, window[:end]].min }
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
Plan::Result.new(segments: segments, hot_buckets: hot_buckets)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def attached_tail_segments
|
|
41
|
+
window = active_window
|
|
42
|
+
|
|
43
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
44
|
+
next if partition.default
|
|
45
|
+
next unless managed_tail_partition?(partition.name, window: window)
|
|
46
|
+
|
|
47
|
+
Plan::Segment.new(
|
|
48
|
+
name: partition.name,
|
|
49
|
+
range_start: partition.range_start,
|
|
50
|
+
range_end: partition.range_end,
|
|
51
|
+
kind: segment_kind_for(partition.name, window: window)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def collect_heatmap(window)
|
|
57
|
+
bucket_counts = Hash.new(0)
|
|
58
|
+
dedicated_partition_counts = {}
|
|
59
|
+
dedicated_partitions = hot_bucket_partitions_in_window(window).to_h
|
|
60
|
+
|
|
61
|
+
heatmap_source_partitions(window).each do |partition_name|
|
|
62
|
+
counts = counts_by_bucket_in_partition(partition_name, window)
|
|
63
|
+
counts.each { |bucket, count| bucket_counts[bucket] += count }
|
|
64
|
+
|
|
65
|
+
bucket = dedicated_partitions[partition_name]
|
|
66
|
+
dedicated_partition_counts[bucket] = counts.values.sum if bucket
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
bucket_counts: bucket_counts,
|
|
71
|
+
default_rows: default_row_count,
|
|
72
|
+
dedicated_partition_counts: dedicated_partition_counts
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def hot_buckets_in_window(heatmap, window)
|
|
77
|
+
hot_buckets = Set.new
|
|
78
|
+
|
|
79
|
+
heatmap[:bucket_counts].each do |bucket, row_count|
|
|
80
|
+
next unless bucket_in_window?(bucket, window)
|
|
81
|
+
next unless row_count >= split_row_threshold
|
|
82
|
+
|
|
83
|
+
hot_buckets << bucket
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
dedicated_counts = heatmap.fetch(:dedicated_partition_counts, {})
|
|
87
|
+
hot_bucket_partitions_in_window(window).each do |_partition_name, bucket|
|
|
88
|
+
row_count = dedicated_counts[bucket]
|
|
89
|
+
next if row_count.nil?
|
|
90
|
+
|
|
91
|
+
if row_count >= split_row_threshold
|
|
92
|
+
hot_buckets << bucket
|
|
93
|
+
else
|
|
94
|
+
hot_buckets.delete(bucket)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
hot_buckets.sort
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def managed_tail_partition_names(window: active_window)
|
|
102
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
103
|
+
next if partition.default
|
|
104
|
+
next unless managed_tail_partition?(partition.name, window: window)
|
|
105
|
+
|
|
106
|
+
partition.name
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def tail_slot_name?(partition_name)
|
|
111
|
+
partition_name == current_partition_name(table_name) ||
|
|
112
|
+
partition_name == open_partition_name(table_name) ||
|
|
113
|
+
partition_name == future_partition_name(table_name) ||
|
|
114
|
+
partition_name.match?(/^#{Regexp.escape(table_name)}_open_\d+$/)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def current_and_future_where_condition(window: active_window)
|
|
118
|
+
partition_key_column = @config[:partition_key_column]
|
|
119
|
+
"#{partition_key_column} >= #{window[:start]}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def bucket_where_condition(bucket)
|
|
123
|
+
partition_key_column = @config[:partition_key_column]
|
|
124
|
+
end_range = bucket + current_band_size
|
|
125
|
+
"#{partition_key_column} >= #{bucket} AND #{partition_key_column} < #{end_range}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def archive_bucket?(bucket)
|
|
129
|
+
bucket < active_window[:start]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def future_bucket?(bucket)
|
|
133
|
+
bucket >= active_window[:end]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def archive_bucket_from_partition_name(partition_name)
|
|
137
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_ids_(\d+)_(\d+)$/)
|
|
138
|
+
return unless match
|
|
139
|
+
|
|
140
|
+
match[1].to_i
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def segment_for_values_clause(segment)
|
|
144
|
+
if segment.range_end == :max
|
|
145
|
+
"FROM (#{segment.range_start}) TO (MAXVALUE)"
|
|
146
|
+
else
|
|
147
|
+
"FROM (#{segment.range_start}) TO (#{segment.range_end})"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def hot_bucket_partition_name(bucket)
|
|
152
|
+
end_range = [bucket + current_band_size, active_window[:end]].min
|
|
153
|
+
"#{table_name}_ids_#{bucket}_#{end_range}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def current_band_size
|
|
157
|
+
@config.fetch(:current_band_size, DEFAULT_CURRENT_BAND_SIZE)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def table_name
|
|
163
|
+
@config[:table_name]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def connection
|
|
167
|
+
Connection.connection
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def active_id_width
|
|
171
|
+
@config.fetch(:active_id_width, DEFAULT_ACTIVE_ID_WIDTH)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def split_row_threshold
|
|
175
|
+
@config.fetch(:split_row_threshold, FUTURE_MONTH_PARTITION_ROW_THRESHOLD)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def each_bucket_in_range(range_start, range_end_exclusive)
|
|
179
|
+
buckets = []
|
|
180
|
+
cursor = range_start
|
|
181
|
+
while cursor < range_end_exclusive
|
|
182
|
+
buckets << cursor
|
|
183
|
+
cursor += current_band_size
|
|
184
|
+
end
|
|
185
|
+
buckets
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def bucket_in_window?(bucket, window)
|
|
189
|
+
bucket >= window[:start] && bucket < window[:end]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def managed_tail_partition?(partition_name, window:)
|
|
193
|
+
return true if tail_slot_name?(partition_name)
|
|
194
|
+
|
|
195
|
+
bucket = archive_bucket_from_partition_name(partition_name)
|
|
196
|
+
bucket && bucket_in_window?(bucket, window)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def segment_kind_for(partition_name, window:)
|
|
200
|
+
return :future if partition_name == future_partition_name(table_name)
|
|
201
|
+
return :filler if tail_slot_name?(partition_name)
|
|
202
|
+
|
|
203
|
+
:hot_bucket
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def heatmap_source_partitions(window)
|
|
207
|
+
names = tail_slot_names_for_heatmap.select { |name| Connection.partition_attached?(table_name, name) }
|
|
208
|
+
names.concat(hot_bucket_partitions_in_window(window).map(&:first))
|
|
209
|
+
names.uniq
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def tail_slot_names_for_heatmap
|
|
213
|
+
[
|
|
214
|
+
current_partition_name(table_name),
|
|
215
|
+
open_partition_name(table_name),
|
|
216
|
+
future_partition_name(table_name)
|
|
217
|
+
] + open_slot_partition_names
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def open_slot_partition_names
|
|
221
|
+
Connection.attached_partitions(table_name).map(&:name).select do |partition_name|
|
|
222
|
+
partition_name.match?(/^#{Regexp.escape(table_name)}_open_\d+$/)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def hot_bucket_partitions_in_window(window)
|
|
227
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
228
|
+
bucket = archive_bucket_from_partition_name(partition.name)
|
|
229
|
+
next unless bucket
|
|
230
|
+
next unless bucket_in_window?(bucket, window)
|
|
231
|
+
|
|
232
|
+
[partition.name, bucket]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def counts_by_bucket_in_partition(partition_name, window)
|
|
237
|
+
partition_key_column = @config[:partition_key_column]
|
|
238
|
+
band = current_band_size
|
|
239
|
+
|
|
240
|
+
sql = <<~SQL
|
|
241
|
+
SELECT (#{connection.quote_column_name(partition_key_column)} / #{band}) * #{band} AS bucket,
|
|
242
|
+
COUNT(*)::int AS row_count
|
|
243
|
+
FROM #{Connection.quoted_table(partition_name)}
|
|
244
|
+
GROUP BY 1
|
|
245
|
+
SQL
|
|
246
|
+
|
|
247
|
+
connection.execute(sql).each_with_object({}) do |row, counts|
|
|
248
|
+
bucket = row["bucket"].to_i
|
|
249
|
+
counts[bucket] = row["row_count"].to_i if bucket_in_window?(bucket, window)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def default_row_count
|
|
254
|
+
default_name = default_partition_name(table_name)
|
|
255
|
+
return 0 unless Connection.partition_exists?(default_name)
|
|
256
|
+
|
|
257
|
+
Connection.count_rows_in_partition_table(default_name)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
class ListSplit
|
|
4
|
+
include Naming
|
|
5
|
+
include RequiresDefaultPartition
|
|
6
|
+
include CursorColumns
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def active_window
|
|
13
|
+
{start: nil, end: nil}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_plan
|
|
17
|
+
segments = branches.map do |branch|
|
|
18
|
+
Plan::Segment.new(
|
|
19
|
+
name: branch_partition_name(branch),
|
|
20
|
+
range_start: branch.fetch(:value),
|
|
21
|
+
range_end: nil,
|
|
22
|
+
kind: :branch
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Plan::Result.new(segments: segments, hot_buckets: [])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attached_tail_segments
|
|
30
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
31
|
+
next if partition.default
|
|
32
|
+
|
|
33
|
+
branch = branch_for_partition_name(partition.name)
|
|
34
|
+
next unless branch
|
|
35
|
+
|
|
36
|
+
Plan::Segment.new(
|
|
37
|
+
name: partition.name,
|
|
38
|
+
range_start: branch.fetch(:value),
|
|
39
|
+
range_end: nil,
|
|
40
|
+
kind: :branch
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def collect_heatmap(_window)
|
|
46
|
+
{bucket_counts: {}, default_rows: default_row_count}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def hot_buckets_in_window(_heatmap, _window)
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def managed_tail_partition_names
|
|
54
|
+
branches.map { |branch| branch_partition_name(branch) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tail_slot_name?(_partition_name)
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def current_and_future_where_condition(window: active_window)
|
|
62
|
+
"TRUE"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def rebalance_default_drain_where_condition(window: active_window)
|
|
66
|
+
"TRUE"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def bucket_where_condition(branch_value)
|
|
70
|
+
branch = branches.find { |entry| entry.fetch(:value) == branch_value }
|
|
71
|
+
branch.fetch(:where_condition)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def archive_bucket?(_bucket)
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def future_bucket?(_bucket)
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def archive_bucket_from_partition_name(partition_name)
|
|
83
|
+
branch = branch_for_partition_name(partition_name)
|
|
84
|
+
branch&.fetch(:value)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def segment_for_values_clause(segment)
|
|
88
|
+
value = segment.range_start
|
|
89
|
+
formatted = format_list_value(value)
|
|
90
|
+
"IN (#{formatted})"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def table_name
|
|
96
|
+
@config[:table_name]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def branches
|
|
100
|
+
@config.fetch(:branches)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def branch_partition_name(branch)
|
|
104
|
+
"#{table_name}_#{branch.fetch(:name)}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def branch_for_partition_name(partition_name)
|
|
108
|
+
branches.find { |branch| branch_partition_name(branch) == partition_name }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def format_list_value(value)
|
|
112
|
+
return "NULL" if value.nil?
|
|
113
|
+
|
|
114
|
+
"'#{value}'"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def default_row_count
|
|
118
|
+
default_name = default_partition_name(table_name)
|
|
119
|
+
return 0 unless Connection.partition_exists?(default_name)
|
|
120
|
+
|
|
121
|
+
Connection.count_rows_in_partition_table(default_name)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
module RequiresDefaultPartition
|
|
4
|
+
def default_partition_required?
|
|
5
|
+
true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Rows in default matching this condition are staged during tail rebalance.
|
|
9
|
+
def rebalance_default_drain_where_condition(window: active_window)
|
|
10
|
+
current_and_future_where_condition(window: window)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Rows moved out of default at the end of maintenance (target: empty default).
|
|
14
|
+
def default_partition_drain_where_condition(window: active_window)
|
|
15
|
+
current_and_future_where_condition(window: window)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Strategy
|
|
3
|
+
class << self
|
|
4
|
+
def for(config)
|
|
5
|
+
build(config)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def build(config)
|
|
9
|
+
case config.fetch(:layout, :sliding_window)
|
|
10
|
+
when :integer_window
|
|
11
|
+
IntegerRange.new(config)
|
|
12
|
+
when :hash_branches
|
|
13
|
+
HashBranches.new(config)
|
|
14
|
+
when :list_split
|
|
15
|
+
ListSplit.new(config)
|
|
16
|
+
when :calendar_year
|
|
17
|
+
DateRange.new(config.merge(bucket: :year))
|
|
18
|
+
when :premake_monthly, :rolling_current
|
|
19
|
+
DateRange.new(config)
|
|
20
|
+
else
|
|
21
|
+
DateRange.new(config)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|