class PgMorph::Polymorphic

Constants

BASE_TABLE_SUFIX

Attributes

base_table[R]
child_table[R]
column_name[R]
parent_table[R]

Public Class Methods

new(parent_table, child_table, options) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 9
def initialize(parent_table, child_table, options)
  @parent_table = parent_table
  @child_table = child_table
  @column_name = options[:column]
  @base_table = options[:base_table] || :"#{parent_table}_#{BASE_TABLE_SUFIX}"

  raise PgMorph::Exception.new("Column not specified") unless @column_name
end

Public Instance Methods

can_rename_to_base_table?() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 25
def can_rename_to_base_table?
  return true unless ActiveRecord::Base.connection.table_exists? base_table

  parent_table_set = ActiveRecord::Base.connection.columns(parent_table).
                      map{|column| column.as_json.except('null')}
  base_table_set = ActiveRecord::Base.connection.columns(base_table).
                      map{|column| column.as_json.except('null')}

  return false if parent_table_set == base_table_set
  raise PgMorph::Exception.new('table name mismatch!')
end
create_base_table_view_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 37
def create_base_table_view_sql
  %Q{
    CREATE OR REPLACE VIEW #{parent_table} AS SELECT * FROM #{base_table};
  }
end
create_before_insert_trigger_fun_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 53
def create_before_insert_trigger_fun_sql
  before_insert_trigger_content do
    %Q{
    IF NEW.id IS NULL THEN
      NEW.id := nextval('#{parent_table}_id_seq');
    END IF;
    #{create_trigger_body.strip}
    }
  end
end
create_before_insert_trigger_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 64
def create_before_insert_trigger_sql
  fun_name = before_insert_fun_name
  trigger_name = before_insert_trigger_name

  create_trigger_sql(parent_table, trigger_name, fun_name, 'INSTEAD OF INSERT')
end
create_proxy_table_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 43
def create_proxy_table_sql
  %Q{
  CREATE TABLE #{proxy_table} (
    CHECK (#{column_name_type} = '#{type}'),
    PRIMARY KEY (id),
    FOREIGN KEY (#{column_name_id}) REFERENCES #{child_table}(id)
  ) INHERITS (#{base_table});
  }
end
remove_base_table_view_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 99
def remove_base_table_view_sql
  if check_more_partitions.present?
    ''
  else
    %Q{ DROP VIEW #{parent_table}; }
  end
end
remove_before_insert_trigger_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 71
def remove_before_insert_trigger_sql
  trigger_name = before_insert_trigger_name
  fun_name = before_insert_fun_name
  cleared = check_more_partitions

  if cleared.present?
    update_before_insert_trigger_sql(cleared)
  else
    drop_trigger_and_fun_sql(trigger_name, parent_table, fun_name)
  end
end
remove_proxy_table() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 90
def remove_proxy_table
  table_empty = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM #{parent_table}_#{child_table}").to_i.zero?
  if table_empty
    %Q{ DROP TABLE IF EXISTS #{proxy_table}; }
  else
    raise PG::Error.new("Partition table #{proxy_table} contains data.\nRemove them before if you want to drop that table.\n")
  end
end
rename_base_table_back_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 107
def rename_base_table_back_sql
  if check_more_partitions.present?
    ''
  else
    %Q{ ALTER TABLE #{base_table} RENAME TO #{parent_table}; }
  end
end
rename_base_table_sql() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 18
def rename_base_table_sql
  return '' unless can_rename_to_base_table?
  %Q{
    ALTER TABLE #{parent_table} RENAME TO #{base_table};
  }
end
update_before_insert_trigger_sql(cleared) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 83
def update_before_insert_trigger_sql(cleared)
  cleared[0][0].sub!('ELSIF', 'IF')
  before_insert_trigger_content do
    cleared.map { |m| m[0] }.join('').strip
  end
end

Private Instance Methods

before_insert_trigger_content( &block) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 137
def before_insert_trigger_content( &block)
  create_trigger_fun(before_insert_fun_name) do
    %Q{#{block.call}
      ELSE
        RAISE EXCEPTION 'Wrong "#{column_name}_type"="%" used. Create proper partition table and update #{before_insert_fun_name} function', NEW.#{column_name}_type;
      END IF;}
  end
end
check_more_partitions() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 117
def check_more_partitions
  fun_name = before_insert_fun_name

  prosrc = get_function(fun_name)
  raise PG::Error.new("There is no such function #{fun_name}()\n") unless prosrc

  scan =  prosrc.scan(/(( +(ELS)?IF.+\n)(\s+INSERT INTO.+;\n))/)
  scan.reject { |x| x[0].match("#{proxy_table}") }
end
create_new_trigger_body() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 166
def create_new_trigger_body
  %Q{
    IF (NEW.#{column_name}_type = '#{child_table.to_s.singularize.camelize}') THEN
      INSERT INTO #{parent_table}_#{child_table} VALUES (NEW.*);
  }
end
create_trigger_body() click to toggle source
# File lib/pg_morph/polymorphic.rb, line 146
def create_trigger_body
  prosrc = get_function(before_insert_fun_name)

  if prosrc
    update_existing_trigger_body(prosrc)
  else
    create_new_trigger_body
  end
end
create_trigger_fun(fun_name, &block) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 127
def create_trigger_fun(fun_name, &block)
  %Q{
    CREATE OR REPLACE FUNCTION #{fun_name}() RETURNS TRIGGER AS $$
    BEGIN
      #{block.call}
      RETURN NEW;
    END; $$ LANGUAGE plpgsql;
  }
end
create_trigger_sql(parent_table, trigger_name, fun_name, when_to_call) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 173
def create_trigger_sql(parent_table, trigger_name, fun_name, when_to_call)
  %Q{
  DROP TRIGGER IF EXISTS #{trigger_name} ON #{parent_table};
  CREATE TRIGGER #{trigger_name}
    #{when_to_call} ON #{parent_table}
    FOR EACH ROW EXECUTE PROCEDURE #{fun_name}();
  }
end
drop_trigger_and_fun_sql(trigger_name, parent_table, fun_name) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 182
def drop_trigger_and_fun_sql(trigger_name, parent_table, fun_name)
  %Q{
  DROP TRIGGER #{trigger_name} ON #{parent_table};
  DROP FUNCTION #{fun_name}();
  }
end
get_function(fun_name) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 189
def get_function(fun_name)
  run("SELECT prosrc FROM pg_proc WHERE proname = '#{fun_name}'")
end
run(query) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 193
def run(query)
  ActiveRecord::Base.connection.select_value(query)
end
update_existing_trigger_body(prosrc) click to toggle source
# File lib/pg_morph/polymorphic.rb, line 156
def update_existing_trigger_body(prosrc)
  scan =  prosrc.scan(/(( +(ELS)?IF.+\n)(\s+INSERT INTO.+;\n))/)
  raise PG::Error.new("Condition for #{proxy_table} table already exists in trigger function") if scan[0][0].match proxy_table
  %Q{
    #{scan.map { |m| m[0] }.join.strip}
    ELSIF (NEW.#{column_name}_type = '#{child_table.to_s.singularize.camelize}') THEN
      INSERT INTO #{parent_table}_#{child_table} VALUES (NEW.*);
  }
end