Partition Gardener

Gem Version Test Status codecov

PostgreSQL partition lifecycle: archive, current, and future zones, heat-driven splits inside the active window, cursor-based rebalance, mandatory default drain, and hot-switch migration helpers. Rails-integrated by default; works standalone with pg and a JSON registry.

Requirements

  • Ruby >= 3.2
  • PostgreSQL with declarative partitioning
  • pg gem (runtime dependency)
  • Rails >= 7.1 optional — railtie loads when Rails is present and sets Active Record connection plus Time.zone.today

Complements migration gems (e.g. pg_party) — use those for creation DDL; use this gem for runtime maintenance (scheduled or indicator-driven) and cutover migrations. See docs/related_postgres_tooling.md for how Gardener pairs with PgHero, Dexter, pgsync, and pgslice.

Documentation

Choosing and configuring

Operations

Application and migration

Schemas

Quick start

# Gemfile
gem "partition_gardener"
# config/initializers/partition_gardener.rb — see docs/configuration.md for all options
PartitionGardener.configure do |config|
  config.notifier = ->(message, context: {}) { Rails.logger.info(message) }
  config.today_resolver = -> { Time.zone.today }
end

PartitionGardener::Registry.register_template(
  :sliding_window_monthly,
  table_name: "events",
  partition_key_column: "occurred_on",
  conflict_key: %w[id occurred_on],
  active_months: 12
)
# app/jobs/partition_maintenance_job.rb — see docs/background_job.md
class PartitionMaintenanceJob < ApplicationJob
  def perform
    PartitionGardener.run!(job_class_name: self.class.name)
  end
end
# Dry-run plan from the app directory
bundle exec partition_gardener --rails plan events --pretty

Standalone (no Rails)

Set DATABASE_URL and point the CLI at a JSON registry:

export DATABASE_URL=postgres://postgres@127.0.0.1:5432/mydb
bundle exec partition_gardener --registry config/partition_garden.json plan events --pretty

Or configure in Ruby:

PartitionGardener.configure do |config|
  config.connection_resolver = -> { PartitionGardener::PgConnection.connect(ENV.fetch("DATABASE_URL")) }
  config.today_resolver = -> { Date.today }
end
PartitionGardener::ConfigDocument.load_registry_file!("config/partition_garden.json")
PartitionGardener.run!

Default layout

Default layout: monthly sliding window (register_template :sliding_window_monthly in the Rails example above). Three-area layout with planner-enforced non-overlapping ranges, default drained last, and keyset moves on (partition_key, conflict_key).

See docs/decision_flow.md for when to pick other templates.

Templates

Templates.sliding_window_monthly (layout :sliding_window, bucket :month) — RANGE (date) monthly time-series.

Templates.sliding_window_daily (bucket :day) — daily buckets for short retention telemetry.

Templates.sliding_window_weekly (bucket :week) — ISO-week buckets.

Templates.sliding_window_quarterly (bucket :quarter) — calendar-quarter buckets.

Templates.calendar_year (layout :calendar_year) — RANGE (date) yearly buckets.

Templates.rolling_current_monthly (layout :rolling_current) — monthly sliding window without heat splits.

Templates.integer_window (layout :integer_window) — RANGE (bigint) id bands.

Templates.list_split (layout :list_split) — fixed LIST branches.

Templates.composite_list_hash (layout :composite) — LIST parent plus HASH sub-trees.

Templates.composite_list_range / Templates.list_range — LIST parent plus RANGE sliding-window sub-trees.

Templates.composite_range_hash — RANGE parent plus HASH child tables.

Templates.composite_range_list — RANGE parent plus LIST child tables.

Templates.hash_branches (layout :hash_branches) — HASH remainders.

Templates.premake_monthly (layout :premake_monthly) — cron-style premake bridge; migrate to sliding window.

See docs/partition_landscape.md for the template catalog, Rails sharding, composite keys, partition pruning, UI and product surfaces, aggregate snapshots, and materialized view limits. Operations: operations.md, cutover.md, monitoring.md.

Hot-switch migrations

PartitionGardener::Migration::HotSwitchConcern (alias HotSwitchPartitionedTable) consolidates cutover migration helpers.

class PartitionEventsHotSwitch < ActiveRecord::Migration[8.0]
  include PartitionGardener::Migration::HotSwitchConcern

  HOT_SWITCH_CONFIG = {
    current_table: "events",
    partitioned_table: "p_events",
    partition_key_column: "occurred_on",
    conflict_key: %w[id occurred_on],
    partition_config: PartitionGardener::Registry.hot_switch_partition_config("events")
  }.freeze

  def up
    ensure_future_partitions_exist(months_ahead: 1)
    hot_switch_tables
  end
end

partition_config may also be an inline hash for tables not yet registered.

Runtime guarantees

  • Per-table advisory lock during maintenance (hashtext namespace plus table name)
  • continue_on_error: true by default — one failing table does not block others (PartitionGardener::RunFailed aggregates errors)
  • Batch moves use composite keyset cursors and delete source rows by batch keys
  • run! returns RunSummary with per-table duration, plan_signature, and rows_moved
  • maintenance_backend: :pg_partman skips gardener for partman-owned tables

Tests

App-agnostic PostgreSQL integration specs live under spec/integration/. CI and local full-suite runs use polyrun to shard specs across parallel workers (each worker uses its own PostgreSQL database when DATABASE_URL is set).

# Full suite (unit + integration; requires PostgreSQL)
make test

# Same as make test
INTEGRATION=1 DATABASE_URL=postgres://postgres@127.0.0.1:5432/partition_gardener_test ./bin/polyrun parallel-rspec --workers 5

# Unit specs only
bundle exec rspec --exclude-pattern "spec/integration/**/*_spec.rb"

# Integration only (requires PostgreSQL)
INTEGRATION=1 DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/partition_gardener_test bundle exec rake spec:integration

License

The gem is available as open source under the terms of the MIT License.

Sponsors

Sponsored by Kisko Labs.

Sponsored by Kisko Labs