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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4be8b78232ba13211d0baad009dea43f6320b96dd9b82398c8a4e86ed6008b3d
4
- data.tar.gz: acb6c02d0192c07e3b8c9f959b6ada44e44014f9f5c144906e6649ebb1309e87
3
+ metadata.gz: abde0192951f5cc668d53ca847f119c3d9dcf5c994404504119cfb1d54c6be17
4
+ data.tar.gz: 47eab4f81f45acf7a59730e2a04ac48769e655557da78c0db12be355a85ce6b2
5
5
  SHA512:
6
- metadata.gz: f387a844418342ef9f4ff644faf1ca2431f8259e3f26e9220bab9ce225ae5f1606ceb4651a94cb39f0e7929a7a0b7c2eae779c6aa7d079a8ad4355901f39a63f
7
- data.tar.gz: a659acdb2ed364d72f44a9ecb0aab7ac925b27b77f8365e9da36582416145c9db9bbe6a1e9da279be036eab0cb4eda3d4a865d5a86a22741564bbcbf87145d32
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.1.0 MIT
20
+ root parallel 2.2.0 MIT
21
21
  root parser 3.3.12.0 MIT
22
- root partition_gardener 0.3.4 MIT
22
+ root partition_gardener 0.3.5 MIT
23
23
  root pg 1.6.3 BSD-2-Clause
24
- root polyrun 2.2.3 MIT
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.6 MIT
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
@@ -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
@@ -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` — start of active window.
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
- segments << Plan::Segment.new(
52
- name: Naming.future_partition_name(table_name),
53
- range_start: active_end,
54
- range_end: :max,
55
- kind: :future
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
- 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
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
@@ -1,3 +1,3 @@
1
1
  module PartitionGardener
2
- VERSION = "0.3.4"
2
+ VERSION = "0.3.5"
3
3
  end
@@ -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
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