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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +36 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +203 -0
  5. data/SECURITY.md +27 -0
  6. data/docs/THIRD_PARTY_LICENSE_MANIFEST.tsv +60 -0
  7. data/docs/application_contract.md +82 -0
  8. data/docs/audit_reference.md +125 -0
  9. data/docs/background_job.md +161 -0
  10. data/docs/cli.md +71 -0
  11. data/docs/configuration.md +261 -0
  12. data/docs/cutover.md +180 -0
  13. data/docs/decision_flow.md +154 -0
  14. data/docs/host_testing.md +91 -0
  15. data/docs/monitoring.md +110 -0
  16. data/docs/naming.md +71 -0
  17. data/docs/operations.md +131 -0
  18. data/docs/partition_landscape.md +323 -0
  19. data/docs/pg_party_recipe.md +44 -0
  20. data/docs/related_postgres_tooling.md +195 -0
  21. data/docs/retention.md +65 -0
  22. data/docs/schemas/partition_garden.schema.json +114 -0
  23. data/docs/schemas/plan_report.schema.json +82 -0
  24. data/docs/tooling_split.md +79 -0
  25. data/exe/partition_gardener +6 -0
  26. data/lib/partition_gardener/active_record_run_record_store.rb +1 -0
  27. data/lib/partition_gardener/advisory_lock.rb +48 -0
  28. data/lib/partition_gardener/archive_retention.rb +88 -0
  29. data/lib/partition_gardener/audit.rb +93 -0
  30. data/lib/partition_gardener/blank.rb +13 -0
  31. data/lib/partition_gardener/cli.rb +190 -0
  32. data/lib/partition_gardener/config_document.rb +240 -0
  33. data/lib/partition_gardener/configuration.rb +102 -0
  34. data/lib/partition_gardener/connection.rb +260 -0
  35. data/lib/partition_gardener/date_bucket.rb +117 -0
  36. data/lib/partition_gardener/date_calendar.rb +65 -0
  37. data/lib/partition_gardener/date_range_maintenance.rb +297 -0
  38. data/lib/partition_gardener/default_partition.rb +54 -0
  39. data/lib/partition_gardener/executor.rb +324 -0
  40. data/lib/partition_gardener/gap_detection.rb +59 -0
  41. data/lib/partition_gardener/hash_routing.rb +70 -0
  42. data/lib/partition_gardener/layout/calendar_year.rb +28 -0
  43. data/lib/partition_gardener/layout/integer_window.rb +28 -0
  44. data/lib/partition_gardener/layout/sliding_window.rb +28 -0
  45. data/lib/partition_gardener/layout/three_area.rb +27 -0
  46. data/lib/partition_gardener/layout/zone_segments.rb +62 -0
  47. data/lib/partition_gardener/lock_not_acquired.rb +3 -0
  48. data/lib/partition_gardener/maintenance_backend.rb +73 -0
  49. data/lib/partition_gardener/memory_run_record_store.rb +19 -0
  50. data/lib/partition_gardener/migration/hot_switch_concern.rb +445 -0
  51. data/lib/partition_gardener/missing_conflict_index.rb +3 -0
  52. data/lib/partition_gardener/naming.rb +29 -0
  53. data/lib/partition_gardener/orphaned_rebalance_staging.rb +3 -0
  54. data/lib/partition_gardener/pg_connection.rb +94 -0
  55. data/lib/partition_gardener/plan.rb +49 -0
  56. data/lib/partition_gardener/plan_applier.rb +289 -0
  57. data/lib/partition_gardener/plan_diff.rb +51 -0
  58. data/lib/partition_gardener/plan_report.rb +95 -0
  59. data/lib/partition_gardener/planner.rb +21 -0
  60. data/lib/partition_gardener/predicate.rb +85 -0
  61. data/lib/partition_gardener/premake_monthly_maintenance.rb +44 -0
  62. data/lib/partition_gardener/rails.rb +12 -0
  63. data/lib/partition_gardener/registry.rb +84 -0
  64. data/lib/partition_gardener/run_failed.rb +10 -0
  65. data/lib/partition_gardener/run_metrics.rb +65 -0
  66. data/lib/partition_gardener/run_record.rb +76 -0
  67. data/lib/partition_gardener/sql_run_record_store.rb +106 -0
  68. data/lib/partition_gardener/stdlib_extensions.rb +15 -0
  69. data/lib/partition_gardener/strategy/composite.rb +27 -0
  70. data/lib/partition_gardener/strategy/cursor_columns.rb +18 -0
  71. data/lib/partition_gardener/strategy/date_range.rb +303 -0
  72. data/lib/partition_gardener/strategy/hash_branches.rb +161 -0
  73. data/lib/partition_gardener/strategy/integer_range.rb +261 -0
  74. data/lib/partition_gardener/strategy/list_split.rb +125 -0
  75. data/lib/partition_gardener/strategy/requires_default_partition.rb +19 -0
  76. data/lib/partition_gardener/strategy.rb +26 -0
  77. data/lib/partition_gardener/templates.rb +373 -0
  78. data/lib/partition_gardener/unmoved_rows_remaining.rb +15 -0
  79. data/lib/partition_gardener/version.rb +3 -0
  80. data/lib/partition_gardener.rb +215 -0
  81. data/sig/partition_gardener.rbs +19 -0
  82. 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