activerecord 1.15.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (185) hide show
  1. data/CHANGELOG +2454 -34
  2. data/README +1 -1
  3. data/RUNNING_UNIT_TESTS +3 -34
  4. data/Rakefile +98 -77
  5. data/install.rb +1 -1
  6. data/lib/active_record.rb +13 -22
  7. data/lib/active_record/aggregations.rb +38 -49
  8. data/lib/active_record/associations.rb +452 -333
  9. data/lib/active_record/associations/association_collection.rb +66 -20
  10. data/lib/active_record/associations/association_proxy.rb +9 -8
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -51
  12. data/lib/active_record/associations/has_many_association.rb +21 -57
  13. data/lib/active_record/associations/has_many_through_association.rb +38 -18
  14. data/lib/active_record/associations/has_one_association.rb +30 -14
  15. data/lib/active_record/attribute_methods.rb +253 -0
  16. data/lib/active_record/base.rb +719 -494
  17. data/lib/active_record/calculations.rb +62 -63
  18. data/lib/active_record/callbacks.rb +57 -83
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +38 -9
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  22. data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -12
  23. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +191 -62
  24. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +37 -34
  25. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -17
  26. data/lib/active_record/connection_adapters/mysql_adapter.rb +119 -37
  27. data/lib/active_record/connection_adapters/postgresql_adapter.rb +473 -210
  28. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +91 -107
  30. data/lib/active_record/fixtures.rb +503 -113
  31. data/lib/active_record/locking/optimistic.rb +72 -34
  32. data/lib/active_record/migration.rb +80 -57
  33. data/lib/active_record/observer.rb +13 -10
  34. data/lib/active_record/query_cache.rb +16 -57
  35. data/lib/active_record/reflection.rb +35 -38
  36. data/lib/active_record/schema.rb +5 -5
  37. data/lib/active_record/schema_dumper.rb +35 -13
  38. data/lib/active_record/serialization.rb +98 -0
  39. data/lib/active_record/serializers/json_serializer.rb +71 -0
  40. data/lib/active_record/{xml_serialization.rb → serializers/xml_serializer.rb} +90 -83
  41. data/lib/active_record/timestamp.rb +20 -21
  42. data/lib/active_record/transactions.rb +39 -43
  43. data/lib/active_record/validations.rb +256 -107
  44. data/lib/active_record/version.rb +3 -3
  45. data/lib/activerecord.rb +1 -0
  46. data/test/aaa_create_tables_test.rb +15 -2
  47. data/test/abstract_unit.rb +24 -17
  48. data/test/active_schema_test_mysql.rb +20 -8
  49. data/test/adapter_test.rb +23 -5
  50. data/test/adapter_test_sqlserver.rb +15 -1
  51. data/test/aggregations_test.rb +16 -1
  52. data/test/all.sh +2 -2
  53. data/test/associations/ar_joins_test.rb +0 -0
  54. data/test/associations/callbacks_test.rb +51 -30
  55. data/test/associations/cascaded_eager_loading_test.rb +1 -29
  56. data/test/associations/eager_singularization_test.rb +145 -0
  57. data/test/associations/eager_test.rb +42 -6
  58. data/test/associations/extension_test.rb +6 -1
  59. data/test/associations/inner_join_association_test.rb +88 -0
  60. data/test/associations/join_model_test.rb +47 -16
  61. data/test/associations_test.rb +449 -226
  62. data/test/attribute_methods_test.rb +97 -0
  63. data/test/base_test.rb +251 -105
  64. data/test/binary_test.rb +22 -27
  65. data/test/calculations_test.rb +37 -5
  66. data/test/callbacks_test.rb +23 -0
  67. data/test/connection_test_firebird.rb +2 -2
  68. data/test/connection_test_mysql.rb +30 -0
  69. data/test/connections/native_mysql/connection.rb +3 -0
  70. data/test/connections/native_sqlite/connection.rb +5 -14
  71. data/test/connections/native_sqlite3/connection.rb +5 -14
  72. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -1
  73. data/test/{copy_table_sqlite.rb → copy_table_test_sqlite.rb} +8 -3
  74. data/test/datatype_test_postgresql.rb +178 -27
  75. data/test/{empty_date_time_test.rb → date_time_test.rb} +13 -1
  76. data/test/defaults_test.rb +8 -1
  77. data/test/deprecated_finder_test.rb +7 -128
  78. data/test/finder_test.rb +192 -54
  79. data/test/fixtures/all/developers.yml +0 -0
  80. data/test/fixtures/all/people.csv +0 -0
  81. data/test/fixtures/all/tasks.yml +0 -0
  82. data/test/fixtures/author.rb +12 -5
  83. data/test/fixtures/binaries.yml +130 -435
  84. data/test/fixtures/category.rb +6 -0
  85. data/test/fixtures/company.rb +8 -1
  86. data/test/fixtures/computer.rb +1 -0
  87. data/test/fixtures/contact.rb +16 -0
  88. data/test/fixtures/customer.rb +2 -2
  89. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  90. data/test/fixtures/db_definitions/db2.sql +4 -0
  91. data/test/fixtures/db_definitions/firebird.drop.sql +3 -1
  92. data/test/fixtures/db_definitions/firebird.sql +6 -0
  93. data/test/fixtures/db_definitions/frontbase.drop.sql +1 -0
  94. data/test/fixtures/db_definitions/frontbase.sql +5 -0
  95. data/test/fixtures/db_definitions/openbase.sql +41 -25
  96. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  97. data/test/fixtures/db_definitions/oracle.sql +5 -0
  98. data/test/fixtures/db_definitions/postgresql.drop.sql +7 -0
  99. data/test/fixtures/db_definitions/postgresql.sql +87 -58
  100. data/test/fixtures/db_definitions/postgresql2.sql +1 -2
  101. data/test/fixtures/db_definitions/schema.rb +280 -0
  102. data/test/fixtures/db_definitions/schema2.rb +11 -0
  103. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  104. data/test/fixtures/db_definitions/sqlite.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/sybase.sql +4 -0
  107. data/test/fixtures/developer.rb +10 -0
  108. data/test/fixtures/example.log +1 -0
  109. data/test/fixtures/flowers.jpg +0 -0
  110. data/test/fixtures/item.rb +7 -0
  111. data/test/fixtures/items.yml +4 -0
  112. data/test/fixtures/joke.rb +0 -3
  113. data/test/fixtures/matey.rb +4 -0
  114. data/test/fixtures/mateys.yml +4 -0
  115. data/test/fixtures/minimalistic.rb +2 -0
  116. data/test/fixtures/minimalistics.yml +2 -0
  117. data/test/fixtures/mixins.yml +2 -100
  118. data/test/fixtures/parrot.rb +13 -0
  119. data/test/fixtures/parrots.yml +27 -0
  120. data/test/fixtures/parrots_pirates.yml +7 -0
  121. data/test/fixtures/pirate.rb +5 -0
  122. data/test/fixtures/pirates.yml +9 -0
  123. data/test/fixtures/post.rb +1 -0
  124. data/test/fixtures/project.rb +3 -2
  125. data/test/fixtures/reserved_words/distinct.yml +5 -0
  126. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  127. data/test/fixtures/reserved_words/group.yml +14 -0
  128. data/test/fixtures/reserved_words/select.yml +8 -0
  129. data/test/fixtures/reserved_words/values.yml +7 -0
  130. data/test/fixtures/ship.rb +3 -0
  131. data/test/fixtures/ships.yml +5 -0
  132. data/test/fixtures/tagging.rb +4 -0
  133. data/test/fixtures/taggings.yml +8 -1
  134. data/test/fixtures/topic.rb +13 -1
  135. data/test/fixtures/treasure.rb +4 -0
  136. data/test/fixtures/treasures.yml +10 -0
  137. data/test/fixtures_test.rb +205 -24
  138. data/test/inheritance_test.rb +7 -1
  139. data/test/json_serialization_test.rb +180 -0
  140. data/test/lifecycle_test.rb +1 -1
  141. data/test/locking_test.rb +85 -2
  142. data/test/migration_test.rb +206 -40
  143. data/test/mixin_test.rb +13 -515
  144. data/test/pk_test.rb +3 -6
  145. data/test/query_cache_test.rb +104 -0
  146. data/test/reflection_test.rb +16 -0
  147. data/test/reserved_word_test_mysql.rb +177 -0
  148. data/test/schema_dumper_test.rb +38 -3
  149. data/test/serialization_test.rb +47 -0
  150. data/test/transactions_test.rb +74 -23
  151. data/test/unconnected_test.rb +1 -1
  152. data/test/validations_test.rb +322 -32
  153. data/test/xml_serialization_test.rb +121 -44
  154. metadata +48 -41
  155. data/examples/associations.rb +0 -87
  156. data/examples/shared_setup.rb +0 -15
  157. data/examples/validation.rb +0 -85
  158. data/lib/active_record/acts/list.rb +0 -256
  159. data/lib/active_record/acts/nested_set.rb +0 -211
  160. data/lib/active_record/acts/tree.rb +0 -96
  161. data/lib/active_record/connection_adapters/db2_adapter.rb +0 -228
  162. data/lib/active_record/connection_adapters/firebird_adapter.rb +0 -728
  163. data/lib/active_record/connection_adapters/frontbase_adapter.rb +0 -861
  164. data/lib/active_record/connection_adapters/openbase_adapter.rb +0 -350
  165. data/lib/active_record/connection_adapters/oracle_adapter.rb +0 -690
  166. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +0 -591
  167. data/lib/active_record/connection_adapters/sybase_adapter.rb +0 -662
  168. data/lib/active_record/deprecated_associations.rb +0 -104
  169. data/lib/active_record/deprecated_finders.rb +0 -44
  170. data/lib/active_record/vendor/simple.rb +0 -693
  171. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  172. data/lib/active_record/wrappings.rb +0 -58
  173. data/test/connections/native_sqlserver/connection.rb +0 -23
  174. data/test/connections/native_sqlserver_odbc/connection.rb +0 -25
  175. data/test/deprecated_associations_test.rb +0 -396
  176. data/test/fixtures/db_definitions/mysql.drop.sql +0 -32
  177. data/test/fixtures/db_definitions/mysql.sql +0 -234
  178. data/test/fixtures/db_definitions/mysql2.drop.sql +0 -2
  179. data/test/fixtures/db_definitions/mysql2.sql +0 -5
  180. data/test/fixtures/db_definitions/sqlserver.drop.sql +0 -34
  181. data/test/fixtures/db_definitions/sqlserver.sql +0 -243
  182. data/test/fixtures/db_definitions/sqlserver2.drop.sql +0 -2
  183. data/test/fixtures/db_definitions/sqlserver2.sql +0 -5
  184. data/test/fixtures/mixin.rb +0 -63
  185. data/test/mixin_nested_set_test.rb +0 -196
@@ -89,10 +89,23 @@ module ActiveRecord
89
89
 
90
90
  # Clears the cache which maps classes
91
91
  def clear_reloadable_connections!
92
- @@active_connections.each do |name, conn|
93
- if conn.requires_reloading?
94
- conn.disconnect!
95
- @@active_connections.delete(name)
92
+ if @@allow_concurrency
93
+ # With concurrent connections @@active_connections is
94
+ # a hash keyed by thread id.
95
+ @@active_connections.each do |thread_id, conns|
96
+ conns.each do |name, conn|
97
+ if conn.requires_reloading?
98
+ conn.disconnect!
99
+ @@active_connections[thread_id].delete(name)
100
+ end
101
+ end
102
+ end
103
+ else
104
+ @@active_connections.each do |name, conn|
105
+ if conn.requires_reloading?
106
+ conn.disconnect!
107
+ @@active_connections.delete(name)
108
+ end
96
109
  end
97
110
  end
98
111
  end
@@ -206,15 +219,31 @@ module ActiveRecord
206
219
  else
207
220
  spec = spec.symbolize_keys
208
221
  unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
222
+
223
+ begin
224
+ require 'rubygems'
225
+ gem "activerecord-#{spec[:adapter]}-adapter"
226
+ require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
227
+ rescue LoadError
228
+ begin
229
+ require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
230
+ rescue LoadError
231
+ raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})"
232
+ end
233
+ end
234
+
209
235
  adapter_method = "#{spec[:adapter]}_connection"
210
- unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
236
+ if !respond_to?(adapter_method)
237
+ raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
238
+ end
239
+
211
240
  remove_connection
212
241
  establish_connection(ConnectionSpecification.new(spec, adapter_method))
213
242
  end
214
243
  end
215
244
 
216
245
  # Locate the connection of the nearest super class. This can be an
217
- # active or defined connections: if it is the latter, it will be
246
+ # active or defined connection: if it is the latter, it will be
218
247
  # opened and set as the active connection for the class it was defined
219
248
  # for (not necessarily the current class).
220
249
  def self.retrieve_connection #:nodoc:
@@ -235,15 +264,15 @@ module ActiveRecord
235
264
  conn or raise ConnectionNotEstablished
236
265
  end
237
266
 
238
- # Returns true if a connection that's accessible to this class have already been opened.
267
+ # Returns true if a connection that's accessible to this class has already been opened.
239
268
  def self.connected?
240
269
  active_connections[active_connection_name] ? true : false
241
270
  end
242
271
 
243
272
  # Remove the connection for this class. This will close the active
244
273
  # connection and the defined connection (if they exist). The result
245
- # can be used as argument for establish_connection, for easy
246
- # re-establishing of the connection.
274
+ # can be used as an argument for establish_connection, for easily
275
+ # re-establishing the connection.
247
276
  def self.remove_connection(klass=self)
248
277
  spec = @@defined_connections[klass.name]
249
278
  konn = active_connections[klass.name]
@@ -10,21 +10,28 @@ module ActiveRecord
10
10
  # Returns a record hash with the column names as keys and column values
11
11
  # as values.
12
12
  def select_one(sql, name = nil)
13
- result = select(sql, name)
13
+ result = select_all(sql, name)
14
14
  result.first if result
15
15
  end
16
16
 
17
17
  # Returns a single value from a record
18
18
  def select_value(sql, name = nil)
19
- result = select_one(sql, name)
20
- result.nil? ? nil : result.values.first
19
+ if result = select_one(sql, name)
20
+ result.values.first
21
+ end
21
22
  end
22
23
 
23
24
  # Returns an array of the values of the first column in a select:
24
25
  # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
25
26
  def select_values(sql, name = nil)
26
- result = select_all(sql, name)
27
- result.map{ |v| v.values.first }
27
+ result = select_rows(sql, name)
28
+ result.map { |v| v[0] }
29
+ end
30
+
31
+ # Returns an array of arrays containing the field values.
32
+ # Order is the same as that returned by #columns.
33
+ def select_rows(sql, name = nil)
34
+ raise NotImplementedError, "select_rows is an abstract method"
28
35
  end
29
36
 
30
37
  # Executes the SQL statement in the context of this connection.
@@ -34,17 +41,17 @@ module ActiveRecord
34
41
 
35
42
  # Returns the last auto-generated ID from the affected table.
36
43
  def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
37
- raise NotImplementedError, "insert is an abstract method"
44
+ insert_sql(sql, name, pk, id_value, sequence_name)
38
45
  end
39
46
 
40
47
  # Executes the update statement and returns the number of rows affected.
41
48
  def update(sql, name = nil)
42
- execute(sql, name)
49
+ update_sql(sql, name)
43
50
  end
44
51
 
45
52
  # Executes the delete statement and returns the number of rows affected.
46
53
  def delete(sql, name = nil)
47
- update(sql, name)
54
+ delete_sql(sql, name)
48
55
  end
49
56
 
50
57
  # Wrap a block in a transaction. Returns result of block.
@@ -53,7 +60,7 @@ module ActiveRecord
53
60
  begin
54
61
  if block_given?
55
62
  if start_db_transaction
56
- begin_db_transaction
63
+ begin_db_transaction
57
64
  transaction_open = true
58
65
  end
59
66
  yield
@@ -63,10 +70,17 @@ module ActiveRecord
63
70
  transaction_open = false
64
71
  rollback_db_transaction
65
72
  end
66
- raise
73
+ raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
67
74
  end
68
75
  ensure
69
- commit_db_transaction if transaction_open
76
+ if transaction_open
77
+ begin
78
+ commit_db_transaction
79
+ rescue Exception => database_transaction_rollback
80
+ rollback_db_transaction
81
+ raise
82
+ end
83
+ end
70
84
  end
71
85
 
72
86
  # Begins the transaction (and turns off auto-committing).
@@ -84,7 +98,7 @@ module ActiveRecord
84
98
  add_limit_offset!(sql, options) if options
85
99
  end
86
100
 
87
- # Appends +LIMIT+ and +OFFSET+ options to a SQL statement.
101
+ # Appends +LIMIT+ and +OFFSET+ options to an SQL statement.
88
102
  # This method *modifies* the +sql+ parameter.
89
103
  # ===== Examples
90
104
  # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
@@ -99,14 +113,15 @@ module ActiveRecord
99
113
  end
100
114
  end
101
115
 
102
- # Appends a locking clause to a SQL statement. *Modifies the +sql+ parameter*.
116
+ # Appends a locking clause to an SQL statement.
117
+ # This method *modifies* the +sql+ parameter.
103
118
  # # SELECT * FROM suppliers FOR UPDATE
104
119
  # add_lock! 'SELECT * FROM suppliers', :lock => true
105
120
  # add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
106
121
  def add_lock!(sql, options)
107
122
  case lock = options[:lock]
108
- when true: sql << ' FOR UPDATE'
109
- when String: sql << " #{lock}"
123
+ when true; sql << ' FOR UPDATE'
124
+ when String; sql << " #{lock}"
110
125
  end
111
126
  end
112
127
 
@@ -119,12 +134,38 @@ module ActiveRecord
119
134
  # Do nothing by default. Implement for PostgreSQL, Oracle, ...
120
135
  end
121
136
 
137
+ # Inserts the given fixture into the table. Overridden in adapters that require
138
+ # something beyond a simple insert (eg. Oracle).
139
+ def insert_fixture(fixture, table_name)
140
+ execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
141
+ end
142
+
143
+ def empty_insert_statement(table_name)
144
+ "INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
145
+ end
146
+
122
147
  protected
123
148
  # Returns an array of record hashes with the column names as keys and
124
149
  # column values as values.
125
150
  def select(sql, name = nil)
126
151
  raise NotImplementedError, "select is an abstract method"
127
152
  end
153
+
154
+ # Returns the last auto-generated ID from the affected table.
155
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
156
+ execute(sql, name)
157
+ id_value
158
+ end
159
+
160
+ # Executes the update statement and returns the number of rows affected.
161
+ def update_sql(sql, name = nil)
162
+ execute(sql, name)
163
+ end
164
+
165
+ # Executes the delete statement and returns the number of rows affected.
166
+ def delete_sql(sql, name = nil)
167
+ update_sql(sql, name)
168
+ end
128
169
  end
129
170
  end
130
171
  end
@@ -0,0 +1,87 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters # :nodoc:
3
+ module QueryCache
4
+ class << self
5
+ def included(base)
6
+ base.class_eval do
7
+ attr_accessor :query_cache_enabled
8
+ alias_method_chain :columns, :query_cache
9
+ alias_method_chain :select_all, :query_cache
10
+ end
11
+
12
+ dirties_query_cache base, :insert, :update, :delete
13
+ end
14
+
15
+ def dirties_query_cache(base, *method_names)
16
+ method_names.each do |method_name|
17
+ base.class_eval <<-end_code, __FILE__, __LINE__
18
+ def #{method_name}_with_query_dirty(*args)
19
+ clear_query_cache if @query_cache_enabled
20
+ #{method_name}_without_query_dirty(*args)
21
+ end
22
+
23
+ alias_method_chain :#{method_name}, :query_dirty
24
+ end_code
25
+ end
26
+ end
27
+ end
28
+
29
+ # Enable the query cache within the block.
30
+ def cache
31
+ old, @query_cache_enabled = @query_cache_enabled, true
32
+ @query_cache ||= {}
33
+ yield
34
+ ensure
35
+ clear_query_cache
36
+ @query_cache_enabled = old
37
+ end
38
+
39
+ # Disable the query cache within the block.
40
+ def uncached
41
+ old, @query_cache_enabled = @query_cache_enabled, false
42
+ yield
43
+ ensure
44
+ @query_cache_enabled = old
45
+ end
46
+
47
+ def clear_query_cache
48
+ @query_cache.clear if @query_cache
49
+ end
50
+
51
+ def select_all_with_query_cache(*args)
52
+ if @query_cache_enabled
53
+ cache_sql(args.first) { select_all_without_query_cache(*args) }
54
+ else
55
+ select_all_without_query_cache(*args)
56
+ end
57
+ end
58
+
59
+ def columns_with_query_cache(*args)
60
+ if @query_cache_enabled
61
+ @query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
62
+ else
63
+ columns_without_query_cache(*args)
64
+ end
65
+ end
66
+
67
+ private
68
+ def cache_sql(sql)
69
+ result =
70
+ if @query_cache.has_key?(sql)
71
+ log_info(sql, "CACHE", 0.0)
72
+ @query_cache[sql]
73
+ else
74
+ @query_cache[sql] = yield
75
+ end
76
+
77
+ if Array === result
78
+ result.collect { |row| row.dup }
79
+ else
80
+ result.duplicable? ? result.dup : result
81
+ end
82
+ rescue TypeError
83
+ result
84
+ end
85
+ end
86
+ end
87
+ end
@@ -11,12 +11,12 @@ module ActiveRecord
11
11
  when String, ActiveSupport::Multibyte::Chars
12
12
  value = value.to_s
13
13
  if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
14
- "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
14
+ "#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
15
15
  elsif column && [:integer, :float].include?(column.type)
16
16
  value = column.type == :integer ? value.to_i : value.to_f
17
17
  value.to_s
18
18
  else
19
- "'#{quote_string(value)}'" # ' (for ruby-mode)
19
+ "#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
20
20
  end
21
21
  when NilClass then "NULL"
22
22
  when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
@@ -24,9 +24,12 @@ module ActiveRecord
24
24
  when Float, Fixnum, Bignum then value.to_s
25
25
  # BigDecimals need to be output in a non-normalized form and quoted.
26
26
  when BigDecimal then value.to_s('F')
27
- when Date then "'#{value.to_s(:db)}'"
28
- when Time, DateTime then "'#{quoted_date(value)}'"
29
- else "'#{quote_string(value.to_yaml)}'"
27
+ else
28
+ if value.acts_like?(:date) || value.acts_like?(:time)
29
+ "'#{quoted_date(value)}'"
30
+ else
31
+ "#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
32
+ end
30
33
  end
31
34
  end
32
35
 
@@ -36,22 +39,30 @@ module ActiveRecord
36
39
  s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
37
40
  end
38
41
 
39
- # Returns a quoted form of the column name. This is highly adapter
40
- # specific.
41
- def quote_column_name(name)
42
- name
42
+ # Quotes the column name. Defaults to no quoting.
43
+ def quote_column_name(column_name)
44
+ column_name
45
+ end
46
+
47
+ # Quotes the table name. Defaults to column name quoting.
48
+ def quote_table_name(table_name)
49
+ quote_column_name(table_name)
43
50
  end
44
51
 
45
52
  def quoted_true
46
53
  "'t'"
47
54
  end
48
-
55
+
49
56
  def quoted_false
50
57
  "'f'"
51
58
  end
52
-
59
+
53
60
  def quoted_date(value)
54
- value.strftime("%Y-%m-%d %H:%M:%S")
61
+ value.to_s(:db)
62
+ end
63
+
64
+ def quoted_string_prefix
65
+ ''
55
66
  end
56
67
  end
57
68
  end
@@ -6,6 +6,11 @@ module ActiveRecord
6
6
  module ConnectionAdapters #:nodoc:
7
7
  # An abstract definition of a column in a table.
8
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
+
9
14
  attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
10
15
  attr_accessor :primary
11
16
 
@@ -19,7 +24,7 @@ module ActiveRecord
19
24
  @name, @sql_type, @null = name, sql_type, null
20
25
  @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
21
26
  @type = simplified_type(sql_type)
22
- @default = type_cast(default)
27
+ @default = extract_default(default)
23
28
 
24
29
  @primary = nil
25
30
  end
@@ -92,70 +97,113 @@ module ActiveRecord
92
97
  Base.human_attribute_name(@name)
93
98
  end
94
99
 
95
- # Used to convert from Strings to BLOBs
96
- def self.string_to_binary(value)
97
- value
100
+ def extract_default(default)
101
+ type_cast(default)
98
102
  end
99
103
 
100
- # Used to convert from BLOBs to Strings
101
- def self.binary_to_string(value)
102
- value
103
- end
104
+ class << self
105
+ # Used to convert from Strings to BLOBs
106
+ def string_to_binary(value)
107
+ value
108
+ end
104
109
 
105
- def self.string_to_date(string)
106
- return string unless string.is_a?(String)
107
- date_array = ParseDate.parsedate(string)
108
- # treat 0000-00-00 as nil
109
- Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
110
- end
110
+ # Used to convert from BLOBs to Strings
111
+ def binary_to_string(value)
112
+ value
113
+ end
111
114
 
112
- def self.string_to_time(string)
113
- return string unless string.is_a?(String)
114
- time_hash = Date._parse(string)
115
- time_hash[:sec_fraction] = microseconds(time_hash)
116
- time_array = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)
117
- # treat 0000-00-00 00:00:00 as nil
118
- Time.send(Base.default_timezone, *time_array) rescue DateTime.new(*time_array[0..5]) rescue nil
119
- end
115
+ def string_to_date(string)
116
+ return string unless string.is_a?(String)
117
+ return nil if string.empty?
120
118
 
121
- def self.string_to_dummy_time(string)
122
- return string unless string.is_a?(String)
123
- return nil if string.empty?
124
- time_hash = Date._parse(string)
125
- time_hash[:sec_fraction] = microseconds(time_hash)
126
- # pad the resulting array with dummy date information
127
- time_array = [2000, 1, 1]
128
- time_array += time_hash.values_at(:hour, :min, :sec, :sec_fraction)
129
- Time.send(Base.default_timezone, *time_array) rescue nil
130
- end
119
+ fast_string_to_date(string) || fallback_string_to_date(string)
120
+ end
131
121
 
132
- # convert something to a boolean
133
- def self.value_to_boolean(value)
134
- if value == true || value == false
135
- value
136
- else
137
- %w(true t 1).include?(value.to_s.downcase)
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)
138
127
  end
139
- end
140
128
 
141
- # convert something to a BigDecimal
142
- def self.value_to_decimal(value)
143
- if value.is_a?(BigDecimal)
144
- value
145
- elsif value.respond_to?(:to_d)
146
- value.to_d
147
- else
148
- value.to_s.to_d
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}"
149
134
  end
150
- end
151
135
 
152
- private
153
- # '0.123456' -> 123456
154
- # '1.123456' -> 123456
155
- def self.microseconds(time)
156
- ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
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
+ if value.is_a?(BigDecimal)
148
+ value
149
+ elsif value.respond_to?(:to_d)
150
+ value.to_d
151
+ else
152
+ value.to_s.to_d
153
+ end
157
154
  end
158
155
 
156
+ protected
157
+ # '0.123456' -> 123456
158
+ # '1.123456' -> 123456
159
+ def microseconds(time)
160
+ ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
161
+ end
162
+
163
+ def new_date(year, mon, mday)
164
+ if year && year != 0
165
+ Date.new(year, mon, mday) rescue nil
166
+ end
167
+ end
168
+
169
+ def new_time(year, mon, mday, hour, min, sec, microsec)
170
+ # Treat 0000-00-00 00:00:00 as nil.
171
+ return nil if year.nil? || year == 0
172
+
173
+ Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec)
174
+ # Over/underflow to DateTime
175
+ rescue ArgumentError, TypeError
176
+ zone_offset = Base.default_timezone == :local ? DateTime.local_offset : 0
177
+ DateTime.civil(year, mon, mday, hour, min, sec, zone_offset) rescue nil
178
+ end
179
+
180
+ def fast_string_to_date(string)
181
+ if string =~ Format::ISO_DATE
182
+ new_date $1.to_i, $2.to_i, $3.to_i
183
+ end
184
+ end
185
+
186
+ # Doesn't handle time zones.
187
+ def fast_string_to_time(string)
188
+ if string =~ Format::ISO_DATETIME
189
+ microsec = ($7.to_f * 1_000_000).to_i
190
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
191
+ end
192
+ end
193
+
194
+ def fallback_string_to_date(string)
195
+ new_date *ParseDate.parsedate(string)[0..2]
196
+ end
197
+
198
+ def fallback_string_to_time(string)
199
+ time_hash = Date._parse(string)
200
+ time_hash[:sec_fraction] = microseconds(time_hash)
201
+
202
+ new_time *time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)
203
+ end
204
+ end
205
+
206
+ private
159
207
  def extract_limit(sql_type)
160
208
  $1.to_i if sql_type =~ /\((.*)\)/
161
209
  end
@@ -223,7 +271,7 @@ module ActiveRecord
223
271
  end
224
272
 
225
273
  # Represents a SQL table in an abstract way.
226
- # Columns are stored as ColumnDefinition in the #columns attribute.
274
+ # Columns are stored as a ColumnDefinition in the #columns attribute.
227
275
  class TableDefinition
228
276
  attr_accessor :columns
229
277
 
@@ -244,24 +292,29 @@ module ActiveRecord
244
292
  end
245
293
 
246
294
  # Instantiates a new column for the table.
247
- # The +type+ parameter must be one of the following values:
295
+ # The +type+ parameter is normally one of the migrations native types,
296
+ # which is one of the following:
248
297
  # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
249
298
  # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
250
299
  # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
251
300
  # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
252
301
  #
302
+ # You may use a type not in this list as long as it is supported by your
303
+ # database (for example, "polygon" in MySQL), but this will not be database
304
+ # agnostic and should usually be avoided.
305
+ #
253
306
  # Available options are (none of these exists by default):
254
- # * <tt>:limit</tt>:
307
+ # * <tt>:limit</tt> -
255
308
  # Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
256
309
  # <tt>:binary</tt> or <tt>:integer</tt> columns only)
257
- # * <tt>:default</tt>:
310
+ # * <tt>:default</tt> -
258
311
  # The column's default value. Use nil for NULL.
259
- # * <tt>:null</tt>:
312
+ # * <tt>:null</tt> -
260
313
  # Allows or disallows +NULL+ values in the column. This option could
261
314
  # have been named <tt>:null_allowed</tt>.
262
- # * <tt>:precision</tt>:
315
+ # * <tt>:precision</tt> -
263
316
  # Specifies the precision for a <tt>:decimal</tt> column.
264
- # * <tt>:scale</tt>:
317
+ # * <tt>:scale</tt> -
265
318
  # Specifies the scale for a <tt>:decimal</tt> column.
266
319
  #
267
320
  # Please be aware of different RDBMS implementations behavior with
@@ -295,7 +348,7 @@ module ActiveRecord
295
348
  #
296
349
  # This method returns <tt>self</tt>.
297
350
  #
298
- # ===== Examples
351
+ # == Examples
299
352
  # # Assuming td is an instance of TableDefinition
300
353
  # td.column(:granted, :boolean)
301
354
  # #=> granted BOOLEAN
@@ -316,6 +369,52 @@ module ActiveRecord
316
369
  # # probably wouldn't hurt to include it.
317
370
  # def.column(:huge_integer, :decimal, :precision => 30)
318
371
  # #=> huge_integer DECIMAL(30)
372
+ #
373
+ # == Short-hand examples
374
+ #
375
+ # Instead of calling column directly, you can also work with the short-hand definitions for the default types.
376
+ # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
377
+ # in a single statement.
378
+ #
379
+ # What can be written like this with the regular calls to column:
380
+ #
381
+ # create_table "products", :force => true do |t|
382
+ # t.column "shop_id", :integer
383
+ # t.column "creator_id", :integer
384
+ # t.column "name", :string, :default => "Untitled"
385
+ # t.column "value", :string, :default => "Untitled"
386
+ # t.column "created_at", :datetime
387
+ # t.column "updated_at", :datetime
388
+ # end
389
+ #
390
+ # Can also be written as follows using the short-hand:
391
+ #
392
+ # create_table :products do |t|
393
+ # t.integer :shop_id, :creator_id
394
+ # t.string :name, :value, :default => "Untitled"
395
+ # t.timestamps
396
+ # end
397
+ #
398
+ # There's a short-hand method for each of the type values declared at the top. And then there's
399
+ # TableDefinition#timestamps that'll add created_at and updated_at as datetimes.
400
+ #
401
+ # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
402
+ # column if the :polymorphic option is supplied. If :polymorphic is a hash of options, these will be
403
+ # used when creating the _type column. So what can be written like this:
404
+ #
405
+ # create_table :taggings do |t|
406
+ # t.integer :tag_id, :tagger_id, :taggable_id
407
+ # t.string :tagger_type
408
+ # t.string :taggable_type, :default => 'Photo'
409
+ # end
410
+ #
411
+ # Can also be written as follows using references:
412
+ #
413
+ # create_table :taggings do |t|
414
+ # t.references :tag
415
+ # t.references :tagger, :polymorphic => true
416
+ # t.references :taggable, :polymorphic => { :default => 'Photo' }
417
+ # end
319
418
  def column(name, type, options = {})
320
419
  column = self[name] || ColumnDefinition.new(@base, name, type)
321
420
  column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym]
@@ -327,8 +426,38 @@ module ActiveRecord
327
426
  self
328
427
  end
329
428
 
429
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
430
+ class_eval <<-EOV
431
+ def #{column_type}(*args)
432
+ options = args.extract_options!
433
+ column_names = args
434
+
435
+ column_names.each { |name| column(name, '#{column_type}', options) }
436
+ end
437
+ EOV
438
+ end
439
+
440
+ # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
441
+ # <tt>:updated_at</tt> to the table.
442
+ def timestamps
443
+ column(:created_at, :datetime)
444
+ column(:updated_at, :datetime)
445
+ end
446
+
447
+ def references(*args)
448
+ options = args.extract_options!
449
+ polymorphic = options.delete(:polymorphic)
450
+ args.each do |col|
451
+ column("#{col}_id", :integer, options)
452
+ unless polymorphic.nil?
453
+ column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : {})
454
+ end
455
+ end
456
+ end
457
+ alias :belongs_to :references
458
+
330
459
  # Returns a String whose contents are the column definitions
331
- # concatenated together. This string can then be pre and appended to
460
+ # concatenated together. This string can then be prepended and appended to
332
461
  # to generate the final SQL to create the table.
333
462
  def to_sql
334
463
  @columns * ', '