activerecord 1.0.0 → 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 (311) hide show
  1. data/CHANGELOG +4928 -3
  2. data/README +45 -46
  3. data/RUNNING_UNIT_TESTS +8 -11
  4. data/Rakefile +247 -0
  5. data/install.rb +8 -38
  6. data/lib/active_record/aggregations.rb +64 -49
  7. data/lib/active_record/associations/association_collection.rb +217 -47
  8. data/lib/active_record/associations/association_proxy.rb +159 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +56 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +155 -37
  12. data/lib/active_record/associations/has_many_association.rb +145 -75
  13. data/lib/active_record/associations/has_many_through_association.rb +283 -0
  14. data/lib/active_record/associations/has_one_association.rb +96 -0
  15. data/lib/active_record/associations.rb +1537 -304
  16. data/lib/active_record/attribute_methods.rb +328 -0
  17. data/lib/active_record/base.rb +2001 -588
  18. data/lib/active_record/calculations.rb +269 -0
  19. data/lib/active_record/callbacks.rb +169 -165
  20. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +308 -0
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -0
  22. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  23. data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
  24. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +472 -0
  25. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +306 -0
  26. data/lib/active_record/connection_adapters/abstract_adapter.rb +125 -279
  27. data/lib/active_record/connection_adapters/mysql_adapter.rb +442 -77
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +805 -135
  29. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  30. data/lib/active_record/connection_adapters/sqlite_adapter.rb +353 -69
  31. data/lib/active_record/fixtures.rb +946 -100
  32. data/lib/active_record/locking/optimistic.rb +144 -0
  33. data/lib/active_record/locking/pessimistic.rb +77 -0
  34. data/lib/active_record/migration.rb +417 -0
  35. data/lib/active_record/observer.rb +142 -32
  36. data/lib/active_record/query_cache.rb +23 -0
  37. data/lib/active_record/reflection.rb +163 -70
  38. data/lib/active_record/schema.rb +58 -0
  39. data/lib/active_record/schema_dumper.rb +171 -0
  40. data/lib/active_record/serialization.rb +98 -0
  41. data/lib/active_record/serializers/json_serializer.rb +71 -0
  42. data/lib/active_record/serializers/xml_serializer.rb +315 -0
  43. data/lib/active_record/timestamp.rb +41 -0
  44. data/lib/active_record/transactions.rb +87 -57
  45. data/lib/active_record/validations.rb +909 -122
  46. data/lib/active_record/vendor/db2.rb +362 -0
  47. data/lib/active_record/vendor/mysql.rb +126 -29
  48. data/lib/active_record/version.rb +9 -0
  49. data/lib/active_record.rb +35 -7
  50. data/lib/activerecord.rb +1 -0
  51. data/test/aaa_create_tables_test.rb +72 -0
  52. data/test/abstract_unit.rb +73 -5
  53. data/test/active_schema_test_mysql.rb +43 -0
  54. data/test/adapter_test.rb +105 -0
  55. data/test/adapter_test_sqlserver.rb +95 -0
  56. data/test/aggregations_test.rb +110 -16
  57. data/test/all.sh +2 -2
  58. data/test/ar_schema_test.rb +33 -0
  59. data/test/association_inheritance_reload.rb +14 -0
  60. data/test/associations/ar_joins_test.rb +0 -0
  61. data/test/associations/callbacks_test.rb +147 -0
  62. data/test/associations/cascaded_eager_loading_test.rb +110 -0
  63. data/test/associations/eager_singularization_test.rb +145 -0
  64. data/test/associations/eager_test.rb +442 -0
  65. data/test/associations/extension_test.rb +47 -0
  66. data/test/associations/inner_join_association_test.rb +88 -0
  67. data/test/associations/join_model_test.rb +553 -0
  68. data/test/associations_test.rb +1930 -267
  69. data/test/attribute_methods_test.rb +146 -0
  70. data/test/base_test.rb +1316 -84
  71. data/test/binary_test.rb +32 -0
  72. data/test/calculations_test.rb +251 -0
  73. data/test/callbacks_test.rb +400 -0
  74. data/test/class_inheritable_attributes_test.rb +3 -4
  75. data/test/column_alias_test.rb +17 -0
  76. data/test/connection_test_firebird.rb +8 -0
  77. data/test/connection_test_mysql.rb +30 -0
  78. data/test/connections/native_db2/connection.rb +25 -0
  79. data/test/connections/native_firebird/connection.rb +26 -0
  80. data/test/connections/native_frontbase/connection.rb +27 -0
  81. data/test/connections/native_mysql/connection.rb +21 -18
  82. data/test/connections/native_openbase/connection.rb +21 -0
  83. data/test/connections/native_oracle/connection.rb +27 -0
  84. data/test/connections/native_postgresql/connection.rb +17 -18
  85. data/test/connections/native_sqlite/connection.rb +17 -16
  86. data/test/connections/native_sqlite3/connection.rb +25 -0
  87. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  88. data/test/connections/native_sybase/connection.rb +23 -0
  89. data/test/copy_table_test_sqlite.rb +69 -0
  90. data/test/datatype_test_postgresql.rb +203 -0
  91. data/test/date_time_test.rb +37 -0
  92. data/test/default_test_firebird.rb +16 -0
  93. data/test/defaults_test.rb +67 -0
  94. data/test/deprecated_finder_test.rb +30 -0
  95. data/test/finder_test.rb +607 -32
  96. data/test/fixtures/accounts.yml +28 -0
  97. data/test/fixtures/all/developers.yml +0 -0
  98. data/test/fixtures/all/people.csv +0 -0
  99. data/test/fixtures/all/tasks.yml +0 -0
  100. data/test/fixtures/author.rb +107 -0
  101. data/test/fixtures/author_favorites.yml +4 -0
  102. data/test/fixtures/authors.yml +7 -0
  103. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  104. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  105. data/test/fixtures/bad_fixtures/blank_line +3 -0
  106. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  107. data/test/fixtures/bad_fixtures/missing_value +1 -0
  108. data/test/fixtures/binaries.yml +132 -0
  109. data/test/fixtures/binary.rb +2 -0
  110. data/test/fixtures/book.rb +4 -0
  111. data/test/fixtures/books.yml +7 -0
  112. data/test/fixtures/categories/special_categories.yml +9 -0
  113. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  114. data/test/fixtures/categories.yml +14 -0
  115. data/test/fixtures/categories_ordered.yml +7 -0
  116. data/test/fixtures/categories_posts.yml +23 -0
  117. data/test/fixtures/categorization.rb +5 -0
  118. data/test/fixtures/categorizations.yml +17 -0
  119. data/test/fixtures/category.rb +26 -0
  120. data/test/fixtures/citation.rb +6 -0
  121. data/test/fixtures/comment.rb +23 -0
  122. data/test/fixtures/comments.yml +59 -0
  123. data/test/fixtures/companies.yml +55 -0
  124. data/test/fixtures/company.rb +81 -4
  125. data/test/fixtures/company_in_module.rb +32 -6
  126. data/test/fixtures/computer.rb +4 -0
  127. data/test/fixtures/computers.yml +4 -0
  128. data/test/fixtures/contact.rb +16 -0
  129. data/test/fixtures/courses.yml +7 -0
  130. data/test/fixtures/customer.rb +28 -3
  131. data/test/fixtures/customers.yml +17 -0
  132. data/test/fixtures/db_definitions/db2.drop.sql +33 -0
  133. data/test/fixtures/db_definitions/db2.sql +235 -0
  134. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  135. data/test/fixtures/db_definitions/db22.sql +5 -0
  136. data/test/fixtures/db_definitions/firebird.drop.sql +65 -0
  137. data/test/fixtures/db_definitions/firebird.sql +310 -0
  138. data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
  139. data/test/fixtures/db_definitions/firebird2.sql +6 -0
  140. data/test/fixtures/db_definitions/frontbase.drop.sql +33 -0
  141. data/test/fixtures/db_definitions/frontbase.sql +273 -0
  142. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  143. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  144. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  145. data/test/fixtures/db_definitions/openbase.sql +318 -0
  146. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  147. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  148. data/test/fixtures/db_definitions/oracle.drop.sql +67 -0
  149. data/test/fixtures/db_definitions/oracle.sql +330 -0
  150. data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
  151. data/test/fixtures/db_definitions/oracle2.sql +6 -0
  152. data/test/fixtures/db_definitions/postgresql.drop.sql +44 -0
  153. data/test/fixtures/db_definitions/postgresql.sql +217 -38
  154. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  155. data/test/fixtures/db_definitions/postgresql2.sql +2 -2
  156. data/test/fixtures/db_definitions/schema.rb +354 -0
  157. data/test/fixtures/db_definitions/schema2.rb +11 -0
  158. data/test/fixtures/db_definitions/sqlite.drop.sql +33 -0
  159. data/test/fixtures/db_definitions/sqlite.sql +139 -5
  160. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  161. data/test/fixtures/db_definitions/sqlite2.sql +1 -0
  162. data/test/fixtures/db_definitions/sybase.drop.sql +35 -0
  163. data/test/fixtures/db_definitions/sybase.sql +222 -0
  164. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  165. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  166. data/test/fixtures/developer.rb +70 -6
  167. data/test/fixtures/developers.yml +21 -0
  168. data/test/fixtures/developers_projects/david_action_controller +2 -1
  169. data/test/fixtures/developers_projects/david_active_record +2 -1
  170. data/test/fixtures/developers_projects.yml +17 -0
  171. data/test/fixtures/edge.rb +5 -0
  172. data/test/fixtures/edges.yml +6 -0
  173. data/test/fixtures/entrants.yml +14 -0
  174. data/test/fixtures/example.log +1 -0
  175. data/test/fixtures/fk_test_has_fk.yml +3 -0
  176. data/test/fixtures/fk_test_has_pk.yml +2 -0
  177. data/test/fixtures/flowers.jpg +0 -0
  178. data/test/fixtures/funny_jokes.yml +10 -0
  179. data/test/fixtures/item.rb +7 -0
  180. data/test/fixtures/items.yml +4 -0
  181. data/test/fixtures/joke.rb +3 -0
  182. data/test/fixtures/keyboard.rb +3 -0
  183. data/test/fixtures/legacy_thing.rb +3 -0
  184. data/test/fixtures/legacy_things.yml +3 -0
  185. data/test/fixtures/matey.rb +4 -0
  186. data/test/fixtures/mateys.yml +4 -0
  187. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  188. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  189. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  190. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  191. data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
  192. data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
  193. data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
  194. data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
  195. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  196. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  197. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  198. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  199. data/test/fixtures/minimalistic.rb +2 -0
  200. data/test/fixtures/minimalistics.yml +2 -0
  201. data/test/fixtures/mixed_case_monkey.rb +3 -0
  202. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  203. data/test/fixtures/mixins.yml +29 -0
  204. data/test/fixtures/movies.yml +7 -0
  205. data/test/fixtures/naked/csv/accounts.csv +1 -0
  206. data/test/fixtures/naked/yml/accounts.yml +1 -0
  207. data/test/fixtures/naked/yml/companies.yml +1 -0
  208. data/test/fixtures/naked/yml/courses.yml +1 -0
  209. data/test/fixtures/order.rb +4 -0
  210. data/test/fixtures/parrot.rb +13 -0
  211. data/test/fixtures/parrots.yml +27 -0
  212. data/test/fixtures/parrots_pirates.yml +7 -0
  213. data/test/fixtures/people.yml +3 -0
  214. data/test/fixtures/person.rb +4 -0
  215. data/test/fixtures/pirate.rb +5 -0
  216. data/test/fixtures/pirates.yml +9 -0
  217. data/test/fixtures/post.rb +59 -0
  218. data/test/fixtures/posts.yml +48 -0
  219. data/test/fixtures/project.rb +27 -2
  220. data/test/fixtures/projects.yml +7 -0
  221. data/test/fixtures/reader.rb +4 -0
  222. data/test/fixtures/readers.yml +4 -0
  223. data/test/fixtures/reply.rb +18 -2
  224. data/test/fixtures/reserved_words/distinct.yml +5 -0
  225. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  226. data/test/fixtures/reserved_words/group.yml +14 -0
  227. data/test/fixtures/reserved_words/select.yml +8 -0
  228. data/test/fixtures/reserved_words/values.yml +7 -0
  229. data/test/fixtures/ship.rb +3 -0
  230. data/test/fixtures/ships.yml +5 -0
  231. data/test/fixtures/subject.rb +4 -0
  232. data/test/fixtures/subscriber.rb +4 -3
  233. data/test/fixtures/tag.rb +7 -0
  234. data/test/fixtures/tagging.rb +10 -0
  235. data/test/fixtures/taggings.yml +25 -0
  236. data/test/fixtures/tags.yml +7 -0
  237. data/test/fixtures/task.rb +3 -0
  238. data/test/fixtures/tasks.yml +7 -0
  239. data/test/fixtures/topic.rb +20 -3
  240. data/test/fixtures/topics.yml +22 -0
  241. data/test/fixtures/treasure.rb +4 -0
  242. data/test/fixtures/treasures.yml +10 -0
  243. data/test/fixtures/vertex.rb +9 -0
  244. data/test/fixtures/vertices.yml +4 -0
  245. data/test/fixtures_test.rb +574 -8
  246. data/test/inheritance_test.rb +113 -27
  247. data/test/json_serialization_test.rb +180 -0
  248. data/test/lifecycle_test.rb +56 -29
  249. data/test/locking_test.rb +273 -0
  250. data/test/method_scoping_test.rb +416 -0
  251. data/test/migration_test.rb +933 -0
  252. data/test/migration_test_firebird.rb +124 -0
  253. data/test/mixin_test.rb +95 -0
  254. data/test/modules_test.rb +23 -10
  255. data/test/multiple_db_test.rb +17 -3
  256. data/test/pk_test.rb +59 -15
  257. data/test/query_cache_test.rb +104 -0
  258. data/test/readonly_test.rb +107 -0
  259. data/test/reflection_test.rb +124 -27
  260. data/test/reserved_word_test_mysql.rb +177 -0
  261. data/test/schema_authorization_test_postgresql.rb +75 -0
  262. data/test/schema_dumper_test.rb +131 -0
  263. data/test/schema_test_postgresql.rb +64 -0
  264. data/test/serialization_test.rb +47 -0
  265. data/test/synonym_test_oracle.rb +17 -0
  266. data/test/table_name_test_sqlserver.rb +23 -0
  267. data/test/threaded_connections_test.rb +48 -0
  268. data/test/transactions_test.rb +227 -29
  269. data/test/unconnected_test.rb +14 -6
  270. data/test/validations_test.rb +1293 -32
  271. data/test/xml_serialization_test.rb +202 -0
  272. metadata +347 -143
  273. data/dev-utils/eval_debugger.rb +0 -9
  274. data/examples/associations.rb +0 -87
  275. data/examples/shared_setup.rb +0 -15
  276. data/examples/validation.rb +0 -88
  277. data/lib/active_record/deprecated_associations.rb +0 -70
  278. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  279. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  280. data/lib/active_record/support/clean_logger.rb +0 -10
  281. data/lib/active_record/support/inflector.rb +0 -70
  282. data/lib/active_record/vendor/simple.rb +0 -702
  283. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  284. data/lib/active_record/wrappings.rb +0 -59
  285. data/rakefile +0 -122
  286. data/test/deprecated_associations_test.rb +0 -336
  287. data/test/fixtures/accounts/signals37 +0 -3
  288. data/test/fixtures/accounts/unknown +0 -2
  289. data/test/fixtures/companies/first_client +0 -6
  290. data/test/fixtures/companies/first_firm +0 -4
  291. data/test/fixtures/companies/second_client +0 -6
  292. data/test/fixtures/courses/java +0 -2
  293. data/test/fixtures/courses/ruby +0 -2
  294. data/test/fixtures/customers/david +0 -6
  295. data/test/fixtures/db_definitions/mysql.sql +0 -96
  296. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  297. data/test/fixtures/developers/david +0 -2
  298. data/test/fixtures/developers/jamis +0 -2
  299. data/test/fixtures/entrants/first +0 -3
  300. data/test/fixtures/entrants/second +0 -3
  301. data/test/fixtures/entrants/third +0 -3
  302. data/test/fixtures/fixture_database.sqlite +0 -0
  303. data/test/fixtures/fixture_database_2.sqlite +0 -0
  304. data/test/fixtures/movies/first +0 -2
  305. data/test/fixtures/movies/second +0 -2
  306. data/test/fixtures/projects/action_controller +0 -2
  307. data/test/fixtures/projects/active_record +0 -2
  308. data/test/fixtures/topics/first +0 -9
  309. data/test/fixtures/topics/second +0 -8
  310. data/test/inflector_test.rb +0 -104
  311. data/test/thread_safety_test.rb +0 -33
@@ -0,0 +1,144 @@
1
+ module ActiveRecord
2
+ module Locking
3
+ # == What is Optimistic Locking
4
+ #
5
+ # Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
6
+ # conflicts with the data. It does this by checking whether another process has made changes to a record since
7
+ # it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
8
+ #
9
+ # Check out ActiveRecord::Locking::Pessimistic for an alternative.
10
+ #
11
+ # == Usage
12
+ #
13
+ # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
14
+ # record increments the lock_version column and the locking facilities ensure that records instantiated twice
15
+ # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
16
+ #
17
+ # p1 = Person.find(1)
18
+ # p2 = Person.find(1)
19
+ #
20
+ # p1.first_name = "Michael"
21
+ # p1.save
22
+ #
23
+ # p2.first_name = "should fail"
24
+ # p2.save # Raises a ActiveRecord::StaleObjectError
25
+ #
26
+ # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
27
+ # or otherwise apply the business logic needed to resolve the conflict.
28
+ #
29
+ # You must ensure that your database schema defaults the lock_version column to 0.
30
+ #
31
+ # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
32
+ # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
33
+ # This method uses the same syntax as <tt>set_table_name</tt>
34
+ module Optimistic
35
+ def self.included(base) #:nodoc:
36
+ base.extend ClassMethods
37
+
38
+ base.cattr_accessor :lock_optimistically, :instance_writer => false
39
+ base.lock_optimistically = true
40
+
41
+ base.alias_method_chain :update, :lock
42
+ base.alias_method_chain :attributes_from_column_definition, :lock
43
+
44
+ class << base
45
+ alias_method :locking_column=, :set_locking_column
46
+ end
47
+ end
48
+
49
+ def locking_enabled? #:nodoc:
50
+ self.class.locking_enabled?
51
+ end
52
+
53
+ private
54
+ def attributes_from_column_definition_with_lock
55
+ result = attributes_from_column_definition_without_lock
56
+
57
+ # If the locking column has no default value set,
58
+ # start the lock version at zero. Note we can't use
59
+ # locking_enabled? at this point as @attributes may
60
+ # not have been initialized yet
61
+
62
+ if lock_optimistically && result.include?(self.class.locking_column)
63
+ result[self.class.locking_column] ||= 0
64
+ end
65
+
66
+ return result
67
+ end
68
+
69
+ def update_with_lock #:nodoc:
70
+ return update_without_lock unless locking_enabled?
71
+
72
+ lock_col = self.class.locking_column
73
+ previous_value = send(lock_col)
74
+ send(lock_col + '=', previous_value + 1)
75
+
76
+ begin
77
+ 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))}
80
+ WHERE #{self.class.primary_key} = #{quote_value(id)}
81
+ AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
82
+ end_sql
83
+
84
+ unless affected_rows == 1
85
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
86
+ end
87
+
88
+ affected_rows
89
+
90
+ # If something went wrong, revert the version.
91
+ rescue Exception
92
+ send(lock_col + '=', previous_value)
93
+ raise
94
+ end
95
+ end
96
+
97
+ module ClassMethods
98
+ DEFAULT_LOCKING_COLUMN = 'lock_version'
99
+
100
+ def self.extended(base)
101
+ class <<base
102
+ alias_method_chain :update_counters, :lock
103
+ end
104
+ end
105
+
106
+ # 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
+ def locking_enabled?
111
+ lock_optimistically && columns_hash[locking_column]
112
+ end
113
+
114
+ # Set the column to use for optimistic locking. Defaults to lock_version.
115
+ def set_locking_column(value = nil, &block)
116
+ define_attr_method :locking_column, value, &block
117
+ value
118
+ end
119
+
120
+ # The version column used for optimistic locking. Defaults to lock_version.
121
+ def locking_column
122
+ reset_locking_column
123
+ end
124
+
125
+ # Quote the column name used for optimistic locking.
126
+ def quoted_locking_column
127
+ connection.quote_column_name(locking_column)
128
+ end
129
+
130
+ # Reset the column used for optimistic locking back to the lock_version default.
131
+ def reset_locking_column
132
+ set_locking_column DEFAULT_LOCKING_COLUMN
133
+ end
134
+
135
+ # make sure the lock version column gets updated when counters are
136
+ # updated.
137
+ def update_counters_with_lock(id, counters)
138
+ counters = counters.merge(locking_column => 1) if locking_enabled?
139
+ update_counters_without_lock(id, counters)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2006 Shugo Maeda <shugo@ruby-lang.org>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject
9
+ # to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
18
+ # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
19
+ # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
23
+ module ActiveRecord
24
+ module Locking
25
+ # Locking::Pessimistic provides support for row-level locking using
26
+ # SELECT ... FOR UPDATE and other lock types.
27
+ #
28
+ # Pass :lock => true to ActiveRecord::Base.find to obtain an exclusive
29
+ # lock on the selected rows:
30
+ # # select * from accounts where id=1 for update
31
+ # Account.find(1, :lock => true)
32
+ #
33
+ # Pass :lock => 'some locking clause' to give a database-specific locking clause
34
+ # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
35
+ #
36
+ # Example:
37
+ # Account.transaction do
38
+ # # select * from accounts where name = 'shugo' limit 1 for update
39
+ # shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true)
40
+ # yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true)
41
+ # shugo.balance -= 100
42
+ # shugo.save!
43
+ # yuko.balance += 100
44
+ # yuko.save!
45
+ # end
46
+ #
47
+ # You can also use ActiveRecord::Base#lock! method to lock one record by id.
48
+ # This may be better if you don't need to lock every row. Example:
49
+ # Account.transaction do
50
+ # # select * from accounts where ...
51
+ # accounts = Account.find(:all, :conditions => ...)
52
+ # account1 = accounts.detect { |account| ... }
53
+ # account2 = accounts.detect { |account| ... }
54
+ # # select * from accounts where id=? for update
55
+ # account1.lock!
56
+ # account2.lock!
57
+ # account1.balance -= 100
58
+ # account1.save!
59
+ # account2.balance += 100
60
+ # account2.save!
61
+ # end
62
+ #
63
+ # Database-specific information on row locking:
64
+ # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
65
+ # PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
66
+ module Pessimistic
67
+ # Obtain a row lock on this record. Reloads the record to obtain the requested
68
+ # lock. Pass an SQL locking clause to append the end of the SELECT statement
69
+ # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
70
+ # the locked record.
71
+ def lock!(lock = true)
72
+ reload(:lock => lock) unless new_record?
73
+ self
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,417 @@
1
+ module ActiveRecord
2
+ class IrreversibleMigration < ActiveRecordError#:nodoc:
3
+ end
4
+
5
+ class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
6
+ def initialize(version)
7
+ super("Multiple migrations have the version number #{version}")
8
+ end
9
+ end
10
+
11
+ class IllegalMigrationNameError < ActiveRecordError#:nodoc:
12
+ def initialize(name)
13
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
14
+ end
15
+ end
16
+
17
+ # Migrations can manage the evolution of a schema used by several physical databases. It's a solution
18
+ # to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
19
+ # push that change to other developers and to the production server. With migrations, you can describe the transformations
20
+ # in self-contained classes that can be checked into version control systems and executed against another database that
21
+ # might be one, two, or five versions behind.
22
+ #
23
+ # Example of a simple migration:
24
+ #
25
+ # class AddSsl < ActiveRecord::Migration
26
+ # def self.up
27
+ # add_column :accounts, :ssl_enabled, :boolean, :default => 1
28
+ # end
29
+ #
30
+ # def self.down
31
+ # remove_column :accounts, :ssl_enabled
32
+ # end
33
+ # end
34
+ #
35
+ # This migration will add a boolean flag to the accounts table and remove it if you're backing out of the migration.
36
+ # It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
37
+ # or remove the migration. These methods can consist of both the migration specific methods like add_column and remove_column,
38
+ # but may also contain regular Ruby code for generating data needed for the transformations.
39
+ #
40
+ # Example of a more complex migration that also needs to initialize data:
41
+ #
42
+ # class AddSystemSettings < ActiveRecord::Migration
43
+ # def self.up
44
+ # create_table :system_settings do |t|
45
+ # t.string :name
46
+ # t.string :label
47
+ # t.text :value
48
+ # t.string :type
49
+ # t.integer :position
50
+ # end
51
+ #
52
+ # SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
53
+ # end
54
+ #
55
+ # def self.down
56
+ # drop_table :system_settings
57
+ # end
58
+ # end
59
+ #
60
+ # This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
61
+ # that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
62
+ # in one block call.
63
+ #
64
+ # == Available transformations
65
+ #
66
+ # * <tt>create_table(name, options)</tt> Creates a table called +name+ and makes the table object available to a block
67
+ # that can then add columns to it, following the same format as add_column. See example above. The options hash is for
68
+ # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
69
+ # * <tt>drop_table(name)</tt>: Drops the table called +name+.
70
+ # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+.
71
+ # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
72
+ # 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 })
76
+ # -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
77
+ # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
78
+ # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
79
+ # parameters as add_column.
80
+ # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
81
+ # * <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 }).
83
+ # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified by +index_name+.
84
+ #
85
+ # == Irreversible transformations
86
+ #
87
+ # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
88
+ # an <tt>ActiveRecord::IrreversibleMigration</tt> exception in their +down+ method.
89
+ #
90
+ # == Running migrations from within Rails
91
+ #
92
+ # The Rails package has several tools to help create and apply migrations.
93
+ #
94
+ # To generate a new migration, use <tt>script/generate migration MyNewMigration</tt>
95
+ # 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>
97
+ # directory where <tt>nnn</tt> is the next largest migration number.
98
+ # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
99
+ # MyNewMigration.
100
+ #
101
+ # To run migrations against the currently configured database, use
102
+ # <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.
104
+ #
105
+ # To roll the database back to a previous migration version, use
106
+ # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
107
+ # you wish to downgrade. If any of the migrations throw an
108
+ # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
109
+ # have some manual work to do.
110
+ #
111
+ # == Database support
112
+ #
113
+ # Migrations are currently supported in MySQL, PostgreSQL, SQLite,
114
+ # SQL Server, Sybase, and Oracle (all supported databases except DB2).
115
+ #
116
+ # == More examples
117
+ #
118
+ # Not all migrations change the schema. Some just fix the data:
119
+ #
120
+ # class RemoveEmptyTags < ActiveRecord::Migration
121
+ # def self.up
122
+ # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
123
+ # end
124
+ #
125
+ # def self.down
126
+ # # not much we can do to restore deleted data
127
+ # raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
128
+ # end
129
+ # end
130
+ #
131
+ # Others remove columns when they migrate up instead of down:
132
+ #
133
+ # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
134
+ # def self.up
135
+ # remove_column :items, :incomplete_items_count
136
+ # remove_column :items, :completed_items_count
137
+ # end
138
+ #
139
+ # def self.down
140
+ # add_column :items, :incomplete_items_count
141
+ # add_column :items, :completed_items_count
142
+ # end
143
+ # end
144
+ #
145
+ # And sometimes you need to do something in SQL not abstracted directly by migrations:
146
+ #
147
+ # class MakeJoinUnique < ActiveRecord::Migration
148
+ # def self.up
149
+ # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
150
+ # end
151
+ #
152
+ # def self.down
153
+ # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
154
+ # end
155
+ # end
156
+ #
157
+ # == Using a model after changing its table
158
+ #
159
+ # Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need
160
+ # to make a call to Base#reset_column_information in order to ensure that the model has the latest column data from
161
+ # after the new column was added. Example:
162
+ #
163
+ # class AddPeopleSalary < ActiveRecord::Migration
164
+ # def self.up
165
+ # add_column :people, :salary, :integer
166
+ # Person.reset_column_information
167
+ # Person.find(:all).each do |p|
168
+ # p.update_attribute :salary, SalaryCalculator.compute(p)
169
+ # end
170
+ # end
171
+ # end
172
+ #
173
+ # == Controlling verbosity
174
+ #
175
+ # By default, migrations will describe the actions they are taking, writing
176
+ # them to the console as they happen, along with benchmarks describing how
177
+ # long each step took.
178
+ #
179
+ # You can quiet them down by setting ActiveRecord::Migration.verbose = false.
180
+ #
181
+ # You can also insert your own messages and benchmarks by using the #say_with_time
182
+ # method:
183
+ #
184
+ # def self.up
185
+ # ...
186
+ # say_with_time "Updating salaries..." do
187
+ # Person.find(:all).each do |p|
188
+ # p.update_attribute :salary, SalaryCalculator.compute(p)
189
+ # end
190
+ # end
191
+ # ...
192
+ # end
193
+ #
194
+ # The phrase "Updating salaries..." would then be printed, along with the
195
+ # benchmark for the block when the block completes.
196
+ class Migration
197
+ @@verbose = true
198
+ cattr_accessor :verbose
199
+
200
+ class << self
201
+ def up_with_benchmarks #:nodoc:
202
+ migrate(:up)
203
+ end
204
+
205
+ def down_with_benchmarks #:nodoc:
206
+ migrate(:down)
207
+ end
208
+
209
+ # Execute this migration in the named direction
210
+ def migrate(direction)
211
+ return unless respond_to?(direction)
212
+
213
+ case direction
214
+ when :up then announce "migrating"
215
+ when :down then announce "reverting"
216
+ end
217
+
218
+ result = nil
219
+ time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
220
+
221
+ case direction
222
+ when :up then announce "migrated (%.4fs)" % time.real; write
223
+ when :down then announce "reverted (%.4fs)" % time.real; write
224
+ end
225
+
226
+ result
227
+ end
228
+
229
+ # Because the method added may do an alias_method, it can be invoked
230
+ # recursively. We use @ignore_new_methods as a guard to indicate whether
231
+ # it is safe for the call to proceed.
232
+ def singleton_method_added(sym) #:nodoc:
233
+ return if @ignore_new_methods
234
+
235
+ begin
236
+ @ignore_new_methods = true
237
+
238
+ case sym
239
+ when :up, :down
240
+ klass = (class << self; self; end)
241
+ klass.send(:alias_method_chain, sym, "benchmarks")
242
+ end
243
+ ensure
244
+ @ignore_new_methods = false
245
+ end
246
+ end
247
+
248
+ def write(text="")
249
+ puts(text) if verbose
250
+ end
251
+
252
+ def announce(message)
253
+ text = "#{@version} #{name}: #{message}"
254
+ length = [0, 75 - text.length].max
255
+ write "== %s %s" % [text, "=" * length]
256
+ end
257
+
258
+ def say(message, subitem=false)
259
+ write "#{subitem ? " ->" : "--"} #{message}"
260
+ end
261
+
262
+ def say_with_time(message)
263
+ say(message)
264
+ result = nil
265
+ time = Benchmark.measure { result = yield }
266
+ say "%.4fs" % time.real, :subitem
267
+ say("#{result} rows", :subitem) if result.is_a?(Integer)
268
+ result
269
+ end
270
+
271
+ def suppress_messages
272
+ save, self.verbose = verbose, false
273
+ yield
274
+ ensure
275
+ self.verbose = save
276
+ end
277
+
278
+ def method_missing(method, *arguments, &block)
279
+ arg_list = arguments.map(&:inspect) * ', '
280
+
281
+ say_with_time "#{method}(#{arg_list})" do
282
+ unless arguments.empty? || method == :execute
283
+ arguments[0] = Migrator.proper_table_name(arguments.first)
284
+ end
285
+ ActiveRecord::Base.connection.send(method, *arguments, &block)
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ class Migrator#:nodoc:
292
+ class << self
293
+ def migrate(migrations_path, target_version = nil)
294
+ Base.connection.initialize_schema_information
295
+
296
+ 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
303
+ end
304
+ end
305
+
306
+ def up(migrations_path, target_version = nil)
307
+ self.new(:up, migrations_path, target_version).migrate
308
+ end
309
+
310
+ def down(migrations_path, target_version = nil)
311
+ self.new(:down, migrations_path, target_version).migrate
312
+ end
313
+
314
+ def schema_info_table_name
315
+ Base.table_name_prefix + "schema_info" + Base.table_name_suffix
316
+ end
317
+
318
+ def current_version
319
+ Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").to_i
320
+ end
321
+
322
+ 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
324
+ name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
325
+ end
326
+ end
327
+
328
+ def initialize(direction, migrations_path, target_version = nil)
329
+ 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
332
+ end
333
+
334
+ def current_version
335
+ self.class.current_version
336
+ end
337
+
338
+ 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
343
+ end
344
+
345
+ next if irrelevant_migration?(migration_class.version)
346
+
347
+ Base.logger.info "Migrating to #{migration_class} (#{migration_class.version})"
348
+ migration_class.migrate(@direction)
349
+ set_schema_version(migration_class.version)
350
+ end
351
+ end
352
+
353
+ def pending_migrations
354
+ migration_classes.select { |m| m.version > current_version }
355
+ end
356
+
357
+ private
358
+ def migration_classes
359
+ migrations = 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
365
+
366
+ sorted = migrations.sort_by { |m| m.version }
367
+ down? ? sorted.reverse : sorted
368
+ end
369
+
370
+ def assert_unique_migration_version(migrations, version)
371
+ if !migrations.empty? && migrations.find { |m| m.version == version }
372
+ raise DuplicateMigrationVersionError.new(version)
373
+ end
374
+ end
375
+
376
+ def migration_files
377
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f|
378
+ m = migration_version_and_name(f)
379
+ raise IllegalMigrationNameError.new(f) unless m
380
+ m.first.to_i
381
+ end
382
+ down? ? files.reverse : files
383
+ end
384
+
385
+ def migration_class(migration_name, version)
386
+ klass = migration_name.camelize.constantize
387
+ class << klass; attr_accessor :version end
388
+ klass.version = version
389
+ klass
390
+ end
391
+
392
+ def migration_version_and_name(migration_file)
393
+ return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
394
+ end
395
+
396
+ def set_schema_version(version)
397
+ Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}")
398
+ end
399
+
400
+ def up?
401
+ @direction == :up
402
+ end
403
+
404
+ def down?
405
+ @direction == :down
406
+ end
407
+
408
+ def reached_target_version?(version)
409
+ return false if @target_version == nil
410
+ (up? && version.to_i - 1 >= @target_version) || (down? && version.to_i <= @target_version)
411
+ end
412
+
413
+ def irrelevant_migration?(version)
414
+ (up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
415
+ end
416
+ end
417
+ end