partition_gardener 0.3.4 → 0.3.5
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/CHANGELOG.md +7 -0
- data/README.md +2 -1
- data/docs/THIRD_PARTY_LICENSE_MANIFEST.tsv +4 -4
- data/docs/audit_reference.md +1 -0
- data/docs/cli.md +2 -1
- data/docs/configuration.md +2 -0
- data/docs/naming.md +5 -3
- data/docs/retention.md +1 -0
- data/lib/partition_gardener/date_bucket.rb +1 -1
- data/lib/partition_gardener/layout/calendar_year.rb +3 -2
- data/lib/partition_gardener/layout/occupied_window.rb +86 -0
- data/lib/partition_gardener/layout/sliding_window.rb +3 -2
- data/lib/partition_gardener/layout/zone_segments.rb +14 -9
- data/lib/partition_gardener/strategy/date_range.rb +10 -16
- data/lib/partition_gardener/version.rb +1 -1
- data/lib/partition_gardener.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abde0192951f5cc668d53ca847f119c3d9dcf5c994404504119cfb1d54c6be17
|
|
4
|
+
data.tar.gz: 47eab4f81f45acf7a59730e2a04ac48769e655557da78c0db12be355a85ce6b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51677ad1c0b6d9513a86e574e9254776a7b9cecbd83f230a38b61d1a3f04be5313987901658aa9aebbf78e47a0f5ddd9b9573c6b3396c599ac6c60fdc932ab96
|
|
7
|
+
data.tar.gz: f1f4271c57f3a1ed9eaf2ca2f47d351cb50f729377f21bfac7657ce9d5342067b8dbc4c005d3db926d9cbf25186fbf0f431d287daf8139111758c2d647e7ac8d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.5 (2026-09-09)
|
|
6
|
+
|
|
7
|
+
- Keep attached monthly children that already cover the start of the sliding-window active range, and fill the remainder as `_open` rather than overlapping `_current`
|
|
8
|
+
- Start `_future` after premade months that occupy keys at or after the active window end, and skip `_future` when an occupying child already ends at MAXVALUE
|
|
9
|
+
- Add the RFC process (RFC 0001) and a Proposed occupancy-first contract for `_current`, `_open`, and `_future` (RFC 0002)
|
|
10
|
+
- Specify shipped registry, plan, CLI, layout, rebalance, lock, hot-switch, and column-align contracts as Stable RFCs 0003 through 0015
|
|
11
|
+
|
|
5
12
|
## 0.3.4 (2026-09-02)
|
|
6
13
|
|
|
7
14
|
- Align LIKE and detached children with additive parent columns before `ATTACH PARTITION`, preserving type, collation, default, and `NOT NULL` atomically. Audit warns when a managed child still lags. Set `align_child_columns: false` to fail fast instead.
|
data/README.md
CHANGED
|
@@ -41,9 +41,10 @@ Complements migration gems (e.g. [pg_party](https://github.com/rkrage/pg_party))
|
|
|
41
41
|
- [docs/naming.md](docs/naming.md) — child partition naming catalog
|
|
42
42
|
- [docs/host_testing.md](docs/host_testing.md) — CI and staging for host apps
|
|
43
43
|
|
|
44
|
-
### Schemas
|
|
44
|
+
### Schemas and RFCs
|
|
45
45
|
|
|
46
46
|
- [docs/schemas/](docs/schemas/) — JSON schemas for registry and plan reports
|
|
47
|
+
- [rfcs/](rfcs/) — design review; RFC 0002 Proposed tail slots; RFC 0003–0015 Stable shipped contracts
|
|
47
48
|
|
|
48
49
|
## Quick start
|
|
49
50
|
|
|
@@ -17,15 +17,15 @@ root language_server-protocol 3.17.0.6 MIT
|
|
|
17
17
|
root lint_roller 1.1.0 MIT
|
|
18
18
|
root logger 1.7.0 Ruby|BSD-2-Clause
|
|
19
19
|
root minitest 6.0.6 MIT
|
|
20
|
-
root parallel 2.
|
|
20
|
+
root parallel 2.2.0 MIT
|
|
21
21
|
root parser 3.3.12.0 MIT
|
|
22
|
-
root partition_gardener 0.3.
|
|
22
|
+
root partition_gardener 0.3.5 MIT
|
|
23
23
|
root pg 1.6.3 BSD-2-Clause
|
|
24
|
-
root polyrun 2.2.
|
|
24
|
+
root polyrun 2.2.4 MIT
|
|
25
25
|
root prism 1.9.0 MIT
|
|
26
26
|
root prosopite 2.2.0 Apache-2.0
|
|
27
27
|
root racc 1.8.1 Ruby|BSD-2-Clause
|
|
28
|
-
root rack 3.2.
|
|
28
|
+
root rack 3.2.7 MIT
|
|
29
29
|
root rainbow 3.1.1 MIT
|
|
30
30
|
root rake 13.4.2 MIT
|
|
31
31
|
root rbs 4.2.0 BSD-2-Clause|Ruby
|
data/docs/audit_reference.md
CHANGED
|
@@ -122,6 +122,7 @@ Disable globally or per table if you do not want checkpoint storage ([configurat
|
|
|
122
122
|
|
|
123
123
|
## Related
|
|
124
124
|
|
|
125
|
+
- [RFC 0005](../rfcs/0005-plan-audit-reports.md) — plan, audit, and RunSummary contract
|
|
125
126
|
- [operations.md](operations.md) — remediation workflows
|
|
126
127
|
- [monitoring.md](monitoring.md) — metrics from these fields
|
|
127
128
|
- [naming.md](naming.md) — segment names in output
|
data/docs/cli.md
CHANGED
|
@@ -58,13 +58,14 @@ bundle exec partition_gardener --rails apply --confirm audits
|
|
|
58
58
|
Apply all registered gardener-owned tables:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
bundle exec partition_gardener --rails apply --all
|
|
61
|
+
bundle exec partition_gardener --rails apply --confirm --all
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
Omitting a table name without `--all` also runs the full registry.
|
|
65
65
|
|
|
66
66
|
## Related
|
|
67
67
|
|
|
68
|
+
- [RFC 0008](../rfcs/0008-cli.md) — verbs, confirm gate, table selection
|
|
68
69
|
- [configuration.md](configuration.md) — registry format and global config
|
|
69
70
|
- [background_job.md](background_job.md) — when to use a job instead of CLI apply
|
|
70
71
|
- [audit_reference.md](audit_reference.md) — audit and plan output reference
|
data/docs/configuration.md
CHANGED
|
@@ -257,6 +257,8 @@ Registry entries are operator-controlled configuration, not end-user input.
|
|
|
257
257
|
|
|
258
258
|
## Related
|
|
259
259
|
|
|
260
|
+
- [RFC 0004](../rfcs/0004-registry-json.md) — portable registry file
|
|
261
|
+
- [RFC 0009](../rfcs/0009-maintenance-backends.md) — gardener, pg_partman, hybrid
|
|
260
262
|
- [background_job.md](background_job.md) — schedule `run!` from a host job
|
|
261
263
|
- [cli.md](cli.md) — plan, audit, apply from the shell
|
|
262
264
|
- [decision_flow.md](decision_flow.md) — when and how to partition
|
data/docs/naming.md
CHANGED
|
@@ -8,13 +8,13 @@ Fixed suffixes on the parent table name:
|
|
|
8
8
|
|
|
9
9
|
`{table}_default` — catch-all; must trend empty.
|
|
10
10
|
|
|
11
|
-
`{table}_current` —
|
|
11
|
+
`{table}_current` — filler that starts at the active window origin. The span is the uncovered remainder of that window, not this calendar month or sequence id.
|
|
12
12
|
|
|
13
|
-
`{table}_open` — gap filler in current zone.
|
|
13
|
+
`{table}_open` — gap filler in the current zone when the origin is already a named child or a heat split. Extra fillers after that are `{table}_open_N`.
|
|
14
14
|
|
|
15
15
|
`{table}_open_N` — extra fillers after heat splits.
|
|
16
16
|
|
|
17
|
-
`{table}_future` — open-ended tail to MAXVALUE.
|
|
17
|
+
`{table}_future` — open-ended tail to MAXVALUE. It starts at the first instant at or after the window end that no occupying child covers.
|
|
18
18
|
|
|
19
19
|
`{table}_rebalance_staging` — temporary during tail rebalance.
|
|
20
20
|
|
|
@@ -66,6 +66,8 @@ Remediation: prefer `plan` + `apply` over manual attach. If manual children are
|
|
|
66
66
|
|
|
67
67
|
## Related
|
|
68
68
|
|
|
69
|
+
- [RFC 0002](../rfcs/0002-tail-slots.md) — names and occupancy-first placement
|
|
70
|
+
- [RFC 0006](../rfcs/0006-archive-naming.md) — date-bucket suffixes and parse-back
|
|
69
71
|
- [operations.md](operations.md) — gap remediation
|
|
70
72
|
- [cutover.md](cutover.md) — shadow `p_` prefix
|
|
71
73
|
- [audit_reference.md](audit_reference.md) — gap messages
|
data/docs/retention.md
CHANGED
|
@@ -60,6 +60,7 @@ When `maintenance_backend: :pg_partman`, gardener skips the table; partman owns
|
|
|
60
60
|
|
|
61
61
|
## Related
|
|
62
62
|
|
|
63
|
+
- [RFC 0007](../rfcs/0007-retention.md) — cutoff, preview, skip list
|
|
63
64
|
- [decision_flow.md](decision_flow.md) — retention in layout choice
|
|
64
65
|
- [operations.md](operations.md) — incident response
|
|
65
66
|
- [monitoring.md](monitoring.md) — alerts
|
|
@@ -101,7 +101,7 @@ module PartitionGardener
|
|
|
101
101
|
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})$/)
|
|
102
102
|
return Date.new(match[1].to_i, 1, 1) if match
|
|
103
103
|
else
|
|
104
|
-
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})_(\d{2})
|
|
104
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})_(\d{2})(?:_01)?$/)
|
|
105
105
|
return Date.new(match[1].to_i, match[2].to_i, 1) if match
|
|
106
106
|
end
|
|
107
107
|
|
|
@@ -8,7 +8,7 @@ module PartitionGardener
|
|
|
8
8
|
DateCalendar.add_years(active_start, active_years)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def build_segments(config:, active_start:, active_end:, hot_years:)
|
|
11
|
+
def build_segments(config:, active_start:, active_end:, hot_years:, occupied_segments: [])
|
|
12
12
|
strategy = Strategy::DateRange.new(config.merge(bucket: :year))
|
|
13
13
|
buckets = strategy.send(:each_bucket_in_range, active_start, active_end)
|
|
14
14
|
|
|
@@ -19,7 +19,8 @@ module PartitionGardener
|
|
|
19
19
|
active_start: active_start,
|
|
20
20
|
active_end: active_end,
|
|
21
21
|
hot_bucket_name: config[:partition_name_format],
|
|
22
|
-
bucket_end: ->(bucket) { DateCalendar.beginning_of_year(DateCalendar.next_year(bucket)) }
|
|
22
|
+
bucket_end: ->(bucket) { DateCalendar.beginning_of_year(DateCalendar.next_year(bucket)) },
|
|
23
|
+
occupied_segments: occupied_segments
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Layout
|
|
3
|
+
module OccupiedWindow
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def plan_segments(config:, window:, hot_buckets:, year_bucket:, tail_slot:)
|
|
7
|
+
occupied_segments = segments(table_name: config[:table_name], window: window, tail_slot: tail_slot)
|
|
8
|
+
layout_kwargs = {
|
|
9
|
+
config: config,
|
|
10
|
+
active_start: window[:start],
|
|
11
|
+
active_end: window[:end],
|
|
12
|
+
occupied_segments: occupied_segments
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if year_bucket
|
|
16
|
+
CalendarYear.build_segments(**layout_kwargs, hot_years: hot_buckets)
|
|
17
|
+
else
|
|
18
|
+
SlidingWindow.build_segments(**layout_kwargs, hot_months: hot_buckets)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def segments(table_name:, window:, tail_slot:)
|
|
23
|
+
Connection.attached_partitions(table_name).filter_map do |partition|
|
|
24
|
+
next if partition.default
|
|
25
|
+
next if tail_slot.call(partition.name)
|
|
26
|
+
next unless overlap?(partition, window)
|
|
27
|
+
|
|
28
|
+
Plan::Segment.new(
|
|
29
|
+
name: partition.name,
|
|
30
|
+
range_start: partition.range_start,
|
|
31
|
+
range_end: partition.range_end,
|
|
32
|
+
kind: :hot_bucket
|
|
33
|
+
)
|
|
34
|
+
end.sort_by(&:range_start)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def overlap?(partition, window)
|
|
38
|
+
return false unless partition.range_start.is_a?(Date)
|
|
39
|
+
return false if partition.range_end.nil?
|
|
40
|
+
return true if partition.range_end == :max
|
|
41
|
+
return true if partition.range_end > window[:end]
|
|
42
|
+
|
|
43
|
+
partition.range_start < window[:end] && partition.range_end > window[:start]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def covering(occupied_segments, bucket)
|
|
47
|
+
occupied_segments.find do |segment|
|
|
48
|
+
next false if segment.range_start > bucket
|
|
49
|
+
|
|
50
|
+
segment.range_end == :max || segment.range_end > bucket
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def next_bucket_index(buckets, bound, from:)
|
|
55
|
+
return buckets.length if bound == :max
|
|
56
|
+
|
|
57
|
+
index = from
|
|
58
|
+
index += 1 while index < buckets.length && buckets[index] < bound
|
|
59
|
+
index
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def finish_with_high_end(segments, occupied_segments:, table_name:, active_end:)
|
|
63
|
+
leftover = occupied_segments.reject { |occupant| segments.any? { |segment| segment.name == occupant.name } }
|
|
64
|
+
leftover.each { |occupant| segments << occupant }
|
|
65
|
+
|
|
66
|
+
return segments if occupied_segments.any? { |segment| segment.range_end == :max }
|
|
67
|
+
|
|
68
|
+
segments << Plan::Segment.new(
|
|
69
|
+
name: Naming.future_partition_name(table_name),
|
|
70
|
+
range_start: future_start(occupied_segments, active_end),
|
|
71
|
+
range_end: :max,
|
|
72
|
+
kind: :future
|
|
73
|
+
)
|
|
74
|
+
segments
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def future_start(occupied_segments, active_end)
|
|
78
|
+
later_ends = occupied_segments.filter_map do |segment|
|
|
79
|
+
segment.range_end if segment.range_end.is_a?(Date) && segment.range_end > active_end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[active_end, *later_ends].max
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -7,7 +7,7 @@ module PartitionGardener
|
|
|
7
7
|
DateBucket.add_buckets(active_start, span, bucket)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def build_segments(config:, active_start:, active_end:, hot_months:)
|
|
10
|
+
def build_segments(config:, active_start:, active_end:, hot_months:, occupied_segments: [])
|
|
11
11
|
strategy = Strategy::DateRange.new(config)
|
|
12
12
|
buckets = strategy.send(:each_bucket_in_range, active_start, active_end)
|
|
13
13
|
bucket = config.fetch(:bucket, :month)
|
|
@@ -19,7 +19,8 @@ module PartitionGardener
|
|
|
19
19
|
active_start: active_start,
|
|
20
20
|
active_end: active_end,
|
|
21
21
|
hot_bucket_name: config[:partition_name_format],
|
|
22
|
-
bucket_end: ->(bucket_start) { DateBucket.end_of_bucket(bucket_start, bucket) }
|
|
22
|
+
bucket_end: ->(bucket_start) { DateBucket.end_of_bucket(bucket_start, bucket) },
|
|
23
|
+
occupied_segments: occupied_segments
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -3,7 +3,7 @@ module PartitionGardener
|
|
|
3
3
|
module ZoneSegments
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
|
-
def build_filler_and_hot_segments(table_name:, buckets:, hot_buckets:, active_start:, active_end:, hot_bucket_name:, bucket_end:)
|
|
6
|
+
def build_filler_and_hot_segments(table_name:, buckets:, hot_buckets:, active_start:, active_end:, hot_bucket_name:, bucket_end:, occupied_segments: [])
|
|
7
7
|
hot_bucket_set = hot_buckets.to_set
|
|
8
8
|
segments = []
|
|
9
9
|
middle_filler_index = 0
|
|
@@ -11,6 +11,13 @@ module PartitionGardener
|
|
|
11
11
|
|
|
12
12
|
while index < buckets.length
|
|
13
13
|
bucket = buckets[index]
|
|
14
|
+
occupant = OccupiedWindow.covering(occupied_segments, bucket)
|
|
15
|
+
|
|
16
|
+
if occupant
|
|
17
|
+
segments << occupant unless segments.any? { |segment| segment.name == occupant.name }
|
|
18
|
+
index = OccupiedWindow.next_bucket_index(buckets, occupant.range_end, from: index)
|
|
19
|
+
next
|
|
20
|
+
end
|
|
14
21
|
|
|
15
22
|
if hot_bucket_set.include?(bucket)
|
|
16
23
|
segments << Plan::Segment.new(
|
|
@@ -24,7 +31,7 @@ module PartitionGardener
|
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
run_start = bucket
|
|
27
|
-
while index < buckets.length && !hot_bucket_set.include?(buckets[index])
|
|
34
|
+
while index < buckets.length && !hot_bucket_set.include?(buckets[index]) && OccupiedWindow.covering(occupied_segments, buckets[index]).nil?
|
|
28
35
|
index += 1
|
|
29
36
|
end
|
|
30
37
|
run_end = (index < buckets.length) ? buckets[index] : active_end
|
|
@@ -48,14 +55,12 @@ module PartitionGardener
|
|
|
48
55
|
)
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
OccupiedWindow.finish_with_high_end(
|
|
59
|
+
segments,
|
|
60
|
+
occupied_segments: occupied_segments,
|
|
61
|
+
table_name: table_name,
|
|
62
|
+
active_end: active_end
|
|
56
63
|
)
|
|
57
|
-
|
|
58
|
-
segments
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
end
|
|
@@ -25,22 +25,13 @@ module PartitionGardener
|
|
|
25
25
|
window = active_window
|
|
26
26
|
heatmap = collect_heatmap(window)
|
|
27
27
|
hot_buckets = hot_buckets_in_window(heatmap, window)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
28
|
+
segments = Layout::OccupiedWindow.plan_segments(
|
|
29
|
+
config: @config,
|
|
30
|
+
window: window,
|
|
31
|
+
hot_buckets: hot_buckets,
|
|
32
|
+
year_bucket: year_bucket?,
|
|
33
|
+
tail_slot: method(:tail_slot_name?)
|
|
34
|
+
)
|
|
44
35
|
|
|
45
36
|
Plan::Result.new(segments: segments, hot_buckets: hot_buckets)
|
|
46
37
|
end
|
|
@@ -231,6 +222,9 @@ module PartitionGardener
|
|
|
231
222
|
def managed_tail_partition?(partition_name, window:)
|
|
232
223
|
return true if tail_slot_name?(partition_name)
|
|
233
224
|
|
|
225
|
+
partition = Connection.attached_partitions(table_name).find { |item| item.name == partition_name }
|
|
226
|
+
return true if partition && Layout::OccupiedWindow.overlap?(partition, window)
|
|
227
|
+
|
|
234
228
|
bucket = archive_bucket_from_partition_name(partition_name)
|
|
235
229
|
bucket && bucket_in_window?(bucket, window)
|
|
236
230
|
end
|
data/lib/partition_gardener.rb
CHANGED
|
@@ -26,6 +26,7 @@ require_relative "partition_gardener/strategy/list_split"
|
|
|
26
26
|
require_relative "partition_gardener/strategy/composite"
|
|
27
27
|
require_relative "partition_gardener/strategy"
|
|
28
28
|
require_relative "partition_gardener/layout/three_area"
|
|
29
|
+
require_relative "partition_gardener/layout/occupied_window"
|
|
29
30
|
require_relative "partition_gardener/layout/zone_segments"
|
|
30
31
|
require_relative "partition_gardener/layout/sliding_window"
|
|
31
32
|
require_relative "partition_gardener/layout/calendar_year"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: partition_gardener
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -317,6 +317,7 @@ files:
|
|
|
317
317
|
- lib/partition_gardener/hash_routing.rb
|
|
318
318
|
- lib/partition_gardener/layout/calendar_year.rb
|
|
319
319
|
- lib/partition_gardener/layout/integer_window.rb
|
|
320
|
+
- lib/partition_gardener/layout/occupied_window.rb
|
|
320
321
|
- lib/partition_gardener/layout/sliding_window.rb
|
|
321
322
|
- lib/partition_gardener/layout/three_area.rb
|
|
322
323
|
- lib/partition_gardener/layout/zone_segments.rb
|