partition_gardener 0.3.3 → 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 +11 -0
- data/README.md +2 -1
- data/SECURITY.md +10 -12
- data/docs/THIRD_PARTY_LICENSE_MANIFEST.tsv +5 -5
- data/docs/audit_reference.md +3 -0
- data/docs/cli.md +2 -1
- data/docs/configuration.md +6 -0
- data/docs/naming.md +5 -3
- data/docs/retention.md +1 -0
- data/docs/schemas/partition_garden.schema.json +4 -0
- data/lib/partition_gardener/audit.rb +16 -0
- data/lib/partition_gardener/child_column_align.rb +165 -0
- data/lib/partition_gardener/config_document.rb +9 -0
- data/lib/partition_gardener/configuration.rb +2 -0
- data/lib/partition_gardener/date_bucket.rb +1 -1
- data/lib/partition_gardener/executor.rb +20 -2
- 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/templates.rb +6 -0
- data/lib/partition_gardener/version.rb +1 -1
- data/lib/partition_gardener.rb +2 -0
- metadata +3 -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,17 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.3.4 (2026-09-02)
|
|
13
|
+
|
|
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.
|
|
15
|
+
|
|
5
16
|
## 0.3.3 (2026-08-13)
|
|
6
17
|
|
|
7
18
|
- Roll the sliding-window current partition before attaching the prior month's archive so a month rollover detaches the old current span and attaches that month as its own child
|
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
|
|
data/SECURITY.md
CHANGED
|
@@ -2,26 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Reporting a Vulnerability
|
|
4
4
|
|
|
5
|
-
Do not open a public issue for security vulnerabilities.
|
|
6
|
-
|
|
7
5
|
Email security details to: security@kiskolabs.com
|
|
8
6
|
|
|
9
|
-
Include
|
|
7
|
+
Send the report only to that address. Include description, steps to reproduce, potential impact, and a suggested fix when you have one.
|
|
10
8
|
|
|
11
9
|
### Response Timeline
|
|
12
10
|
|
|
13
|
-
- We will acknowledge receipt of your report
|
|
14
|
-
- We will provide an initial assessment
|
|
15
|
-
- We will keep you informed of our progress and resolution timeline
|
|
11
|
+
- We will acknowledge receipt of your report.
|
|
12
|
+
- We will provide an initial assessment.
|
|
13
|
+
- We will keep you informed of our progress and resolution timeline.
|
|
16
14
|
|
|
17
15
|
### Disclosure Policy
|
|
18
16
|
|
|
19
|
-
- We will work with you to understand and resolve the issue
|
|
20
|
-
- We will credit you for the discovery
|
|
21
|
-
- We will publish a security advisory after the vulnerability is patched
|
|
22
|
-
- We will coordinate public disclosure with you
|
|
17
|
+
- We will work with you to understand and resolve the issue.
|
|
18
|
+
- We will credit you for the discovery unless you prefer to remain anonymous.
|
|
19
|
+
- We will publish a security advisory after the vulnerability is patched.
|
|
20
|
+
- We will coordinate public disclosure with you.
|
|
23
21
|
|
|
24
22
|
## Automation Security
|
|
25
23
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
24
|
+
- Keep production credentials, API keys, and personal data out of prompts sent to third-party models or automation services.
|
|
25
|
+
- Verify automated dependencies.
|
|
@@ -17,18 +17,18 @@ 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
|
-
root rbs 4.
|
|
31
|
+
root rbs 4.2.0 BSD-2-Clause|Ruby
|
|
32
32
|
root regexp_parser 2.12.0 MIT
|
|
33
33
|
root rspec 3.13.2 MIT
|
|
34
34
|
root rspec-core 3.13.6 MIT
|
data/docs/audit_reference.md
CHANGED
|
@@ -36,6 +36,8 @@ Horizon warning threshold: 30 days (`Audit::HORIZON_WARNING_DAYS`). High child c
|
|
|
36
36
|
|
|
37
37
|
`partition gap: no attached tail partition extends to MAXVALUE` — missing future/open tail. `apply` to attach `_future`.
|
|
38
38
|
|
|
39
|
+
`child {name} is missing column {columns} from parent {table}` — a managed LIKE or detached child lacks ordinary columns added on the parent later. `apply` adds those columns atomically then attaches (default). Set `align_child_columns: false` to fail fast. Built-in naming schemes exclude unrelated prefixed tables; custom partition name formatters retain prefix discovery.
|
|
40
|
+
|
|
39
41
|
Gap objects include `range_start`, `range_end`, and `message`.
|
|
40
42
|
|
|
41
43
|
## Plan report
|
|
@@ -120,6 +122,7 @@ Disable globally or per table if you do not want checkpoint storage ([configurat
|
|
|
120
122
|
|
|
121
123
|
## Related
|
|
122
124
|
|
|
125
|
+
- [RFC 0005](../rfcs/0005-plan-audit-reports.md) — plan, audit, and RunSummary contract
|
|
123
126
|
- [operations.md](operations.md) — remediation workflows
|
|
124
127
|
- [monitoring.md](monitoring.md) — metrics from these fields
|
|
125
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
|
@@ -53,6 +53,8 @@ When the gem loads inside Rails, the railtie sets `connection_resolver` and `tod
|
|
|
53
53
|
|
|
54
54
|
`retention_detach_concurrently` (default: `false`) — detach dropped archive partitions with `CONCURRENTLY` (global default).
|
|
55
55
|
|
|
56
|
+
`align_child_columns` (default: `true`) — before `ATTACH PARTITION`, add ordinary parent columns that a LIKE or detached child is missing. Audit warns when a child still lags. Set `false` to fail fast when a child is missing those columns.
|
|
57
|
+
|
|
56
58
|
### Advisory locks
|
|
57
59
|
|
|
58
60
|
Each table run acquires `pg_advisory_lock(hashtext('partition_gardener'), hashtext(table_name))` (or the transaction-scoped variant).
|
|
@@ -164,6 +166,8 @@ All templates accept shared options passed through to the registry hash:
|
|
|
164
166
|
|
|
165
167
|
`retention_detach_concurrently` (default: global default) — per-table override for concurrent detach.
|
|
166
168
|
|
|
169
|
+
`align_child_columns` (default: global default) — per-table override. Additive only: add missing ordinary columns in one statement (skip identity and generated), preserving type, non-default collation, default, and `NOT NULL`. If existing rows cannot satisfy `NOT NULL`, PostgreSQL rejects the complete alignment and leaves the child unchanged. Existing types, extra child columns, and other constraints stay as they are.
|
|
170
|
+
|
|
167
171
|
`maintenance_backend` (default: `:gardener`) — `:gardener`, `:pg_partman`, or `:hybrid_layout_only`.
|
|
168
172
|
|
|
169
173
|
`incremental_rebalance` (default: global default) — per-table override.
|
|
@@ -253,6 +257,8 @@ Registry entries are operator-controlled configuration, not end-user input.
|
|
|
253
257
|
|
|
254
258
|
## Related
|
|
255
259
|
|
|
260
|
+
- [RFC 0004](../rfcs/0004-registry-json.md) — portable registry file
|
|
261
|
+
- [RFC 0009](../rfcs/0009-maintenance-backends.md) — gardener, pg_partman, hybrid
|
|
256
262
|
- [background_job.md](background_job.md) — schedule `run!` from a host job
|
|
257
263
|
- [cli.md](cli.md) — plan, audit, apply from the shell
|
|
258
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
|
|
@@ -92,6 +92,10 @@
|
|
|
92
92
|
"retention_detach_concurrently": {
|
|
93
93
|
"type": "boolean"
|
|
94
94
|
},
|
|
95
|
+
"align_child_columns": {
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"description": "When true (default), atomically add missing ordinary parent columns with their collation, default, and NOT NULL contract on a LIKE or detached child before ATTACH PARTITION"
|
|
98
|
+
},
|
|
95
99
|
"hash_modulus": {
|
|
96
100
|
"type": "integer",
|
|
97
101
|
"minimum": 2,
|
|
@@ -61,6 +61,10 @@ module PartitionGardener
|
|
|
61
61
|
warnings << "partition gap: #{gap.message}"
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
warning_options = {}
|
|
65
|
+
warning_options[:candidate_table] = method(:managed_child_table?) if @config
|
|
66
|
+
warnings.concat(ChildColumnAlign.lagging_child_warnings(@table_name, **warning_options))
|
|
67
|
+
|
|
64
68
|
AuditResult.new(
|
|
65
69
|
table_name: @table_name,
|
|
66
70
|
partitioned: true,
|
|
@@ -74,6 +78,18 @@ module PartitionGardener
|
|
|
74
78
|
|
|
75
79
|
private
|
|
76
80
|
|
|
81
|
+
def managed_child_table?(child_table)
|
|
82
|
+
return true if @config[:partition_name_format_is_custom] || @config[:archive_partition_name_format]
|
|
83
|
+
return true if child_table == Naming.rebalance_staging_partition_name(@table_name)
|
|
84
|
+
|
|
85
|
+
strategy = (@strategy ||= Strategy.for(@config))
|
|
86
|
+
return true if strategy.respond_to?(:tail_slot_name?) && strategy.tail_slot_name?(child_table)
|
|
87
|
+
|
|
88
|
+
!strategy.archive_bucket_from_partition_name(child_table).nil?
|
|
89
|
+
rescue Date::Error
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
77
93
|
def compute_horizon_days(partitions)
|
|
78
94
|
today = PartitionGardener.configuration.today
|
|
79
95
|
finite_ends = partitions.filter_map do |partition|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class ChildColumnAlign
|
|
3
|
+
Column = Data.define(
|
|
4
|
+
:name,
|
|
5
|
+
:type,
|
|
6
|
+
:not_null,
|
|
7
|
+
:default_expression,
|
|
8
|
+
:collation_schema,
|
|
9
|
+
:collation_name
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def self.lagging_child_warnings(parent_table, connection: Connection.connection, candidate_table: nil)
|
|
13
|
+
new(connection: connection).lagging_child_warnings(parent_table, candidate_table: candidate_table)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(connection: Connection.connection)
|
|
17
|
+
@connection = connection
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def align!(parent_table, child_table)
|
|
21
|
+
missing = missing_ordinary_columns(parent_table, child_table)
|
|
22
|
+
add_columns!(child_table, missing) if missing.any?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def lagging_child_warnings(parent_table, candidate_table: nil)
|
|
26
|
+
child_tables = unattached_child_tables(parent_table)
|
|
27
|
+
child_tables.select!(&candidate_table) if candidate_table
|
|
28
|
+
columns_by_table = ordinary_columns_by_table([parent_table, *child_tables])
|
|
29
|
+
parent_columns = columns_by_table.fetch(parent_table, [])
|
|
30
|
+
|
|
31
|
+
child_tables.filter_map do |child_table|
|
|
32
|
+
missing = missing_columns(parent_columns, columns_by_table.fetch(child_table, []))
|
|
33
|
+
next if missing.empty?
|
|
34
|
+
|
|
35
|
+
missing_columns_warning(child_table, parent_table, missing)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def missing_ordinary_columns(parent_table, child_table)
|
|
40
|
+
columns_by_table = ordinary_columns_by_table([parent_table, child_table])
|
|
41
|
+
missing_columns(
|
|
42
|
+
columns_by_table.fetch(parent_table, []),
|
|
43
|
+
columns_by_table.fetch(child_table, [])
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def add_columns!(child_table, columns)
|
|
50
|
+
actions = columns.map { |column| add_column_action(column) }.join(",\n ")
|
|
51
|
+
@connection.execute(<<~SQL)
|
|
52
|
+
ALTER TABLE #{quoted_table(child_table)}
|
|
53
|
+
#{actions}
|
|
54
|
+
SQL
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_column_action(column)
|
|
58
|
+
clauses = ["ADD COLUMN #{quoted_column(column.name)} #{column.type}"]
|
|
59
|
+
if column.collation_name
|
|
60
|
+
clauses << "COLLATE #{quoted_column(column.collation_schema)}.#{quoted_column(column.collation_name)}"
|
|
61
|
+
end
|
|
62
|
+
clauses << "DEFAULT #{column.default_expression}" if usable_default?(column.default_expression)
|
|
63
|
+
clauses << "NOT NULL" if column.not_null
|
|
64
|
+
clauses.join(" ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ordinary_columns_by_table(table_names)
|
|
68
|
+
names = table_names.uniq
|
|
69
|
+
quoted_names = names.map { |table_name| @connection.quote(table_name) }.join(", ")
|
|
70
|
+
sql = <<~SQL
|
|
71
|
+
SELECT
|
|
72
|
+
table_class.relname AS table_name,
|
|
73
|
+
attribute.attname AS name,
|
|
74
|
+
pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) AS type,
|
|
75
|
+
attribute.attnotnull AS not_null,
|
|
76
|
+
pg_get_expr(column_default.adbin, column_default.adrelid) AS default_expression,
|
|
77
|
+
attribute.attidentity AS identity,
|
|
78
|
+
attribute.attgenerated AS generated,
|
|
79
|
+
collation_namespace.nspname AS collation_schema,
|
|
80
|
+
column_collation.collname AS collation_name
|
|
81
|
+
FROM pg_attribute attribute
|
|
82
|
+
LEFT JOIN pg_attrdef column_default
|
|
83
|
+
ON column_default.adrelid = attribute.attrelid
|
|
84
|
+
AND column_default.adnum = attribute.attnum
|
|
85
|
+
JOIN pg_class table_class ON table_class.oid = attribute.attrelid
|
|
86
|
+
JOIN pg_namespace table_namespace ON table_namespace.oid = table_class.relnamespace
|
|
87
|
+
JOIN pg_type attribute_type ON attribute_type.oid = attribute.atttypid
|
|
88
|
+
LEFT JOIN pg_collation column_collation
|
|
89
|
+
ON column_collation.oid = attribute.attcollation
|
|
90
|
+
AND attribute.attcollation <> attribute_type.typcollation
|
|
91
|
+
LEFT JOIN pg_namespace collation_namespace
|
|
92
|
+
ON collation_namespace.oid = column_collation.collnamespace
|
|
93
|
+
WHERE table_namespace.nspname = #{@connection.quote(schema_name)}
|
|
94
|
+
AND table_class.relname IN (#{quoted_names})
|
|
95
|
+
AND attribute.attnum > 0
|
|
96
|
+
AND NOT attribute.attisdropped
|
|
97
|
+
ORDER BY table_class.relname, attribute.attnum
|
|
98
|
+
SQL
|
|
99
|
+
@connection.execute(sql).each_with_object({}) do |row, columns_by_table|
|
|
100
|
+
next if Blank.present?(row["identity"]) || Blank.present?(row["generated"])
|
|
101
|
+
|
|
102
|
+
column = Column.new(
|
|
103
|
+
name: row["name"],
|
|
104
|
+
type: row["type"],
|
|
105
|
+
not_null: flag?(row["not_null"]),
|
|
106
|
+
default_expression: row["default_expression"],
|
|
107
|
+
collation_schema: row["collation_schema"],
|
|
108
|
+
collation_name: row["collation_name"]
|
|
109
|
+
)
|
|
110
|
+
(columns_by_table[row["table_name"]] ||= []) << column
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def missing_columns(parent_columns, child_columns)
|
|
115
|
+
child_names = child_columns.to_h { |column| [column.name, true] }
|
|
116
|
+
parent_columns.reject { |column| child_names.key?(column.name) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def unattached_child_tables(parent_table)
|
|
120
|
+
sql = <<~SQL
|
|
121
|
+
SELECT child.relname
|
|
122
|
+
FROM pg_class child
|
|
123
|
+
JOIN pg_namespace child_namespace ON child_namespace.oid = child.relnamespace
|
|
124
|
+
WHERE child_namespace.nspname = #{@connection.quote(schema_name)}
|
|
125
|
+
AND child.relkind = 'r'
|
|
126
|
+
AND starts_with(child.relname, #{@connection.quote("#{parent_table}_")})
|
|
127
|
+
AND NOT EXISTS (
|
|
128
|
+
SELECT 1
|
|
129
|
+
FROM pg_inherits inheritance
|
|
130
|
+
WHERE inheritance.inhrelid = child.oid
|
|
131
|
+
)
|
|
132
|
+
ORDER BY child.relname
|
|
133
|
+
SQL
|
|
134
|
+
@connection.execute(sql).map { |row| row["relname"] }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def missing_columns_warning(child_table, parent_table, missing)
|
|
138
|
+
names = missing.map(&:name).join(", ")
|
|
139
|
+
noun = (missing.size == 1) ? "column" : "columns"
|
|
140
|
+
"child #{child_table} is missing #{noun} #{names} from parent #{parent_table}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def usable_default?(expression)
|
|
144
|
+
return false if Blank.blank?(expression)
|
|
145
|
+
|
|
146
|
+
!expression.strip.match?(/\ANULL(::[\w.]+)?\z/i)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def flag?(value)
|
|
150
|
+
value == true || value.to_s == "t" || value.to_s == "true"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def quoted_table(name)
|
|
154
|
+
@connection.quote_table_name(name)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def quoted_column(name)
|
|
158
|
+
@connection.quote_column_name(name)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def schema_name
|
|
162
|
+
PartitionGardener.configuration.schema_name
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -22,6 +22,7 @@ module PartitionGardener
|
|
|
22
22
|
retention_apply
|
|
23
23
|
retention_keep_table
|
|
24
24
|
retention_detach_concurrently
|
|
25
|
+
align_child_columns
|
|
25
26
|
hash_modulus
|
|
26
27
|
maintenance_backend
|
|
27
28
|
incremental_rebalance
|
|
@@ -173,6 +174,7 @@ module PartitionGardener
|
|
|
173
174
|
validate_registry_conflict_key!(document)
|
|
174
175
|
validate_registry_optional_bucket!(document)
|
|
175
176
|
validate_registry_optional_maintenance_backend!(document)
|
|
177
|
+
validate_registry_optional_boolean!(document, "align_child_columns")
|
|
176
178
|
document
|
|
177
179
|
end
|
|
178
180
|
|
|
@@ -233,6 +235,13 @@ module PartitionGardener
|
|
|
233
235
|
raise ArgumentError, "maintenance_backend must be gardener, pg_partman, or hybrid_layout_only"
|
|
234
236
|
end
|
|
235
237
|
|
|
238
|
+
def validate_registry_optional_boolean!(document, key)
|
|
239
|
+
return unless document.key?(key)
|
|
240
|
+
return if document.fetch(key) == true || document.fetch(key) == false
|
|
241
|
+
|
|
242
|
+
raise ArgumentError, "#{key} must be true or false"
|
|
243
|
+
end
|
|
244
|
+
|
|
236
245
|
def register_validated_document!(document)
|
|
237
246
|
Registry.register(from_hash(validate_registry_document!(document)))
|
|
238
247
|
end
|
|
@@ -11,6 +11,7 @@ module PartitionGardener
|
|
|
11
11
|
:incremental_rebalance,
|
|
12
12
|
:run_record_enabled,
|
|
13
13
|
:retention_detach_concurrently,
|
|
14
|
+
:align_child_columns,
|
|
14
15
|
:strict_maintenance_backend_validation,
|
|
15
16
|
:current_run_metrics
|
|
16
17
|
|
|
@@ -29,6 +30,7 @@ module PartitionGardener
|
|
|
29
30
|
@run_record_enabled = true
|
|
30
31
|
@run_record_store = nil
|
|
31
32
|
@retention_detach_concurrently = false
|
|
33
|
+
@align_child_columns = true
|
|
32
34
|
@strict_maintenance_backend_validation = false
|
|
33
35
|
@current_run_metrics = nil
|
|
34
36
|
end
|
|
@@ -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
|
|
|
@@ -5,16 +5,26 @@ module PartitionGardener
|
|
|
5
5
|
def self.for_config(config, connection: Connection.connection)
|
|
6
6
|
new(
|
|
7
7
|
connection: connection,
|
|
8
|
-
batch_size: config.fetch(:move_batch_size, MOVE_BATCH_SIZE)
|
|
8
|
+
batch_size: config.fetch(:move_batch_size, MOVE_BATCH_SIZE),
|
|
9
|
+
align_child_columns: config.fetch(
|
|
10
|
+
:align_child_columns,
|
|
11
|
+
PartitionGardener.configuration.align_child_columns
|
|
12
|
+
)
|
|
9
13
|
)
|
|
10
14
|
end
|
|
11
15
|
|
|
12
|
-
def initialize(
|
|
16
|
+
def initialize(
|
|
17
|
+
connection: Connection.connection,
|
|
18
|
+
batch_size: MOVE_BATCH_SIZE,
|
|
19
|
+
align_child_columns: PartitionGardener.configuration.align_child_columns
|
|
20
|
+
)
|
|
13
21
|
@connection = connection
|
|
14
22
|
@batch_size = batch_size
|
|
23
|
+
@align_child_columns = align_child_columns
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
def attach_partition(table_name, partition_name, for_values_clause)
|
|
27
|
+
align_child_columns!(table_name, partition_name)
|
|
18
28
|
sql = <<~SQL
|
|
19
29
|
ALTER TABLE #{quoted_table(table_name)} ATTACH PARTITION #{quoted_table(partition_name)}
|
|
20
30
|
FOR VALUES #{for_values_clause}
|
|
@@ -23,6 +33,7 @@ module PartitionGardener
|
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
def attach_default_partition(table_name, partition_name)
|
|
36
|
+
align_child_columns!(table_name, partition_name)
|
|
26
37
|
sql = <<~SQL
|
|
27
38
|
ALTER TABLE #{quoted_table(table_name)} ATTACH PARTITION #{quoted_table(partition_name)} DEFAULT
|
|
28
39
|
SQL
|
|
@@ -59,6 +70,7 @@ module PartitionGardener
|
|
|
59
70
|
)
|
|
60
71
|
SQL
|
|
61
72
|
@connection.execute(sql)
|
|
73
|
+
align_child_columns!(table_name, partition_name)
|
|
62
74
|
|
|
63
75
|
conflict_columns = conflict_key.map { |column| @connection.quote_column_name(column) }.join(", ")
|
|
64
76
|
sql = <<~SQL
|
|
@@ -141,6 +153,12 @@ module PartitionGardener
|
|
|
141
153
|
|
|
142
154
|
private
|
|
143
155
|
|
|
156
|
+
def align_child_columns!(parent_table, child_table)
|
|
157
|
+
return unless @align_child_columns
|
|
158
|
+
|
|
159
|
+
ChildColumnAlign.new(connection: @connection).align!(parent_table, child_table)
|
|
160
|
+
end
|
|
161
|
+
|
|
144
162
|
def quoted_table(name)
|
|
145
163
|
@connection.quote_table_name(name)
|
|
146
164
|
end
|
|
@@ -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
|
|
@@ -4,6 +4,9 @@ module PartitionGardener
|
|
|
4
4
|
|
|
5
5
|
def normalize(config)
|
|
6
6
|
config = config.dup
|
|
7
|
+
unless config.key?(:partition_name_format_is_custom)
|
|
8
|
+
config[:partition_name_format_is_custom] = config.key?(:partition_name_format)
|
|
9
|
+
end
|
|
7
10
|
config[:layout] ||= :sliding_window
|
|
8
11
|
config[:bucket] ||= :month if date_layout?(config[:layout])
|
|
9
12
|
config
|
|
@@ -106,6 +109,7 @@ module PartitionGardener
|
|
|
106
109
|
conflict_key: conflict_key,
|
|
107
110
|
premake_months: premake_months,
|
|
108
111
|
maintenance_backend: options.fetch(:maintenance_backend, :gardener),
|
|
112
|
+
partition_name_format_is_custom: options.key?(:partition_name_format),
|
|
109
113
|
partition_name_format: options.fetch(:partition_name_format) {
|
|
110
114
|
->(identifier) { DateBucket.partition_name(table_name, identifier, :month) }
|
|
111
115
|
},
|
|
@@ -128,6 +132,7 @@ module PartitionGardener
|
|
|
128
132
|
partition_key_column: partition_key_column,
|
|
129
133
|
conflict_key: conflict_key,
|
|
130
134
|
active_years: active_years,
|
|
135
|
+
partition_name_format_is_custom: options.key?(:partition_name_format),
|
|
131
136
|
partition_name_format: options.fetch(:partition_name_format) {
|
|
132
137
|
->(identifier) { DateBucket.partition_name(table_name, identifier, :year) }
|
|
133
138
|
},
|
|
@@ -301,6 +306,7 @@ module PartitionGardener
|
|
|
301
306
|
:partition_key_column => partition_key_column,
|
|
302
307
|
:conflict_key => conflict_key,
|
|
303
308
|
active_key => active_value,
|
|
309
|
+
:partition_name_format_is_custom => options.key?(:partition_name_format),
|
|
304
310
|
:partition_name_format => options.fetch(:partition_name_format) {
|
|
305
311
|
->(identifier) { DateBucket.partition_name(table_name, identifier, bucket) }
|
|
306
312
|
},
|
data/lib/partition_gardener.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "partition_gardener/configuration"
|
|
|
14
14
|
require_relative "partition_gardener/naming"
|
|
15
15
|
require_relative "partition_gardener/plan"
|
|
16
16
|
require_relative "partition_gardener/connection"
|
|
17
|
+
require_relative "partition_gardener/child_column_align"
|
|
17
18
|
require_relative "partition_gardener/executor"
|
|
18
19
|
require_relative "partition_gardener/default_partition"
|
|
19
20
|
require_relative "partition_gardener/strategy/requires_default_partition"
|
|
@@ -25,6 +26,7 @@ require_relative "partition_gardener/strategy/list_split"
|
|
|
25
26
|
require_relative "partition_gardener/strategy/composite"
|
|
26
27
|
require_relative "partition_gardener/strategy"
|
|
27
28
|
require_relative "partition_gardener/layout/three_area"
|
|
29
|
+
require_relative "partition_gardener/layout/occupied_window"
|
|
28
30
|
require_relative "partition_gardener/layout/zone_segments"
|
|
29
31
|
require_relative "partition_gardener/layout/sliding_window"
|
|
30
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
|
|
@@ -303,6 +303,7 @@ files:
|
|
|
303
303
|
- lib/partition_gardener/archive_retention.rb
|
|
304
304
|
- lib/partition_gardener/audit.rb
|
|
305
305
|
- lib/partition_gardener/blank.rb
|
|
306
|
+
- lib/partition_gardener/child_column_align.rb
|
|
306
307
|
- lib/partition_gardener/cli.rb
|
|
307
308
|
- lib/partition_gardener/config_document.rb
|
|
308
309
|
- lib/partition_gardener/configuration.rb
|
|
@@ -316,6 +317,7 @@ files:
|
|
|
316
317
|
- lib/partition_gardener/hash_routing.rb
|
|
317
318
|
- lib/partition_gardener/layout/calendar_year.rb
|
|
318
319
|
- lib/partition_gardener/layout/integer_window.rb
|
|
320
|
+
- lib/partition_gardener/layout/occupied_window.rb
|
|
319
321
|
- lib/partition_gardener/layout/sliding_window.rb
|
|
320
322
|
- lib/partition_gardener/layout/three_area.rb
|
|
321
323
|
- lib/partition_gardener/layout/zone_segments.rb
|