aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb115
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb65
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb52
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb115
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb42
24 files changed, 525 insertions, 263 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index ff92375820..eb88845913 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -9,12 +9,7 @@ module ActiveRecord
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
- # FIXME: The only case we get an object other than nil or a real column
- # is `SchemaStatements#add_column` with a PG array that has a non-empty default
- # value. Is this really the only case? Are we missing tests for other types?
- # We should have a real column object passed (or nil) here, and check for that
- # instead
- if column.respond_to?(:cast_type)
+ if column
value = column.cast_type.type_cast_for_database(value)
end
@@ -29,12 +24,7 @@ module ActiveRecord
return value.id
end
- # FIXME: The only case we get an object other than nil or a real column
- # is `SchemaStatements#add_column` with a PG array that has a non-empty default
- # value. Is this really the only case? Are we missing tests for other types?
- # We should have a real column object passed (or nil) here, and check for that
- # instead
- if column.respond_to?(:cast_type)
+ if column
value = column.cast_type.type_cast_for_database(value)
end
@@ -66,7 +56,7 @@ module ActiveRecord
# This works for mysql and mysql2 where table.column can be used to
# resolve ambiguity.
#
- # We override this in the sqlite and postgresql adapters to use only
+ # We override this in the sqlite3 and postgresql adapters to use only
# the column name (as per syntax requirements).
def quote_table_name_for_assignment(table, attr)
quote_table_name("#{table}.#{attr}")
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 47fe501752..adad6cd542 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -23,6 +23,8 @@ module ActiveRecord
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
end
def visit_ColumnDefinition(o)
@@ -41,6 +43,21 @@ module ActiveRecord
create_sql
end
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -77,6 +94,7 @@ module ActiveRecord
def quote_value(value, column)
column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+ column.cast_type ||= type_for_column(column)
@conn.quote(value, column)
end
@@ -84,6 +102,23 @@ module ActiveRecord
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
+
+ def type_for_column(column)
+ @conn.lookup_cast_type(column.sql_type)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index a9b3e9cfb9..98e6795f10 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -15,7 +15,7 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -25,6 +25,37 @@ module ActiveRecord
class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
end
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -303,14 +334,26 @@ module ActiveRecord
class AlterTable # :nodoc:
attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
def initialize(td)
@td = td
@adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
end
def name; @td.name; end
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 22823a8c58..5814c2b711 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -642,6 +642,115 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
+ end
+
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -852,6 +961,12 @@ module ActiveRecord
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
+
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index cc494a7f40..f8c054eb69 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -233,6 +233,11 @@ module ActiveRecord
false
end
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -370,12 +375,12 @@ module ActiveRecord
Column.new(name, default, cast_type, sql_type, null)
end
- protected
-
def lookup_cast_type(sql_type) # :nodoc:
type_map.lookup(sql_type)
end
+ protected
+
def initialize_type_map(m) # :nodoc:
register_class_with_limit m, %r(boolean)i, Type::Boolean
register_class_with_limit m, %r(char)i, Type::String
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 3ef8878ad1..e5417a9556 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -12,6 +12,10 @@ module ActiveRecord
private
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
def visit_TableDefinition(o)
name = o.name
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
@@ -192,6 +196,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -381,7 +389,7 @@ module ActiveRecord
end
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -465,7 +473,7 @@ module ActiveRecord
end
def rename_index(table_name, old_name, new_name)
- if (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ if supports_rename_index?
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
else
super
@@ -501,6 +509,34 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
case type.to_s
@@ -738,10 +774,22 @@ module ActiveRecord
private
+ def version
+ @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ end
+
+ def mariadb?
+ full_version =~ /mariadb/i
+ end
+
def supports_views?
version[0] >= 5
end
+ def supports_rename_index?
+ mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ end
+
def configure_connection
variables = @config.fetch(:variables, {}).stringify_keys
@@ -757,8 +805,8 @@ module ActiveRecord
# Make MySQL reject illegal values rather than truncating or blanking them, see
# http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- if strict_mode? && !variables.has_key?('sql_mode')
- variables['sql_mode'] = 'STRICT_ALL_TABLES'
+ unless variables.has_key?('sql_mode')
+ variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
@@ -779,6 +827,15 @@ module ActiveRecord
# ...and send them all in one query
@connection.query "SET #{encoding} #{variable_assignments}"
end
+
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 0a14cdfe89..39d52e6349 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -269,8 +269,8 @@ module ActiveRecord
super
end
- def version
- @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ def full_version
+ @full_version ||= @connection.info[:version]
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index ad07a46e51..a03bc28744 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -470,9 +470,9 @@ module ActiveRecord
rows
end
- # Returns the version of the connected MySQL server.
- def version
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ # Returns the full version of the connected MySQL server.
+ def full_version
+ @full_version ||= @connection.server_info
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index a865c5c310..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module Cast # :nodoc:
- def point_to_string(point) # :nodoc:
- "(#{number_for_point(point[0])},#{number_for_point(point[1])})"
- end
-
- def number_for_point(number)
- number.to_s.gsub(/\.0$/, '')
- end
-
- def hstore_to_string(object, array_member = false) # :nodoc:
- if Hash === object
- string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
- string = escape_hstore(string) if array_member
- string
- else
- object
- end
- end
-
- def string_to_hstore(string) # :nodoc:
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k, v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
- [k, v]
- }]
- else
- string
- end
- end
-
- def json_to_string(object) # :nodoc:
- if Hash === object || Array === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def range_to_string(object) # :nodoc:
- from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
- to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
- "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
- end
-
- def string_to_json(string) # :nodoc:
- if String === string
- ActiveSupport::JSON.decode(string)
- else
- string
- end
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 847fd4dded..37e5c3859c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -1,11 +1,7 @@
-require 'active_record/connection_adapters/postgresql/cast'
-
module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
- extend PostgreSQL::Cast
-
attr_accessor :array
def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 33a98b4fcb..d05ce61330 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -21,6 +21,7 @@ require 'active_record/connection_adapters/postgresql/oid/specialized_string'
require 'active_record/connection_adapters/postgresql/oid/time'
require 'active_record/connection_adapters/postgresql/oid/uuid'
require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 243ecd13cf..1dbb40ca1d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -19,6 +19,32 @@ module ActiveRecord
value
end
end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
index 26c5d3d78f..78ef94b912 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -7,6 +7,7 @@ module ActiveRecord
def cast_value(value)
case value
+ when ::Float then value
when 'Infinity' then ::Float::INFINITY
when '-Infinity' then -::Float::INFINITY
when 'NaN' then ::Float::NAN
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index 57b20477df..be4525c94f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -10,16 +10,48 @@ module ActiveRecord
end
def type_cast_from_database(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value)
+ if value.is_a?(::String)
+ ::Hash[value.scan(HstorePair).map { |k, v|
+ v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ [k, v]
+ }]
+ else
+ value
+ end
end
def type_cast_for_database(value)
- ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index ab1165f301..e12ddd9901 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -10,11 +10,19 @@ module ActiveRecord
end
def type_cast_from_database(value)
- ConnectionAdapters::PostgreSQLColumn.string_to_json(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
end
def type_cast_for_database(value)
- ConnectionAdapters::PostgreSQLColumn.json_to_string(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
end
def accessor
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index 9b6494867f..bac8b01d6b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -25,11 +25,17 @@ module ActiveRecord
def type_cast_for_database(value)
if value.is_a?(::Array)
- PostgreSQLColumn.point_to_string(value)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
else
super
end
end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index 991cdd0913..ae967d5167 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -10,28 +10,10 @@ module ActiveRecord
@type = type
end
- def extract_bounds(value)
- from, to = value[1..-2].split(',')
- {
- from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
- exclude_start: (value[0] == '('),
- exclude_end: (value[-1] == ')')
- }
- end
-
- def infinity?(value)
- value.respond_to?(:infinite?) && value.infinite?
- end
-
def type_cast_for_schema(value)
value.inspect.gsub('Infinity', '::Float::INFINITY')
end
- def type_cast_single(value)
- infinity?(value) ? value : @subtype.type_cast_from_database(value)
- end
-
def cast_value(value)
return if value == 'empty'
return value if value.is_a?(::Range)
@@ -53,6 +35,40 @@ This is not reliable and will be removed in the future.
end
::Range.new(from, to, extracted[:exclude_end])
end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
index b2a42e9ebb..2d2fede4e8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -8,10 +8,6 @@ module ActiveRecord
def initialize(type)
@type = type
end
-
- def text?
- false
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index 89728b0fe2..dd97393eac 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -3,12 +3,21 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Uuid < Type::Value # :nodoc:
+ RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [1-5][a-fA-F0-9]{3}-?
+ [8-Bab][a-fA-F0-9]{3}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?\}?\z}x
+
def type
:uuid
end
def type_cast(value)
- value.presence
+ value.to_s[RFC_4122, 0]
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..7323f12763
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def text?
+ false
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 4caed77952..cf5c8d288e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -18,95 +18,18 @@ module ActiveRecord
def quote(value, column = nil) #:nodoc:
return super unless column
- sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale)
-
case value
- when Range
- if /range$/ =~ sql_type
- "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}"
- else
- super
- end
- when Array
- case sql_type
- when 'point' then super(PostgreSQLColumn.point_to_string(value))
- when 'json' then super(PostgreSQLColumn.json_to_string(value))
- else
- super(value, array_column(column))
- end
- when Hash
- case sql_type
- when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
- when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
- else super
- end
when Float
- if value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- elsif value.infinite? || value.nan?
+ if value.infinite? || value.nan?
"'#{value.to_s}'"
else
super
end
- when Numeric
- if sql_type == 'money' || [:string, :text].include?(column.type)
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
- else
- super
- end
- when String
- case sql_type
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
else
super
end
end
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when Range
- if /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
- else
- super(value, column)
- end
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- case column.sql_type
- when 'point' then PostgreSQLColumn.point_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else
- super(value, array_column(column))
- end
- when Hash
- case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- else
- super(value, column)
- end
- end
-
# Quotes strings for use in SQL input.
def quote_string(s) #:nodoc:
@connection.escape(s)
@@ -160,43 +83,35 @@ module ActiveRecord
private
def _quote(value)
- if value.is_a?(Type::Binary::Data)
+ case value
+ when Type::Binary::Data
"'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
else
super
end
end
def _type_cast(value)
- if value.is_a?(Type::Binary::Data)
+ case value
+ when Type::Binary::Data
# Return a bind param hash with format as binary.
# See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
# for more information
{ value: value.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
else
super
end
end
-
- def array_column(column)
- if column.array && !column.respond_to?(:cast_type)
- Column.new('', nil, OID::Array.new(AdapterProxyType.new(column, self)))
- else
- column
- end
- end
-
- class AdapterProxyType < SimpleDelegator # :nodoc:
- def initialize(column, adapter)
- @column = column
- @adapter = adapter
- super(column)
- end
-
- def type_cast_for_database(value)
- @adapter.type_cast(value, @column)
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index b2aeb3a058..e09672d239 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -31,6 +31,14 @@ module ActiveRecord
super
end
end
+
+ def type_for_column(column)
+ if column.array
+ @conn.lookup_cast_type("#{column.sql_type}[]")
+ else
+ super
+ end
+ end
end
module SchemaStatements
@@ -375,8 +383,8 @@ module ActiveRecord
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
@@ -384,7 +392,7 @@ module ActiveRecord
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq.identifier == "#{table_name}_#{pk}_seq"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
end
@@ -448,6 +456,42 @@ module ActiveRecord
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6d5151dfe4..f660fc41cf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -159,6 +159,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -344,14 +348,13 @@ module ActiveRecord
if supports_extensions?
res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
'SCHEMA'
- res.column_types['enabled'].type_cast_from_database res.rows.first.first
+ res.cast_values.first
end
end
def extensions
if supports_extensions?
- res = exec_query "SELECT extname from pg_extension", "SCHEMA"
- res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first }
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
else
super
end
@@ -380,6 +383,11 @@ module ActiveRecord
PostgreSQL::Table.new(table_name, base)
end
+ def lookup_cast_type(sql_type) # :nodoc:
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
+ super(oid)
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -446,7 +454,7 @@ module ActiveRecord
m.register_type 'cidr', OID::Cidr.new
m.register_type 'inet', OID::Inet.new
m.register_type 'uuid', OID::Uuid.new
- m.register_type 'xml', OID::SpecializedString.new(:xml)
+ m.register_type 'xml', OID::Xml.new
m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
m.register_type 'citext', OID::SpecializedString.new(:citext)
@@ -561,7 +569,7 @@ module ActiveRecord
end
def exec_no_cache(sql, name, binds)
- log(sql, name, binds) { @connection.async_exec(sql) }
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
end
def exec_cache(sql, name, binds)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index e6163771e8..bf96acad4a 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -14,9 +14,9 @@ module ActiveRecord
raise ArgumentError, "No database file specified. Missing argument: database"
end
- # Allow database path relative to Rails.root, but only if
- # the database path is not the special path that tells
- # Sqlite to build a database only in memory.
+ # Allow database path relative to Rails.root, but only if the database
+ # path is not the special path that tells sqlite to build a database only
+ # in memory.
if ':memory:' != config[:database]
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
dirname = File.dirname(config[:database])
@@ -50,6 +50,16 @@ module ActiveRecord
end
end
+ class SQLite3String < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
+ value.encode(Encoding::UTF_8)
+ else
+ super
+ end
+ end
+ end
+
# The SQLite3 adapter works SQLite 3.6.16 or newer
# with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
#
@@ -220,13 +230,23 @@ module ActiveRecord
# QUOTING ==================================================
def _quote(value) # :nodoc:
- if value.is_a?(Type::Binary::Data)
+ case value
+ when Type::Binary::Data
"x'#{value.hex}'"
else
super
end
end
+ def _type_cast(value) # :nodoc:
+ case value
+ when BigDecimal
+ value.to_f
+ else
+ super
+ end
+ end
+
def quote_string(s) #:nodoc:
@connection.class.quote(s)
end
@@ -249,19 +269,6 @@ module ActiveRecord
end
end
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
- value = value.encode Encoding::UTF_8
- end
- value
- end
-
# DATABASE STATEMENTS ======================================
def explain(arel, binds = [])
@@ -503,6 +510,7 @@ module ActiveRecord
def initialize_type_map(m)
super
m.register_type(/binary/i, SQLite3Binary.new)
+ register_class_with_limit m, %r(char)i, SQLite3String
end
def select(sql, name = nil, binds = []) #:nodoc: