diff options
Diffstat (limited to 'activerecord')
44 files changed, 561 insertions, 151 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d288d9c04b..563df44d76 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Numeric and decimal columns map to BigDecimal instead of Float. Those with scale 0 map to Integer. #5454 [robbat2@gentoo.org, work@ashleymoran.me.uk] + * Firebird migrations support. #5337 [Ken Kunz <kennethkunz@gmail.com>] * PostgreSQL: create/drop as postgres user. #4790 [mail@matthewpainter.co.uk, mlaster@metavillage.com] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1c8c819efa..e4c47c9bc3 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1726,7 +1726,7 @@ module ActiveRecord #:nodoc: if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) self.id = connection.next_sequence_value(self.class.sequence_name) end - + self.id = connection.insert( "INSERT INTO #{self.class.table_name} " + "(#{quoted_column_names.join(', ')}) " + diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 05beddac75..1c1b00252c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -16,13 +16,15 @@ module ActiveRecord else "'#{quote_string(value)}'" # ' (for ruby-mode) end - when NilClass then "NULL" - when TrueClass then (column && column.type == :integer ? '1' : quoted_true) - when FalseClass then (column && column.type == :integer ? '0' : quoted_false) - when Float, Fixnum, Bignum then value.to_s - when Date then "'#{value.to_s}'" - when Time, DateTime then "'#{quoted_date(value)}'" - else "'#{quote_string(value.to_yaml)}'" + when NilClass then "NULL" + when TrueClass then (column && column.type == :integer ? '1' : quoted_true) + when FalseClass then (column && column.type == :integer ? '0' : quoted_false) + when Float, Fixnum, Bignum then value.to_s + # BigDecimals need to be output in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Date then "'#{value.to_s}'" + when Time, DateTime then "'#{quoted_date(value)}'" + else "'#{quote_string(value.to_yaml)}'" end 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 3398bc68cd..4c46a2fde2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,10 +1,12 @@ require 'date' +require 'bigdecimal' +require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: # An abstract definition of a column in a table. class Column - attr_reader :name, :default, :type, :limit, :null, :sql_type + attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale attr_accessor :primary # Instantiates a new column in the table. @@ -15,6 +17,7 @@ module ActiveRecord # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type = nil, null = true) @name, @sql_type, @null, @limit = name, sql_type, null, extract_limit(sql_type) + @precision, @scale = extract_precision(sql_type), extract_scale(sql_type) # simplified_type may depend on #limit, type_cast depends on #type @type = simplified_type(sql_type) @@ -28,7 +31,7 @@ module ActiveRecord end def number? - [:float, :integer].include? type + [:float, :integer, :decimal].include? type end # Returns the Ruby class that corresponds to the abstract data type. @@ -36,6 +39,7 @@ module ActiveRecord case type when :integer then Fixnum when :float then Float + when :decimal then BigDecimal when :datetime then Time when :date then Date when :timestamp then Time @@ -54,6 +58,7 @@ module ActiveRecord when :text then value when :integer then value.to_i rescue value ? 1 : 0 when :float then value.to_f + when :decimal then self.class.value_to_decimal(value) when :datetime then self.class.string_to_time(value) when :timestamp then self.class.string_to_time(value) when :time then self.class.string_to_dummy_time(value) @@ -70,6 +75,7 @@ module ActiveRecord when :text then nil when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" when :float then "#{var_name}.to_f" + when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" when :datetime then "#{self.class.name}.string_to_time(#{var_name})" when :timestamp then "#{self.class.name}.string_to_time(#{var_name})" when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})" @@ -127,10 +133,21 @@ module ActiveRecord # convert something to a boolean def self.value_to_boolean(value) - return value if value==true || value==false - case value.to_s.downcase - when "true", "t", "1" then true - else false + if value == true || value == false + value + else + %w(true t 1).include?(value.to_s.downcase) + end + end + + # convert something to a BigDecimal + def self.value_to_decimal(value) + if value.is_a?(BigDecimal) + value + elsif value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d end end @@ -142,16 +159,28 @@ module ActiveRecord end def extract_limit(sql_type) - return unless sql_type $1.to_i if sql_type =~ /\((.*)\)/ end + def extract_precision(sql_type) + $2.to_i if sql_type =~ /^(numeric|decimal)\((\d+)(,\d+)?\)/i + end + + def extract_scale(sql_type) + case sql_type + when /^(numeric|decimal)\((\d+)\)/i then 0 + when /^(numeric|decimal)\((\d+)(,(\d+))\)/i then $4.to_i + end + end + def simplified_type(field_type) case field_type when /int/i :integer - when /float|double|decimal|numeric/i + when /float|double/i :float + when /decimal|numeric/i + extract_scale(field_type) == 0 ? :integer : :decimal when /datetime/i :datetime when /timestamp/i @@ -175,17 +204,17 @@ module ActiveRecord class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc: end - class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) #:nodoc: + class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: def to_sql - column_sql = "#{base.quote_column_name(name)} #{type_to_sql(type.to_sym, limit)}" + column_sql = "#{base.quote_column_name(name)} #{type_to_sql(type.to_sym, limit, precision, scale)}" add_column_options!(column_sql, :null => null, :default => default) column_sql end alias to_s :to_sql private - def type_to_sql(name, limit) - base.type_to_sql(name, limit) rescue name + def type_to_sql(name, limit, precision, scale) + base.type_to_sql(name, limit, precision, scale) rescue name end def add_column_options!(sql, options) @@ -217,9 +246,9 @@ module ActiveRecord # Instantiates a new column for the table. # The +type+ parameter must be one of the following values: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:float</tt>, <tt>:datetime</tt>, - # <tt>:timestamp</tt>, <tt>:time</tt>, <tt>:date</tt>, - # <tt>:binary</tt>, <tt>:boolean</tt>. + # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, + # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. # # Available options are (none of these exists by default): # * <tt>:limit</tt>: @@ -232,6 +261,39 @@ module ActiveRecord # * <tt>:null</tt>: # Allows or disallows +NULL+ values in the column. This option could # have been named <tt>:null_allowed</tt>. + # * <tt>:precision</tt>: + # Specifies the precision for a <tt>:decimal</tt> column. + # * <tt>:scale</tt>: + # Specifies the scale for a <tt>:decimal</tt> column. + # + # Please be aware of different RDBMS implementations behavior with + # <tt>:decimal</tt> columns: + # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= + # <tt>:precision</tt>, and makes no comments about the requirements of + # <tt>:precision</tt>. + # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. + # Default is (10,0). + # * PostGres?: <tt>:precision</tt> [1..infinity], + # <tt>:scale</tt> [0..infinity]. No default. + # * Sqlite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. + # Internal storage as strings. No default. + # * Sqlite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, + # but the maximum supported <tt>:precision</tt> is 16. No default. + # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. + # Default is (38,0). + # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. + # Default unknown. + # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18]. + # Default (9,0). Internal types NUMERIC and DECIMAL have different + # storage rules, decimal being better. + # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for + # NUMERIC is 19, and DECIMAL is 38. + # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). + # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). + # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>. # # This method returns <tt>self</tt>. # @@ -245,9 +307,22 @@ module ActiveRecord # # td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false) # #=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL + # + # def.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2) + # #=> bill_gates_money DECIMAL(15,2) + # + # def.column(:sensor_reading, :decimal, :precision => 30, :scale => 20) + # #=> sensor_reading DECIMAL(30,20) + # + # # While <tt>:scale</tt> defaults to zero on most databases, it + # # probably wouldn't hurt to include it. + # def.column(:huge_integer, :decimal, :precision => 30) + # #=> huge_integer DECIMAL(30) def column(name, type, options = {}) column = self[name] || ColumnDefinition.new(@base, name, type) column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym] + column.precision = options[:precision] + column.scale = options[:scale] column.default = options[:default] column.null = options[:null] @columns << column unless @columns.include? column 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 b57f2c86f7..542d3d131d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -119,7 +119,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit])}" + add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(add_column_sql, options) execute(add_column_sql) end @@ -254,12 +254,27 @@ module ActiveRecord end - def type_to_sql(type, limit = nil) #:nodoc: + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: native = native_database_types[type] - limit ||= native[:limit] column_type_sql = native[:name] - column_type_sql << "(#{limit})" if limit - column_type_sql + if type == :decimal # ignore limit, use precison and scale + precision ||= native[:precision] + scale ||= native[:scale] + if precision + if scale + column_type_sql << "(#{precision},#{scale})" + else + column_type_sql << "(#{precision})" + end + else + raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specifed" if scale + end + column_type_sql + else + limit ||= native[:limit] + column_type_sql << "(#{limit})" if limit + column_type_sql + end end def add_column_options!(sql, options) #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 4eea7e5434..949b8f7951 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,5 +1,7 @@ require 'benchmark' require 'date' +require 'bigdecimal' +require 'bigdecimal/util' require 'active_record/connection_adapters/abstract/schema_definitions' require 'active_record/connection_adapters/abstract/schema_statements' diff --git a/activerecord/lib/active_record/connection_adapters/db2_adapter.rb b/activerecord/lib/active_record/connection_adapters/db2_adapter.rb index 3b81c526f2..3ff6bdfb98 100644 --- a/activerecord/lib/active_record/connection_adapters/db2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/db2_adapter.rb @@ -162,6 +162,7 @@ begin :text => { :name => 'clob', :limit => 32768 }, :integer => { :name => 'int' }, :float => { :name => 'float' }, + :decimal => { :name => 'decimal' }, :datetime => { :name => 'timestamp' }, :timestamp => { :name => 'timestamp' }, :time => { :name => 'time' }, diff --git a/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb b/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb index e25198fa0d..db9d2ccf3d 100644 --- a/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb @@ -160,7 +160,7 @@ module ActiveRecord @default = default @null = nullable == "YES" @text = [:string, :text].include? @type - @number = [:float, :integer].include? @type + @number = [:float, :integer, :decimal].include? @type @fb_autogen = false if @default @@ -278,6 +278,7 @@ module ActiveRecord :text => { :name => "CLOB" }, :integer => { :name => "INTEGER" }, :float => { :name => "FLOAT" }, + :decimal => { :name => "DECIMAL" }, :datetime => { :name => "TIMESTAMP" }, :timestamp => { :name => "TIMESTAMP" }, :time => { :name => "TIME" }, @@ -319,6 +320,8 @@ module ActiveRecord end when :float value.to_f.to_s + when :decimal + value.to_d.to_s("F") when :datetime, :timestamp "TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'" when :time @@ -359,7 +362,7 @@ module ActiveRecord if column && column.type == :binary s = value.unpack("H*").first "X'#{s}'" - elsif column && [:integer, :float].include?(column.type) + elsif column && [:integer, :float, :decimal].include?(column.type) value.to_s else "'#{quote_string(value)}'" # ' (for ruby-mode) @@ -370,7 +373,7 @@ module ActiveRecord (column && column.type == :integer ? '1' : quoted_true) when FalseClass (column && column.type == :integer ? '0' : quoted_false) - when Float, Fixnum, Bignum + when Float, Fixnum, Bignum, BigDecimal value.to_s when Time, Date, DateTime if column diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 417eca180e..b9fb5fa6af 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -102,6 +102,7 @@ module ActiveRecord :text => { :name => "text" }, :integer => { :name => "int", :limit => 11 }, :float => { :name => "float" }, + :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "datetime" }, :time => { :name => "time" }, @@ -118,6 +119,8 @@ module ActiveRecord if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] "x'#{s}'" + elsif value.kind_of?(BigDecimal) + "'#{value.to_s("F")}'" else super end @@ -312,7 +315,7 @@ module ActiveRecord options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"] end - change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}" + change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(change_column_sql, options) execute(change_column_sql) end diff --git a/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb b/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb index 886779d273..e52cde45b0 100644 --- a/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb @@ -32,7 +32,7 @@ module ActiveRecord private def simplified_type(field_type) return :integer if field_type.downcase =~ /long/ - return :float if field_type.downcase == "money" + return :decimal if field_type.downcase == "money" return :binary if field_type.downcase == "object" super end @@ -68,6 +68,7 @@ module ActiveRecord :text => { :name => "text" }, :integer => { :name => "integer" }, :float => { :name => "float" }, + :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "timestamp" }, :time => { :name => "time" }, diff --git a/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb b/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb index 5a1c693ac9..81e4932fce 100644 --- a/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb @@ -93,9 +93,8 @@ begin def simplified_type(field_type) return :boolean if OracleAdapter.emulate_booleans && field_type == 'NUMBER(1)' case field_type - when /num/i : @scale == 0 ? :integer : :float - when /date|time/i : :datetime - else super + when /date|time/i then :datetime + else super end end @@ -161,6 +160,7 @@ begin :text => { :name => "CLOB" }, :integer => { :name => "NUMBER", :limit => 38 }, :float => { :name => "NUMBER" }, + :decimal => { :name => "DECIMAL" }, :datetime => { :name => "DATE" }, :timestamp => { :name => "DATE" }, :time => { :name => "DATE" }, diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5d42de1c6b..2fa357993f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -93,6 +93,7 @@ module ActiveRecord :text => { :name => "text" }, :integer => { :name => "integer" }, :float => { :name => "float" }, + :decimal => { :name => "decimal" }, :datetime => { :name => "timestamp" }, :timestamp => { :name => "timestamp" }, :time => { :name => "time" }, @@ -232,9 +233,9 @@ module ActiveRecord end def columns(table_name, name = nil) #:nodoc: - column_definitions(table_name).collect do |name, type, default, notnull| - Column.new(name, default_value(default), translate_field_type(type), - notnull == "f") + column_definitions(table_name).collect do |name, type, default, notnull, typmod| + # typmod now unused as limit, precision, scale all handled by superclass + Column.new(name, default_value(default), translate_field_type(type), notnull == "f") end end @@ -346,12 +347,12 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: begin - execute "ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type_to_sql(type, options[:limit])}" + execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" rescue ActiveRecord::StatementInvalid # This is PG7, so we use a more arcane way of doing it. begin_db_transaction add_column(table_name, "#{column_name}_ar_tmp", type, options) - execute "UPDATE #{table_name} SET #{column_name}_ar_tmp = CAST(#{column_name} AS #{type_to_sql(type, options[:limit])})" + execute "UPDATE #{table_name} SET #{column_name}_ar_tmp = CAST(#{column_name} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})" remove_column(table_name, column_name) rename_column(table_name, "#{column_name}_ar_tmp", column_name) commit_db_transaction @@ -360,18 +361,18 @@ module ActiveRecord end def change_column_default(table_name, column_name, default) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT '#{default}'" + execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT '#{default}'" end def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}" + execute "ALTER TABLE #{table_name} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" end def remove_index(table_name, options) #:nodoc: execute "DROP INDEX #{index_name(table_name, options)}" end - def type_to_sql(type, limit = nil) #:nodoc: + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: return super unless type.to_s == 'integer' if limit.nil? || limit == 4 @@ -385,6 +386,7 @@ module ActiveRecord private BYTEA_COLUMN_TYPE_OID = 17 + NUMERIC_COLUMN_TYPE_OID = 1700 TIMESTAMPOID = 1114 TIMESTAMPTZOID = 1184 @@ -417,6 +419,8 @@ module ActiveRecord column = unescape_bytea(column) when TIMESTAMPTZOID, TIMESTAMPOID column = cast_to_time(column) + when NUMERIC_COLUMN_TYPE_OID + column = column.to_d if column.respond_to?(:to_d) end hashed_row[fields[cel_index]] = column diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index fec30fb021..308a21191d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -110,6 +110,7 @@ module ActiveRecord :text => { :name => "text" }, :integer => { :name => "integer" }, :float => { :name => "float" }, + :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "datetime" }, :time => { :name => "datetime" }, diff --git a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb index 2e1ac2350b..04a6443159 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -1,5 +1,8 @@ require 'active_record/connection_adapters/abstract_adapter' +require 'bigdecimal' +require 'bigdecimal/util' + # sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server # # Author: Joey Gibson <joey@joeygibson.com> @@ -45,45 +48,37 @@ module ActiveRecord end # class Base module ConnectionAdapters - class ColumnWithIdentity < Column# :nodoc: - attr_reader :identity, :is_special, :scale + class SQLServerColumn < Column# :nodoc: + attr_reader :identity, :is_special - def initialize(name, default, sql_type = nil, is_identity = false, null = true, scale_value = 0) + def initialize(name, default, sql_type = nil, identity = false, null = true) # TODO: check ok to remove scale_value = 0 super(name, default, sql_type, null) - @identity = is_identity - @is_special = sql_type =~ /text|ntext|image/i ? true : false - @scale = scale_value + @identity = identity + @is_special = sql_type =~ /text|ntext|image/i + # TODO: check ok to remove @scale = scale_value # SQL Server only supports limits on *char and float types @limit = nil unless @type == :float or @type == :string end def simplified_type(field_type) case field_type - when /int|bigint|smallint|tinyint/i then :integer - when /float|double|decimal|money|numeric|real|smallmoney/i then @scale == 0 ? :integer : :float - when /datetime|smalldatetime/i then :datetime - when /timestamp/i then :timestamp - when /time/i then :time - when /text|ntext/i then :text - when /binary|image|varbinary/i then :binary - when /char|nchar|nvarchar|string|varchar/i then :string - when /bit/i then :boolean - when /uniqueidentifier/i then :string + when /money/i then :decimal + when /image/i then :binary + when /bit/i then :boolean + when /uniqueidentifier/i then :string + else super end end def type_cast(value) return nil if value.nil? || value =~ /^\s*null\s*$/i case type - when :string then value - when :integer then value == true || value == false ? value == true ? 1 : 0 : value.to_i - when :float then value.to_f when :datetime then cast_to_datetime(value) when :timestamp then cast_to_time(value) when :time then cast_to_time(value) when :date then cast_to_datetime(value) when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1' - else value + else super end end @@ -184,12 +179,13 @@ module ActiveRecord :text => { :name => "text" }, :integer => { :name => "int" }, :float => { :name => "float", :limit => 8 }, + :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "datetime" }, :time => { :name => "datetime" }, :date => { :name => "datetime" }, - :binary => { :name => "image"}, - :boolean => { :name => "bit"} + :binary => { :name => "image" }, + :boolean => { :name => "bit" } } end @@ -240,7 +236,16 @@ module ActiveRecord return [] if table_name.blank? table_name = table_name.to_s if table_name.is_a?(Symbol) table_name = table_name.split('.')[-1] unless table_name.nil? - sql = "SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, DATA_TYPE as ColType, IS_NULLABLE As IsNullable, COL_LENGTH('#{table_name}', COLUMN_NAME) as Length, COLUMNPROPERTY(OBJECT_ID('#{table_name}'), COLUMN_NAME, 'IsIdentity') as IsIdentity, NUMERIC_SCALE as Scale FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '#{table_name}'" + sql = "SELECT COLUMN_NAME as ColName, + COLUMN_DEFAULT as DefaultValue, + DATA_TYPE as ColType, + IS_NULLABLE As IsNullable, + COL_LENGTH('#{table_name}', COLUMN_NAME) as Length, + COLUMNPROPERTY(OBJECT_ID('#{table_name}'), COLUMN_NAME, 'IsIdentity') as IsIdentity, + NUMERIC_PRECISION as [Precision], + NUMERIC_SCALE as Scale + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '#{table_name}'" # Comment out if you want to have the Columns select statment logged. # Personally, I think it adds unnecessary bloat to the log. # If you do comment it out, make sure to un-comment the "result" line that follows @@ -249,10 +254,14 @@ module ActiveRecord columns = [] result.each do |field| default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue] - type = "#{field[:ColType]}(#{field[:Length]})" + if field[:ColType] =~ /numeric|decimal/i + type = "#{field[:ColType]}(#{field[:Precision]},#{field[:Scale]})" + else + type = "#{field[:ColType]}(#{field[:Length]})" + end is_identity = field[:IsIdentity] == 1 is_nullable = field[:IsNullable] == 'YES' - columns << ColumnWithIdentity.new(field[:ColName], default, type, is_identity, is_nullable, field[:Scale]) + columns << SQLServerColumn.new(field[:ColName], default, type, is_identity, is_nullable) end columns end @@ -336,19 +345,10 @@ module ActiveRecord return value.quoted_id if value.respond_to?(:quoted_id) case value - when String - if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "'#{quote_string(column.class.string_to_binary(value))}'" - else - "'#{quote_string(value)}'" - end - when NilClass then "NULL" when TrueClass then '1' when FalseClass then '0' - when Float, Fixnum, Bignum then value.to_s - when Date then "'#{value.to_s}'" when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'" - else "'#{quote_string(value.to_yaml)}'" + else super end end @@ -459,7 +459,7 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}"] + sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"] if options[:default] remove_default_constraint(table_name, column_name) sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}" @@ -485,15 +485,6 @@ module ActiveRecord execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}" end - def type_to_sql(type, limit = nil) #:nodoc: - native = native_database_types[type] - # if there's no :limit in the default type definition, assume that type doesn't support limits - limit = limit || native[:limit] - column_type_sql = native[:name] - column_type_sql << "(#{limit})" if limit - column_type_sql - end - private def select(sql, name = nil) rows = [] diff --git a/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb b/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb index e464fc56ad..dc1cce451a 100644 --- a/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb @@ -86,14 +86,15 @@ module ActiveRecord def simplified_type(field_type) case field_type - when /int|bigint|smallint|tinyint/i then :integer - when /float|double|decimal|money|numeric|real|smallmoney/i then :float - when /text|ntext/i then :text - when /binary|image|varbinary/i then :binary - when /char|nchar|nvarchar|string|varchar/i then :string - when /bit/i then :boolean - when /datetime|smalldatetime/i then :datetime - else super + when /int|bigint|smallint|tinyint/i then :integer + when /float|double|real/i then :float + when /decimal|money|numeric|smallmoney/i then :decimal + when /text|ntext/i then :text + when /binary|image|varbinary/i then :binary + when /char|nchar|nvarchar|string|varchar/i then :string + when /bit/i then :boolean + when /datetime|smalldatetime/i then :datetime + else super end end @@ -137,6 +138,7 @@ module ActiveRecord :text => { :name => "text" }, :integer => { :name => "int" }, :float => { :name => "float", :limit => 8 }, + :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "timestamp" }, :time => { :name => "time" }, @@ -287,18 +289,16 @@ module ActiveRecord when NilClass then (column && column.type == :boolean) ? '0' : "NULL" when TrueClass then '1' when FalseClass then '0' - when Float, Fixnum, Bignum - force_numeric?(column) ? value.to_s : "'#{value.to_s}'" - when Date then "'#{value.to_s}'" + when Float, Fixnum, Bignum then force_numeric?(column) ? value.to_s : "'#{value.to_s}'" when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'" - else "'#{quote_string(value.to_yaml)}'" + else super end end # True if column is explicitly declared non-numeric, or # if column is nil (not specified). def force_numeric?(column) - (column.nil? || [:integer, :float].include?(column.type)) + (column.nil? || [:integer, :float, :decimal].include?(column.type)) end def quote_string(s) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 8dd3086413..ffb4ffd030 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -64,8 +64,9 @@ module ActiveRecord # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+. # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+ # named +column_name+ specified to be one of the following types: - # :string, :text, :integer, :float, :datetime, :timestamp, :time, :date, :binary, :boolean. A default value can be specified - # by passing an +options+ hash like { :default => 11 }. + # :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, + # :date, :binary, :boolean. A default value can be specified by passing an + # +options+ hash like { :default => 11 }. # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content. # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same # parameters as add_column. diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 6c896f7bed..2d31b5d88e 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -1,4 +1,5 @@ require 'stringio' +require 'bigdecimal' module ActiveRecord # This class is used to dump the database schema for some connection to some @@ -90,13 +91,15 @@ HEADER spec = {} spec[:name] = column.name.inspect spec[:type] = column.type.inspect - spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] - spec[:default] = column.default.inspect if !column.default.nil? + spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal + spec[:precision] = column.precision.inspect if !column.precision.nil? + spec[:scale] = column.scale.inspect if !column.scale.nil? spec[:null] = 'false' if !column.null + spec[:default] = (column.default.is_a?(BigDecimal) ? column.default.to_s : column.default.inspect) if !column.default.nil? (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} spec end.compact - keys = [:name, :type, :limit, :default, :null] & column_specs.map{ |spec| spec.keys }.inject([]){ |a,b| a | b } + keys = [:name, :type, :limit, :precision, :scale, :default, :null] & column_specs.map{ |spec| spec.keys }.inject([]){ |a,b| a | b } lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } format_string = lengths.map{ |len| "%-#{len}s" }.join("") column_specs.each do |colspec| diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb index 6ccf14af1b..b07ec3eacd 100755 --- a/activerecord/test/base_test.rb +++ b/activerecord/test/base_test.rb @@ -910,12 +910,44 @@ class BasicsTest < Test::Unit::TestCase end end + class NumericData < ActiveRecord::Base + self.table_name = 'numeric_data' + end + + def test_numeric_fields + m = NumericData.new( + :bank_balance => 1586.43, + :big_bank_balance => BigDecimal("1000234000567.95"), + :world_population => 6000000000, + :my_house_population => 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Fixnum, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance + end + def test_auto_id auto = AutoId.new auto.save assert (auto.id > 0) end - + def quote_column_name(name) "<#{name}>" end diff --git a/activerecord/test/calculations_test.rb b/activerecord/test/calculations_test.rb index 7b89e8c62c..9a0d0434a5 100644 --- a/activerecord/test/calculations_test.rb +++ b/activerecord/test/calculations_test.rb @@ -13,8 +13,8 @@ class CalculationsTest < Test::Unit::TestCase def test_should_average_field value = Account.average(:credit_limit) - assert_equal 53, value assert_kind_of Float, value + assert_in_delta 53.0, value, 0.001 end def test_should_get_maximum_of_field diff --git a/activerecord/test/defaults_test.rb b/activerecord/test/defaults_test.rb index f51f77cd71..aba3c66de4 100644 --- a/activerecord/test/defaults_test.rb +++ b/activerecord/test/defaults_test.rb @@ -1,18 +1,16 @@ require 'abstract_unit' require 'fixtures/default' -class DefaultsTest < Test::Unit::TestCase - if %w(PostgreSQL).include? ActiveRecord::Base.connection.adapter_name +if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) + class DefaultsTest < Test::Unit::TestCase def test_default_integers default = Default.new - assert_instance_of(Fixnum, default.positive_integer) - assert_equal(default.positive_integer, 1) - assert_instance_of(Fixnum, default.negative_integer) - assert_equal(default.negative_integer, -1) - end - else - def test_dummy - assert true + assert_instance_of Fixnum, default.positive_integer + assert_equal 1, default.positive_integer + assert_instance_of Fixnum, default.negative_integer + assert_equal -1, default.negative_integer + assert_instance_of BigDecimal, default.decimal_number + assert_equal BigDecimal.new("2.78"), default.decimal_number end end end diff --git a/activerecord/test/fixtures/db_definitions/db2.drop.sql b/activerecord/test/fixtures/db_definitions/db2.drop.sql index c5b32bb9fc..50cffe3b47 100644 --- a/activerecord/test/fixtures/db_definitions/db2.drop.sql +++ b/activerecord/test/fixtures/db_definitions/db2.drop.sql @@ -28,3 +28,4 @@ DROP TABLE fk_test_has_pk; DROP TABLE fk_test_has_fk; DROP TABLE keyboards; DROP TABLE legacy_things; +DROP TABLE numeric_data; diff --git a/activerecord/test/fixtures/db_definitions/db2.sql b/activerecord/test/fixtures/db_definitions/db2.sql index 2f67e9ce53..ee76fc4d05 100644 --- a/activerecord/test/fixtures/db_definitions/db2.sql +++ b/activerecord/test/fixtures/db_definitions/db2.sql @@ -215,3 +215,12 @@ CREATE TABLE legacy_things ( version INT DEFAULT 0, PRIMARY KEY (id) ); + +CREATE TABLE numeric_data ( + id INT NOT NULL PRIMARY KEY, + bank_balance DECIMAL(10,2), + big_bank_balance DECIMAL(15,2), + world_population DECIMAL(10), + my_house_population DECIMAL(2), + decimal_number_with_default DECIMAL(3,2) DEFAULT 2.78 +); diff --git a/activerecord/test/fixtures/db_definitions/firebird.drop.sql b/activerecord/test/fixtures/db_definitions/firebird.drop.sql index 807361ed45..2e43243af7 100644 --- a/activerecord/test/fixtures/db_definitions/firebird.drop.sql +++ b/activerecord/test/fixtures/db_definitions/firebird.drop.sql @@ -29,6 +29,7 @@ DROP TABLE fk_test_has_pk; DROP TABLE keyboards; DROP TABLE defaults; DROP TABLE legacy_things; +DROP TABLE numeric_data; DROP DOMAIN D_BOOLEAN; @@ -57,3 +58,4 @@ DROP GENERATOR categories_seq; DROP GENERATOR keyboards_seq; DROP GENERATOR defaults_seq; DROP GENERATOR legacy_things_seq; +DROP GENERATOR numeric_data_seq; diff --git a/activerecord/test/fixtures/db_definitions/firebird.sql b/activerecord/test/fixtures/db_definitions/firebird.sql index c523bfe7bb..8ca0bf6222 100644 --- a/activerecord/test/fixtures/db_definitions/firebird.sql +++ b/activerecord/test/fixtures/db_definitions/firebird.sql @@ -283,3 +283,15 @@ CREATE TABLE legacy_things ( ); CREATE GENERATOR legacy_things_seq; SET GENERATOR legacy_things_seq TO 10000; + +CREATE TABLE numeric_data ( + id BIGINT NOT NULL, + bank_balance DECIMAL(10,2), + big_bank_balance DECIMAL(15,2), + world_population DECIMAL(10), + my_house_population DECIMAL(2), + decimal_number_with_default DECIMAL(3,2) DEFAULT 2.78, + PRIMARY KEY (id) +); +CREATE GENERATOR numeric_data_seq; +SET GENERATOR numeric_data_seq TO 10000; diff --git a/activerecord/test/fixtures/db_definitions/frontbase.drop.sql b/activerecord/test/fixtures/db_definitions/frontbase.drop.sql index 231af7895c..5fc64f769b 100644 --- a/activerecord/test/fixtures/db_definitions/frontbase.drop.sql +++ b/activerecord/test/fixtures/db_definitions/frontbase.drop.sql @@ -28,3 +28,4 @@ DROP TABLE fk_test_has_fk CASCADE; DROP TABLE fk_test_has_pk CASCADE; DROP TABLE keyboards CASCADE; DROP TABLE legacy_things CASCADE; +DROP TABLE numeric_data CASCADE; diff --git a/activerecord/test/fixtures/db_definitions/frontbase.sql b/activerecord/test/fixtures/db_definitions/frontbase.sql index 13ce65297b..e102f65aa7 100644 --- a/activerecord/test/fixtures/db_definitions/frontbase.sql +++ b/activerecord/test/fixtures/db_definitions/frontbase.sql @@ -249,3 +249,14 @@ create table "legacy_things" primary key ("id") ); SET UNIQUE FOR legacy_things(id); + +CREATE TABLE "numeric_data" ( + "id" integer NOT NULL + "bank_balance" DECIMAL(10,2), + "big_bank_balance" DECIMAL(15,2), + "world_population" DECIMAL(10), + "my_house_population" DECIMAL(2), + "decimal_number_with_default" DECIMAL(3,2) DEFAULT 2.78, + primary key ("id") +); +SET UNIQUE FOR numeric_data(id); diff --git a/activerecord/test/fixtures/db_definitions/mysql.drop.sql b/activerecord/test/fixtures/db_definitions/mysql.drop.sql index 14df93fd2a..cb6a870a7c 100644 --- a/activerecord/test/fixtures/db_definitions/mysql.drop.sql +++ b/activerecord/test/fixtures/db_definitions/mysql.drop.sql @@ -28,3 +28,4 @@ DROP TABLE fk_test_has_fk; DROP TABLE fk_test_has_pk; DROP TABLE keyboards; DROP TABLE legacy_things; +DROP TABLE numeric_data; diff --git a/activerecord/test/fixtures/db_definitions/mysql.sql b/activerecord/test/fixtures/db_definitions/mysql.sql index 4107155450..61ba43e954 100755 --- a/activerecord/test/fixtures/db_definitions/mysql.sql +++ b/activerecord/test/fixtures/db_definitions/mysql.sql @@ -217,3 +217,12 @@ CREATE TABLE `legacy_things` ( `version` int(11) NOT NULL default 0, PRIMARY KEY (`id`) ) TYPE=InnoDB; + +CREATE TABLE `numeric_data` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `bank_balance` decimal(10,2), + `big_bank_balance` decimal(15,2), + `world_population` decimal(10), + `my_house_population` decimal(2), + `decimal_number_with_default` decimal(3,2) DEFAULT 2.78 +) TYPE=InnoDB; diff --git a/activerecord/test/fixtures/db_definitions/openbase.sql b/activerecord/test/fixtures/db_definitions/openbase.sql index 9ca1a7d06e..c3da2efee6 100644 --- a/activerecord/test/fixtures/db_definitions/openbase.sql +++ b/activerecord/test/fixtures/db_definitions/openbase.sql @@ -279,4 +279,16 @@ CREATE TABLE legacy_things ( ) go CREATE PRIMARY KEY legacy_things (id) -go
\ No newline at end of file +go + +CREATE TABLE numeric_data ( + id INTEGER NOT NULL DEFAULT _rowid, + bank_balance DECIMAL(10,2), + big_bank_balance DECIMAL(15,2), + world_population DECIMAL(10), + my_house_population DECIMAL(2), + decimal_number_with_default DECIMAL(3,2) DEFAULT 2.78 +); +go +CREATE PRIMARY KEY numeric_data (id) +go diff --git a/activerecord/test/fixtures/db_definitions/oracle.drop.sql b/activerecord/test/fixtures/db_definitions/oracle.drop.sql index 4d4ddb835d..6fa01cdf51 100644 --- a/activerecord/test/fixtures/db_definitions/oracle.drop.sql +++ b/activerecord/test/fixtures/db_definitions/oracle.drop.sql @@ -29,6 +29,7 @@ drop table fk_test_has_pk; drop table fk_test_has_fk; drop table keyboards; drop table legacy_things; +drop table numeric_data; drop sequence accounts_seq; drop sequence funny_jokes_seq; @@ -59,3 +60,4 @@ drop sequence fk_test_has_pk_seq; drop sequence fk_test_has_fk_seq; drop sequence keyboards_seq; drop sequence legacy_things_seq; +drop sequence numeric_data_seq; diff --git a/activerecord/test/fixtures/db_definitions/oracle.sql b/activerecord/test/fixtures/db_definitions/oracle.sql index 48d6bdf073..ccf472c905 100644 --- a/activerecord/test/fixtures/db_definitions/oracle.sql +++ b/activerecord/test/fixtures/db_definitions/oracle.sql @@ -290,3 +290,13 @@ create table legacy_things ( version integer default 0 ); create sequence legacy_things_seq minvalue 10000; + +CREATE TABLE numeric_data ( + id integer NOT NULL PRIMARY KEY, + bank_balance decimal(10,2), + big_bank_balance decimal(15,2), + world_population decimal(10), + my_house_population decimal(2), + decimal_number_with_default decimal(3,2) DEFAULT 2.78 +); +create sequence numeric_data_seq minvalue 10000; diff --git a/activerecord/test/fixtures/db_definitions/postgresql.drop.sql b/activerecord/test/fixtures/db_definitions/postgresql.drop.sql index 2662839214..4910b0ecee 100644 --- a/activerecord/test/fixtures/db_definitions/postgresql.drop.sql +++ b/activerecord/test/fixtures/db_definitions/postgresql.drop.sql @@ -1,5 +1,5 @@ -DROP SEQUENCE accounts_id_seq; DROP TABLE accounts; +DROP SEQUENCE accounts_id_seq; DROP TABLE funny_jokes; DROP TABLE companies; DROP SEQUENCE companies_nonstd_seq; @@ -32,3 +32,5 @@ DROP TABLE fk_test_has_pk; DROP TABLE geometrics; DROP TABLE keyboards; DROP TABLE legacy_things; +DROP TABLE numeric_data; +DROP TABLE column_data; diff --git a/activerecord/test/fixtures/db_definitions/postgresql.sql b/activerecord/test/fixtures/db_definitions/postgresql.sql index 175e8494bb..ce2c775aba 100644 --- a/activerecord/test/fixtures/db_definitions/postgresql.sql +++ b/activerecord/test/fixtures/db_definitions/postgresql.sql @@ -118,7 +118,8 @@ CREATE TABLE defaults ( char2 character varying(50) default 'a varchar field', char3 text default 'a text field', positive_integer integer default 1, - negative_integer integer default -1 + negative_integer integer default -1, + decimal_number decimal(3,2) default 2.78 ); CREATE TABLE auto_id_tests ( @@ -246,3 +247,12 @@ CREATE TABLE legacy_things ( tps_report_number integer, version integer default 0 ); + +CREATE TABLE numeric_data ( + id serial primary key, + bank_balance decimal(10,2), + big_bank_balance decimal(15,2), + world_population decimal(10), + my_house_population decimal(2), + decimal_number_with_default decimal(3,2) default 2.78 +); diff --git a/activerecord/test/fixtures/db_definitions/sqlite.drop.sql b/activerecord/test/fixtures/db_definitions/sqlite.drop.sql index 14df93fd2a..cb6a870a7c 100644 --- a/activerecord/test/fixtures/db_definitions/sqlite.drop.sql +++ b/activerecord/test/fixtures/db_definitions/sqlite.drop.sql @@ -28,3 +28,4 @@ DROP TABLE fk_test_has_fk; DROP TABLE fk_test_has_pk; DROP TABLE keyboards; DROP TABLE legacy_things; +DROP TABLE numeric_data; diff --git a/activerecord/test/fixtures/db_definitions/sqlite.sql b/activerecord/test/fixtures/db_definitions/sqlite.sql index 5a7fec3d7e..8f89c419a1 100644 --- a/activerecord/test/fixtures/db_definitions/sqlite.sql +++ b/activerecord/test/fixtures/db_definitions/sqlite.sql @@ -198,4 +198,13 @@ CREATE TABLE 'legacy_things' ( 'id' INTEGER NOT NULL PRIMARY KEY, 'tps_report_number' INTEGER DEFAULT NULL, 'version' INTEGER NOT NULL DEFAULT 0 -) +); + +CREATE TABLE 'numeric_data' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'bank_balance' DECIMAL(10,2), + 'big_bank_balance' DECIMAL(15,2), + 'world_population' DECIMAL(10), + 'my_house_population' DECIMAL(2), + 'decimal_number_with_default' DECIMAL(3,2) DEFAULT 2.78 +); diff --git a/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql b/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql index ea14697bd3..b157d7c0a7 100644 --- a/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql +++ b/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql @@ -10,6 +10,7 @@ DROP TABLE orders; DROP TABLE movies; DROP TABLE subscribers; DROP TABLE booleantests; +DROP TABLE defaults; DROP TABLE auto_id_tests; DROP TABLE entrants; DROP TABLE colnametests; @@ -28,3 +29,4 @@ DROP TABLE fk_test_has_fk; DROP TABLE fk_test_has_pk; DROP TABLE keyboards; DROP TABLE legacy_things; +DROP TABLE numeric_data; diff --git a/activerecord/test/fixtures/db_definitions/sqlserver.sql b/activerecord/test/fixtures/db_definitions/sqlserver.sql index acbcaa1384..7b6f1d7afd 100644 --- a/activerecord/test/fixtures/db_definitions/sqlserver.sql +++ b/activerecord/test/fixtures/db_definitions/sqlserver.sql @@ -88,6 +88,24 @@ CREATE TABLE booleantests ( value bit default NULL ); +CREATE TABLE defaults ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, +-- these brought from the PostgreSQL defaults_test.rb but +-- tests only exist for integers and decimals, currently +-- modified_date date default CURRENT_DATE, +-- modified_date_function date default now(), +-- fixed_date date default '2004-01-01', +-- modified_time timestamp default CURRENT_TIMESTAMP, +-- modified_time_function timestamp default now(), +-- fixed_time timestamp default '2004-01-01 00:00:00.000000-00', +-- char1 char(1) default 'Y', +-- char2 character varying(50) default 'a varchar field', +-- char3 text default 'a text field', + positive_integer integer default 1, + negative_integer integer default -1, + decimal_number decimal(3,2) default 2.78 +); + CREATE TABLE auto_id_tests ( auto_id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, value int default NULL @@ -201,3 +219,12 @@ CREATE TABLE legacy_things ( version int default 0, PRIMARY KEY (id) ); + +CREATE TABLE numeric_data ( + id int NOT NULL IDENTITY(1, 1), + bank_balance decimal(10,2), + big_bank_balance decimal(15,2), + world_population decimal(10), + my_house_population decimal(2), + decimal_number_with_default decimal(3,2) DEFAULT 2.78 +); diff --git a/activerecord/test/fixtures/db_definitions/sybase.drop.sql b/activerecord/test/fixtures/db_definitions/sybase.drop.sql index f843a80f7a..fa51eefeeb 100644 --- a/activerecord/test/fixtures/db_definitions/sybase.drop.sql +++ b/activerecord/test/fixtures/db_definitions/sybase.drop.sql @@ -28,4 +28,5 @@ DROP TABLE fk_test_has_fk DROP TABLE fk_test_has_pk DROP TABLE keyboards DROP TABLE legacy_things +DROP TABLE numeric_data go diff --git a/activerecord/test/fixtures/db_definitions/sybase.sql b/activerecord/test/fixtures/db_definitions/sybase.sql index 98022f8887..79c7b940b5 100644 --- a/activerecord/test/fixtures/db_definitions/sybase.sql +++ b/activerecord/test/fixtures/db_definitions/sybase.sql @@ -200,5 +200,14 @@ CREATE TABLE legacy_things ( version int default 0, ) -go +CREATE TABLE numeric_data ( + id numeric((9,0) IDENTITY PRIMARY KEY, + bank_balance numeric(10,2), + big_bank_balance numeric(15,2), + world_population numeric(10), + my_house_population numeric(2), + decimal_number_with_default numeric(3,2) DEFAULT 2.78 +) + +go diff --git a/activerecord/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb b/activerecord/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb new file mode 100644 index 0000000000..0aed7cbd84 --- /dev/null +++ b/activerecord/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb @@ -0,0 +1,15 @@ +class GiveMeBigNumbers < ActiveRecord::Migration + def self.up + create_table :big_numbers do |table| + table.column :bank_balance, :decimal, :precision => 10, :scale => 2 + table.column :big_bank_balance, :decimal, :precision => 15, :scale => 2 + table.column :world_population, :decimal, :precision => 10 + table.column :my_house_population, :decimal, :precision => 2 + table.column :value_of_e, :decimal + end + end + + def self.down + drop_table :big_numbers + end +end diff --git a/activerecord/test/locking_test.rb b/activerecord/test/locking_test.rb index 8ddfb2289c..58c34a69e3 100644 --- a/activerecord/test/locking_test.rb +++ b/activerecord/test/locking_test.rb @@ -115,12 +115,14 @@ class PessimisticLockingTest < Test::Unit::TestCase end def test_second_lock_waits - first, second = duel { Person.find 1, :lock => true } - assert second.end > first.end + assert [0.2, 1, 5].any? { |zzz| + first, second = duel(zzz) { Person.find 1, :lock => true } + second.end > first.end + } end protected - def duel(zzz = 1.0) + def duel(zzz = 5) t0, t1, t2, t3 = nil, nil, nil, nil a = Thread.new do diff --git a/activerecord/test/migration_test.rb b/activerecord/test/migration_test.rb index 9514eb369d..337d4c8861 100644 --- a/activerecord/test/migration_test.rb +++ b/activerecord/test/migration_test.rb @@ -1,10 +1,15 @@ require 'abstract_unit' +require 'bigdecimal/util' + require 'fixtures/person' require 'fixtures/topic' require File.dirname(__FILE__) + '/fixtures/migrations/1_people_have_last_names' require File.dirname(__FILE__) + '/fixtures/migrations/2_we_need_reminders' +require File.dirname(__FILE__) + '/fixtures/migrations_with_decimal/1_give_me_big_numbers' if ActiveRecord::Base.connection.supports_migrations? + class BigNumber < ActiveRecord::Base; end + class Reminder < ActiveRecord::Base; end class ActiveRecord::Migration @@ -29,20 +34,15 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.connection.initialize_schema_information ActiveRecord::Base.connection.update "UPDATE #{ActiveRecord::Migrator.schema_info_table_name} SET version = 0" - Reminder.connection.drop_table("reminders") rescue nil - Reminder.connection.drop_table("people_reminders") rescue nil - Reminder.connection.drop_table("prefix_reminders_suffix") rescue nil + %w(reminders people_reminders prefix_reminders_suffix).each do |table| + Reminder.connection.drop_table(table) rescue nil + end Reminder.reset_column_information - Person.connection.remove_column("people", "last_name") rescue nil - Person.connection.remove_column("people", "key") rescue nil - Person.connection.remove_column("people", "bio") rescue nil - Person.connection.remove_column("people", "age") rescue nil - Person.connection.remove_column("people", "height") rescue nil - Person.connection.remove_column("people", "birthday") rescue nil - Person.connection.remove_column("people", "favorite_day") rescue nil - Person.connection.remove_column("people", "male") rescue nil - Person.connection.remove_column("people", "administrator") rescue nil + %w(last_name key bio age height wealth birthday favorite_day + mail administrator).each do |column| + Person.connection.remove_column('people', column) rescue nil + end Person.connection.remove_column("people", "first_name") rescue nil Person.connection.add_column("people", "first_name", :string, :limit => 40) Person.reset_column_information @@ -187,23 +187,74 @@ if ActiveRecord::Base.connection.supports_migrations? Person.connection.drop_table :testings rescue nil end + # We specifically do a manual INSERT here, and then test only the SELECT + # functionality. This allows us to more easily catch INSERT being broken, + # but SELECT actually working fine. + def test_native_decimal_insert_manual_vs_automatic + # SQLite3 always uses float in violation of SQL + # 16 decimal places + correct_value = (current_adapter?(:SQLiteAdapter) ? '0.123456789012346E20' : '0012345678901234567890.0123456789').to_d + + Person.delete_all + Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10' + Person.reset_column_information + + # Do a manual insertion + Person.connection.execute "insert into people (wealth) values (12345678901234567890.0123456789)" + + # SELECT + row = Person.find(:first) + assert_kind_of BigDecimal, row.wealth + + # If this assert fails, that means the SELECT is broken! + assert_equal correct_value, row.wealth + + # Reset to old state + Person.delete_all + + # Now use the Rails insertion + assert_nothing_raised { Person.create :wealth => BigDecimal.new("12345678901234567890.0123456789") } + + # SELECT + row = Person.find(:first) + assert_kind_of BigDecimal, row.wealth + + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! + assert_equal correct_value, row.wealth + + # Reset to old state + Person.connection.del_column "people", "wealth" rescue nil + Person.reset_column_information + end + def test_native_types Person.delete_all Person.connection.add_column "people", "last_name", :string Person.connection.add_column "people", "bio", :text Person.connection.add_column "people", "age", :integer Person.connection.add_column "people", "height", :float + Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10' Person.connection.add_column "people", "birthday", :datetime Person.connection.add_column "people", "favorite_day", :date Person.connection.add_column "people", "male", :boolean - assert_nothing_raised { Person.create :first_name => 'bob', :last_name => 'bobsen', :bio => "I was born ....", :age => 18, :height => 1.78, :birthday => 18.years.ago, :favorite_day => 10.days.ago, :male => true } + assert_nothing_raised { Person.create :first_name => 'bob', :last_name => 'bobsen', :bio => "I was born ....", :age => 18, :height => 1.78, :wealth => BigDecimal.new("12345678901234567890.0123456789"), :birthday => 18.years.ago, :favorite_day => 10.days.ago, :male => true } bob = Person.find(:first) - assert_equal bob.first_name, 'bob' - assert_equal bob.last_name, 'bobsen' - assert_equal bob.bio, "I was born ...." - assert_equal bob.age, 18 - assert_equal bob.male?, true + assert_equal 'bob', bob.first_name + assert_equal 'bobsen', bob.last_name + assert_equal "I was born ....", bob.bio + assert_equal 18, bob.age + + # Test for 30 significent digits (beyond the 16 of float), 10 of them + # after the decimal place. + if current_adapter?(:SQLiteAdapter) + # SQLite3 uses float in violation of SQL. Test for 16 decimal places. + assert_equal BigDecimal.new('0.123456789012346E20'), bob.wealth + else + assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth + end + + assert_equal true, bob.male? assert_equal String, bob.first_name.class assert_equal String, bob.last_name.class @@ -219,6 +270,7 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_equal TrueClass, bob.male?.class + assert_kind_of BigDecimal, bob.wealth end def test_add_remove_single_field_using_string_arguments @@ -351,6 +403,71 @@ if ActiveRecord::Base.connection.supports_migrations? assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) } end + def test_add_table_with_decimals + Person.connection.drop_table :big_numbers rescue nil + + assert !BigNumber.table_exists? + GiveMeBigNumbers.up + + assert BigNumber.create( + :bank_balance => 1586.43, + :big_bank_balance => BigDecimal("1000234000567.95"), + :world_population => 6000000000, + :my_house_population => 3, + :value_of_e => BigDecimal("2.7182818284590452353602875") + ) + + b = BigNumber.find(:first) + assert_not_nil b + + assert_not_nil b.bank_balance + assert_not_nil b.big_bank_balance + assert_not_nil b.world_population + assert_not_nil b.my_house_population + assert_not_nil b.value_of_e + + # TODO: set world_population >= 2**62 to cover 64-bit platforms and test + # is_a?(Bignum) + assert_kind_of Integer, b.world_population + assert_equal 6000000000, b.world_population + assert_kind_of Fixnum, b.my_house_population + assert_equal 3, b.my_house_population + assert_kind_of BigDecimal, b.bank_balance + assert_equal BigDecimal("1586.43"), b.bank_balance + assert_kind_of BigDecimal, b.big_bank_balance + assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance + + # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with + # precision/scale explictly left out. By the SQL standard, numbers + # assigned to this field should be truncated but that's seldom respected. + if current_adapter?(:PostgreSQLAdapter, :SQLite2Adapter) + # - PostgreSQL changes the SQL spec on columns declared simply as + # "decimal" to something more useful: instead of being given a scale + # of 0, they take on the compile-time limit for precision and scale, + # so the following should succeed unless you have used really wacky + # compilation options + # - SQLite2 has the default behavior of preserving all data sent in, + # so this happens there too + assert_kind_of BigDecimal, b.value_of_e + assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e + elsif current_adapter?(:SQLiteAdapter) + # - SQLite3 stores a float, in violation of SQL + assert_kind_of BigDecimal, b.value_of_e + assert_equal BigDecimal("2.71828182845905"), b.value_of_e + elsif current_adapter?(:SQLServer) + # - SQL Server rounds instead of truncating + assert_kind_of Fixnum, b.value_of_e + assert_equal 3, b.value_of_e + else + # - SQL standard is an integer + assert_kind_of Fixnum, b.value_of_e + assert_equal 2, b.value_of_e + end + + GiveMeBigNumbers.down + assert_raises(ActiveRecord::StatementInvalid) { BigNumber.find(:first) } + end + def test_migrator assert !Person.column_methods_hash.include?(:last_name) assert !Reminder.table_exists? diff --git a/activerecord/test/schema_dumper_test.rb b/activerecord/test/schema_dumper_test.rb index 37e8a99048..21e47ce609 100644 --- a/activerecord/test/schema_dumper_test.rb +++ b/activerecord/test/schema_dumper_test.rb @@ -7,6 +7,7 @@ if ActiveRecord::Base.connection.respond_to?(:tables) class SchemaDumperTest < Test::Unit::TestCase def standard_dump stream = StringIO.new + ActiveRecord::SchemaDumper.ignore_tables = [] ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) stream.string end @@ -30,7 +31,7 @@ if ActiveRecord::Base.connection.respond_to?(:tables) def test_arguments_line_up output = standard_dump output.scan(/^( *)create_table.*?\n(.*?)^\1end/m).map{ |m| m.last.split(/\n/) }.each do |column_set| - assert_line_up(column_set, /:(?:integer|float|datetime|timestamp|time|date|text|binary|string|boolean)/, true) + assert_line_up(column_set, /:(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean)/, true) assert_line_up(column_set, /:default => /) assert_line_up(column_set, /:limit => /) assert_line_up(column_set, /:null => /) @@ -82,6 +83,14 @@ if ActiveRecord::Base.connection.respond_to?(:tables) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) end end + + def test_schema_dump_includes_decimal_options + stream = StringIO.new + ActiveRecord::SchemaDumper.ignore_tables = [/^[^n]/] + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + assert_match %r{:precision => 3,[[:space:]]+:scale => 2,[[:space:]]+:default => 0.278E1}, output + end end end diff --git a/activerecord/test/validations_test.rb b/activerecord/test/validations_test.rb index c84408b1b8..ddfdbb7ef0 100755 --- a/activerecord/test/validations_test.rb +++ b/activerecord/test/validations_test.rb @@ -1015,10 +1015,12 @@ end class ValidatesNumericalityTest NIL = [nil, "", " ", " \t \r \n"] - FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1) + BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits + FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5) INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS INTEGERS = [0, 10, -10] + INTEGER_STRINGS + BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12"] def setup @@ -1031,27 +1033,27 @@ class ValidatesNumericalityTest Topic.validates_numericality_of :approved invalid!(NIL + JUNK) - valid!(FLOATS + INTEGERS) + valid!(FLOATS + INTEGERS + BIGDECIMAL) end def test_validates_numericality_of_with_nil_allowed Topic.validates_numericality_of :approved, :allow_nil => true invalid!(JUNK) - valid!(NIL + FLOATS + INTEGERS) + valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL) end def test_validates_numericality_of_with_integer_only Topic.validates_numericality_of :approved, :only_integer => true - invalid!(NIL + JUNK + FLOATS) + invalid!(NIL + JUNK + FLOATS + BIGDECIMAL) valid!(INTEGERS) end def test_validates_numericality_of_with_integer_only_and_nil_allowed Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true - invalid!(JUNK + FLOATS) + invalid!(JUNK + FLOATS + BIGDECIMAL) valid!(NIL + INTEGERS) end |