module SchemaPlus
module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
module PostgreSQLColumn
# Extracts the value from a PostgreSQL column default definition.
def self.included(base) #:nodoc:
base.extend ClassMethods
if defined?(JRUBY_VERSION)
base.alias_method_chain :default_value, :schema_plus
else
base.class_eval do
class << self
alias_method_chain :extract_value_from_default, :schema_plus
end
end
end
end
def initialize(name, default, sql_type = nil, null = true)
if default.is_a? Hash
if default[:expr]
@default_expr = default[:expr]
end
default = nil
end
super(name, default, sql_type, null)
end
def default_value_with_schema_plus(default)
value = default_value_without_schema_plus(default)
self.class.convert_default_value(default, value)
end
module ClassMethods
def extract_value_from_default_with_schema_plus(default)
value = extract_value_from_default_without_schema_plus(default)
convert_default_value(default, value)
end
# in some cases (e.g. if change_column_default(table, column,
# nil) is used), postgresql will return NULL::xxxxx (rather
# than nil) for a null default -- make sure we treat it as nil,
# not as a function.
def convert_default_value(default, value)
default = nil if value.nil? && default =~ /\ANULL::(?:character varying|bpchar|text)\z/m
if value.nil? && !default.nil?
value = { :expr => default }
end
value
end
end
end
# The Postgresql adapter implements the SchemaPlus extensions and
# enhancements
module PostgresqlAdapter
def self.included(base) #:nodoc:
base.class_eval do
if ::ActiveRecord::VERSION::MAJOR.to_i < 4 && !defined?(JRUBY_VERSION)
remove_method :indexes
end
alias_method_chain :rename_table, :schema_plus
alias_method_chain :exec_cache, :schema_plus unless defined?(JRUBY_VERSION)
end
::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.send(:include, PostgreSQLColumn) unless ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.include?(PostgreSQLColumn)
end
# SchemaPlus provides the following extra options for Postgres
# indexes:
# * +:conditions+ - SQL conditions for the WHERE clause of the index
# * +:expression+ - SQL expression to index. column_name can be nil or ommitted, in which case :name must be provided
# * +:kind+ - index method for Postgresql to use
# * +:case_sensitive - setting to +false+ is a shorthand for :expression => 'LOWER(column_name)'
#
# The :case_sensitive => false option ties in with Rails built-in support for case-insensitive searching:
# validates_uniqueness_of :name, :case_sensitive => false
#
# Since since :case_sensitive => false is implemented by
# using :expression, this raises an ArgumentError if both
# are specified simultaneously.
#
def add_index(table_name, column_name, options = {})
options = {} if options.nil? # some callers explicitly pass options=nil
column_name, options = [], column_name if column_name.is_a?(Hash)
column_names = Array(column_name).compact
if column_names.empty?
raise ArgumentError, "No columns and :expression missing from options - cannot create index" unless options[:expression]
raise ArgumentError, "Index name not given. Pass :name option" unless options[:name]
end
index_type = options[:unique] ? "UNIQUE" : ""
index_name = options[:name] || index_name(table_name, column_names)
conditions = options[:conditions]
kind = options[:kind]
if expression = options[:expression] then
raise ArgumentError, "Cannot specify :case_sensitive => false with an expression. Use LOWER(column_name)" if options[:case_sensitive] == false
# Wrap expression in parentheses if necessary
expression = "(#{expression})" if expression !~ /(using|with|tablespace|where)/i
expression = "USING #{kind} #{expression}" if kind
expression = "#{expression} WHERE #{conditions}" if conditions
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{expression}"
else
option_strings = Hash[column_names.map {|name| [name, '']}]
option_strings = add_index_sort_order(option_strings, column_names, options)
if options[:case_sensitive] == false
caseable_columns = columns(table_name).select { |col| [:string, :text].include?(col.type) }.map(&:name)
quoted_column_names = column_names.map do |col_name|
(caseable_columns.include?(col_name.to_s) ? "LOWER(#{quote_column_name(col_name)})" : quote_column_name(col_name)) + option_strings[col_name]
end
else
quoted_column_names = column_names.map { |col_name| quote_column_name(col_name) + option_strings[col_name] }
end
expression = "(#{quoted_column_names.join(', ')})"
expression = "USING #{kind} #{expression}" if kind
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{expression}"
sql += " WHERE (#{ ::ActiveRecord::Base.send(:sanitize_sql, conditions, quote_table_name(table_name)) })" if conditions
end
execute sql
rescue => e
SchemaStatements.add_index_exception_handler(self, table_name, column_names, options, e)
end
def supports_partial_indexes? #:nodoc:
true
end
# This method entirely duplicated from AR's postgresql_adapter.c,
# but includes the extra bit to determine the column name for a
# case-insensitive index. (Haven't come up with any clever way to
# only code up the case-insensitive column name bit here and
# otherwise use the existing method.)
def indexes(table_name, name = nil) #:nodoc:
result = query(<<-SQL, name)
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
m.amname, pg_get_expr(d.indpred, t.oid) as conditions, pg_get_expr(d.indexprs, t.oid) as expression
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
INNER JOIN pg_am m ON i.relam = m.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = '#{table_name_without_namespace(table_name)}'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
ORDER BY i.relname
SQL
result.map do |(index_name, is_unique, indkey, inddef, oid, kind, conditions, expression)|
unique = (is_unique == 't')
index_keys = indkey.split(" ")
rows = query(<<-SQL, "Columns for index #{index_name} on #{table_name}")
SELECT CAST(a.attnum as VARCHAR), a.attname, t.typname
FROM pg_attribute a
INNER JOIN pg_type t ON a.atttypid = t.oid
WHERE a.attrelid = #{oid}
SQL
columns = {}
types = {}
rows.each do |num, name, type|
columns[num] = name
types[name] = type
end
column_names = columns.values_at(*index_keys).compact
case_sensitive = true
# extract column names from the expression, for a
# case-insensitive index.
# only applies to character, character varying, and text
if expression
rexp_lower = %r{\blower\(\(?([^)]+)(\)::text)?\)}
if expression.match /\A#{rexp_lower}(?:, #{rexp_lower})*\z/
case_insensitive_columns = expression.scan(rexp_lower).map(&:first).select{|column| %W[char varchar text].include? types[column]}
if case_insensitive_columns.any?
case_sensitive = false
column_names = index_keys.map { |index_key|
index_key == '0' ? case_insensitive_columns.shift : columns[index_key]
}.compact
end
end
end
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
orders = desc_order_columns.any? ? Hash[column_names.map {|column| [column, desc_order_columns.include?(column) ? :desc : :asc]}] : {}
::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, column_names,
:name => index_name,
:unique => unique,
:orders => orders,
:conditions => conditions,
:case_sensitive => case_sensitive,
:kind => kind.downcase == "btree" ? nil : kind,
:expression => expression)
end
end
def query(*args)
select(*args).map(&:values)
end if defined?(JRUBY_VERSION)
def rename_table_with_schema_plus(oldname, newname) #:nodoc:
rename_table_without_schema_plus(oldname, newname)
rename_indexes_and_foreign_keys(oldname, newname)
end
# Prepass to replace each ActiveRecord::DB_DEFAULT with a literal
# DEFAULT in the sql string. (The underlying pg gem provides no
# way to bind a value that will replace $n with DEFAULT)
def exec_cache_with_schema_plus(sql, *args)
name_passed = (2 == args.size)
binds, name = args.reverse
if binds.any?{ |col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
j = 0
binds.each_with_index do |(col, val), i|
if val.equal? ::ActiveRecord::DB_DEFAULT
sql = sql.sub(/\$#{i+1}/, 'DEFAULT')
else
sql = sql.sub(/\$#{i+1}/, "$#{j+1}") if i != j
j += 1
end
end
binds = binds.reject{|col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
end
args = name_passed ? [name, binds] : [binds]
exec_cache_without_schema_plus(sql, *args)
end
def foreign_keys(table_name, name = nil) #:nodoc:
load_foreign_keys(<<-SQL, name)
SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
FROM pg_class t, pg_constraint f
WHERE f.conrelid = t.oid
AND f.contype = 'f'
AND t.relname = '#{table_name_without_namespace(table_name)}'
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
SQL
end
def reverse_foreign_keys(table_name, name = nil) #:nodoc:
load_foreign_keys(<<-SQL, name)
SELECT f.conname, pg_get_constraintdef(f.oid), t2.relname
FROM pg_class t, pg_class t2, pg_constraint f
WHERE f.confrelid = t.oid
AND f.conrelid = t2.oid
AND f.contype = 'f'
AND t.relname = '#{table_name_without_namespace(table_name)}'
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
SQL
end
def views(name = nil) #:nodoc:
sql = <<-SQL
SELECT viewname
FROM pg_views
WHERE schemaname = ANY (current_schemas(false))
SQL
sql += " AND schemaname != 'postgis'" if adapter_name == 'PostGIS'
query(sql, name).map { |row| row[0] }
end
def view_definition(view_name, name = nil) #:nodoc:
result = query(<<-SQL, name)
SELECT pg_get_viewdef(oid)
FROM pg_class
WHERE relkind = 'v'
AND relname = '#{view_name}'
SQL
row = result.first
row.first.chomp(';') unless row.nil?
end
private
def namespace_sql(table_name)
(table_name.to_s =~ /(.*)[.]/) ? "'#{$1}'" : "ANY (current_schemas(false))"
end
def table_name_without_namespace(table_name)
table_name.to_s.sub /.*[.]/, ''
end
def load_foreign_keys(sql, name = nil) #:nodoc:
foreign_keys = []
query(sql, name).each do |row|
if row[1] =~ /^FOREIGN KEY \((.+?)\) REFERENCES (.+?)\((.+?)\)( ON UPDATE (.+?))?( ON DELETE (.+?))?( (DEFERRABLE|NOT DEFERRABLE)( (INITIALLY DEFERRED|INITIALLY IMMEDIATE))?)?$/
name = row[0]
from_table_name = row[2]
column_names = $1
references_table_name = $2
references_column_names = $3
on_update = $5
on_delete = $7
deferrable = $9 == "DEFERRABLE"
deferrable = :initially_deferred if ($11 == "INITIALLY DEFERRED" )
on_update = on_update ? on_update.downcase.gsub(' ', '_').to_sym : :no_action
on_delete = on_delete ? on_delete.downcase.gsub(' ', '_').to_sym : :no_action
foreign_keys << ForeignKeyDefinition.new(name,
from_table_name, column_names.split(', '),
references_table_name.sub(/^"(.*)"$/, '\1'), references_column_names.split(', '),
on_update, on_delete, deferrable)
end
end
foreign_keys
end
module AddColumnOptions
def default_expr_valid?(expr)
true # arbitrary sql is okay in PostgreSQL
end
def sql_for_function(function)
case function
when :now
"NOW()"
end
end
end
end
end
end
end