activerecord 2.0.5 → 2.1.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 (289) hide show
  1. data/CHANGELOG +168 -6
  2. data/README +27 -22
  3. data/RUNNING_UNIT_TESTS +7 -4
  4. data/Rakefile +22 -25
  5. data/lib/active_record.rb +8 -2
  6. data/lib/active_record/aggregations.rb +21 -12
  7. data/lib/active_record/association_preload.rb +277 -0
  8. data/lib/active_record/associations.rb +481 -295
  9. data/lib/active_record/associations/association_collection.rb +162 -37
  10. data/lib/active_record/associations/association_proxy.rb +71 -7
  11. data/lib/active_record/associations/belongs_to_association.rb +5 -3
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +5 -6
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +12 -64
  14. data/lib/active_record/associations/has_many_association.rb +8 -73
  15. data/lib/active_record/associations/has_many_through_association.rb +68 -117
  16. data/lib/active_record/associations/has_one_association.rb +7 -5
  17. data/lib/active_record/associations/has_one_through_association.rb +28 -0
  18. data/lib/active_record/attribute_methods.rb +69 -19
  19. data/lib/active_record/base.rb +496 -275
  20. data/lib/active_record/calculations.rb +28 -21
  21. data/lib/active_record/callbacks.rb +9 -38
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -2
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +2 -2
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -0
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +232 -45
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +141 -27
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +9 -13
  28. data/lib/active_record/connection_adapters/mysql_adapter.rb +57 -24
  29. data/lib/active_record/connection_adapters/postgresql_adapter.rb +143 -42
  30. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
  31. data/lib/active_record/connection_adapters/sqlite_adapter.rb +18 -10
  32. data/lib/active_record/dirty.rb +158 -0
  33. data/lib/active_record/fixtures.rb +121 -156
  34. data/lib/active_record/locking/optimistic.rb +14 -11
  35. data/lib/active_record/locking/pessimistic.rb +2 -2
  36. data/lib/active_record/migration.rb +157 -77
  37. data/lib/active_record/named_scope.rb +163 -0
  38. data/lib/active_record/observer.rb +19 -5
  39. data/lib/active_record/reflection.rb +34 -14
  40. data/lib/active_record/schema.rb +7 -14
  41. data/lib/active_record/schema_dumper.rb +4 -4
  42. data/lib/active_record/serialization.rb +5 -5
  43. data/lib/active_record/serializers/json_serializer.rb +37 -28
  44. data/lib/active_record/serializers/xml_serializer.rb +52 -29
  45. data/lib/active_record/test_case.rb +36 -0
  46. data/lib/active_record/timestamp.rb +4 -4
  47. data/lib/active_record/transactions.rb +3 -3
  48. data/lib/active_record/validations.rb +182 -248
  49. data/lib/active_record/version.rb +2 -2
  50. data/test/{fixtures → assets}/example.log +0 -0
  51. data/test/{fixtures → assets}/flowers.jpg +0 -0
  52. data/test/cases/aaa_create_tables_test.rb +24 -0
  53. data/test/cases/active_schema_test_mysql.rb +95 -0
  54. data/test/cases/active_schema_test_postgresql.rb +24 -0
  55. data/test/{adapter_test.rb → cases/adapter_test.rb} +15 -14
  56. data/test/{adapter_test_sqlserver.rb → cases/adapter_test_sqlserver.rb} +95 -95
  57. data/test/{aggregations_test.rb → cases/aggregations_test.rb} +20 -20
  58. data/test/{ar_schema_test.rb → cases/ar_schema_test.rb} +6 -6
  59. data/test/cases/associations/belongs_to_associations_test.rb +412 -0
  60. data/test/{associations → cases/associations}/callbacks_test.rb +24 -10
  61. data/test/{associations → cases/associations}/cascaded_eager_loading_test.rb +18 -17
  62. data/test/cases/associations/eager_load_nested_include_test.rb +83 -0
  63. data/test/{associations → cases/associations}/eager_singularization_test.rb +5 -5
  64. data/test/{associations → cases/associations}/eager_test.rb +216 -51
  65. data/test/{associations → cases/associations}/extension_test.rb +8 -8
  66. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +684 -0
  67. data/test/cases/associations/has_many_associations_test.rb +932 -0
  68. data/test/cases/associations/has_many_through_associations_test.rb +190 -0
  69. data/test/cases/associations/has_one_associations_test.rb +323 -0
  70. data/test/cases/associations/has_one_through_associations_test.rb +74 -0
  71. data/test/{associations → cases/associations}/inner_join_association_test.rb +20 -20
  72. data/test/{associations → cases/associations}/join_model_test.rb +175 -35
  73. data/test/cases/associations_test.rb +262 -0
  74. data/test/{attribute_methods_test.rb → cases/attribute_methods_test.rb} +103 -11
  75. data/test/{base_test.rb → cases/base_test.rb} +338 -191
  76. data/test/{binary_test.rb → cases/binary_test.rb} +6 -4
  77. data/test/{calculations_test.rb → cases/calculations_test.rb} +35 -23
  78. data/test/{callbacks_test.rb → cases/callbacks_test.rb} +7 -7
  79. data/test/{class_inheritable_attributes_test.rb → cases/class_inheritable_attributes_test.rb} +3 -3
  80. data/test/{column_alias_test.rb → cases/column_alias_test.rb} +3 -3
  81. data/test/{connection_test_firebird.rb → cases/connection_test_firebird.rb} +2 -2
  82. data/test/{connection_test_mysql.rb → cases/connection_test_mysql.rb} +2 -2
  83. data/test/{copy_table_test_sqlite.rb → cases/copy_table_test_sqlite.rb} +13 -13
  84. data/test/{datatype_test_postgresql.rb → cases/datatype_test_postgresql.rb} +8 -8
  85. data/test/{date_time_test.rb → cases/date_time_test.rb} +5 -5
  86. data/test/{default_test_firebird.rb → cases/default_test_firebird.rb} +3 -3
  87. data/test/{defaults_test.rb → cases/defaults_test.rb} +8 -6
  88. data/test/{deprecated_finder_test.rb → cases/deprecated_finder_test.rb} +3 -3
  89. data/test/cases/dirty_test.rb +163 -0
  90. data/test/cases/finder_respond_to_test.rb +76 -0
  91. data/test/{finder_test.rb → cases/finder_test.rb} +266 -33
  92. data/test/{fixtures_test.rb → cases/fixtures_test.rb} +88 -72
  93. data/test/cases/helper.rb +47 -0
  94. data/test/{inheritance_test.rb → cases/inheritance_test.rb} +61 -17
  95. data/test/cases/invalid_date_test.rb +24 -0
  96. data/test/{json_serialization_test.rb → cases/json_serialization_test.rb} +36 -11
  97. data/test/{lifecycle_test.rb → cases/lifecycle_test.rb} +16 -13
  98. data/test/{locking_test.rb → cases/locking_test.rb} +17 -10
  99. data/test/{method_scoping_test.rb → cases/method_scoping_test.rb} +75 -39
  100. data/test/{migration_test.rb → cases/migration_test.rb} +420 -80
  101. data/test/{migration_test_firebird.rb → cases/migration_test_firebird.rb} +3 -3
  102. data/test/{mixin_test.rb → cases/mixin_test.rb} +7 -6
  103. data/test/{modules_test.rb → cases/modules_test.rb} +11 -6
  104. data/test/{multiple_db_test.rb → cases/multiple_db_test.rb} +5 -5
  105. data/test/cases/named_scope_test.rb +157 -0
  106. data/test/{pk_test.rb → cases/pk_test.rb} +10 -10
  107. data/test/{query_cache_test.rb → cases/query_cache_test.rb} +12 -10
  108. data/test/{readonly_test.rb → cases/readonly_test.rb} +11 -11
  109. data/test/{reflection_test.rb → cases/reflection_test.rb} +15 -14
  110. data/test/{reserved_word_test_mysql.rb → cases/reserved_word_test_mysql.rb} +4 -5
  111. data/test/{schema_authorization_test_postgresql.rb → cases/schema_authorization_test_postgresql.rb} +5 -5
  112. data/test/cases/schema_dumper_test.rb +138 -0
  113. data/test/cases/schema_test_postgresql.rb +102 -0
  114. data/test/{serialization_test.rb → cases/serialization_test.rb} +7 -7
  115. data/test/{synonym_test_oracle.rb → cases/synonym_test_oracle.rb} +5 -5
  116. data/test/{table_name_test_sqlserver.rb → cases/table_name_test_sqlserver.rb} +3 -3
  117. data/test/{threaded_connections_test.rb → cases/threaded_connections_test.rb} +7 -7
  118. data/test/{transactions_test.rb → cases/transactions_test.rb} +31 -5
  119. data/test/{unconnected_test.rb → cases/unconnected_test.rb} +2 -2
  120. data/test/{validations_test.rb → cases/validations_test.rb} +141 -39
  121. data/test/{xml_serialization_test.rb → cases/xml_serialization_test.rb} +12 -12
  122. data/test/config.rb +5 -0
  123. data/test/connections/native_db2/connection.rb +1 -1
  124. data/test/connections/native_firebird/connection.rb +1 -1
  125. data/test/connections/native_frontbase/connection.rb +1 -1
  126. data/test/connections/native_mysql/connection.rb +1 -1
  127. data/test/connections/native_openbase/connection.rb +1 -1
  128. data/test/connections/native_oracle/connection.rb +1 -1
  129. data/test/connections/native_postgresql/connection.rb +1 -3
  130. data/test/connections/native_sqlite/connection.rb +2 -2
  131. data/test/connections/native_sqlite3/connection.rb +2 -2
  132. data/test/connections/native_sqlite3/in_memory_connection.rb +3 -3
  133. data/test/connections/native_sybase/connection.rb +1 -1
  134. data/test/fixtures/author_addresses.yml +5 -0
  135. data/test/fixtures/authors.yml +2 -0
  136. data/test/fixtures/clubs.yml +6 -0
  137. data/test/fixtures/jobs.yml +7 -0
  138. data/test/fixtures/members.yml +4 -0
  139. data/test/fixtures/memberships.yml +20 -0
  140. data/test/fixtures/owners.yml +7 -0
  141. data/test/fixtures/people.yml +4 -1
  142. data/test/fixtures/pets.yml +14 -0
  143. data/test/fixtures/posts.yml +1 -0
  144. data/test/fixtures/price_estimates.yml +7 -0
  145. data/test/fixtures/readers.yml +5 -0
  146. data/test/fixtures/references.yml +17 -0
  147. data/test/fixtures/sponsors.yml +9 -0
  148. data/test/fixtures/subscribers.yml +7 -0
  149. data/test/fixtures/subscriptions.yml +12 -0
  150. data/test/fixtures/taggings.yml +4 -1
  151. data/test/fixtures/topics.yml +22 -2
  152. data/test/fixtures/warehouse-things.yml +3 -0
  153. data/test/{fixtures/migrations_with_decimal → migrations/decimal}/1_give_me_big_numbers.rb +0 -0
  154. data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/1_people_have_last_names.rb +1 -1
  155. data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/2_we_need_reminders.rb +1 -1
  156. data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/3_foo.rb +0 -0
  157. data/test/{fixtures/migrations → migrations/duplicate}/3_innocent_jointable.rb +0 -0
  158. data/test/migrations/duplicate_names/20080507052938_chunky.rb +7 -0
  159. data/test/migrations/duplicate_names/20080507053028_chunky.rb +7 -0
  160. data/test/{fixtures/migrations_with_duplicate → migrations/interleaved/pass_1}/3_innocent_jointable.rb +0 -0
  161. data/test/{fixtures/migrations → migrations/interleaved/pass_2}/1_people_have_last_names.rb +1 -1
  162. data/test/{fixtures/migrations_with_missing_versions/4_innocent_jointable.rb → migrations/interleaved/pass_2/3_innocent_jointable.rb} +0 -0
  163. data/test/{fixtures/migrations_with_missing_versions → migrations/interleaved/pass_3}/1_people_have_last_names.rb +1 -1
  164. data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +8 -0
  165. data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +12 -0
  166. data/test/{fixtures/migrations_with_missing_versions → migrations/missing}/1000_people_have_middle_names.rb +1 -1
  167. data/test/migrations/missing/1_people_have_last_names.rb +9 -0
  168. data/test/{fixtures/migrations_with_missing_versions → migrations/missing}/3_we_need_reminders.rb +1 -1
  169. data/test/migrations/missing/4_innocent_jointable.rb +12 -0
  170. data/test/migrations/valid/1_people_have_last_names.rb +9 -0
  171. data/test/{fixtures/migrations → migrations/valid}/2_we_need_reminders.rb +1 -1
  172. data/test/migrations/valid/3_innocent_jointable.rb +12 -0
  173. data/test/{fixtures → models}/author.rb +28 -4
  174. data/test/{fixtures → models}/auto_id.rb +0 -0
  175. data/test/{fixtures → models}/binary.rb +0 -0
  176. data/test/{fixtures → models}/book.rb +0 -0
  177. data/test/{fixtures → models}/categorization.rb +0 -0
  178. data/test/{fixtures → models}/category.rb +8 -5
  179. data/test/{fixtures → models}/citation.rb +0 -0
  180. data/test/models/club.rb +7 -0
  181. data/test/{fixtures → models}/column_name.rb +0 -0
  182. data/test/{fixtures → models}/comment.rb +5 -3
  183. data/test/{fixtures → models}/company.rb +15 -6
  184. data/test/{fixtures → models}/company_in_module.rb +5 -3
  185. data/test/{fixtures → models}/computer.rb +0 -1
  186. data/test/{fixtures → models}/contact.rb +1 -1
  187. data/test/{fixtures → models}/course.rb +0 -0
  188. data/test/{fixtures → models}/customer.rb +8 -8
  189. data/test/{fixtures → models}/default.rb +0 -0
  190. data/test/{fixtures → models}/developer.rb +14 -10
  191. data/test/{fixtures → models}/edge.rb +0 -0
  192. data/test/{fixtures → models}/entrant.rb +0 -0
  193. data/test/models/guid.rb +2 -0
  194. data/test/{fixtures → models}/item.rb +0 -0
  195. data/test/models/job.rb +5 -0
  196. data/test/{fixtures → models}/joke.rb +0 -0
  197. data/test/{fixtures → models}/keyboard.rb +0 -0
  198. data/test/{fixtures → models}/legacy_thing.rb +0 -0
  199. data/test/{fixtures → models}/matey.rb +0 -0
  200. data/test/models/member.rb +9 -0
  201. data/test/models/membership.rb +9 -0
  202. data/test/{fixtures → models}/minimalistic.rb +0 -0
  203. data/test/{fixtures → models}/mixed_case_monkey.rb +0 -0
  204. data/test/{fixtures → models}/movie.rb +0 -0
  205. data/test/{fixtures → models}/order.rb +2 -2
  206. data/test/models/owner.rb +4 -0
  207. data/test/{fixtures → models}/parrot.rb +0 -0
  208. data/test/models/person.rb +10 -0
  209. data/test/models/pet.rb +4 -0
  210. data/test/models/pirate.rb +9 -0
  211. data/test/{fixtures → models}/post.rb +23 -2
  212. data/test/models/price_estimate.rb +3 -0
  213. data/test/{fixtures → models}/project.rb +1 -0
  214. data/test/{fixtures → models}/reader.rb +0 -0
  215. data/test/models/reference.rb +4 -0
  216. data/test/{fixtures → models}/reply.rb +7 -5
  217. data/test/{fixtures → models}/ship.rb +0 -0
  218. data/test/models/sponsor.rb +4 -0
  219. data/test/{fixtures → models}/subject.rb +0 -0
  220. data/test/{fixtures → models}/subscriber.rb +2 -0
  221. data/test/models/subscription.rb +4 -0
  222. data/test/{fixtures → models}/tag.rb +0 -0
  223. data/test/{fixtures → models}/tagging.rb +0 -0
  224. data/test/{fixtures → models}/task.rb +0 -0
  225. data/test/{fixtures → models}/topic.rb +32 -4
  226. data/test/{fixtures → models}/treasure.rb +2 -0
  227. data/test/{fixtures → models}/vertex.rb +0 -0
  228. data/test/models/warehouse_thing.rb +5 -0
  229. data/test/schema/mysql_specific_schema.rb +12 -0
  230. data/test/schema/postgresql_specific_schema.rb +103 -0
  231. data/test/schema/schema.rb +421 -0
  232. data/test/schema/schema2.rb +6 -0
  233. data/test/schema/sqlite_specific_schema.rb +25 -0
  234. data/test/schema/sqlserver_specific_schema.rb +5 -0
  235. metadata +192 -176
  236. data/test/aaa_create_tables_test.rb +0 -72
  237. data/test/abstract_unit.rb +0 -84
  238. data/test/active_schema_test_mysql.rb +0 -46
  239. data/test/all.sh +0 -8
  240. data/test/association_inheritance_reload.rb +0 -14
  241. data/test/associations_test.rb +0 -2177
  242. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +0 -1
  243. data/test/fixtures/bad_fixtures/attr_with_spaces +0 -1
  244. data/test/fixtures/bad_fixtures/blank_line +0 -3
  245. data/test/fixtures/bad_fixtures/duplicate_attributes +0 -3
  246. data/test/fixtures/bad_fixtures/missing_value +0 -1
  247. data/test/fixtures/db_definitions/db2.drop.sql +0 -33
  248. data/test/fixtures/db_definitions/db2.sql +0 -235
  249. data/test/fixtures/db_definitions/db22.drop.sql +0 -2
  250. data/test/fixtures/db_definitions/db22.sql +0 -5
  251. data/test/fixtures/db_definitions/firebird.drop.sql +0 -65
  252. data/test/fixtures/db_definitions/firebird.sql +0 -310
  253. data/test/fixtures/db_definitions/firebird2.drop.sql +0 -2
  254. data/test/fixtures/db_definitions/firebird2.sql +0 -6
  255. data/test/fixtures/db_definitions/frontbase.drop.sql +0 -33
  256. data/test/fixtures/db_definitions/frontbase.sql +0 -273
  257. data/test/fixtures/db_definitions/frontbase2.drop.sql +0 -1
  258. data/test/fixtures/db_definitions/frontbase2.sql +0 -4
  259. data/test/fixtures/db_definitions/openbase.drop.sql +0 -2
  260. data/test/fixtures/db_definitions/openbase.sql +0 -318
  261. data/test/fixtures/db_definitions/openbase2.drop.sql +0 -2
  262. data/test/fixtures/db_definitions/openbase2.sql +0 -7
  263. data/test/fixtures/db_definitions/oracle.drop.sql +0 -67
  264. data/test/fixtures/db_definitions/oracle.sql +0 -330
  265. data/test/fixtures/db_definitions/oracle2.drop.sql +0 -2
  266. data/test/fixtures/db_definitions/oracle2.sql +0 -6
  267. data/test/fixtures/db_definitions/postgresql.drop.sql +0 -44
  268. data/test/fixtures/db_definitions/postgresql.sql +0 -292
  269. data/test/fixtures/db_definitions/postgresql2.drop.sql +0 -2
  270. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  271. data/test/fixtures/db_definitions/schema.rb +0 -354
  272. data/test/fixtures/db_definitions/schema2.rb +0 -11
  273. data/test/fixtures/db_definitions/sqlite.drop.sql +0 -33
  274. data/test/fixtures/db_definitions/sqlite.sql +0 -219
  275. data/test/fixtures/db_definitions/sqlite2.drop.sql +0 -2
  276. data/test/fixtures/db_definitions/sqlite2.sql +0 -5
  277. data/test/fixtures/db_definitions/sybase.drop.sql +0 -35
  278. data/test/fixtures/db_definitions/sybase.sql +0 -222
  279. data/test/fixtures/db_definitions/sybase2.drop.sql +0 -4
  280. data/test/fixtures/db_definitions/sybase2.sql +0 -5
  281. data/test/fixtures/developers_projects/david_action_controller +0 -3
  282. data/test/fixtures/developers_projects/david_active_record +0 -3
  283. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  284. data/test/fixtures/person.rb +0 -4
  285. data/test/fixtures/pirate.rb +0 -5
  286. data/test/fixtures/subscribers/first +0 -2
  287. data/test/fixtures/subscribers/second +0 -2
  288. data/test/schema_dumper_test.rb +0 -131
  289. data/test/schema_test_postgresql.rb +0 -64
@@ -66,17 +66,20 @@ module ActiveRecord
66
66
  return result
67
67
  end
68
68
 
69
- def update_with_lock #:nodoc:
70
- return update_without_lock unless locking_enabled?
69
+ def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
70
+ return update_without_lock(attribute_names) unless locking_enabled?
71
71
 
72
72
  lock_col = self.class.locking_column
73
73
  previous_value = send(lock_col).to_i
74
74
  send(lock_col + '=', previous_value + 1)
75
75
 
76
+ attribute_names += [lock_col]
77
+ attribute_names.uniq!
78
+
76
79
  begin
77
80
  affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
78
- UPDATE #{self.class.table_name}
79
- SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false))}
81
+ UPDATE #{self.class.quoted_table_name}
82
+ SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))}
80
83
  WHERE #{self.class.primary_key} = #{quote_value(id)}
81
84
  AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
82
85
  end_sql
@@ -104,20 +107,20 @@ module ActiveRecord
104
107
  end
105
108
 
106
109
  # Is optimistic locking enabled for this table? Returns true if the
107
- # #lock_optimistically flag is set to true (which it is, by default)
108
- # and the table includes the #locking_column column (defaults to
109
- # lock_version).
110
+ # +lock_optimistically+ flag is set to true (which it is, by default)
111
+ # and the table includes the +locking_column+ column (defaults to
112
+ # +lock_version+).
110
113
  def locking_enabled?
111
114
  lock_optimistically && columns_hash[locking_column]
112
115
  end
113
116
 
114
- # Set the column to use for optimistic locking. Defaults to lock_version.
117
+ # Set the column to use for optimistic locking. Defaults to +lock_version+.
115
118
  def set_locking_column(value = nil, &block)
116
119
  define_attr_method :locking_column, value, &block
117
120
  value
118
121
  end
119
122
 
120
- # The version column used for optimistic locking. Defaults to lock_version.
123
+ # The version column used for optimistic locking. Defaults to +lock_version+.
121
124
  def locking_column
122
125
  reset_locking_column
123
126
  end
@@ -127,12 +130,12 @@ module ActiveRecord
127
130
  connection.quote_column_name(locking_column)
128
131
  end
129
132
 
130
- # Reset the column used for optimistic locking back to the lock_version default.
133
+ # Reset the column used for optimistic locking back to the +lock_version+ default.
131
134
  def reset_locking_column
132
135
  set_locking_column DEFAULT_LOCKING_COLUMN
133
136
  end
134
137
 
135
- # make sure the lock version column gets updated when counters are
138
+ # Make sure the lock version column gets updated when counters are
136
139
  # updated.
137
140
  def update_counters_with_lock(id, counters)
138
141
  counters = counters.merge(locking_column => 1) if locking_enabled?
@@ -25,12 +25,12 @@ module ActiveRecord
25
25
  # Locking::Pessimistic provides support for row-level locking using
26
26
  # SELECT ... FOR UPDATE and other lock types.
27
27
  #
28
- # Pass :lock => true to ActiveRecord::Base.find to obtain an exclusive
28
+ # Pass <tt>:lock => true</tt> to ActiveRecord::Base.find to obtain an exclusive
29
29
  # lock on the selected rows:
30
30
  # # select * from accounts where id=1 for update
31
31
  # Account.find(1, :lock => true)
32
32
  #
33
- # Pass :lock => 'some locking clause' to give a database-specific locking clause
33
+ # Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
34
34
  # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
35
35
  #
36
36
  # Example:
@@ -8,6 +8,18 @@ module ActiveRecord
8
8
  end
9
9
  end
10
10
 
11
+ class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
12
+ def initialize(name)
13
+ super("Multiple migrations have the name #{name}")
14
+ end
15
+ end
16
+
17
+ class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
18
+ def initialize(version)
19
+ super("No migration with version number #{version}")
20
+ end
21
+ end
22
+
11
23
  class IllegalMigrationNameError < ActiveRecordError#:nodoc:
12
24
  def initialize(name)
13
25
  super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
@@ -70,16 +82,16 @@ module ActiveRecord
70
82
  # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+.
71
83
  # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
72
84
  # named +column_name+ specified to be one of the following types:
73
- # :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time,
74
- # :date, :binary, :boolean. A default value can be specified by passing an
75
- # +options+ hash like { :default => 11 }. Other options include :limit and :null (e.g. { :limit => 50, :null => false })
85
+ # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
86
+ # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be specified by passing an
87
+ # +options+ hash like <tt>{ :default => 11 }</tt>. Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. <tt>{ :limit => 50, :null => false }</tt>)
76
88
  # -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
77
89
  # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
78
90
  # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
79
91
  # parameters as add_column.
80
92
  # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
81
93
  # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index with the name of the column. Other options include
82
- # :name and :unique (e.g. { :name => "users_name_index", :unique => true }).
94
+ # <tt>:name</tt> and <tt>:unique</tt> (e.g. <tt>{ :name => "users_name_index", :unique => true }</tt>).
83
95
  # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified by +index_name+.
84
96
  #
85
97
  # == Irreversible transformations
@@ -91,16 +103,34 @@ module ActiveRecord
91
103
  #
92
104
  # The Rails package has several tools to help create and apply migrations.
93
105
  #
94
- # To generate a new migration, use <tt>script/generate migration MyNewMigration</tt>
106
+ # To generate a new migration, you can use
107
+ # script/generate migration MyNewMigration
108
+ #
95
109
  # where MyNewMigration is the name of your migration. The generator will
96
- # create a file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
110
+ # create an empty migration file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
97
111
  # directory where <tt>nnn</tt> is the next largest migration number.
112
+ #
98
113
  # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
99
114
  # MyNewMigration.
100
115
  #
116
+ # There is a special syntactic shortcut to generate migrations that add fields to a table.
117
+ # script/generate migration add_fieldname_to_tablename fieldname:string
118
+ #
119
+ # This will generate the file <tt>nnn_add_fieldname_to_tablename</tt>, which will look like this:
120
+ # class AddFieldnameToTablename < ActiveRecord::Migration
121
+ # def self.up
122
+ # add_column :tablenames, :fieldname, :string
123
+ # end
124
+ #
125
+ # def self.down
126
+ # remove_column :tablenames, :fieldname
127
+ # end
128
+ # end
129
+ #
101
130
  # To run migrations against the currently configured database, use
102
131
  # <tt>rake db:migrate</tt>. This will update the database by running all of the
103
- # pending migrations, creating the <tt>schema_info</tt> table if missing.
132
+ # pending migrations, creating the <tt>schema_migrations</tt> table
133
+ # (see "About the schema_migrations table" section below) if missing.
104
134
  #
105
135
  # To roll the database back to a previous migration version, use
106
136
  # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
@@ -178,7 +208,7 @@ module ActiveRecord
178
208
  #
179
209
  # You can quiet them down by setting ActiveRecord::Migration.verbose = false.
180
210
  #
181
- # You can also insert your own messages and benchmarks by using the #say_with_time
211
+ # You can also insert your own messages and benchmarks by using the +say_with_time+
182
212
  # method:
183
213
  #
184
214
  # def self.up
@@ -193,6 +223,21 @@ module ActiveRecord
193
223
  #
194
224
  # The phrase "Updating salaries..." would then be printed, along with the
195
225
  # benchmark for the block when the block completes.
226
+ #
227
+ # == About the schema_migrations table
228
+ #
229
+ # Rails versions 2.0 and prior used to create a table called
230
+ # <tt>schema_info</tt> when using migrations. This table contained the
231
+ # version of the schema as of the last applied migration.
232
+ #
233
+ # Starting with Rails 2.1, the <tt>schema_info</tt> table is
234
+ # (automatically) replaced by the <tt>schema_migrations</tt> table, which
235
+ # contains the version numbers of all the migrations applied.
236
+ #
237
+ # As a result, it is now possible to add migration files that are numbered
238
+ # lower than the current schema version: when migrating up, those
239
+ # never-applied "interleaved" migrations will be automatically applied, and
240
+ # when migrating down, never-applied "interleaved" migrations will be skipped.
196
241
  class Migration
197
242
  @@verbose = true
198
243
  cattr_accessor :verbose
@@ -291,18 +336,23 @@ module ActiveRecord
291
336
  class Migrator#:nodoc:
292
337
  class << self
293
338
  def migrate(migrations_path, target_version = nil)
294
- Base.connection.initialize_schema_information
295
-
296
339
  case
297
- when target_version.nil?, current_version < target_version
298
- up(migrations_path, target_version)
299
- when current_version > target_version
300
- down(migrations_path, target_version)
301
- when current_version == target_version
302
- return # You're on the right version
340
+ when target_version.nil? then up(migrations_path, target_version)
341
+ when current_version > target_version then down(migrations_path, target_version)
342
+ else up(migrations_path, target_version)
303
343
  end
304
344
  end
305
345
 
346
+ def rollback(migrations_path, steps=1)
347
+ migrator = self.new(:down, migrations_path)
348
+ start_index = migrator.migrations.index(migrator.current_migration)
349
+
350
+ return unless start_index
351
+
352
+ finish = migrator.migrations[start_index + steps]
353
+ down(migrations_path, finish ? finish.version : 0)
354
+ end
355
+
306
356
  def up(migrations_path, target_version = nil)
307
357
  self.new(:up, migrations_path, target_version).migrate
308
358
  end
@@ -310,90 +360,129 @@ module ActiveRecord
310
360
  def down(migrations_path, target_version = nil)
311
361
  self.new(:down, migrations_path, target_version).migrate
312
362
  end
363
+
364
+ def run(direction, migrations_path, target_version)
365
+ self.new(direction, migrations_path, target_version).run
366
+ end
313
367
 
314
- def schema_info_table_name
315
- Base.table_name_prefix + "schema_info" + Base.table_name_suffix
368
+ def schema_migrations_table_name
369
+ Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
316
370
  end
317
371
 
318
372
  def current_version
319
- Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").to_i
373
+ version = Base.connection.select_values(
374
+ "SELECT version FROM #{schema_migrations_table_name}"
375
+ ).map(&:to_i).max rescue nil
376
+ version || 0
320
377
  end
321
378
 
322
379
  def proper_table_name(name)
323
- # Use the ActiveRecord objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
380
+ # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
324
381
  name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
325
382
  end
326
383
  end
327
384
 
328
385
  def initialize(direction, migrations_path, target_version = nil)
329
386
  raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
330
- @direction, @migrations_path, @target_version = direction, migrations_path, target_version
331
- Base.connection.initialize_schema_information
387
+ Base.connection.initialize_schema_migrations_table
388
+ @direction, @migrations_path, @target_version = direction, migrations_path, target_version
332
389
  end
333
390
 
334
391
  def current_version
335
392
  self.class.current_version
336
393
  end
394
+
395
+ def current_migration
396
+ migrations.detect { |m| m.version == current_version }
397
+ end
398
+
399
+ def run
400
+ target = migrations.detect { |m| m.version == @target_version }
401
+ raise UnknownMigrationVersionError.new(@target_version) if target.nil?
402
+ target.migrate(@direction)
403
+ end
337
404
 
338
405
  def migrate
339
- migration_classes.each do |migration_class|
340
- if reached_target_version?(migration_class.version)
341
- Base.logger.info("Reached target version: #{@target_version}")
342
- break
406
+ current = migrations.detect { |m| m.version == current_version }
407
+ target = migrations.detect { |m| m.version == @target_version }
408
+
409
+ if target.nil? && !@target_version.nil? && @target_version > 0
410
+ raise UnknownMigrationVersionError.new(@target_version)
411
+ end
412
+
413
+ start = up? ? 0 : (migrations.index(current) || 0)
414
+ finish = migrations.index(target) || migrations.size - 1
415
+ runnable = migrations[start..finish]
416
+
417
+ # skip the last migration if we're headed down, but not ALL the way down
418
+ runnable.pop if down? && !target.nil?
419
+
420
+ runnable.each do |migration|
421
+ Base.logger.info "Migrating to #{migration} (#{migration.version})"
422
+
423
+ # On our way up, we skip migrating the ones we've already migrated
424
+ # On our way down, we skip reverting the ones we've never migrated
425
+ next if up? && migrated.include?(migration.version.to_i)
426
+
427
+ if down? && !migrated.include?(migration.version.to_i)
428
+ migration.announce 'never migrated, skipping'; migration.write
429
+ else
430
+ migration.migrate(@direction)
431
+ record_version_state_after_migrating(migration.version)
343
432
  end
433
+ end
434
+ end
344
435
 
345
- next if irrelevant_migration?(migration_class.version)
436
+ def migrations
437
+ @migrations ||= begin
438
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
439
+
440
+ migrations = files.inject([]) do |klasses, file|
441
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
442
+
443
+ raise IllegalMigrationNameError.new(file) unless version
444
+ version = version.to_i
445
+
446
+ if klasses.detect { |m| m.version == version }
447
+ raise DuplicateMigrationVersionError.new(version)
448
+ end
346
449
 
347
- Base.logger.info "Migrating to #{migration_class} (#{migration_class.version})"
348
- migration_class.migrate(@direction)
349
- set_schema_version(migration_class.version)
450
+ if klasses.detect { |m| m.name == name.camelize }
451
+ raise DuplicateMigrationNameError.new(name.camelize)
452
+ end
453
+
454
+ load(file)
455
+
456
+ klasses << returning(name.camelize.constantize) do |klass|
457
+ class << klass; attr_accessor :version end
458
+ klass.version = version
459
+ end
460
+ end
461
+
462
+ migrations = migrations.sort_by(&:version)
463
+ down? ? migrations.reverse : migrations
350
464
  end
351
465
  end
352
466
 
353
467
  def pending_migrations
354
- migration_classes.select { |m| m.version > current_version }
468
+ already_migrated = migrated
469
+ migrations.reject { |m| already_migrated.include?(m.version.to_i) }
355
470
  end
356
471
 
357
- private
358
- def migration_classes
359
- classes = migration_files.inject([]) do |migrations, migration_file|
360
- load(migration_file)
361
- version, name = migration_version_and_name(migration_file)
362
- assert_unique_migration_version(migrations, version.to_i)
363
- migrations << migration_class(name, version.to_i)
364
- end.sort_by(&:version)
365
-
366
- down? ? classes.reverse : classes
367
- end
472
+ def migrated
473
+ sm_table = self.class.schema_migrations_table_name
474
+ Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
475
+ end
368
476
 
369
- def assert_unique_migration_version(migrations, version)
370
- if !migrations.empty? && migrations.find { |m| m.version == version }
371
- raise DuplicateMigrationVersionError.new(version)
372
- end
373
- end
477
+ private
478
+ def record_version_state_after_migrating(version)
479
+ sm_table = self.class.schema_migrations_table_name
374
480
 
375
- def migration_files
376
- files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f|
377
- m = migration_version_and_name(f)
378
- raise IllegalMigrationNameError.new(f) unless m
379
- m.first.to_i
481
+ if down?
482
+ Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
483
+ else
484
+ Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
380
485
  end
381
- down? ? files.reverse : files
382
- end
383
-
384
- def migration_class(migration_name, version)
385
- klass = migration_name.camelize.constantize
386
- class << klass; attr_accessor :version end
387
- klass.version = version
388
- klass
389
- end
390
-
391
- def migration_version_and_name(migration_file)
392
- return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
393
- end
394
-
395
- def set_schema_version(version)
396
- Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}")
397
486
  end
398
487
 
399
488
  def up?
@@ -403,14 +492,5 @@ module ActiveRecord
403
492
  def down?
404
493
  @direction == :down
405
494
  end
406
-
407
- def reached_target_version?(version)
408
- return false if @target_version == nil
409
- (up? && version.to_i - 1 >= @target_version) || (down? && version.to_i <= @target_version)
410
- end
411
-
412
- def irrelevant_migration?(version)
413
- (up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
414
- end
415
495
  end
416
496
  end
@@ -0,0 +1,163 @@
1
+ module ActiveRecord
2
+ module NamedScope
3
+ # All subclasses of ActiveRecord::Base have two named_scopes:
4
+ # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
5
+ # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
6
+ #
7
+ # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
8
+ # intermediate values (scopes) around as first-class objects is convenient.
9
+ def self.included(base)
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ named_scope :scoped, lambda { |scope| scope }
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def scopes
18
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
19
+ end
20
+
21
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
22
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
23
+ #
24
+ # class Shirt < ActiveRecord::Base
25
+ # named_scope :red, :conditions => {:color => 'red'}
26
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
27
+ # end
28
+ #
29
+ # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
30
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
31
+ #
32
+ # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
33
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
34
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
35
+ # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
36
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
37
+ #
38
+ # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
39
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
40
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
41
+ #
42
+ # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
43
+ # <tt>has_many</tt> associations. If,
44
+ #
45
+ # class Person < ActiveRecord::Base
46
+ # has_many :shirts
47
+ # end
48
+ #
49
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
50
+ # only shirts.
51
+ #
52
+ # Named scopes can also be procedural.
53
+ #
54
+ # class Shirt < ActiveRecord::Base
55
+ # named_scope :colored, lambda { |color|
56
+ # { :conditions => { :color => color } }
57
+ # }
58
+ # end
59
+ #
60
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
61
+ #
62
+ # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
63
+ #
64
+ # class Shirt < ActiveRecord::Base
65
+ # named_scope :red, :conditions => {:color => 'red'} do
66
+ # def dom_id
67
+ # 'red_shirts'
68
+ # end
69
+ # end
70
+ # end
71
+ #
72
+ #
73
+ # For testing complex named scopes, you can examine the scoping options using the
74
+ # <tt>proxy_options</tt> method on the proxy itself.
75
+ #
76
+ # class Shirt < ActiveRecord::Base
77
+ # named_scope :colored, lambda { |color|
78
+ # { :conditions => { :color => color } }
79
+ # }
80
+ # end
81
+ #
82
+ # expected_options = { :conditions => { :colored => 'red' } }
83
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
84
+ def named_scope(name, options = {}, &block)
85
+ scopes[name] = lambda do |parent_scope, *args|
86
+ Scope.new(parent_scope, case options
87
+ when Hash
88
+ options
89
+ when Proc
90
+ options.call(*args)
91
+ end, &block)
92
+ end
93
+ (class << self; self end).instance_eval do
94
+ define_method name do |*args|
95
+ scopes[name].call(self, *args)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ class Scope
102
+ attr_reader :proxy_scope, :proxy_options
103
+
104
+ [].methods.each do |m|
105
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/
106
+ delegate m, :to => :proxy_found
107
+ end
108
+ end
109
+
110
+ delegate :scopes, :with_scope, :to => :proxy_scope
111
+
112
+ def initialize(proxy_scope, options, &block)
113
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
114
+ extend Module.new(&block) if block_given?
115
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
116
+ end
117
+
118
+ def reload
119
+ load_found; self
120
+ end
121
+
122
+ def first(*args)
123
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
124
+ proxy_found.first(*args)
125
+ else
126
+ find(:first, *args)
127
+ end
128
+ end
129
+
130
+ def last(*args)
131
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
132
+ proxy_found.last(*args)
133
+ else
134
+ find(:last, *args)
135
+ end
136
+ end
137
+
138
+ def empty?
139
+ @found ? @found.empty? : count.zero?
140
+ end
141
+
142
+ protected
143
+ def proxy_found
144
+ @found || load_found
145
+ end
146
+
147
+ private
148
+ def method_missing(method, *args, &block)
149
+ if scopes.include?(method)
150
+ scopes[method].call(self, *args)
151
+ else
152
+ with_scope :find => proxy_options do
153
+ proxy_scope.send(method, *args, &block)
154
+ end
155
+ end
156
+ end
157
+
158
+ def load_found
159
+ @found = find(:all)
160
+ end
161
+ end
162
+ end
163
+ end