Skip to content

Instantly share code, notes, and snippets.

@amkisko
Last active April 30, 2024 13:06
Show Gist options
  • Save amkisko/b464e8edb886489f095138a9be8e986a to your computer and use it in GitHub Desktop.
Save amkisko/b464e8edb886489f095138a9be8e986a to your computer and use it in GitHub Desktop.
pg_party range partitioning helper concern for ActiveRecord and PostgreSQL
class CreateSystemMetrics < ActiveRecord::Migration[7.1]
def up
safety_assured do
create_range_partition :system_metrics, partition_key: -> { "date" } do |t|
t.decimal :value, null: false, default: 0
t.integer :group, null: false
t.integer :period, null: false
t.date :date, null: false
t.timestamps
end
SystemMetric.partitions_maintenance
add_index :system_metrics, %i[group period date], unique: true
end
end
def down
safety_assured do
drop_table :system_metrics, force: :cascade
end
end
end
# NOTE: requires pg_party gem
module HasDateRangePartitions
extend ActiveSupport::Concern
included do
ActiveRecord::SchemaDumper.ignore_tables << /#{table_name}_.+/
def self.partition_range_column(name)
column_name = name.to_s
range_partition_by { column_name }
end
def self.partitions_maintenance
create_partitions.each { |name| ActionReporter.notify("Created partition #{name}") }
retire_partitions.each { |name| ActionReporter.notify("Retired partition #{name}") }
partitions
end
def self.create_partitions(periods: nil)
periods ||= [Date.current, Date.current.next_month]
periods.sort.each_with_object([]) do |date, created|
name = partition_name_for(date)
next if ActiveRecord::Base.connection.table_exists?(name)
range = partition_range_for(date)
create_partition(
name: name,
start_range: range.begin,
end_range: range.end
)
created << name
end
end
def self.retired_partitions(retention_period: nil)
retention_period ||= 3.years
partitions.sort.each_with_object([]) do |name, retired|
break [] if range_from_partition_name(name).end.since(retention_period).future?
next unless ActiveRecord::Base.connection.table_exists?(name)
retired << name
end
end
def self.retire_partitions(retention_period: nil)
retired_partitions(retention_period: retention_period).each_with_object([]) do |name, retired|
retired << drop_partition(name)
end
end
def self.detach_partition(name)
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} DETACH PARTITION #{name}")
name
end
def self.drop_partition(name)
ActiveRecord::Base.connection.execute("DROP TABLE #{name}")
name
end
def self.range_from_partition_name(name)
year, month, day = name.split("#{table_name}_").last.split("_")
partition_range_for Date.new(year.to_i, month.to_i, day.to_i)
end
def self.partition_name_for(date)
range = partition_range_for(date)
"#{table_name}_#{range.begin.strftime("%Y_%m_%d")}"
end
def self.partition_range_for(date)
(date.beginning_of_month..date.next_month.beginning_of_month)
end
end
end
PgParty.configure do |config|
config.caching_ttl = 60
config.create_template_tables = false
config.create_with_primary_key = false
config.schema_exclude_partitions = true
end
class RunPartitionsMaintenanceJob < ApplicationJob
queue_as :critical
def perform(model_name: nil)
models = model_name.present? ? [model_name.constantize] : partitioned_models
models.each(&:partition_maintenance)
end
def partitioned_models
ActiveRecord::Base.descendants.select { _1.try(:partitioned?) }
end
end
class SystemMetric < ApplicationRecord
include HasDateRangePartitions
partition_range_column :date
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment