diff options
Diffstat (limited to 'activerecord')
29 files changed, 391 insertions, 91 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 923d780b90..09045087d9 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,25 @@ +*   Dump indexes in `create_table` instead of `add_index`. + +    If the adapter supports indexes in create table, generated SQL is +    slightly more efficient. + +    *Ryuta Kamizono* + +*   Correctly dump `:options` on `create_table` for MySQL. + +    *Ryuta Kamizono* + +*   PostgreSQL: `:collation` support for string and text columns. + +    Example: + +        create_table :foos do |t| +          t.string :string_en, collation: 'en_US.UTF-8' +          t.text   :text_ja,   collation: 'ja_JP.UTF-8' +        end + +    *Ryuta Kamizono* +  *   Make `unscope` aware of "less than" and "greater than" conditions.      *TAKAHASHI Kazuaki* diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 8c50f3d1a3..77e64a22be 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -220,7 +220,7 @@ module ActiveRecord        include MonitorMixin -      attr_accessor :automatic_reconnect, :checkout_timeout +      attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache        attr_reader :spec, :connections, :size, :reaper        # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -432,7 +432,9 @@ module ActiveRecord        end        def new_connection -        Base.send(spec.adapter_method, spec.config) +        Base.send(spec.adapter_method, spec.config).tap do |conn| +          conn.schema_cache = schema_cache.dup if schema_cache +        end        end        def current_connection_id #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 91c7298983..2c7409b2dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -123,6 +123,8 @@ module ActiveRecord          'f'        end +      # Quote date/time values for use in SQL input. Includes microseconds +      # if the value is a Time responding to usec.        def quoted_date(value)          if value.acts_like?(:time)            zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal 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 f754df93b6..18d943f452 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,10 +14,6 @@ module ActiveRecord            send m, o          end -        def visit_AddColumn(o) -          "ADD #{accept(o)}" -        end -          delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn          private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql @@ -25,7 +21,7 @@ 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.adds.map { |col| accept 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 @@ -37,6 +33,10 @@ module ActiveRecord              column_sql            end +          def visit_AddColumnDefinition(o) +            "ADD #{accept(o.column)}" +          end +            def visit_TableDefinition(o)              create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "              create_sql << "#{quote_table_name(o.name)} " @@ -70,6 +70,7 @@ module ActiveRecord              column_options[:after] = o.after              column_options[:auto_increment] = o.auto_increment              column_options[:primary_key] = o.primary_key +            column_options[:collation] = o.collation              column_options            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 4761024ad0..0ccf0c498b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,13 +15,16 @@ 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, :auto_increment, :primary_key, :sql_type) #:nodoc: +    class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:        def primary_key?          primary_key || type.to_sym == :primary_key        end      end +    class AddColumnDefinition < Struct.new(:column) # :nodoc: +    end +      class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc:      end @@ -227,7 +230,7 @@ module ActiveRecord        # The +type+ parameter is normally one of the migrations native types,        # which is one of the following:        # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, -      # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, +      # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>,        # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,        # <tt>:binary</tt>, <tt>:boolean</tt>.        # @@ -434,6 +437,7 @@ module ActiveRecord          column.after       = options[:after]          column.auto_increment = options[:auto_increment]          column.primary_key = type == :primary_key || options[:primary_key] +        column.collation   = options[:collation]          column        end @@ -476,7 +480,7 @@ module ActiveRecord        def add_column(name, type, options)          name = name.to_s          type = type.to_sym -        @adds << @td.new_column_definition(name, type, options) +        @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options))        end      end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 999cb0ec5a..deb014ad46 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -35,12 +35,16 @@ module ActiveRecord          default = schema_default(column) if column.has_default?          spec[:default]   = default unless default.nil? +        if collation = schema_collation(column) +          spec[:collation] = collation +        end +          spec        end        # Lists the valid migration options        def migration_keys -        [:name, :limit, :precision, :scale, :default, :null] +        [:name, :limit, :precision, :scale, :default, :null, :collation]        end        private @@ -56,6 +60,10 @@ module ActiveRecord            type.type_cast_for_schema(default)          end        end + +      def schema_collation(column) +        column.collation.inspect if column.collation +      end      end    end  end 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 c4a37f8538..9004d86b04 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -14,6 +14,10 @@ module ActiveRecord          {}        end +      def table_options(table_name) +        nil +      end +        # Truncates a table alias according to the limits of the current adapter.        def table_alias_for(table_name)          table_name[0...table_alias_length].tr('.', '_') @@ -930,13 +934,13 @@ module ActiveRecord        def add_index_options(table_name, column_name, options = {}) #:nodoc:          column_names = Array(column_name) -        index_name   = index_name(table_name, column: column_names)          options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) -        index_type = options[:unique] ? "UNIQUE" : ""          index_type = options[:type].to_s if options.key?(:type) +        index_type ||= options[:unique] ? "UNIQUE" : ""          index_name = options[:name].to_s if options.key?(:name) +        index_name ||= index_name(table_name, column: column_names)          max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length          if options.key?(:algorithm) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ae42e8ef8d..0705c22a8c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -387,8 +387,8 @@ module ActiveRecord          end        end -      def new_column(name, default, sql_type_metadata = nil, null = true) -        Column.new(name, default, sql_type_metadata, null) +      def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) +        Column.new(name, default, sql_type_metadata, null, default_function, collation)        end        def lookup_cast_type(sql_type) # :nodoc: 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 f4f52e85e3..09aee1f467 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -14,7 +14,7 @@ module ActiveRecord        end        class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition -        attr_accessor :charset, :collation +        attr_accessor :charset        end        class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition @@ -28,7 +28,6 @@ module ActiveRecord              column.auto_increment = true            end            column.charset = options[:charset] -          column.collation = options[:collation]            column          end @@ -44,10 +43,6 @@ module ActiveRecord        end        class SchemaCreation < AbstractAdapter::SchemaCreation -        def visit_AddColumn(o) -          add_column_position!(super, column_options(o)) -        end -          private          def visit_DropForeignKey(name) @@ -67,6 +62,10 @@ module ActiveRecord            create_sql          end +        def visit_AddColumnDefinition(o) +          add_column_position!(super, column_options(o.column)) +        end +          def visit_ChangeColumnDefinition(o)            change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"            add_column_position!(change_column_sql, column_options(o.column)) @@ -75,7 +74,6 @@ module ActiveRecord          def column_options(o)            column_options = super            column_options[:charset] = o.charset -          column_options[:collation] = o.collation            column_options          end @@ -99,8 +97,8 @@ module ActiveRecord          end          def index_in_create(table_name, column_name, options) -          index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options) -          "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}" +          index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) +          "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "          end        end @@ -128,20 +126,20 @@ module ActiveRecord          spec = super          spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0          spec.delete(:limit)     if :boolean === column.type +        spec +      end + +      def schema_collation(column)          if column.collation && table_name = column.instance_variable_get(:@table_name)            @collation_cache ||= {}            @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] -          spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] +          column.collation.inspect if column.collation != @collation_cache[table_name]          end -        spec -      end - -      def migration_keys -        super + [:collation]        end +      private :schema_collation        class Column < ConnectionAdapters::Column # :nodoc: -        delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true +        delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true          def initialize(*)            super @@ -195,12 +193,11 @@ module ActiveRecord        end        class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: -        attr_reader :collation, :extra, :strict +        attr_reader :extra, :strict -        def initialize(type_metadata, collation: "", extra: "", strict: false) +        def initialize(type_metadata, extra: "", strict: false)            super(type_metadata)            @type_metadata = type_metadata -          @collation = collation            @extra = extra            @strict = strict          end @@ -218,7 +215,7 @@ module ActiveRecord          protected          def attributes_for_hash -          [self.class, @type_metadata, collation, extra, strict] +          [self.class, @type_metadata, extra, strict]          end        end @@ -342,8 +339,8 @@ module ActiveRecord          raise NotImplementedError        end -      def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: -        Column.new(field, default, sql_type_metadata, null) +      def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: +        Column.new(field, default, sql_type_metadata, null, default_function, collation)        end        # Must return the MySQL error number from the exception, if the exception has an @@ -566,8 +563,8 @@ module ActiveRecord            each_hash(result).map do |field|              field_name = set_field_encoding(field[:Field])              sql_type = field[:Type] -            type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) -            new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") +            type_metadata = fetch_type_metadata(sql_type, field[:Extra]) +            new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])            end          end        end @@ -654,8 +651,8 @@ module ActiveRecord        end        def add_index(table_name, column_name, options = {}) #:nodoc: -        index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) -        execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" +        index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) +        execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"        end        def foreign_keys(table_name) @@ -686,6 +683,16 @@ module ActiveRecord          end        end +      def table_options(table_name) +        create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + +        # strip create_definitions and partition_options +        raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip + +        # strip AUTO_INCREMENT +        raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') +      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 @@ -826,8 +833,8 @@ module ActiveRecord          end        end -      def fetch_type_metadata(sql_type, collation = "", extra = "") -        MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) +      def fetch_type_metadata(sql_type, extra = "") +        MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)        end        # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -882,7 +889,7 @@ module ActiveRecord        def add_column_sql(table_name, column_name, type, options = {})          td = create_table_definition(table_name)          cd = td.new_column_definition(column_name, type, options) -        schema_creation.visit_AddColumn cd +        schema_creation.accept(AddColumnDefinition.new(cd))        end        def change_column_sql(table_name, column_name, type, options = {}) @@ -924,8 +931,9 @@ module ActiveRecord        end        def add_index_sql(table_name, column_name, options = {}) -        index_name, index_type, index_columns = add_index_options(table_name, column_name, options) -        "ADD #{index_type} INDEX #{index_name} (#{index_columns})" +        index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) +        index_algorithm[0, 0] = ", " if index_algorithm.present? +        "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"        end        def remove_index_sql(table_name, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index f4dda5154e..4b95b0681d 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -12,7 +12,7 @@ module ActiveRecord          ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/        end -      attr_reader :name, :null, :sql_type_metadata, :default, :default_function +      attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation        delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true @@ -22,12 +22,13 @@ module ActiveRecord        # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.        # +sql_type_metadata+ is various information about the type of the column        # +null+ determines if this column allows +NULL+ values. -      def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) +      def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)          @name = name          @sql_type_metadata = sql_type_metadata          @null = null          @default = default          @default_function = default_function +        @collation = collation          @table_name = nil        end @@ -60,7 +61,7 @@ module ActiveRecord        protected        def attributes_for_hash -        [self.class, name, default, sql_type_metadata, null, default_function] +        [self.class, name, default, sql_type_metadata, null, default_function, collation]        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 b7755c4593..f175730551 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -40,8 +40,7 @@ module ActiveRecord            PGconn.quote_ident(name.to_s)          end -        # Quote date/time values for use in SQL input. Includes microseconds -        # if the value is a Time responding to usec. +        # Quote date/time values for use in SQL input.          def quoted_date(value) #:nodoc:            if value.year <= 0              bce_year = format("%04d", -value.year + 1) 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 050b815aad..662c6b4d38 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -8,6 +8,13 @@ module ActiveRecord            o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array)            super          end + +        def add_column_options!(sql, options) +          if options[:collation] +            sql << " COLLATE \"#{options[:collation]}\"" +          end +          super +        end        end        module SchemaStatements @@ -159,18 +166,18 @@ module ActiveRecord          # Returns the list of all column definitions for a table.          def columns(table_name)            # Limit, precision, and scale are all handled by the superclass. -          column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| +          column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|              oid = oid.to_i              fmod = fmod.to_i              type_metadata = fetch_type_metadata(column_name, type, oid, fmod)              default_value = extract_value_from_default(default)              default_function = extract_default_function(default_value, default) -            new_column(column_name, default_value, type_metadata, !notnull, default_function) +            new_column(column_name, default_value, type_metadata, !notnull, default_function, collation)            end          end -        def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc: -          PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) +        def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: +          PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation)          end          # Returns the current database name. @@ -409,6 +416,9 @@ module ActiveRecord            quoted_column_name = quote_column_name(column_name)            sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array])            sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" +          if options[:collation] +            sql << " COLLATE \"#{options[:collation]}\"" +          end            if options[:using]              sql << " USING #{options[:using]}"            elsif options[:cast_as] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 332ac9d88c..7e15c2ab26 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -772,7 +772,9 @@ module ActiveRecord          def column_definitions(table_name) # :nodoc:            exec_query(<<-end_sql, 'SCHEMA').rows                SELECT a.attname, format_type(a.atttypid, a.atttypmod), -                     pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod +                     pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, +             (SELECT c.collname FROM pg_collation c, pg_type t +               WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation)                  FROM pg_attribute a LEFT JOIN pg_attrdef d                    ON a.attrelid = d.adrelid AND a.attnum = d.adnum                 WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 37ff4e4613..981d5d7a3c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -13,6 +13,14 @@ module ActiveRecord          @tables       = {}        end +      def initialize_dup(other) +        super +        @columns      = @columns.dup +        @columns_hash = @columns_hash.dup +        @primary_keys = @primary_keys.dup +        @tables       = @tables.dup +      end +        def primary_keys(table_name)          @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil        end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 98aee77557..0f1759abaa 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -71,9 +71,9 @@ module ActiveRecord    class RecordNotDestroyed < ActiveRecordError      attr_reader :record -    def initialize(record) +    def initialize(message, record = nil)        @record = record -      super() +      super(message)      end    end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index da8f4d027a..466175690e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -148,7 +148,7 @@ module ActiveRecord      # Attributes marked as readonly are silently ignored if the record is      # being updated.      def save!(*args) -      create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) +      create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self))      end      # Deletes the record in the database and freezes this instance to @@ -193,7 +193,7 @@ module ActiveRecord      # and #destroy! raises ActiveRecord::RecordNotDestroyed.      # See ActiveRecord::Callbacks for further details.      def destroy! -      destroy || raise(ActiveRecord::RecordNotDestroyed, self) +      destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self))      end      # Returns an instance of the specified +klass+ with the attributes of the diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 7e907beec0..5af64b717a 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -93,6 +93,7 @@ module ActiveRecord                cache = Marshal.load File.binread filename                if cache.version == ActiveRecord::Migrator.current_version                  self.connection.schema_cache = cache +                self.connection_pool.schema_cache = cache.dup                else                  warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."                end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index eaeaf0321b..a4a986e6ed 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -131,6 +131,10 @@ HEADER              tbl.print ", id: false"            end            tbl.print ", force: :cascade" + +          table_options = @connection.table_options(table) +          tbl.print ", options: #{table_options.inspect}" unless table_options.blank? +            tbl.puts " do |t|"            # then dump all non-primary key columns @@ -168,11 +172,11 @@ HEADER              tbl.puts            end +          indexes(table, tbl) +            tbl.puts "  end"            tbl.puts -          indexes(table, tbl) -            tbl.rewind            stream.print tbl.read          rescue => e @@ -188,8 +192,7 @@ HEADER          if (indexes = @connection.indexes(table)).any?            add_index_statements = indexes.map do |index|              statement_parts = [ -              "add_index #{remove_prefix_and_suffix(index.table).inspect}", -              index.columns.inspect, +              "t.index #{index.columns.inspect}",                "name: #{index.name.inspect}",              ]              statement_parts << 'unique: true' if index.unique @@ -203,11 +206,10 @@ HEADER              statement_parts << "using: #{index.using.inspect}" if index.using              statement_parts << "type: #{index.type.inspect}" if index.type -            "  #{statement_parts.join(', ')}" +            "    #{statement_parts.join(', ')}"            end            stream.puts add_index_statements.sort.join("\n") -          stream.puts          end        end diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 6577d56240..57eb5d0e18 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -59,6 +59,43 @@ class ActiveSchemaTest < ActiveRecord::TestCase      assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)    end +  def test_index_in_create +    def (ActiveRecord::Base.connection).table_exists?(*); false; end + +    %w(SPATIAL FULLTEXT UNIQUE).each do |type| +      expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name`  (`last_name`) ) ENGINE=InnoDB" +      actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| +        t.index :last_name, type: type +      end +      assert_equal expected, actual +    end + +    expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" +    actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| +      t.index :last_name, length: 10, using: :btree +    end +    assert_equal expected, actual +  end + +  def test_index_in_bulk_change +    def (ActiveRecord::Base.connection).table_exists?(*); true; end +    def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + +    %w(SPATIAL FULLTEXT UNIQUE).each do |type| +      expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name`  (`last_name`)" +      actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| +        t.index :last_name, type: type +      end +      assert_equal expected, actual +    end + +    expected = "ALTER TABLE `peaple` ADD  INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" +    actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| +      t.index :last_name, length: 10, using: :btree, algorithm: :copy +    end +    assert_equal expected, actual +  end +    def test_drop_table      assert_equal "DROP TABLE `people`", drop_table(:people)    end diff --git a/activerecord/test/cases/adapters/mysql/table_options_test.rb b/activerecord/test/cases/adapters/mysql/table_options_test.rb new file mode 100644 index 0000000000..0e5b0e8aec --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class MysqlTableOptionsTest < ActiveRecord::TestCase +  include SchemaDumpingHelper + +  def setup +    @connection = ActiveRecord::Base.connection +  end + +  def teardown +    @connection.drop_table "mysql_table_options", if_exists: true +  end + +  test "table options with ENGINE" do +    @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{ENGINE=MyISAM}, options +  end + +  test "table options with ROW_FORMAT" do +    @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{ROW_FORMAT=REDUNDANT}, options +  end + +  test "table options with CHARSET" do +    @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{CHARSET=utf8mb4}, options +  end + +  test "table options with COLLATE" do +    @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{COLLATE=utf8mb4_bin}, options +  end +end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index e87cd3886a..0ea556d4fa 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -59,6 +59,43 @@ class ActiveSchemaTest < ActiveRecord::TestCase      assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)    end +  def test_index_in_create +    def (ActiveRecord::Base.connection).table_exists?(*); false; end + +    %w(SPATIAL FULLTEXT UNIQUE).each do |type| +      expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name`  (`last_name`) ) ENGINE=InnoDB" +      actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| +        t.index :last_name, type: type +      end +      assert_equal expected, actual +    end + +    expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" +    actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| +      t.index :last_name, length: 10, using: :btree +    end +    assert_equal expected, actual +  end + +  def test_index_in_bulk_change +    def (ActiveRecord::Base.connection).table_exists?(*); true; end +    def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + +    %w(SPATIAL FULLTEXT UNIQUE).each do |type| +      expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name`  (`last_name`)" +      actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| +        t.index :last_name, type: type +      end +      assert_equal expected, actual +    end + +    expected = "ALTER TABLE `peaple` ADD  INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" +    actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| +      t.index :last_name, length: 10, using: :btree, algorithm: :copy +    end +    assert_equal expected, actual +  end +    def test_drop_table      assert_equal "DROP TABLE `people`", drop_table(:people)    end diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb new file mode 100644 index 0000000000..0e5b0e8aec --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class MysqlTableOptionsTest < ActiveRecord::TestCase +  include SchemaDumpingHelper + +  def setup +    @connection = ActiveRecord::Base.connection +  end + +  def teardown +    @connection.drop_table "mysql_table_options", if_exists: true +  end + +  test "table options with ENGINE" do +    @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{ENGINE=MyISAM}, options +  end + +  test "table options with ROW_FORMAT" do +    @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{ROW_FORMAT=REDUNDANT}, options +  end + +  test "table options with CHARSET" do +    @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{CHARSET=utf8mb4}, options +  end + +  test "table options with COLLATE" do +    @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" +    output = dump_table_schema("mysql_table_options") +    options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] +    assert_match %r{COLLATE=utf8mb4_bin}, options +  end +end diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb new file mode 100644 index 0000000000..17ef5f304c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlCollationTest < ActiveRecord::TestCase +  include SchemaDumpingHelper + +  def setup +    @connection = ActiveRecord::Base.connection +    @connection.create_table :postgresql_collations, force: true do |t| +      t.string :string_c, collation: 'C' +      t.text :text_posix, collation: 'POSIX' +    end +  end + +  def teardown +    @connection.drop_table :postgresql_collations, if_exists: true +  end + +  test "string column with collation" do +    column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' } +    assert_equal :string, column.type +    assert_equal 'C', column.collation +  end + +  test "text column with collation" do +    column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' } +    assert_equal :text, column.type +    assert_equal 'POSIX', column.collation +  end + +  test "add column with collation" do +    @connection.add_column :postgresql_collations, :title, :string, collation: 'C' + +    column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' } +    assert_equal :string, column.type +    assert_equal 'C', column.collation +  end + +  test "change column with collation" do +    @connection.add_column :postgresql_collations, :description, :string +    @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX' + +    column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' } +    assert_equal :text, column.type +    assert_equal 'POSIX', column.collation +  end + +  test "schema dump includes collation" do +    output = dump_table_schema("postgresql_collations") +    assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output +    assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output +  end +end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 14cdf37f46..2c4e2a875c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2131,11 +2131,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase      car = Car.create!      original_child = FailedBulb.create!(car: car) -    assert_raise(ActiveRecord::RecordNotDestroyed) do +    error = assert_raise(ActiveRecord::RecordNotDestroyed) do        car.failed_bulbs = [FailedBulb.create!]      end      assert_equal [original_child], car.reload.failed_bulbs +    assert_equal "Failed to destroy the record", error.message    end    test 'updates counter cache when default scope is given' do diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 3ae4a6eade..73ac30e547 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -451,6 +451,7 @@ class CallbacksTest < ActiveRecord::TestCase        assert !david.save        exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }        assert_equal exc.record, david +      assert_equal "Failed to save the record", exc.message      end      david = ImmutableDeveloper.find(1) @@ -494,6 +495,7 @@ class CallbacksTest < ActiveRecord::TestCase        assert !david.destroy        exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }        assert_equal exc.record, david +      assert_equal "Failed to destroy the record", exc.message      end      assert_not_nil ImmutableDeveloper.find_by_id(1) diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index aa50efc979..f5928814a3 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -341,6 +341,21 @@ module ActiveRecord            handler.establish_connection anonymous, nil          }        end + +      def test_pool_sets_connection_schema_cache +        connection = pool.checkout +        schema_cache = SchemaCache.new connection +        schema_cache.add(:posts) +        pool.schema_cache = schema_cache + +        pool.with_connection do |conn| +          assert_not_same pool.schema_cache, conn.schema_cache +          assert_equal pool.schema_cache.size, conn.schema_cache.size +          assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts) +        end + +        pool.checkin connection +      end      end    end  end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 63612e33af..e6f0fe6f75 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -168,24 +168,24 @@ class SchemaDumperTest < ActiveRecord::TestCase    end    def test_schema_dumps_index_columns_in_right_order -    index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip +    index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip      if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) -      assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition +      assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition      else -      assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition +      assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition      end    end    def test_schema_dumps_partial_indices -    index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip +    index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip      if current_adapter?(:PostgreSQLAdapter) -      assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition +      assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition      elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) -      assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition +      assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition      elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? -      assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition +      assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition      else -      assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition +      assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition      end    end @@ -232,8 +232,8 @@ class SchemaDumperTest < ActiveRecord::TestCase      def test_schema_dumps_index_type        output = standard_dump -      assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output -      assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output +      assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output +      assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output      end    end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 008503bc24..872fa595b4 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,5 +1,16 @@  ActiveRecord::Schema.define do +  enable_extension!('uuid-ossp', ActiveRecord::Base.connection) + +  create_table :uuid_parents, id: :uuid, force: true do |t| +    t.string :name +  end + +  create_table :uuid_children, id: :uuid, force: true do |t| +    t.string :name +    t.uuid :uuid_parent_id +  end +    %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones        postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|      drop_table table_name, if_exists: true diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7b42f8a4a5..66f8f1611d 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -6,20 +6,6 @@ ActiveRecord::Schema.define do      end    end -  #put adapter specific setup here -  case adapter_name -  when "PostgreSQL" -    enable_extension!('uuid-ossp', ActiveRecord::Base.connection) -    create_table :uuid_parents, id: :uuid, force: true do |t| -      t.string :name -    end -    create_table :uuid_children, id: :uuid, force: true do |t| -      t.string :name -      t.uuid :uuid_parent_id -    end -  end - -    # ------------------------------------------------------------------- #    #                                                                     #    #   Please keep these create table statements in alphabetical order   #  | 
