diff options
author | Carlos Antonio da Silva <carlosantoniodasilva@gmail.com> | 2012-12-18 09:08:11 -0200 |
---|---|---|
committer | Carlos Antonio da Silva <carlosantoniodasilva@gmail.com> | 2012-12-18 09:08:11 -0200 |
commit | f6d0eda918756bf56e2c05afcaaabda723487017 (patch) | |
tree | 35974c37f5ff4cf9dbab0728716ccaff6f41e3b5 /activerecord | |
parent | c3e573db54c55c4e4ac694940c6d2f5b42b6155b (diff) | |
parent | 0bb15174558fdeaa0be1889a94347eacfc605f0b (diff) | |
download | rails-f6d0eda918756bf56e2c05afcaaabda723487017.tar.gz rails-f6d0eda918756bf56e2c05afcaaabda723487017.tar.bz2 rails-f6d0eda918756bf56e2c05afcaaabda723487017.zip |
Merge branch 'uniqueness-validation-postgresql-arrays'
When setting an array column (or other PostgreSQL-specific data type)
to be validated for uniqueness, the resulting SQL query is generated with
syntax errors, like this:
SELECT 1 AS one FROM "postgresql_arrays"
WHERE "postgresql_arrays"."nicknames" = '["john", "johnny"]' LIMIT 1
This happens because Ruby Arrays must be type-casted to their PostgreSQL
equivalent. This small patch fixes this issue, calling the adapter's
type_cast method.
Closes #8077
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG.md | 5 | ||||
-rw-r--r-- | activerecord/lib/active_record/validations/uniqueness.rb | 65 | ||||
-rw-r--r-- | activerecord/test/cases/validations/uniqueness_validation_test.rb | 23 |
3 files changed, 58 insertions, 35 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 11e23ad0f4..e7d3b529fe 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,10 @@ ## Rails 4.0.0 (unreleased) ## +* Added support for `validates_uniqueness_of` in PostgreSQL array columns. + Fixes #8075. + + *Pedro Padron* + * Allow int4range and int8range columns to be created in PostgreSQL and properly convert to/from database. *Alexey Vasiliev aka leopard* diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5fa6a0b892..1427189851 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,10 +1,8 @@ -require 'active_support/core_ext/array/prepend_and_append' - module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator # :nodoc: def initialize(options) - super(options.reverse_merge(:case_sensitive => true)) + super({ case_sensitive: true }.merge!(options)) end # Unfortunately, we have to tie Uniqueness validators to a class. @@ -15,35 +13,19 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) table = finder_class.arel_table - - coder = record.class.serialized_attributes[attribute.to_s] - - if value && coder - value = coder.dump value - end + value = deserialize_attribute(record, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? - - Array(options[:scope]).each do |scope_item| - reflection = record.class.reflect_on_association(scope_item) - if reflection - scope_value = record.send(reflection.foreign_key) - scope_item = reflection.foreign_key - else - scope_value = record.read_attribute(scope_item) - end - relation = relation.and(table[scope_item].eq(scope_value)) - end - + relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = scope_relation(record, table, relation) relation = finder_class.unscoped.where(relation) - - if options[:conditions] - relation = relation.merge(options[:conditions]) - end + relation.merge!(options[:conditions]) if options[:conditions] if relation.exists? - record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value)) + error_options = options.except(:case_sensitive, :scope, :conditions) + error_options[:value] = value + + record.errors.add(attribute, :taken, error_options) end end @@ -58,7 +40,7 @@ module ActiveRecord class_hierarchy = [record.class] while class_hierarchy.first != @klass - class_hierarchy.prepend(class_hierarchy.first.superclass) + class_hierarchy.unshift(class_hierarchy.first.superclass) end class_hierarchy.detect { |klass| !klass.abstract_class? } @@ -71,18 +53,37 @@ module ActiveRecord end column = klass.columns_hash[attribute.to_s] - value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text? + value = klass.connection.type_cast(value, column) + value = value.to_s[0, column.limit] if value && column.limit && column.text? if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation - relation = klass.connection.case_insensitive_comparison(table, attribute, column, value) + klass.connection.case_insensitive_comparison(table, attribute, column, value) else - value = klass.connection.case_sensitive_modifier(value) unless value.nil? - relation = table[attribute].eq(value) + value = klass.connection.case_sensitive_modifier(value) unless value.nil? + table[attribute].eq(value) + end + end + + def scope_relation(record, table, relation) + Array(options[:scope]).each do |scope_item| + if reflection = record.class.reflect_on_association(scope_item) + scope_value = record.send(reflection.foreign_key) + scope_item = reflection.foreign_key + else + scope_value = record.read_attribute(scope_item) + end + relation = relation.and(table[scope_item].eq(scope_value)) end relation end + + def deserialize_attribute(record, attribute, value) + coder = record.class.serialized_attributes[attribute.to_s] + value = coder.dump value if value && coder + value + end end module ClassMethods diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 46212e49b6..46e767af1a 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -30,6 +30,11 @@ class ReplyWithTitleObject < Reply def title; ReplyTitle.new; end end +class Employee < ActiveRecord::Base + self.table_name = 'postgresql_arrays' + validates_uniqueness_of :nicknames +end + class UniquenessValidationTest < ActiveRecord::TestCase fixtures :topics, 'warehouse-things', :developers @@ -341,16 +346,28 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert w6.errors[:city].any?, "Should have errors for city" assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city" end - + def test_validate_uniqueness_with_conditions Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true)) Topic.create("title" => "I'm a topic", "approved" => true) Topic.create("title" => "I'm an unapproved topic", "approved" => false) - + t3 = Topic.new("title" => "I'm a topic", "approved" => true) assert !t3.valid?, "t3 shouldn't be valid" - + t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false) assert t4.valid?, "t4 should be valid" end + + def test_validate_uniqueness_with_array_column + return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter + + e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) + assert e1.persisted?, "Saving e1" + + e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) + assert !e2.persisted?, "e2 shouldn't be valid" + assert e2.errors[:nicknames].any?, "Should have errors for nicknames" + assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" + end end |