Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb

Revision 8881, 17.0 kB (checked in by gbuesing, 5 months ago)

Refactor ActiveRecord::ConnectionAdapters::Column.new_time: leverage DateTime failover behavior of Time#time_with_datetime_fallback

Line 
1 require 'date'
2 require 'bigdecimal'
3 require 'bigdecimal/util'
4
5 module ActiveRecord
6   module ConnectionAdapters #:nodoc:
7     # An abstract definition of a column in a table.
8     class Column
9       module Format
10         ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
11         ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
12       end
13
14       attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
15       attr_accessor :primary
16
17       # Instantiates a new column in the table.
18       #
19       # +name+ is the column's name, as in <tt><b>supplier_id</b> int(11)</tt>.
20       # +default+ is the type-casted default value, such as <tt>sales_stage varchar(20) default <b>'new'</b></tt>.
21       # +sql_type+ is only used to extract the column's length, if necessary.  For example, <tt>company_name varchar(<b>60</b>)</tt>.
22       # +null+ determines if this column allows +NULL+ values.
23       def initialize(name, default, sql_type = nil, null = true)
24         @name, @sql_type, @null = name, sql_type, null
25         @limit, @precision, @scale  = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
26         @type = simplified_type(sql_type)
27         @default = extract_default(default)
28
29         @primary = nil
30       end
31
32       def text?
33         [:string, :text].include? type
34       end
35
36       def number?
37         [:float, :integer, :decimal].include? type
38       end
39
40       # Returns the Ruby class that corresponds to the abstract data type.
41       def klass
42         case type
43           when :integer       then Fixnum
44           when :float         then Float
45           when :decimal       then BigDecimal
46           when :datetime      then Time
47           when :date          then Date
48           when :timestamp     then Time
49           when :time          then Time
50           when :text, :string then String
51           when :binary        then String
52           when :boolean       then Object
53         end
54       end
55
56       # Casts value (which is a String) to an appropriate instance.
57       def type_cast(value)
58         return nil if value.nil?
59         case type
60           when :string    then value
61           when :text      then value
62           when :integer   then value.to_i rescue value ? 1 : 0
63           when :float     then value.to_f
64           when :decimal   then self.class.value_to_decimal(value)
65           when :datetime  then self.class.string_to_time(value)
66           when :timestamp then self.class.string_to_time(value)
67           when :time      then self.class.string_to_dummy_time(value)
68           when :date      then self.class.string_to_date(value)
69           when :binary    then self.class.binary_to_string(value)
70           when :boolean   then self.class.value_to_boolean(value)
71           else value
72         end
73       end
74
75       def type_cast_code(var_name)
76         case type
77           when :string    then nil
78           when :text      then nil
79           when :integer   then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
80           when :float     then "#{var_name}.to_f"
81           when :decimal   then "#{self.class.name}.value_to_decimal(#{var_name})"
82           when :datetime  then "#{self.class.name}.string_to_time(#{var_name})"
83           when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
84           when :time      then "#{self.class.name}.string_to_dummy_time(#{var_name})"
85           when :date      then "#{self.class.name}.string_to_date(#{var_name})"
86           when :binary    then "#{self.class.name}.binary_to_string(#{var_name})"
87           when :boolean   then "#{self.class.name}.value_to_boolean(#{var_name})"
88           else nil
89         end
90       end
91
92       # Returns the human name of the column name.
93       #
94       # ===== Examples
95       #  Column.new('sales_stage', ...).human_name #=> 'Sales stage'
96       def human_name
97         Base.human_attribute_name(@name)
98       end
99
100       def extract_default(default)
101         type_cast(default)
102       end
103
104       class << self
105         # Used to convert from Strings to BLOBs
106         def string_to_binary(value)
107           value
108         end
109
110         # Used to convert from BLOBs to Strings
111         def binary_to_string(value)
112           value
113         end
114
115         def string_to_date(string)
116           return string unless string.is_a?(String)
117           return nil if string.empty?
118
119           fast_string_to_date(string) || fallback_string_to_date(string)
120         end
121
122         def string_to_time(string)
123           return string unless string.is_a?(String)
124           return nil if string.empty?
125
126           fast_string_to_time(string) || fallback_string_to_time(string)
127         end
128
129         def string_to_dummy_time(string)
130           return string unless string.is_a?(String)
131           return nil if string.empty?
132
133           string_to_time "2000-01-01 #{string}"
134         end
135
136         # convert something to a boolean
137         def value_to_boolean(value)
138           if value == true || value == false
139             value
140           else
141             %w(true t 1).include?(value.to_s.downcase)
142           end
143         end
144
145         # convert something to a BigDecimal
146         def value_to_decimal(value)
147           # Using .class is faster than .is_a? and
148           # subclasses of BigDecimal will be handled
149           # in the else clause
150           if value.class == BigDecimal
151             value
152           elsif value.respond_to?(:to_d)
153             value.to_d
154           else
155             value.to_s.to_d
156           end
157         end
158
159         protected
160           # '0.123456' -> 123456
161           # '1.123456' -> 123456
162           def microseconds(time)
163             ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
164           end
165
166           def new_date(year, mon, mday)
167             if year && year != 0
168               Date.new(year, mon, mday) rescue nil
169             end
170           end
171
172           def new_time(year, mon, mday, hour, min, sec, microsec)
173             # Treat 0000-00-00 00:00:00 as nil.
174             return nil if year.nil? || year == 0
175            
176             Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
177           end
178
179           def fast_string_to_date(string)
180             if string =~ Format::ISO_DATE
181               new_date $1.to_i, $2.to_i, $3.to_i
182             end
183           end
184
185           # Doesn't handle time zones.
186           def fast_string_to_time(string)
187             if string =~ Format::ISO_DATETIME
188               microsec = ($7.to_f * 1_000_000).to_i
189               new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
190             end
191           end
192
193           def fallback_string_to_date(string)
194             new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
195           end
196
197           def fallback_string_to_time(string)
198             time_hash = Date._parse(string)
199             time_hash[:sec_fraction] = microseconds(time_hash)
200
201             new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
202           end
203       end
204
205       private
206         def extract_limit(sql_type)
207           $1.to_i if sql_type =~ /\((.*)\)/
208         end
209
210         def extract_precision(sql_type)
211           $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
212         end
213
214         def extract_scale(sql_type)
215           case sql_type
216             when /^(numeric|decimal|number)\((\d+)\)/i then 0
217             when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
218           end
219         end
220
221         def simplified_type(field_type)
222           case field_type
223             when /int/i
224               :integer
225             when /float|double/i
226               :float
227             when /decimal|numeric|number/i
228               extract_scale(field_type) == 0 ? :integer : :decimal
229             when /datetime/i
230               :datetime
231             when /timestamp/i
232               :timestamp
233             when /time/i
234               :time
235             when /date/i
236               :date
237             when /clob/i, /text/i
238               :text
239             when /blob/i, /binary/i
240               :binary
241             when /char/i, /string/i
242               :string
243             when /boolean/i
244               :boolean
245           end
246         end
247     end
248
249     class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
250     end
251
252     class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
253      
254       def sql_type
255         base.type_to_sql(type.to_sym, limit, precision, scale) rescue type
256       end
257      
258       def to_sql
259         column_sql = "#{base.quote_column_name(name)} #{sql_type}"
260         add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key
261         column_sql
262       end
263       alias to_s :to_sql
264
265       private
266
267         def add_column_options!(sql, options)
268           base.add_column_options!(sql, options.merge(:column => self))
269         end
270     end
271
272     # Represents a SQL table in an abstract way.
273     # Columns are stored as a ColumnDefinition in the #columns attribute.
274     class TableDefinition
275       attr_accessor :columns
276
277       def initialize(base)
278         @columns = []
279         @base = base
280       end
281
282       # Appends a primary key definition to the table definition.
283       # Can be called multiple times, but this is probably not a good idea.
284       def primary_key(name)
285         column(name, :primary_key)
286       end
287
288       # Returns a ColumnDefinition for the column with name +name+.
289       def [](name)
290         @columns.find {|column| column.name.to_s == name.to_s}
291       end
292
293       # Instantiates a new column for the table.
294       # The +type+ parameter is normally one of the migrations native types,
295       # which is one of the following:
296       # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
297       # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
298       # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
299       # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
300       #
301       # You may use a type not in this list as long as it is supported by your
302       # database (for example, "polygon" in MySQL), but this will not be database
303       # agnostic and should usually be avoided.
304       #
305       # Available options are (none of these exists by default):
306       # * <tt>:limit</tt> -
307       #   Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
308       #   <tt>:binary</tt> or <tt>:integer</tt> columns only)
309       # * <tt>:default</tt> -
310       #   The column's default value. Use nil for NULL.
311       # * <tt>:null</tt> -
312       #   Allows or disallows +NULL+ values in the column.  This option could
313       #   have been named <tt>:null_allowed</tt>.
314       # * <tt>:precision</tt> -
315       #   Specifies the precision for a <tt>:decimal</tt> column.
316       # * <tt>:scale</tt> -
317       #   Specifies the scale for a <tt>:decimal</tt> column.
318       #
319       # Please be aware of different RDBMS implementations behavior with
320       # <tt>:decimal</tt> columns:
321       # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
322       #   <tt>:precision</tt>, and makes no comments about the requirements of
323       #   <tt>:precision</tt>.
324       # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
325       #   Default is (10,0).
326       # * PostgreSQL: <tt>:precision</tt> [1..infinity],
327       #   <tt>:scale</tt> [0..infinity]. No default.
328       # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
329       #   Internal storage as strings. No default.
330       # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
331       #   but the maximum supported <tt>:precision</tt> is 16. No default.
332       # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
333       #   Default is (38,0).
334       # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
335       #   Default unknown.
336       # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
337       #   Default (9,0). Internal types NUMERIC and DECIMAL have different
338       #   storage rules, decimal being better.
339       # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
340       #   Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
341       #   NUMERIC is 19, and DECIMAL is 38.
342       # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
343       #   Default (38,0).
344       # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
345       #   Default (38,0).
346       # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
347       #
348       # This method returns <tt>self</tt>.
349       #
350       # == Examples
351       #  # Assuming td is an instance of TableDefinition
352       #  td.column(:granted, :boolean)
353       #    #=> granted BOOLEAN
354       #
355       #  td.column(:picture, :binary, :limit => 2.megabytes)
356       #    #=> picture BLOB(2097152)
357       #
358       #  td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
359       #    #=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
360       #
361       #  def.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
362       #    #=> bill_gates_money DECIMAL(15,2)
363       #
364       #  def.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
365       #    #=> sensor_reading DECIMAL(30,20)
366       #
367       #  # While <tt>:scale</tt> defaults to zero on most databases, it
368       #  # probably wouldn't hurt to include it.
369       #  def.column(:huge_integer, :decimal, :precision => 30)
370       #    #=> huge_integer DECIMAL(30)
371       #
372       # == Short-hand examples
373       #
374       # Instead of calling column directly, you can also work with the short-hand definitions for the default types.
375       # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
376       # in a single statement.
377       #
378       # What can be written like this with the regular calls to column:
379       #
380       #   create_table "products", :force => true do |t|
381       #     t.column "shop_id",    :integer
382       #     t.column "creator_id", :integer
383       #     t.column "name",       :string,   :default => "Untitled"
384       #     t.column "value",      :string,   :default => "Untitled"
385       #     t.column "created_at", :datetime
386       #     t.column "updated_at", :datetime
387       #   end
388       #
389       # Can also be written as follows using the short-hand:
390       #
391       #   create_table :products do |t|
392       #     t.integer :shop_id, :creator_id
393       #     t.string  :name, :value, :default => "Untitled"
394       #     t.timestamps
395       #   end
396       #
397       # There's a short-hand method for each of the type values declared at the top. And then there's
398       # TableDefinition#timestamps that'll add created_at and updated_at as datetimes.
399       #
400       # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
401       # column if the :polymorphic option is supplied. If :polymorphic is a hash of options, these will be
402       # used when creating the _type column. So what can be written like this:
403       #
404       #   create_table :taggings do |t|
405       #     t.integer :tag_id, :tagger_id, :taggable_id
406       #     t.string  :tagger_type
407       #     t.string  :taggable_type, :default => 'Photo'
408       #   end
409       #
410       # Can also be written as follows using references:
411       #
412       #   create_table :taggings do |t|
413       #     t.references :tag
414       #     t.references :tagger, :polymorphic => true
415       #     t.references :taggable, :polymorphic => { :default => 'Photo' }
416       #   end
417       def column(name, type, options = {})
418         column = self[name] || ColumnDefinition.new(@base, name, type)
419         if options[:limit]
420           column.limit = options[:limit]
421         elsif native[type.to_sym].is_a?(Hash)
422           column.limit = native[type.to_sym][:limit]
423         end
424         column.precision = options[:precision]
425         column.scale = options[:scale]
426         column.default = options[:default]
427         column.null = options[:null]
428         @columns << column unless @columns.include? column
429         self
430       end
431
432       %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
433         class_eval <<-EOV
434           def #{column_type}(*args)
435             options = args.extract_options!
436             column_names = args
437            
438             column_names.each { |name| column(name, '#{column_type}', options) }
439           end
440         EOV
441       end
442      
443       # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
444       # <tt>:updated_at</tt> to the table.
445       def timestamps
446         column(:created_at, :datetime)
447         column(:updated_at, :datetime)
448       end
449
450       def references(*args)
451         options = args.extract_options!
452         polymorphic = options.delete(:polymorphic)
453         args.each do |col|
454           column("#{col}_id", :integer, options)
455           column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
456         end
457       end
458       alias :belongs_to :references
459
460       # Returns a String whose contents are the column definitions
461       # concatenated together.  This string can then be prepended and appended to
462       # to generate the final SQL to create the table.
463       def to_sql
464         @columns * ', '
465       end
466
467       private
468         def native
469           @base.native_database_types
470         end
471     end
472   end
473 end
Note: See TracBrowser for help on using the browser.