diff options
16 files changed, 223 insertions, 142 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 18ec203fa3..1aeba4f856 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* The PostgreSQL adapter supports custom domains. Fixes #14305. + + *Yves Senn* + +* PostgreSQL `Column#type` is now determined through the corresponding OID. + The column types stay the same except for enum columns. They no longer have + `nil` as type but `enum`. + + See #7814. + + *Yves Senn* + * Fixed error when specifying a non-empty default value on a PostgreSQL array column. Fixes #10613. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 1f314e0677..80ae38b3fb 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -248,7 +248,7 @@ module ActiveRecord dependent = _options[:dependent] || options[:dependent] if records.first == :all - if loaded? || dependent == :destroy + if (loaded? || dependent == :destroy) && dependent != :delete_all delete_or_destroy(load_target, dependent) else delete_records(:all, dependent) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 2b5049f5a5..5e82fdcbe0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -20,9 +20,9 @@ module ActiveRecord ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 49f0bfbcde..e6aa2ba921 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -36,9 +36,9 @@ module ActiveRecord ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) rescue Mysql::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise 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 823189934d..2cbcd5fd50 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -151,70 +151,7 @@ module ActiveRecord # Maps PostgreSQL-specific data types to logical Rails types. def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore - when 'ltree' - :ltree - # Network address types - when 'inet' - :inet - when 'cidr' - :cidr - when 'macaddr' - :macaddr - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - when /^citext(?:\(\d+\))?$/ - :citext - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when /^interval(?:|\(\d+\))$/ - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :uuid - # JSON type - when 'json' - :json - # Small and big integer types - when /^(?:small|big)int$/ - :integer - when /(num|date|tstz|ts|int4|int8)range$/ - field_type.to_sym - # Pass through all types that are not specific to PostgreSQL. - else - super - end + @oid_type.simplified_type(field_type) || super end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 5d32aaed50..57bdc3bb19 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -6,6 +6,7 @@ module ActiveRecord module OID class Type def type; end + def simplified_type(sql_type); type end def infinity(options = {}) ::Float::INFINITY * (options[:negative] ? -1 : 1) @@ -18,7 +19,9 @@ module ActiveRecord end end - class Text < Type + class String < Type + def type; :string end + def type_cast(value) return if value.nil? @@ -26,9 +29,23 @@ module ActiveRecord end end + class SpecializedString < OID::String + def type; @type end + + def initialize(type) + @type = type + end + end + + class Text < OID::String + def type; :text end + end + class Bit < Type + def type; :string end + def type_cast(value) - if String === value + if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_bit value else value @@ -37,6 +54,8 @@ module ActiveRecord end class Bytea < Type + def type; :binary end + def type_cast(value) return if value.nil? PGconn.unescape_bytea value @@ -44,9 +63,11 @@ module ActiveRecord end class Money < Type + def type; :decimal end + def type_cast(value) return if value.nil? - return value unless String === value + return value unless ::String === value # Because money output is formatted according to the locale, there are two # cases to consider (note the decimal separators): @@ -88,8 +109,10 @@ module ActiveRecord end class Point < Type + def type; :string end + def type_cast(value) - if String === value + if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_point value else value @@ -98,13 +121,15 @@ module ActiveRecord end class Array < Type + def type; @subtype.type end + attr_reader :subtype def initialize(subtype) @subtype = subtype end def type_cast(value) - if String === value + if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype else value @@ -114,6 +139,8 @@ module ActiveRecord class Range < Type attr_reader :subtype + def simplified_type(sql_type); sql_type.to_sym end + def initialize(subtype) @subtype = subtype end @@ -160,6 +187,8 @@ This is not reliable and will be removed in the future. end class Integer < Type + def type; :integer end + def type_cast(value) return if value.nil? @@ -168,6 +197,8 @@ This is not reliable and will be removed in the future. end class Boolean < Type + def type; :boolean end + def type_cast(value) return if value.nil? @@ -177,6 +208,14 @@ This is not reliable and will be removed in the future. class Timestamp < Type def type; :timestamp; end + def simplified_type(sql_type) + case sql_type + when /^timestamp with(?:out)? time zone$/ + :datetime + else + :timestamp + end + end def type_cast(value) return if value.nil? @@ -188,7 +227,7 @@ This is not reliable and will be removed in the future. end class Date < Type - def type; :datetime; end + def type; :date; end def type_cast(value) return if value.nil? @@ -200,6 +239,8 @@ This is not reliable and will be removed in the future. end class Time < Type + def type; :time end + def type_cast(value) return if value.nil? @@ -210,6 +251,8 @@ This is not reliable and will be removed in the future. end class Float < Type + def type; :float end + def type_cast(value) return if value.nil? @@ -218,6 +261,8 @@ This is not reliable and will be removed in the future. end class Decimal < Type + def type; :decimal end + def type_cast(value) return if value.nil? @@ -230,12 +275,16 @@ This is not reliable and will be removed in the future. end class Enum < Type + def type; :enum end + def type_cast(value) value.to_s end end class Hstore < Type + def type; :hstore end + def type_cast_for_write(value) ConnectionAdapters::PostgreSQLColumn.hstore_to_string value end @@ -252,14 +301,20 @@ This is not reliable and will be removed in the future. end class Cidr < Type + def type; :cidr end def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_cidr value end end + class Inet < Cidr + def type; :inet end + end class Json < Type + def type; :json end + def type_cast_for_write(value) ConnectionAdapters::PostgreSQLColumn.json_to_string value end @@ -321,7 +376,7 @@ This is not reliable and will be removed in the future. } # Register an OID type named +name+ with a typecasting object in - # +type+. +name+ should correspond to the `typname` column in + # +type+. +name+ should correspond to the `typname` column in # the `pg_type` table. def self.register_type(name, type) NAMES[name] = type @@ -338,48 +393,46 @@ This is not reliable and will be removed in the future. end register_type 'int2', OID::Integer.new - alias_type 'int4', 'int2' - alias_type 'int8', 'int2' - alias_type 'oid', 'int2' - + alias_type 'int4', 'int2' + alias_type 'int8', 'int2' + alias_type 'oid', 'int2' register_type 'numeric', OID::Decimal.new + register_type 'float4', OID::Float.new + alias_type 'float8', 'float4' register_type 'text', OID::Text.new - alias_type 'varchar', 'text' - alias_type 'char', 'text' - alias_type 'bpchar', 'text' - alias_type 'xml', 'text' - - # FIXME: why are we keeping these types as strings? - alias_type 'tsvector', 'text' - alias_type 'interval', 'text' - alias_type 'macaddr', 'text' - alias_type 'uuid', 'text' - - register_type 'money', OID::Money.new - register_type 'bytea', OID::Bytea.new + register_type 'varchar', OID::String.new + alias_type 'char', 'varchar' + alias_type 'bpchar', 'varchar' register_type 'bool', OID::Boolean.new register_type 'bit', OID::Bit.new - register_type 'varbit', OID::Bit.new - - register_type 'float4', OID::Float.new - alias_type 'float8', 'float4' - + alias_type 'varbit', 'bit' register_type 'timestamp', OID::Timestamp.new - register_type 'timestamptz', OID::Timestamp.new + alias_type 'timestamptz', 'timestamp' register_type 'date', OID::Date.new register_type 'time', OID::Time.new - register_type 'path', OID::Text.new + register_type 'money', OID::Money.new + register_type 'bytea', OID::Bytea.new register_type 'point', OID::Point.new - register_type 'polygon', OID::Text.new - register_type 'circle', OID::Text.new register_type 'hstore', OID::Hstore.new register_type 'json', OID::Json.new - register_type 'citext', OID::Text.new - register_type 'ltree', OID::Text.new - register_type 'cidr', OID::Cidr.new - alias_type 'inet', 'cidr' + register_type 'inet', OID::Inet.new + register_type 'xml', SpecializedString.new(:xml) + register_type 'tsvector', SpecializedString.new(:tsvector) + register_type 'macaddr', SpecializedString.new(:macaddr) + register_type 'uuid', SpecializedString.new(:uuid) + register_type 'citext', SpecializedString.new(:citext) + register_type 'ltree', SpecializedString.new(:ltree) + + # FIXME: why are we keeping these types as strings? + alias_type 'interval', 'varchar' + alias_type 'path', 'varchar' + alias_type 'line', 'varchar' + alias_type 'polygon', 'varchar' + alias_type 'circle', 'varchar' + alias_type 'lseg', 'varchar' + alias_type 'box', 'varchar' end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index c31fe96842..748436de12 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -582,22 +582,23 @@ module ActiveRecord def initialize_type_map(type_map) if supports_ranges? result = execute(<<-SQL, 'SCHEMA') - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype FROM pg_type as t LEFT JOIN pg_range as r ON oid = rngtypid SQL else result = execute(<<-SQL, 'SCHEMA') - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype FROM pg_type as t SQL end - ranges, nodes = result.partition { |row| row['typinput'] == 'range_in' } - leaves, nodes = nodes.partition { |row| row['typelem'] == '0' } + ranges, nodes = result.partition { |row| row['typtype'] == 'r' } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + leaves, nodes = nodes.partition { |row| row['typelem'] == '0' } # populate the enum types - enums, leaves = leaves.partition { |row| row['typinput'] == 'enum_in' } enums.each do |row| type_map[row['oid'].to_i] = OID::Enum.new end @@ -626,6 +627,16 @@ module ActiveRecord range = OID::Range.new subtype type_map[row['oid'].to_i] = range end + + # populate domain types + domains.each do |row| + base_type_oid = row["typbasetype"].to_i + if base_type = type_map[base_type_oid] + type_map[row['oid'].to_i] = base_type + else + warn "unknown base type (OID: #{base_type_oid}) for domain #{row["typname"]}." + end + end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: @@ -707,9 +718,9 @@ module ActiveRecord configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 3c5f7a981e..6e6a51dab8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -33,9 +33,9 @@ module ActiveRecord ConnectionAdapters::SQLite3Adapter.new(db, logger, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message) + raise ActiveRecord::NoDatabaseError.new(error.message, error) else - raise error + raise end end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 7f6228131f..71efbb8f93 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -95,15 +95,7 @@ module ActiveRecord end # Raised when a given database does not exist - class NoDatabaseError < ActiveRecordError - def initialize(message) - super extend_message(message) - end - - # can be over written to add additional error information. - def extend_message(message) - message - end + class NoDatabaseError < StatementInvalid end # Raised on attempt to save stale record. Record is stale when it's being saved in another query after diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 11b564f8f9..a4ceacbf44 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -116,17 +116,22 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do + self.configurations = Rails.application.config.database_configuration - class ActiveRecord::NoDatabaseError - remove_possible_method :extend_message - def extend_message(message) - message << "Run `$ bin/rake db:create db:migrate` to create your database" - message - end - end + begin + establish_connection + rescue ActiveRecord::NoDatabaseError + warn <<-end_warning +Oops - You have a database configured, but it doesn't exist yet! - self.configurations = Rails.application.config.database_configuration - establish_connection +Here's how to get started: + + 1. Configure your database in config/database.yml. + 2. Run `bin/rake db:create` to create the database. + 3. Run `bin/rake db:setup` to load your database schema. +end_warning + raise + end end end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb new file mode 100644 index 0000000000..fdf5ce974d --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlDomainTest < ActiveRecord::TestCase + class PostgresqlDomain < ActiveRecord::Base + self.table_name = "postgresql_domains" + end + + def setup + # reset connection to bust all cached statement plans + connection_spec = ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(connection_spec) + + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute "CREATE DOMAIN custom_money as numeric(8,2)" + @connection.create_table('postgresql_domains') do |t| + t.column :price, :custom_money + end + end + + # reload type map after creating the enum type + @connection.send(:reload_type_map) + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_domains' + @connection.execute 'DROP DOMAIN IF EXISTS custom_money' + end + + def test_column + column = PostgresqlDomain.columns_hash["price"] + assert_equal :decimal, column.type + assert_equal "custom_money", column.sql_type + assert column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + end + + def test_domain_acts_like_basetype + PostgresqlDomain.create price: "" + record = PostgresqlDomain.first + assert_nil record.price + + record.price = "34.15" + record.save! + + assert_equal BigDecimal.new("34.15"), record.reload.price + end +end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 381c397922..ac78c2426e 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -29,8 +29,7 @@ class PostgresqlEnumTest < ActiveRecord::TestCase def test_column column = PostgresqlEnum.columns_hash["current_mood"] - # TODO: enum columns should be of type enum or string, not nil. - assert_nil column.type + assert_equal :enum, column.type assert_equal "mood", column.sql_type assert_not column.number? assert_not column.text? diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 39f5b9b18a..51846e22d9 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -47,9 +47,9 @@ module ActiveRecord def test_quote_cast_numeric fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, OID::Decimal.new, 'varchar') + c = PostgreSQLColumn.new(nil, nil, OID::String.new, 'varchar') assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, OID::Decimal.new, 'text') + c = PostgreSQLColumn.new(nil, nil, OID::Text.new, 'text') assert_equal "'666'", @conn.quote(fixnum, c) end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 49d3301044..3dfb0a27ba 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -22,6 +22,8 @@ require 'models/engine' require 'models/categorization' require 'models/minivan' require 'models/speedometer' +require 'models/reference' +require 'models/job' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -39,7 +41,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations + :categorizations, :jobs def setup Client.destroyed_client_ids.clear @@ -107,6 +109,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy" end + def test_delete_all_on_association_is_the_same_as_not_loaded + author = authors :david + author.thinking_posts.create!(:body => "test") + author.reload + expected_sql = capture_sql { author.thinking_posts.delete_all } + + author.thinking_posts.create!(:body => "test") + author.reload + author.thinking_posts.inspect + loaded_sql = capture_sql { author.thinking_posts.delete_all } + assert_equal(expected_sql, loaded_sql) + end + def test_building_the_associated_object_with_implicit_sti_base_class firm = DependentFirm.new company = firm.companies.build diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index c7b64f29c3..c1dd1f1c69 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -127,13 +127,13 @@ module ActiveRecord if current_adapter?(:PostgreSQLAdapter) def test_bigint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Identity.new + oid = PostgreSQLAdapter::OID::Integer.new bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint") assert_equal :integer, bigint_column.type end def test_smallint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Identity.new + oid = PostgreSQLAdapter::OID::Integer.new smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") assert_equal :integer, smallint_column.type end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 4476ce3410..803a054d7e 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -19,10 +19,14 @@ module ActiveRecord end end - def assert_sql(*patterns_to_match) + def capture_sql SQLCounter.clear_log yield - SQLCounter.log_all + SQLCounter.log_all.dup + end + + def assert_sql(*patterns_to_match) + capture_sql { yield } ensure failed_patterns = [] patterns_to_match.each do |pattern| |