require 'active_record/connection_adapters/postgresql/utils'
module ActiveRecord #:nodoc:
module ConnectionAdapters #:nodoc:
module PostgreSQL
module SchemaStatements
# Use this in migration to create child table and view.
# Options:
# [:parent]
# parent relation
# [:table]
# default is "#{child_view}_data"
# [:skip_creating_child_table]
# use together with :table option
def create_child(child_view, options)
raise 'Please call me with a parent, for example: create_child(:steam_locomotives, :parent => :locomotives)' unless options[:parent]
parent_relation = options[:parent].to_s
parent_table = if is_view?(parent_relation) # interpreted as inheritance chain deeper than two levels
query(<<~SQL)[0][0]
SELECT child_relation
FROM updateable_views_inheritance
WHERE child_aggregate_view = #{quote(parent_relation)}
SQL
else
parent_relation
end
child_table = options[:table] || quote_table_name("#{child_view}_data")
unless options.key?(:skip_creating_child_table)
unqualified_child_view_name = Utils.extract_schema_qualified_name(child_view).identifier
child_table_pk = "#{unqualified_child_view_name.singularize}_id"
create_table(child_table, :id => false) do |t|
t.integer child_table_pk, :null => false
yield t
end
execute "ALTER TABLE #{child_table} ADD PRIMARY KEY (#{child_table_pk})"
execute "ALTER TABLE #{child_table} ADD FOREIGN KEY (#{child_table_pk})
REFERENCES #{parent_table} ON DELETE CASCADE ON UPDATE CASCADE"
end
create_child_view(parent_relation, child_view, child_table)
end
# Drop child view and table
def drop_child(child_view)
drop_view(child_view)
child_table = query("SELECT child_relation FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}")[0][0]
drop_table(child_table)
execute "DELETE FROM updateable_views_inheritance WHERE child_aggregate_view = #{quote(child_view)}"
end
# Creates aggregate updateable view of parent and child relations. The convention for naming child tables is
# "#{child_view}_data". If you don't follow it, supply +child_table_name+ as third argument.
def create_child_view(parent_table, child_view, child_table=nil)
child_table ||= child_view.to_s + "_data"
parent_columns = columns(parent_table)
child_columns = columns(child_table)
child_column_names = child_columns.collect{|c| c.name}
parent_column_names = parent_columns.collect{|c| c.name}
child_pk = pk_and_sequence_for(child_table)[0]
child_column_names.delete(child_pk)
parent_pk, parent_pk_seq = pk_and_sequence_for(parent_table)
parent_column_names.delete(parent_pk)
do_create_child_view(parent_table, parent_column_names, parent_pk, child_view, child_column_names, child_pk, child_table)
make_child_view_updateable(parent_table, parent_column_names, parent_pk, parent_pk_seq, child_view, child_column_names, child_pk, child_table)
# assign default values for table columns on the view - it is not automatic in Postgresql 8.1
set_defaults(child_view, parent_table)
set_defaults(child_view, child_table)
create_system_table_records(parent_table, child_view, child_table)
end
# Resets sequence to the max value of the table's pk if present respecting inheritance (i.e. one sequence can be shared by many tables).
def reset_pk_sequence!(table, pk = nil, sequence = nil)
parent = parent_table(table)
if parent
reset_pk_sequence!(parent, pk, sequence)
else
unless pk and sequence
default_pk, default_sequence = pk_and_sequence_for(table)
pk ||= default_pk
sequence ||= default_sequence
end
if pk
if sequence
select_value <<-end_sql, 'Reset sequence'
SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
end_sql
else
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
end
end
end
end
def primary_key(relation)
res = pk_and_sequence_for(relation)
res && res.first
end
# Returns a relation's primary key and belonging sequence. If +relation+ is a table the result is its PK and sequence.
# When it is a view, PK and sequence of the table at the root of the inheritance chain are returned.
def pk_and_sequence_for(relation)
result = query(<<-end_sql, 'PK')[0]
SELECT attr.attname
FROM pg_attribute attr,
pg_constraint cons
WHERE cons.conrelid = attr.attrelid
AND cons.conrelid = '#{relation}'::regclass
AND cons.contype = 'p'
AND attr.attnum = ANY(cons.conkey)
end_sql
if result.nil? or result.empty?
parent = parent_table(relation)
pk_and_sequence_for(parent) if parent
else
# log(result[0], "PK for #{relation}") {}
[result[0], query("SELECT pg_get_serial_sequence('#{relation}', '#{result[0]}') ")[0][0]]
end
rescue
nil
end
# Drops a view from the database.
def drop_view(name)
execute "DROP VIEW #{name}"
end
# Return the list of all views in the schema search path.
def views(name=nil)
schemas = schema_search_path.split(/,\s*/).map { |p| quote(p) }.join(',')
query(<<~SQL, name).map { |row| row[0] }
SELECT viewname
FROM pg_views
WHERE schemaname IN (#{schemas})
SQL
end
# Checks whether relation +name+ is a view.
def is_view?(name)
result = query(<<~SQL, name).map { |row| row[0] }
SELECT viewname
FROM pg_views
WHERE viewname = '#{name}'
SQL
!result.empty?
end
# Recursively delete +parent_relation+ (if it is a view) and the children views the depend on it.
def remove_parent_and_children_views(parent_relation)
children_views = query(<<-end_sql).map{|row| row[0]}
SELECT child_aggregate_view
FROM updateable_views_inheritance
WHERE parent_relation = '#{parent_relation}'
end_sql
children_views.each do |cv|
remove_parent_and_children_views(cv)
# drop the view only if it wasn't dropped beforehand in recursive call from other method.
drop_view(cv) if is_view?(cv)
end
drop_view(parent_relation) if is_view?(parent_relation)
end
# Recreates all views in all hierarchy chains
def rebuild_all_parent_and_children_views
parent_relations = select_values('SELECT DISTINCT parent_relation FROM updateable_views_inheritance')
parent_relations.each { |parent_relation| rebuild_parent_and_children_views(parent_relation) }
end
# Recreates views in the part of the hierarchy chain starting from the +parent_relation+.
def rebuild_parent_and_children_views(parent_relation)
# Current implementation is not very efficient - it can drop and recreate one and the same view in the bottom of the hierarchy many times.
remove_parent_and_children_views(parent_relation)
children = query(<<-end_sql)
SELECT parent_relation, child_aggregate_view, child_relation
FROM updateable_views_inheritance
WHERE parent_relation = '#{parent_relation}'
end_sql
#if the parent is in the middle of the inheritance chain, it's a view that should be rebuilt as well
parent = query(<<-end_sql)[0]
SELECT parent_relation, child_aggregate_view, child_relation
FROM updateable_views_inheritance
WHERE child_aggregate_view = '#{parent_relation}'
end_sql
create_child_view(parent[0], parent[1], parent[2]) if (parent && !parent.empty?)
children.each do |child|
create_child_view(child[0], child[1], child[2])
rebuild_parent_and_children_views(child[1])
end
end
# Creates Single Table Inheritanche-like aggregate view called +sti_aggregate_view+
# for +parent_relation+ and all its descendants. The view isn't updateable.
# The order of all or just the first few columns in the aggregate view can be explicitly set
# by passing array of column names as third argument.
# If there are columns with the same name but different types in two or more relations
# they will appear as a single column of type +text+ in the view.
def create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
columns_for_view ||= []
relations_heirarchy = get_view_hierarchy_for(parent_relation)
relations = relations_heirarchy.flatten
leaves_relations = get_leaves_relations(relations_heirarchy)
all_columns = leaves_relations.map{|rel| columns(rel)}.flatten
columns_hash = {}
conflict_column_names = []
all_columns.each do |col|
c = columns_hash[col.name]
if(c && col.sql_type != c.sql_type)
conflict_column_names << col.name
else
columns_hash[col.name] = col
end
end
conflict_column_names = conflict_column_names.uniq.sort if !conflict_column_names.empty?
sorted_column_names = (columns_for_view + columns_hash.keys.sort).uniq
parent_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(parent_relation)
quoted_inheritance_column = quote_column_name(parent_klass_name.inheritance_column)
queries = relations.map{|rel| generate_single_table_inheritanche_union_clause(rel, sorted_column_names, conflict_column_names, columns_hash, quoted_inheritance_column)}
unioin_clauses = queries.join("\n UNION ")
execute <<-end_sql
CREATE VIEW #{sti_aggregate_view} AS (
#{unioin_clauses}
)
end_sql
end
# Recreates the Single_Table_Inheritanche-like aggregate view +sti_aggregate_view+
# for +parent_relation+ and all its descendants.
def rebuild_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view = nil)
drop_view(sti_aggregate_view)
create_single_table_inheritance_view(sti_aggregate_view, parent_relation, columns_for_view)
end
# Overriden - it solargraph-must return false, otherwise deleting fixtures won't work
def supports_disable_referential_integrity?
false
end
def table_exists_with_updateable_views_inheritance_support?(name)
is_view?(name) ? true : table_exists_without_updateable_views_inheritance_support?(name)
end
alias_method_chain :table_exists?, :updateable_views_inheritance_support
module Tutuf #:nodoc:
class ClassTableReflection
class << self
# Returns all models' class objects that are ActiveRecord::Base descendants
def all_db_klasses
return @@klasses if defined?(@@klasses)
@@klasses = []
# load model classes so that inheritance_column is set correctly where defined
model_filenames.collect{|m| load "#{Rails.root}/app/models/#{m}";m.match(%r{([^/]+?)\.rb$})[1].camelize.constantize }.each do |klass|
@@klasses << klass if klass < ActiveRecord::Base
end
@@klasses.uniq
end
# Returns the class object for +table_name+
def get_klass_for_table(table_name)
klass_for_tables()[table_name.to_s]
end
# Returns hash with tables and thier corresponding class.
# {table_name1 => ClassName1, ...}
def klass_for_tables
return @@tables_klasses if defined?(@@tables_klasses)
@@tables_klasses = {}
all_db_klasses.each do |klass|
@@tables_klasses[klass.table_name] = klass if klass.respond_to?(:table_name)
end
@@tables_klasses
end
# Returns filenames for models in the current Rails application
def model_filenames
Dir.chdir("#{Rails.root}/app/models"){ Dir["**/*.rb"] }
end
end
end
end
# Set default values from the table columns for a view
def set_defaults(view_name, table_name)
column_definitions(table_name).each do |column_name, type, default, notnull|
if !default.nil?
execute("ALTER TABLE #{quote_table_name(view_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{default}")
end
end
end
private
def do_create_child_view(parent_table, parent_columns, parent_pk, child_view, child_columns, child_pk, child_table)
view_columns = parent_columns + child_columns
execute <<-end_sql
CREATE OR REPLACE VIEW #{child_view} AS (
SELECT parent.#{parent_pk},
#{ view_columns.join(",") }
FROM #{parent_table} parent
INNER JOIN #{child_table} child
ON ( parent.#{parent_pk}=child.#{child_pk} )
)
end_sql
end
# Creates rules for +INSERT+, +UPDATE+ and +DELETE+ on the view
def make_child_view_updateable(parent_table, parent_columns, parent_pk, parent_pk_seq, child_view, child_columns, child_pk, child_table)
# insert
# NEW.#{parent_pk} can be explicitly specified and when it is null every call to it increments the sequence.
# Setting the sequence to its value (explicitly supplied or the default) covers both cases.
execute <<-end_sql
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_insert")} AS
ON INSERT TO #{child_view} DO INSTEAD (
INSERT INTO #{parent_table}
( #{ [parent_pk, parent_columns].flatten.join(", ") } )
VALUES( DEFAULT #{ parent_columns.empty? ? '' : ' ,' + parent_columns.collect{ |col| "NEW." + col}.join(", ") } ) ;
INSERT INTO #{child_table}
( #{ [child_pk, child_columns].flatten.join(",")} )
VALUES( currval('#{parent_pk_seq}') #{ child_columns.empty? ? '' : ' ,' + child_columns.collect{ |col| "NEW." + col}.join(", ") } )
#{insert_returning_clause(parent_pk, child_pk, child_view)}
)
end_sql
# delete
execute <<-end_sql
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_delete")} AS
ON DELETE TO #{child_view} DO INSTEAD
DELETE FROM #{parent_table} WHERE #{parent_pk} = OLD.#{parent_pk}
end_sql
# update
execute <<-end_sql
CREATE OR REPLACE RULE #{quote_column_name("#{child_view}_update")} AS
ON UPDATE TO #{child_view} DO INSTEAD (
#{ parent_columns.empty? ? '':
"UPDATE #{parent_table}
SET #{ parent_columns.collect{ |col| col + "= NEW." + col }.join(", ") }
WHERE #{parent_pk} = OLD.#{parent_pk};"}
#{ child_columns.empty? ? '':
"UPDATE #{child_table}
SET #{ child_columns.collect{ |col| col + " = NEW." + col }.join(", ") }
WHERE #{child_pk} = OLD.#{parent_pk}"
}
)
end_sql
end
def insert_returning_clause(parent_pk, child_pk, child_view)
columns_cast_to_null = columns(child_view)
.reject { |c| c.name == parent_pk}
.map { |c| "CAST (NULL AS #{c.sql_type})" }
.join(", ")
"RETURNING #{child_pk}, #{columns_cast_to_null}"
end
def create_system_table_records(parent_relation, child_aggregate_view, child_relation)
parent_relation, child_aggregate_view, child_relation = [parent_relation, child_aggregate_view, child_relation].collect{|rel| quote(rel.to_s)}
exists = query <<~SQL
SELECT parent_relation, child_aggregate_view, child_relation
FROM updateable_views_inheritance
WHERE parent_relation = #{parent_relation}
AND child_aggregate_view = #{child_aggregate_view}
AND child_relation = #{child_relation}
SQL
# log "res: #{exists}"
if exists.nil? or exists.empty?
execute "INSERT INTO updateable_views_inheritance (parent_relation, child_aggregate_view, child_relation)" +
"VALUES( #{parent_relation}, #{child_aggregate_view}, #{child_relation} )"
end
end
def parent_table(relation)
if table_exists?('updateable_views_inheritance')
res = query(<<-end_sql, 'Parent relation')[0]
SELECT parent_relation
FROM updateable_views_inheritance
WHERE child_aggregate_view = '#{relation}'
end_sql
res[0] if res
end
end
# Single Table Inheritance Aggregate View
# Nested list for the +parent_relation+ inheritance hierarchy
# Every descendant relation is presented as an array with relation's name as first element
# and the other elements are the relation's children presented in the same way as lists.
# For example:
# [[child_view1, [grandchild11,[...]], [grandchild12]],
# [child_view2, [...]
# ]
def get_view_hierarchy_for(parent_relation)
hierarchy = []
children = query(<<-end_sql)
SELECT parent_relation, child_aggregate_view, child_relation
FROM updateable_views_inheritance
WHERE parent_relation = '#{parent_relation}'
end_sql
children.each do |child|
hierarchy << [child[1], *get_view_hierarchy_for(child[1])]
end
hierarchy
end
def get_leaves_relations(hierarchy)
return [] if hierarchy.nil? || hierarchy.empty?
head, hierarchy = hierarchy.first, hierarchy[1..(hierarchy.size)]
if(head.is_a? Array)
return (get_leaves_relations(head) + get_leaves_relations(hierarchy)).compact
elsif(hierarchy.nil? || hierarchy.empty?)
return [head]
else
return get_leaves_relations(hierarchy).compact
end
end
def generate_single_table_inheritanche_union_clause(rel, column_names, conflict_column_names, columns_hash, quoted_inheritance_column)
relation_columns = columns(rel).collect{|c| c.name}
columns_select = column_names.inject([]) do |arr, col_name|
sql_type = conflict_column_names.include?(col_name) ? 'text' : columns_hash[col_name].sql_type
value = "NULL::#{sql_type}"
if(relation_columns.include?(col_name))
value = col_name
value = "#{value}::text" if conflict_column_names.include?(col_name)
end
statement = " AS #{col_name}"
statement = "#{value} #{statement}"
arr << " #{statement}"
end
columns_select = columns_select.join(", ")
rel_klass_name = Tutuf::ClassTableReflection.get_klass_for_table(rel)
where_clause = " WHERE #{quoted_inheritance_column} = '#{rel_klass_name}'"
["SELECT", columns_select, "FROM #{rel} #{where_clause}"].join(" ")
end
end
end
end
end