class PgParty::AdapterDecorator
Constants
- SUPPORTED_PARTITION_TYPES
Public Class Methods
new(adapter)
click to toggle source
Calls superclass method
# File lib/pg_party/adapter_decorator.rb, line 10 def initialize(adapter) super(adapter) raise "Partitioning only supported in PostgreSQL >= 10.0" unless supports_partitions? end
Public Instance Methods
add_index_on_all_partitions(table_name, column_name, in_threads: nil, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 146 def add_index_on_all_partitions(table_name, column_name, in_threads: nil, **options) if in_threads && open_transactions > 0 raise ArgumentError, '`in_threads:` cannot be used within a transaction. If running in a migration, use '\ '`disable_ddl_transaction!` and break out this operation into its own migration.' end index_name, index_type, index_columns, index_options, algorithm, using = extract_index_options( add_index_options(table_name, column_name, **options) ) # Postgres limits index name to 63 bytes (characters). We will use 8 characters for a `_random_suffix` # on partitions to ensure no conflicts, leaving 55 chars for the specified index name raise ArgumentError 'index name is too long - must be 55 characters or fewer' if index_name.length > 55 recursive_add_index( table_name: table_name, index_name: index_name, index_type: index_type, index_columns: index_columns, index_options: index_options, algorithm: algorithm, using: using, in_threads: in_threads ) end
attach_default_partition(parent_table_name, child_table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 93 def attach_default_partition(parent_table_name, child_table_name) execute(<<-SQL) ALTER TABLE #{quote_table_name(parent_table_name)} ATTACH PARTITION #{quote_table_name(child_table_name)} DEFAULT SQL PgParty.cache.clear! end
attach_hash_partition(parent_table_name, child_table_name, modulus:, remainder:)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 89 def attach_hash_partition(parent_table_name, child_table_name, modulus:, remainder:) attach_partition(parent_table_name, child_table_name, hash_constraint_clause(modulus, remainder)) end
attach_list_partition(parent_table_name, child_table_name, values:)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 85 def attach_list_partition(parent_table_name, child_table_name, values:) attach_partition(parent_table_name, child_table_name, list_constraint_clause(values)) end
attach_range_partition(parent_table_name, child_table_name, start_range:, end_range:)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 81 def attach_range_partition(parent_table_name, child_table_name, start_range:, end_range:) attach_partition(parent_table_name, child_table_name, range_constraint_clause(start_range, end_range)) end
create_default_partition_of(table_name, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 40 def create_default_partition_of(table_name, **options) create_partition_of(table_name, nil, default_partition: true, **options) end
create_hash_partition(table_name, partition_key:, **options, &blk)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 24 def create_hash_partition(table_name, partition_key:, **options, &blk) create_partition(table_name, :hash, partition_key, **options, &blk) end
create_hash_partition_of(table_name, modulus:, remainder:, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 36 def create_hash_partition_of(table_name, modulus:, remainder:, **options) create_partition_of(table_name, hash_constraint_clause(modulus, remainder), **options) end
create_list_partition(table_name, partition_key:, **options, &blk)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 20 def create_list_partition(table_name, partition_key:, **options, &blk) create_partition(table_name, :list, partition_key, **options, &blk) end
create_list_partition_of(table_name, values:, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 32 def create_list_partition_of(table_name, values:, **options) create_partition_of(table_name, list_constraint_clause(values), **options) end
create_range_partition(table_name, partition_key:, **options, &blk)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 16 def create_range_partition(table_name, partition_key:, **options, &blk) create_partition(table_name, :range, partition_key, **options, &blk) end
create_range_partition_of(table_name, start_range:, end_range:, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 28 def create_range_partition_of(table_name, start_range:, end_range:, **options) create_partition_of(table_name, range_constraint_clause(start_range, end_range), **options) end
create_table_like(table_name, new_table_name, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 44 def create_table_like(table_name, new_table_name, **options) primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) } partition_key = options.fetch(:partition_key, nil) partition_type = options.fetch(:partition_type, nil) create_with_pks = options.fetch( :create_with_primary_key, PgParty.config.create_with_primary_key ) validate_primary_key(primary_key) unless create_with_pks if partition_type validate_supported_partition_type!(partition_type) raise ArgumentError, '`partition_key` is required when specifying a partition_type' unless partition_key end like_option = if !partition_type || create_with_pks 'INCLUDING ALL' else 'INCLUDING ALL EXCLUDING INDEXES' end execute(<<-SQL) CREATE TABLE #{quote_table_name(new_table_name)} ( LIKE #{quote_table_name(table_name)} #{like_option} ) #{partition_type ? partition_by_clause(partition_type, partition_key) : nil} SQL return if partition_type return if !primary_key return if has_primary_key?(new_table_name) execute(<<-SQL) ALTER TABLE #{quote_table_name(new_table_name)} ADD PRIMARY KEY (#{quote_column_name(primary_key)}) SQL end
detach_partition(parent_table_name, child_table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 103 def detach_partition(parent_table_name, child_table_name) execute(<<-SQL) ALTER TABLE #{quote_table_name(parent_table_name)} DETACH PARTITION #{quote_table_name(child_table_name)} SQL PgParty.cache.clear! end
parent_for_table_name(table_name, traverse: false)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 128 def parent_for_table_name(table_name, traverse: false) parent = select_values(%[ SELECT pg_inherits.inhparent::regclass::text FROM pg_tables INNER JOIN pg_inherits ON pg_tables.tablename::regclass = pg_inherits.inhrelid::regclass WHERE pg_tables.schemaname = current_schema() AND pg_tables.tablename = #{quote(table_name)} ]).first return parent if parent.nil? || !traverse while (parents_parent = parent_for_table_name(parent)) do parent = parents_parent end parent end
partitions_for_table_name(table_name, include_subpartitions:, _accumulator: [])
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 112 def partitions_for_table_name(table_name, include_subpartitions:, _accumulator: []) select_values(%[ SELECT pg_inherits.inhrelid::regclass::text FROM pg_tables INNER JOIN pg_inherits ON pg_tables.tablename::regclass = pg_inherits.inhparent::regclass WHERE pg_tables.schemaname = current_schema() AND pg_tables.tablename = #{quote(table_name)} ]).each_with_object(_accumulator) do |partition, acc| acc << partition next unless include_subpartitions partitions_for_table_name(partition, include_subpartitions: true, _accumulator: acc) end end
table_partitioned?(table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 172 def table_partitioned?(table_name) select_values(%[ SELECT relkind FROM pg_catalog.pg_class AS c JOIN pg_catalog.pg_namespace AS ns ON c.relnamespace = ns.oid WHERE relname = #{quote(table_name)} AND nspname = current_schema() ]).first == 'p' end
Private Instance Methods
add_index_from_options(table_name, name:, type:, algorithm:, using:, columns:, options:)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 314 def add_index_from_options(table_name, name:, type:, algorithm:, using:, columns:, options:) execute "CREATE #{type} INDEX #{algorithm} #{quote_column_name(name)} ON "\ "#{quote_table_name(table_name)} #{using} (#{columns})#{options}" end
add_index_only(table_name, type:, name:, using:, columns:, options:)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 307 def add_index_only(table_name, type:, name:, using:, columns:, options:) return unless postgres_major_version >= 11 execute "CREATE #{type} INDEX #{quote_column_name(name)} ON ONLY "\ "#{quote_table_name(table_name)} #{using} (#{columns})#{options}" end
attach_child_index(child, parent)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 301 def attach_child_index(child, parent) return unless postgres_major_version >= 11 execute "ALTER INDEX #{quote_column_name(parent)} ATTACH PARTITION #{quote_column_name(child)}" end
attach_partition(parent_table_name, child_table_name, constraint_clause)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 250 def attach_partition(parent_table_name, child_table_name, constraint_clause) execute(<<-SQL) ALTER TABLE #{quote_table_name(parent_table_name)} ATTACH PARTITION #{quote_table_name(child_table_name)} FOR VALUES #{constraint_clause} SQL PgParty.cache.clear! end
calculate_primary_key(table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 373 def calculate_primary_key(table_name) ActiveRecord::Base.get_primary_key(table_name.to_s.singularize).to_sym end
create_partition(table_name, type, partition_key, **options) { |td| ... }
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 182 def create_partition(table_name, type, partition_key, **options) modified_options = options.except(:id, :primary_key, :template, :create_with_primary_key) template = options.fetch(:template, PgParty.config.create_template_tables) id = options.fetch(:id, :bigserial) primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) } create_with_pks = options.fetch( :create_with_primary_key, PgParty.config.create_with_primary_key ) validate_supported_partition_type!(type) if create_with_pks modified_options[:primary_key] = primary_key modified_options[:id] = id else validate_primary_key(primary_key) modified_options[:id] = false end modified_options[:options] = partition_by_clause(type, partition_key) create_table(table_name, **modified_options) do |td| if !modified_options[:id] && id == :uuid td.column(primary_key, id, null: false, default: uuid_function) elsif !modified_options[:id] && id td.column(primary_key, id, null: false) end yield(td) if block_given? end # Rails 4 has a bug where uuid columns are always nullable change_column_null(table_name, primary_key, false) if !modified_options[:id] && id == :uuid return unless template create_table_like( table_name, template_table_name(table_name), primary_key: id && primary_key, create_with_primary_key: create_with_pks ) end
create_partition_of(table_name, constraint_clause, **options)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 226 def create_partition_of(table_name, constraint_clause, **options) child_table_name = options.fetch(:name) { hashed_table_name(table_name, constraint_clause) } primary_key = options.fetch(:primary_key) { calculate_primary_key(table_name) } template_table_name = template_table_name(table_name) validate_default_partition_support! if options[:default_partition] if schema_cache.data_source_exists?(template_table_name) create_table_like(template_table_name, child_table_name, primary_key: false, partition_type: options[:partition_type], partition_key: options[:partition_key]) else create_table_like(table_name, child_table_name, primary_key: primary_key, partition_type: options[:partition_type], partition_key: options[:partition_key]) end if options[:default_partition] attach_default_partition(table_name, child_table_name) else attach_partition(table_name, child_table_name, constraint_clause) end child_table_name end
drop_indices_if_exist(index_names)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 340 def drop_indices_if_exist(index_names) index_names.uniq.each { |name| execute "DROP INDEX IF EXISTS #{quote_column_name(name)}" } end
extract_index_options(add_index_options_result)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 319 def extract_index_options(add_index_options_result) # Rails 6.1 changes the result of #add_index_options index_definition = add_index_options_result.first return add_index_options_result unless index_definition.is_a?(ActiveRecord::ConnectionAdapters::IndexDefinition) index_columns = if index_definition.columns.is_a?(String) index_definition.columns else quoted_columns_for_index(index_definition.columns, index_definition.column_options) end [ index_definition.name, index_definition.unique ? 'UNIQUE' : index_definition.type, index_columns, index_definition.where ? " WHERE #{index_definition.where}" : nil, add_index_options_result.second, # algorithm option index_definition.using ? "USING #{index_definition.using}" : nil ] end
generate_index_name(index_name, table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 431 def generate_index_name(index_name, table_name) "#{index_name}_#{Digest::MD5.hexdigest(table_name)[0..6]}" end
has_primary_key?(table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 369 def has_primary_key?(table_name) primary_key(table_name).present? end
hash_constraint_clause(modulus, remainder)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 401 def hash_constraint_clause(modulus, remainder) "WITH (MODULUS #{modulus.to_i}, REMAINDER #{remainder.to_i})" end
hashed_table_name(table_name, key)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 417 def hashed_table_name(table_name, key) return "#{table_name}_#{Digest::MD5.hexdigest(key)[0..6]}" if key # use _default suffix for default partitions (without a constraint clause) "#{table_name}_default" end
index_valid?(index_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 424 def index_valid?(index_name) select_values( "SELECT relname FROM pg_class, pg_index WHERE pg_index.indisvalid = false AND "\ "pg_index.indexrelid = pg_class.oid AND relname = #{quote(index_name)}" ).empty? end
list_constraint_clause(values)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 405 def list_constraint_clause(values) "IN (#{quote_collection(values.try(:to_a) || values)})" end
parallel_map(arr, in_threads:) { |item| ... }
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 344 def parallel_map(arr, in_threads:) return [] if arr.empty? return arr.map { |item| yield(item) } unless in_threads && in_threads > 1 if ActiveRecord::Base.connection_pool.size <= in_threads raise ArgumentError, 'in_threads: must be lower than your database connection pool size' end Parallel.map(arr, in_threads: in_threads) do |item| ActiveRecord::Base.connection_pool.with_connection { yield(item) } end end
partition_by_clause(type, partition_key)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 409 def partition_by_clause(type, partition_key) "PARTITION BY #{type.to_s.upcase} (#{quote_partition_key(partition_key)})" end
postgres_major_version()
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 455 def postgres_major_version __getobj__.send(:postgresql_version)/10000 end
quote(value)
click to toggle source
Rails 5.2 now returns boolean literals This causes partition creation to fail when the constraint clause includes a boolean Might be a PostgreSQL bug, but for now let's revert to the old quoting behavior
# File lib/pg_party/adapter_decorator.rb, line 360 def quote(value) case value when true then "'t'" when false then "'f'" else __getobj__.quote(value) end end
quote_collection(values)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 389 def quote_collection(values) Array.wrap(values).map(&method(:quote)).join(",") end
quote_partition_key(key)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 381 def quote_partition_key(key) if key.is_a?(Proc) key.call.to_s # very difficult to determine how to sanitize a complex expression else Array.wrap(key).map(&method(:quote_column_name)).join(",") end end
range_constraint_clause(start_range, end_range)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 397 def range_constraint_clause(start_range, end_range) "FROM (#{quote_collection(start_range)}) TO (#{quote_collection(end_range)})" end
recursive_add_index(table_name:, index_name:, index_type:, index_columns:, index_options:, using:, algorithm:, in_threads: nil, _parent_index_name: nil, _created_index_names: [])
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 260 def recursive_add_index(table_name:, index_name:, index_type:, index_columns:, index_options:, using:, algorithm:, in_threads: nil, _parent_index_name: nil, _created_index_names: []) partitions = partitions_for_table_name(table_name, include_subpartitions: false) updated_name = _created_index_names.empty? ? index_name : generate_index_name(index_name, table_name) # If this is a partitioned table, add index ONLY on this table. if table_partitioned?(table_name) add_index_only(table_name, type: index_type, name: updated_name, using: using, columns: index_columns, options: index_options) _created_index_names << updated_name parallel_map(partitions, in_threads: in_threads) do |partition_name| recursive_add_index( table_name: partition_name, index_name: index_name, index_type: index_type, index_columns: index_columns, index_options: index_options, using: using, algorithm: algorithm, _parent_index_name: updated_name, _created_index_names: _created_index_names ) end else _created_index_names << updated_name # Track as created before execution of concurrent index command add_index_from_options(table_name, name: updated_name, type: index_type, algorithm: algorithm, using: using, columns: index_columns, options: index_options) end attach_child_index(updated_name, _parent_index_name) if _parent_index_name return true if index_valid?(updated_name) raise 'index creation failed - an index was marked invalid' rescue => e # Clean up any indexes created so this command can be retried later drop_indices_if_exist(_created_index_names) raise e end
supports_partitions?()
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 451 def supports_partitions? postgres_major_version >= 10 end
template_table_name(table_name)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 393 def template_table_name(table_name) "#{parent_for_table_name(table_name, traverse: true) || table_name}_template" end
uuid_function()
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 413 def uuid_function try(:supports_pgcrypto_uuid?) ? "gen_random_uuid()" : "uuid_generate_v4()" end
validate_default_partition_support!()
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 445 def validate_default_partition_support! return if postgres_major_version >= 11 raise NotImplementedError, 'Default partitions are only available in Postgres 11 or higher' end
validate_primary_key(key)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 377 def validate_primary_key(key) raise ArgumentError, "composite primary key not supported" if key.is_a?(Array) end
validate_supported_partition_type!(partition_type)
click to toggle source
# File lib/pg_party/adapter_decorator.rb, line 435 def validate_supported_partition_type!(partition_type) if (sym = partition_type.to_s.downcase.to_sym) && sym.in?(SUPPORTED_PARTITION_TYPES) return if sym != :hash || postgres_major_version >= 11 raise NotImplementedError, 'Hash partitions are only available in Postgres 11 or higher' end raise ArgumentError, "Supported partition types are #{SUPPORTED_PARTITION_TYPES.join(', ')}" end