diff options
Diffstat (limited to 'activerecord/lib')
22 files changed, 172 insertions, 81 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a830b0e0e4..19ef37e228 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -619,10 +619,10 @@ module ActiveRecord      #   @tag = @post.tags.build name: "ruby"      #   @tag.save      # -    # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the +    # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the      # <tt>:inverse_of</tt> is set:      # -    #   class Taggable < ActiveRecord::Base +    #   class Tagging < ActiveRecord::Base      #     belongs_to :post      #     belongs_to :tag, inverse_of: :taggings      #   end @@ -643,7 +643,7 @@ module ActiveRecord      # You can turn off the automatic detection of inverse associations by setting      # the <tt>:inverse_of</tt> option to <tt>false</tt> like so:      # -    #   class Taggable < ActiveRecord::Base +    #   class Tagging < ActiveRecord::Base      #     belongs_to :tag, inverse_of: false      #   end      # diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index b5a8c81fe4..29c711082a 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -227,6 +227,31 @@ module ActiveRecord          @association.last(*args)        end +      # Gives a record (or N records if a parameter is supplied) from the collection +      # using the same rules as <tt>ActiveRecord::Base.take</tt>. +      # +      #   class Person < ActiveRecord::Base +      #     has_many :pets +      #   end +      # +      #   person.pets +      #   # => [ +      #   #       #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, +      #   #       #<Pet id: 2, name: "Spook", person_id: 1>, +      #   #       #<Pet id: 3, name: "Choo-Choo", person_id: 1> +      #   #    ] +      # +      #   person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> +      # +      #   person.pets.take(2) +      #   # => [ +      #   #      #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, +      #   #      #<Pet id: 2, name: "Spook", person_id: 1> +      #   #    ] +      # +      #   another_person_without.pets         # => [] +      #   another_person_without.pets.take    # => nil +      #   another_person_without.pets.take(2) # => []        def take(n = nil)          @association.take(n)        end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 5a92bc5e8a..1829453d73 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -65,7 +65,7 @@ module ActiveRecord              when :destroy                target.destroy              when :nullify -              target.update_columns(reflection.foreign_key => nil) +              target.update_columns(reflection.foreign_key => nil) if target.persisted?            end          end        end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 2363cf7608..5197e21fa4 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -56,14 +56,12 @@ module ActiveRecord          end        end -      ID = 'id'.freeze -        # Returns the value of the attribute identified by <tt>attr_name</tt> after        # it has been typecast (for example, "2004-12-12" in a date column is cast        # to a date object, like Date.new(2004, 12, 12)).        def read_attribute(attr_name, &block)          name = attr_name.to_s -        name = self.class.primary_key if name == ID +        name = self.class.primary_key if name == 'id'.freeze          _read_attribute(name, &block)        end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index dbb0e2fab2..d0de42d27c 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -234,7 +234,7 @@ module ActiveRecord        super      end -    # Marks this record to be destroyed as part of the parents save transaction. +    # Marks this record to be destroyed as part of the parent's save transaction.      # This does _not_ actually destroy the record instantly, rather child record will be destroyed      # when <tt>parent.save</tt> is called.      # @@ -243,7 +243,7 @@ module ActiveRecord        @marked_for_destruction = true      end -    # Returns whether or not this record will be destroyed as part of the parents save transaction. +    # Returns whether or not this record will be destroyed as part of the parent's save transaction.      #      # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.      def marked_for_destruction? diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 55a7e053bc..4b66d8cd36 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,5 +1,4 @@  require 'yaml' -require 'set'  require 'active_support/benchmarkable'  require 'active_support/dependencies'  require 'active_support/descendants_tracker' 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 d17e272ed1..3115e03ea2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -214,6 +214,7 @@ module ActiveRecord          @name = name        end +      # Returns an array of ColumnDefinition objects for the columns of the table.        def columns; @columns_hash.values; end        # Returns a ColumnDefinition for the column with name +name+. @@ -369,6 +370,8 @@ module ActiveRecord          self        end +      # remove the column +name+ from the table. +      #   remove_column(:account_id)        def remove_column(name)          @columns_hash.delete name.to_s        end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 56227ddd80..ed14c781c6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -266,6 +266,11 @@ module ActiveRecord          false        end +      # Does this adapter support json data type? +      def supports_json? +        false +      end +        # This is meant to be implemented by the adapters that support extensions        def disable_extension(name)        end 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 af156c9c78..7b47539596 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -10,6 +10,10 @@ module ActiveRecord            options[:auto_increment] = true if type == :bigint            super          end + +        def json(*args, **options) +          args.each { |name| column(name, :json, options) } +        end        end        class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition @@ -242,17 +246,19 @@ module ActiveRecord        QUOTED_TRUE, QUOTED_FALSE = '1', '0'        NATIVE_DATABASE_TYPES = { -        :primary_key => "int(11) auto_increment PRIMARY KEY", -        :string      => { :name => "varchar", :limit => 255 }, -        :text        => { :name => "text" }, -        :integer     => { :name => "int", :limit => 4 }, -        :float       => { :name => "float" }, -        :decimal     => { :name => "decimal" }, -        :datetime    => { :name => "datetime" }, -        :time        => { :name => "time" }, -        :date        => { :name => "date" }, -        :binary      => { :name => "blob" }, -        :boolean     => { :name => "tinyint", :limit => 1 } +        primary_key: "int(11) auto_increment PRIMARY KEY", +        string:      { name: "varchar", limit: 255 }, +        text:        { name: "text" }, +        integer:     { name: "int", limit: 4 }, +        float:       { name: "float" }, +        decimal:     { name: "decimal" }, +        datetime:    { name: "datetime" }, +        time:        { name: "time" }, +        date:        { name: "date" }, +        binary:      { name: "blob" }, +        boolean:     { name: "tinyint", limit: 1 }, +        bigint:      { name: "bigint" }, +        json:        { name: "json" },        }        INDEX_TYPES  = [:fulltext, :spatial] @@ -721,8 +727,10 @@ module ActiveRecord        # SHOW VARIABLES LIKE 'name'        def show_variable(name) -        variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') +        variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')          variables.first['Value'] unless variables.empty? +      rescue ActiveRecord::StatementInvalid +        nil        end        # Returns a table's primary key and belonging sequence. @@ -790,6 +798,7 @@ module ActiveRecord          m.register_type %r(longblob)i,   Type::Binary.new(limit: 2**32 - 1)          m.register_type %r(^float)i,     Type::Float.new(limit: 24)          m.register_type %r(^double)i,    Type::Float.new(limit: 53) +        m.register_type %r(^json)i,      MysqlJson.new          register_integer_type m, %r(^bigint)i,    limit: 8          register_integer_type m, %r(^int)i,       limit: 4 @@ -1043,6 +1052,14 @@ module ActiveRecord          end        end +      class MysqlJson < Type::Internal::AbstractJson # :nodoc: +        def changed_in_place?(raw_old_value, new_value) +          # Normalization is required because MySQL JSON data format includes +          # the space between the elements. +          super(serialize(deserialize(raw_old_value)), new_value) +        end +      end +        class MysqlString < Type::String # :nodoc:          def serialize(value)            case value @@ -1063,6 +1080,8 @@ module ActiveRecord          end        end +      ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql) +      ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)        ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql)        ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)      end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index b7db57c9fe..ff43c7ec42 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -41,6 +41,10 @@ module ActiveRecord          true        end +      def supports_json? +        version >= '5.7.8' +      end +        # HELPER METHODS ===========================================        def each_hash(result) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 2ae462d773..0738c59ddf 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -223,7 +223,7 @@ module ActiveRecord          return @client_encoding if @client_encoding          result = exec_query( -          "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", +          "select @@character_set_client",            'SCHEMA')          @client_encoding = ENCODINGS[result.rows.last.last]        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 8e1256baad..dbc879ffd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,32 +2,7 @@ module ActiveRecord    module ConnectionAdapters      module PostgreSQL        module OID # :nodoc: -        class Json < Type::Value # :nodoc: -          include Type::Helpers::Mutable - -          def type -            :json -          end - -          def deserialize(value) -            if value.is_a?(::String) -              ::ActiveSupport::JSON.decode(value) rescue nil -            else -              value -            end -          end - -          def serialize(value) -            if value.is_a?(::Array) || value.is_a?(::Hash) -              ::ActiveSupport::JSON.encode(value) -            else -              value -            end -          end - -          def accessor -            ActiveRecord::Store::StringKeyedHashAccessor -          end +        class Json < Type::Internal::AbstractJson          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 f175730551..d5879ea7df 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -31,6 +31,11 @@ module ActiveRecord            Utils.extract_schema_qualified_name(name.to_s).quoted          end +        # Quotes schema names for use in SQL queries. +        def quote_schema_name(name) +          PGconn.quote_ident(name) +        end +          def quote_table_name_for_assignment(table, attr)            quote_column_name(attr)          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 d114cad16b..a3fc8fbc51 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -68,7 +68,7 @@ module ActiveRecord            execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"          end -        # Returns the list of all tables in the schema search path or a specified schema. +        # Returns the list of all tables in the schema search path.          def tables(name = nil)            select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')          end @@ -210,12 +210,12 @@ module ActiveRecord          # Creates a schema for the given schema name.          def create_schema schema_name -          execute "CREATE SCHEMA #{schema_name}" +          execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"          end          # Drops the schema for the given schema name. -        def drop_schema schema_name -          execute "DROP SCHEMA #{schema_name} CASCADE" +        def drop_schema(schema_name, options = {}) +          execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"          end          # Sets the schema search path to a string of comma-separated schema names. @@ -376,7 +376,7 @@ module ActiveRecord              new_seq = "#{new_name}_#{pk}_seq"              idx = "#{table_name}_pkey"              new_idx = "#{new_name}_pkey" -            execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" +            execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"              execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"            end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2c43c46a3d..27291bd2ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -201,6 +201,10 @@ module ActiveRecord          true        end +      def supports_json? +        postgresql_version >= 90200 +      end +        def index_algorithms          { concurrently: 'CONCURRENTLY' }        end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 91a13cb0cd..4902fcb1a2 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -18,10 +18,9 @@ module ActiveRecord    #   conversation.archived? # => true    #   conversation.status    # => "archived"    # -  #   # conversation.update! status: 1 +  #   # conversation.status = 1    #   conversation.status = "archived"    # -  #   # conversation.update! status: nil    #   conversation.status = nil    #   conversation.status.nil? # => true    #   conversation.status      # => nil diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index d589620f8a..718f04871d 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -218,11 +218,12 @@ module ActiveRecord    class UnknownPrimaryKey < ActiveRecordError      attr_reader :model -    def initialize(model) -      super("Unknown primary key for table #{model.table_name} in model #{model}.") +    def initialize(model, description = nil) +      message = "Unknown primary key for table #{model.table_name} in model #{model}." +      message += "\n#{description}" if description +      super(message)        @model = model      end -    end    # Raised when a relation cannot be mutated because it's already loaded. diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b5b91451c7..b4dd8eff5a 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -168,7 +168,7 @@ module ActiveRecord    #    #   rails generate migration add_fieldname_to_tablename fieldname:string    # -  # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: +  # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this:    #   class AddFieldnameToTablename < ActiveRecord::Migration    #     def change    #       add_column :tablenames, :fieldname, :string @@ -409,7 +409,10 @@ module ActiveRecord          new.migrate direction        end -      # Disable DDL transactions for this migration. +      # Disable the transaction wrapping this migration. +      # You can still create your own transactions even after calling #disable_ddl_transaction! +      # +      # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration].        def disable_ddl_transaction!          @disable_ddl_transaction = true        end @@ -456,7 +459,7 @@ module ActiveRecord      # Or equivalently, if +TenderloveMigration+ is defined as in the      # documentation for Migration:      # -    #   require_relative '2012121212_tenderlove_migration' +    #   require_relative '20121212123456_tenderlove_migration'      #      #   class FixupTLMigration < ActiveRecord::Migration      #     def change @@ -472,13 +475,13 @@ module ActiveRecord      def revert(*migration_classes)        run(*migration_classes.reverse, revert: true) unless migration_classes.empty?        if block_given? -        if @connection.respond_to? :revert -          @connection.revert { yield } +        if connection.respond_to? :revert +          connection.revert { yield }          else -          recorder = CommandRecorder.new(@connection) +          recorder = CommandRecorder.new(connection)            @connection = recorder            suppress_messages do -            @connection.revert { yield } +            connection.revert { yield }            end            @connection = recorder.delegate            recorder.commands.each do |cmd, args, block| @@ -489,7 +492,7 @@ module ActiveRecord      end      def reverting? -      @connection.respond_to?(:reverting) && @connection.reverting +      connection.respond_to?(:reverting) && connection.reverting      end      class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: @@ -546,7 +549,7 @@ module ActiveRecord          revert { run(*migration_classes, direction: dir, revert: true) }        else          migration_classes.each do |migration_class| -          migration_class.new.exec_migration(@connection, dir) +          migration_class.new.exec_migration(connection, dir)          end        end      end @@ -638,7 +641,7 @@ module ActiveRecord        arg_list = arguments.map(&:inspect) * ', '        say_with_time "#{method}(#{arg_list})" do -        unless @connection.respond_to? :revert +        unless connection.respond_to? :revert            unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)              arguments[0] = proper_table_name(arguments.first, table_name_options)              if [:rename_table, :add_foreign_key].include?(method) || @@ -811,7 +814,7 @@ module ActiveRecord          new(:up, migrations, target_version).migrate        end -      def down(migrations_paths, target_version = nil, &block) +      def down(migrations_paths, target_version = nil)          migrations = migrations(migrations_paths)          migrations.select! { |m| yield m } if block_given? diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index dcc2362397..3ab0f28c9b 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -5,15 +5,34 @@ module ActiveRecord      # knows how to invert the following commands:      #      # * add_column +    # * add_foreign_key      # * add_index +    # * add_reference      # * add_timestamps -    # * create_table +    # * change_column_default (must supply a :from and :to option) +    # * change_column_null      # * create_join_table +    # * create_table +    # * disable_extension +    # * drop_join_table +    # * drop_table (must supply a block) +    # * enable_extension +    # * remove_column (must supply a type) +    # * remove_foreign_key (must supply a second table) +    # * remove_index +    # * remove_reference      # * remove_timestamps      # * rename_column      # * rename_index      # * rename_table      class CommandRecorder +      ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, +        :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, +        :change_column_default, :add_reference, :remove_reference, :transaction, +        :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension, +        :change_column, :execute, :remove_columns, :change_column_null, +        :add_foreign_key, :remove_foreign_key +      ]        include JoinTable        attr_accessor :commands, :delegate, :reverting @@ -41,7 +60,7 @@ module ActiveRecord          @reverting = !@reverting        end -      # record +command+. +command+ should be a method name and arguments. +      # Record +command+. +command+ should be a method name and arguments.        # For example:        #        #   recorder.record(:method_name, [:arg1, :arg2]) @@ -70,14 +89,7 @@ module ActiveRecord          super || delegate.respond_to?(*args)        end -      [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, -        :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, -        :add_reference, :remove_reference, :transaction, -        :drop_join_table, :drop_table, :execute_block, :enable_extension, -        :change_column, :execute, :remove_columns, :change_column_null, -        :add_foreign_key, :remove_foreign_key -       # irreversible methods need to be here too -      ].each do |method| +      ReversibleAndIrreversibleMethods.each do |method|          class_eval <<-EOV, __FILE__, __LINE__ + 1            def #{method}(*args, &block)          # def create_table(*args, &block)              record(:"#{method}", args, &block)  #   record(:create_table, args, &block) diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 2c0cda69d0..53f3b53bec 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -20,6 +20,8 @@ require 'active_record/type/adapter_specific_registry'  require 'active_record/type/type_map'  require 'active_record/type/hash_lookup_type_map' +require 'active_record/type/internal/abstract_json' +  module ActiveRecord    module Type      @registry = AdapterSpecificRegistry.new diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb new file mode 100644 index 0000000000..963a8245d0 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -0,0 +1,33 @@ +module ActiveRecord +  module Type +    module Internal # :nodoc: +      class AbstractJson < Type::Value # :nodoc: +        include Type::Helpers::Mutable + +        def type +          :json +        end + +        def deserialize(value) +          if value.is_a?(::String) +            ::ActiveSupport::JSON.decode(value) rescue nil +          else +            value +          end +        end + +        def serialize(value) +          if value.is_a?(::Array) || value.is_a?(::Hash) +            ::ActiveSupport::JSON.encode(value) +          else +            value +          end +        end + +        def accessor +          ActiveRecord::Store::StringKeyedHashAccessor +        end +      end +    end +  end +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 32d17a1392..5706bbd903 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -18,7 +18,11 @@ module ActiveRecord          relation = build_relation(finder_class, table, attribute, value)          if record.persisted? && finder_class.primary_key.to_s != attribute.to_s -          relation = relation.where.not(finder_class.primary_key => record.id) +          if finder_class.primary_key +            relation = relation.where.not(finder_class.primary_key => record.id) +          else +            raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") +          end          end          relation = scope_relation(record, table, relation)          relation = relation.merge(options[:conditions]) if options[:conditions]  | 
