From 9a4a095ed7ea6f0f65cc9e3bf3258cbdd0ddc210 Mon Sep 17 00:00:00 2001 From: Alexey Date: Sun, 16 Dec 2012 04:10:58 +0200 Subject: AR supporting new intrange data type on PostgreSQL >= 9.2 --- .../active_record/connection_adapters/column.rb | 1 + .../connection_adapters/postgresql/cast.rb | 23 ++++++++++++++++++++++ .../connection_adapters/postgresql/oid.rb | 11 +++++++++++ .../connection_adapters/postgresql/quoting.rb | 10 ++++++++++ .../postgresql/schema_statements.rb | 8 ++++++++ .../connection_adapters/postgresql_adapter.rb | 19 ++++++++++++++---- 6 files changed, 68 insertions(+), 4 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index df23dbfb60..fd36a5b075 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -126,6 +126,7 @@ module ActiveRecord when :hstore then "#{klass}.string_to_hstore(#{var_name})" when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" when :json then "#{klass}.string_to_json(#{var_name})" + when :intrange then "#{klass}.string_to_intrange(#{var_name})" else var_name end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index c04a799b8d..3772f7ddaa 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -92,6 +92,29 @@ module ActiveRecord parse_pg_array(string).map{|val| oid.type_cast val} end + def string_to_intrange(string) + if string.nil? + nil + elsif "empty" == string + (nil..nil) + elsif String === string + matches = /^(\(|\[)([0-9]+),(\s?)([0-9]+)(\)|\])$/i.match(string) + lower_bound = ("(" == matches[1] ? (matches[2].to_i + 1) : matches[2].to_i) + upper_bound = (")" == matches[5] ? (matches[4].to_i - 1) : matches[4].to_i) + (lower_bound..upper_bound) + else + string + end + end + + def intrange_to_string(object) + if Range === object + "[#{object.first},#{object.exclude_end? ? object.last : object.last.to_i + 1})" + else + object + end + end + private HstorePair = begin diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 52344f61c0..18ea83ce42 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -168,6 +168,14 @@ module ActiveRecord end end + class IntRange < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_intrange value + end + end + class TypeMap def initialize @mapping = {} @@ -269,6 +277,9 @@ module ActiveRecord register_type 'hstore', OID::Hstore.new register_type 'json', OID::Json.new + register_type 'int4range', OID::IntRange.new + alias_type 'int8range', 'int4range' + register_type 'cidr', OID::Cidr.new alias_type 'inet', 'cidr' end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 62a4d76928..c2fcef94da 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 when 'json' then super(PostgreSQLColumn.json_to_string(value), column) else super end + when Range + case column.sql_type + when 'int4range', 'int8range' then super(PostgreSQLColumn.intrange_to_string(value), column) + else super + end when IPAddr case column.sql_type when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) @@ -89,6 +94,11 @@ module ActiveRecord when 'json' then PostgreSQLColumn.json_to_string(value) else super(value, column) end + when Range + case column.sql_type + when 'int4range', 'int8range' then PostgreSQLColumn.intrange_to_string(value) + else super(value, column) + end when IPAddr return super(value, column) unless ['inet','cidr'].include? column.sql_type PostgreSQLColumn.cidr_to_string(value) 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 18bf14d1fb..e10b562fa4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -417,6 +417,14 @@ module ActiveRecord when 0..6; "timestamp(#{precision})" else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") end + when 'intrange' + return 'int4range' unless limit + + case limit + when 1..4; 'int4range' + when 5..8; 'int8range' + else raise(ActiveRecordError, "No range type has byte size #{limit}. Use a numeric with precision 0 instead.") + end else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e24ee1efdd..d62a375470 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -114,6 +114,9 @@ module ActiveRecord # JSON when /\A'(.*)'::json\z/ $1 + # int4range, int8range + when /\A'(.*)'::int(4|8)range\z/ + $1 # Object identifier types when /\A-?\d+\z/ $1 @@ -209,9 +212,12 @@ module ActiveRecord # UUID type when 'uuid' :uuid - # JSON type - when 'json' - :json + # JSON type + when 'json' + :json + # int4range, int8range types + when 'int4range', 'int8range' + :intrange # Small and big integer types when /^(?:small|big)int$/ :integer @@ -289,6 +295,10 @@ module ActiveRecord column(name, 'json', options) end + def intrange(name, options = {}) + column(name, 'intrange', options) + end + def column(name, type = nil, options = {}) super column = self[name] @@ -329,7 +339,8 @@ module ActiveRecord cidr: { name: "cidr" }, macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, - json: { name: "json" } + json: { name: "json" }, + intrange: { name: "int4range" } } include Quoting -- cgit v1.2.3 From 49182b87b258786ae5942eeb475618794291e976 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 17 Dec 2012 21:16:40 +0200 Subject: AR supporting new int4range and int8range data type on PostgreSQL >= 9.2. Fix realization --- .../active_record/connection_adapters/postgresql/cast.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 3772f7ddaa..f7d734a2f1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -97,8 +97,7 @@ module ActiveRecord nil elsif "empty" == string (nil..nil) - elsif String === string - matches = /^(\(|\[)([0-9]+),(\s?)([0-9]+)(\)|\])$/i.match(string) + elsif String === string && (matches = /^(\(|\[)([0-9]+),(\s?)([0-9]+)(\)|\])$/i.match(string)) lower_bound = ("(" == matches[1] ? (matches[2].to_i + 1) : matches[2].to_i) upper_bound = (")" == matches[5] ? (matches[4].to_i - 1) : matches[4].to_i) (lower_bound..upper_bound) @@ -108,8 +107,16 @@ module ActiveRecord end def intrange_to_string(object) - if Range === object - "[#{object.first},#{object.exclude_end? ? object.last : object.last.to_i + 1})" + if object.nil? + nil + elsif Range === object + if [object.first, object.last].all? { |el| Integer === el } + "[#{object.first.to_i},#{object.exclude_end? ? object.last.to_i : object.last.to_i + 1})" + elsif [object.first, object.last].all? { |el| NilClass === el } + "empty" + else + nil + end else object end -- cgit v1.2.3 From ba98dad113c494ac7a3c09abf16044fce4c0ef5e Mon Sep 17 00:00:00 2001 From: Pedro Padron Date: Tue, 30 Oct 2012 15:29:47 -0200 Subject: Added support for validates_uniqueness_of in PostgreSQL array columns. Fixes: #8075. --- activerecord/lib/active_record/validations/uniqueness.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5fa6a0b892..f27fc78717 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -71,7 +71,12 @@ 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? + + if !value.nil? && column.text? && column.limit + value = value.to_s[0, column.limit] + else + value = klass.connection.type_cast(value, column) + end if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation -- cgit v1.2.3 From ba492af25ac985be350e5a3812574b1b11b2dc6c Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 31 Oct 2012 00:19:21 -0200 Subject: Refactor uniqueness validator logic a bit Cleanup some code, avoid extra hash with reverse_merge, and don't use send for :id, since it's always present. --- activerecord/lib/active_record/validations/uniqueness.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index f27fc78717..31dbf7a889 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -4,7 +4,7 @@ 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. @@ -17,19 +17,17 @@ module ActiveRecord table = finder_class.arel_table coder = record.class.serialized_attributes[attribute.to_s] - if value && coder value = coder.dump value end 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? + relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? Array(options[:scope]).each do |scope_item| - reflection = record.class.reflect_on_association(scope_item) - if reflection + if reflection = record.class.reflect_on_association(scope_item) scope_value = record.send(reflection.foreign_key) - scope_item = reflection.foreign_key + scope_item = reflection.foreign_key else scope_value = record.read_attribute(scope_item) end @@ -37,10 +35,7 @@ module ActiveRecord end relation = finder_class.unscoped.where(relation) - - if options[:conditions] - relation = relation.merge(options[:conditions]) - end + relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value)) -- cgit v1.2.3 From af0582968dfb400375bbef6ace7166aac1041f3d Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 31 Oct 2012 00:20:50 -0200 Subject: Remove prepend_and_append requirement from AS It's not necessary to use this extension here, we are fine with Array#unshift. --- activerecord/lib/active_record/validations/uniqueness.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 31dbf7a889..5db17155c5 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/array/prepend_and_append' - module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator # :nodoc: @@ -53,7 +51,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? } -- cgit v1.2.3 From 73fb6349c98742bb404bd7ad97184d19f32a8366 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 31 Oct 2012 00:23:47 -0200 Subject: Avoid unnecessary hashes with error options --- activerecord/lib/active_record/validations/uniqueness.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5db17155c5..dc07645732 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -36,7 +36,10 @@ module ActiveRecord relation = 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 -- cgit v1.2.3 From 23750b4733e1d8e7ef334d70998258d1d2e2c6c8 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 31 Oct 2012 00:33:51 -0200 Subject: Simplify value logic by always typecasting --- activerecord/lib/active_record/validations/uniqueness.rb | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index dc07645732..5700350cf9 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -67,22 +67,16 @@ module ActiveRecord end column = klass.columns_hash[attribute.to_s] - - if !value.nil? && column.text? && column.limit - value = value.to_s[0, column.limit] - else - value = klass.connection.type_cast(value, column) - end + 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 - - relation end end -- cgit v1.2.3 From 1127262a610486ff54596967e6707a78e944b502 Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 31 Oct 2012 00:37:13 -0200 Subject: Change relation in place --- activerecord/lib/active_record/validations/uniqueness.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5700350cf9..2a4a70cd59 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -33,7 +33,7 @@ module ActiveRecord end relation = finder_class.unscoped.where(relation) - relation = relation.merge(options[:conditions]) if options[:conditions] + relation.merge!(options[:conditions]) if options[:conditions] if relation.exists? error_options = options.except(:case_sensitive, :scope, :conditions) -- cgit v1.2.3 From 0bb15174558fdeaa0be1889a94347eacfc605f0b Mon Sep 17 00:00:00 2001 From: Carlos Antonio da Silva Date: Wed, 31 Oct 2012 00:45:47 -0200 Subject: Extract some methods --- .../lib/active_record/validations/uniqueness.rb | 38 +++++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 2a4a70cd59..1427189851 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -13,25 +13,11 @@ 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.id)) if record.persisted? - - 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 = scope_relation(record, table, relation) relation = finder_class.unscoped.where(relation) relation.merge!(options[:conditions]) if options[:conditions] @@ -78,6 +64,26 @@ module ActiveRecord 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 -- cgit v1.2.3