activerecord_authorails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. data/CHANGELOG +3043 -0
  2. data/README +360 -0
  3. data/RUNNING_UNIT_TESTS +64 -0
  4. data/Rakefile +226 -0
  5. data/examples/associations.png +0 -0
  6. data/examples/associations.rb +87 -0
  7. data/examples/shared_setup.rb +15 -0
  8. data/examples/validation.rb +85 -0
  9. data/install.rb +30 -0
  10. data/lib/active_record.rb +85 -0
  11. data/lib/active_record/acts/list.rb +244 -0
  12. data/lib/active_record/acts/nested_set.rb +211 -0
  13. data/lib/active_record/acts/tree.rb +89 -0
  14. data/lib/active_record/aggregations.rb +191 -0
  15. data/lib/active_record/associations.rb +1637 -0
  16. data/lib/active_record/associations/association_collection.rb +190 -0
  17. data/lib/active_record/associations/association_proxy.rb +158 -0
  18. data/lib/active_record/associations/belongs_to_association.rb +56 -0
  19. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  20. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +169 -0
  21. data/lib/active_record/associations/has_many_association.rb +210 -0
  22. data/lib/active_record/associations/has_many_through_association.rb +247 -0
  23. data/lib/active_record/associations/has_one_association.rb +80 -0
  24. data/lib/active_record/attribute_methods.rb +75 -0
  25. data/lib/active_record/base.rb +2164 -0
  26. data/lib/active_record/calculations.rb +270 -0
  27. data/lib/active_record/callbacks.rb +367 -0
  28. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +279 -0
  29. data/lib/active_record/connection_adapters/abstract/database_statements.rb +130 -0
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +58 -0
  31. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +343 -0
  32. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +310 -0
  33. data/lib/active_record/connection_adapters/abstract_adapter.rb +161 -0
  34. data/lib/active_record/connection_adapters/db2_adapter.rb +228 -0
  35. data/lib/active_record/connection_adapters/firebird_adapter.rb +728 -0
  36. data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
  37. data/lib/active_record/connection_adapters/mysql_adapter.rb +414 -0
  38. data/lib/active_record/connection_adapters/openbase_adapter.rb +350 -0
  39. data/lib/active_record/connection_adapters/oracle_adapter.rb +689 -0
  40. data/lib/active_record/connection_adapters/postgresql_adapter.rb +584 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +407 -0
  42. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +591 -0
  43. data/lib/active_record/connection_adapters/sybase_adapter.rb +662 -0
  44. data/lib/active_record/deprecated_associations.rb +104 -0
  45. data/lib/active_record/deprecated_finders.rb +44 -0
  46. data/lib/active_record/fixtures.rb +628 -0
  47. data/lib/active_record/locking/optimistic.rb +106 -0
  48. data/lib/active_record/locking/pessimistic.rb +77 -0
  49. data/lib/active_record/migration.rb +394 -0
  50. data/lib/active_record/observer.rb +178 -0
  51. data/lib/active_record/query_cache.rb +64 -0
  52. data/lib/active_record/reflection.rb +222 -0
  53. data/lib/active_record/schema.rb +58 -0
  54. data/lib/active_record/schema_dumper.rb +149 -0
  55. data/lib/active_record/timestamp.rb +51 -0
  56. data/lib/active_record/transactions.rb +136 -0
  57. data/lib/active_record/validations.rb +843 -0
  58. data/lib/active_record/vendor/db2.rb +362 -0
  59. data/lib/active_record/vendor/mysql.rb +1214 -0
  60. data/lib/active_record/vendor/simple.rb +693 -0
  61. data/lib/active_record/version.rb +9 -0
  62. data/lib/active_record/wrappers/yaml_wrapper.rb +15 -0
  63. data/lib/active_record/wrappings.rb +58 -0
  64. data/lib/active_record/xml_serialization.rb +308 -0
  65. data/test/aaa_create_tables_test.rb +59 -0
  66. data/test/abstract_unit.rb +77 -0
  67. data/test/active_schema_test_mysql.rb +31 -0
  68. data/test/adapter_test.rb +87 -0
  69. data/test/adapter_test_sqlserver.rb +81 -0
  70. data/test/aggregations_test.rb +95 -0
  71. data/test/all.sh +8 -0
  72. data/test/ar_schema_test.rb +33 -0
  73. data/test/association_inheritance_reload.rb +14 -0
  74. data/test/associations/callbacks_test.rb +126 -0
  75. data/test/associations/cascaded_eager_loading_test.rb +138 -0
  76. data/test/associations/eager_test.rb +393 -0
  77. data/test/associations/extension_test.rb +42 -0
  78. data/test/associations/join_model_test.rb +497 -0
  79. data/test/associations_test.rb +1809 -0
  80. data/test/attribute_methods_test.rb +49 -0
  81. data/test/base_test.rb +1586 -0
  82. data/test/binary_test.rb +37 -0
  83. data/test/calculations_test.rb +219 -0
  84. data/test/callbacks_test.rb +377 -0
  85. data/test/class_inheritable_attributes_test.rb +32 -0
  86. data/test/column_alias_test.rb +17 -0
  87. data/test/connection_test_firebird.rb +8 -0
  88. data/test/connections/native_db2/connection.rb +25 -0
  89. data/test/connections/native_firebird/connection.rb +26 -0
  90. data/test/connections/native_frontbase/connection.rb +27 -0
  91. data/test/connections/native_mysql/connection.rb +24 -0
  92. data/test/connections/native_openbase/connection.rb +21 -0
  93. data/test/connections/native_oracle/connection.rb +27 -0
  94. data/test/connections/native_postgresql/connection.rb +23 -0
  95. data/test/connections/native_sqlite/connection.rb +34 -0
  96. data/test/connections/native_sqlite3/connection.rb +34 -0
  97. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  98. data/test/connections/native_sqlserver/connection.rb +23 -0
  99. data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
  100. data/test/connections/native_sybase/connection.rb +23 -0
  101. data/test/copy_table_sqlite.rb +64 -0
  102. data/test/datatype_test_postgresql.rb +52 -0
  103. data/test/default_test_firebird.rb +16 -0
  104. data/test/defaults_test.rb +60 -0
  105. data/test/deprecated_associations_test.rb +396 -0
  106. data/test/deprecated_finder_test.rb +151 -0
  107. data/test/empty_date_time_test.rb +25 -0
  108. data/test/finder_test.rb +504 -0
  109. data/test/fixtures/accounts.yml +28 -0
  110. data/test/fixtures/author.rb +99 -0
  111. data/test/fixtures/author_favorites.yml +4 -0
  112. data/test/fixtures/authors.yml +7 -0
  113. data/test/fixtures/auto_id.rb +4 -0
  114. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  115. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  116. data/test/fixtures/bad_fixtures/blank_line +3 -0
  117. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  118. data/test/fixtures/bad_fixtures/missing_value +1 -0
  119. data/test/fixtures/binary.rb +2 -0
  120. data/test/fixtures/categories.yml +14 -0
  121. data/test/fixtures/categories/special_categories.yml +9 -0
  122. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  123. data/test/fixtures/categories_ordered.yml +7 -0
  124. data/test/fixtures/categories_posts.yml +23 -0
  125. data/test/fixtures/categorization.rb +5 -0
  126. data/test/fixtures/categorizations.yml +17 -0
  127. data/test/fixtures/category.rb +20 -0
  128. data/test/fixtures/column_name.rb +3 -0
  129. data/test/fixtures/comment.rb +23 -0
  130. data/test/fixtures/comments.yml +59 -0
  131. data/test/fixtures/companies.yml +55 -0
  132. data/test/fixtures/company.rb +107 -0
  133. data/test/fixtures/company_in_module.rb +59 -0
  134. data/test/fixtures/computer.rb +3 -0
  135. data/test/fixtures/computers.yml +4 -0
  136. data/test/fixtures/course.rb +3 -0
  137. data/test/fixtures/courses.yml +7 -0
  138. data/test/fixtures/customer.rb +55 -0
  139. data/test/fixtures/customers.yml +17 -0
  140. data/test/fixtures/db_definitions/db2.drop.sql +32 -0
  141. data/test/fixtures/db_definitions/db2.sql +231 -0
  142. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  143. data/test/fixtures/db_definitions/db22.sql +5 -0
  144. data/test/fixtures/db_definitions/firebird.drop.sql +63 -0
  145. data/test/fixtures/db_definitions/firebird.sql +304 -0
  146. data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
  147. data/test/fixtures/db_definitions/firebird2.sql +6 -0
  148. data/test/fixtures/db_definitions/frontbase.drop.sql +32 -0
  149. data/test/fixtures/db_definitions/frontbase.sql +268 -0
  150. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  151. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  152. data/test/fixtures/db_definitions/mysql.drop.sql +32 -0
  153. data/test/fixtures/db_definitions/mysql.sql +234 -0
  154. data/test/fixtures/db_definitions/mysql2.drop.sql +2 -0
  155. data/test/fixtures/db_definitions/mysql2.sql +5 -0
  156. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  157. data/test/fixtures/db_definitions/openbase.sql +302 -0
  158. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  159. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  160. data/test/fixtures/db_definitions/oracle.drop.sql +65 -0
  161. data/test/fixtures/db_definitions/oracle.sql +325 -0
  162. data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
  163. data/test/fixtures/db_definitions/oracle2.sql +6 -0
  164. data/test/fixtures/db_definitions/postgresql.drop.sql +37 -0
  165. data/test/fixtures/db_definitions/postgresql.sql +263 -0
  166. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  167. data/test/fixtures/db_definitions/postgresql2.sql +5 -0
  168. data/test/fixtures/db_definitions/schema.rb +60 -0
  169. data/test/fixtures/db_definitions/sqlite.drop.sql +32 -0
  170. data/test/fixtures/db_definitions/sqlite.sql +215 -0
  171. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  172. data/test/fixtures/db_definitions/sqlite2.sql +5 -0
  173. data/test/fixtures/db_definitions/sqlserver.drop.sql +34 -0
  174. data/test/fixtures/db_definitions/sqlserver.sql +243 -0
  175. data/test/fixtures/db_definitions/sqlserver2.drop.sql +2 -0
  176. data/test/fixtures/db_definitions/sqlserver2.sql +5 -0
  177. data/test/fixtures/db_definitions/sybase.drop.sql +34 -0
  178. data/test/fixtures/db_definitions/sybase.sql +218 -0
  179. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  180. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  181. data/test/fixtures/default.rb +2 -0
  182. data/test/fixtures/developer.rb +52 -0
  183. data/test/fixtures/developers.yml +21 -0
  184. data/test/fixtures/developers_projects.yml +17 -0
  185. data/test/fixtures/developers_projects/david_action_controller +3 -0
  186. data/test/fixtures/developers_projects/david_active_record +3 -0
  187. data/test/fixtures/developers_projects/jamis_active_record +2 -0
  188. data/test/fixtures/edge.rb +5 -0
  189. data/test/fixtures/edges.yml +6 -0
  190. data/test/fixtures/entrant.rb +3 -0
  191. data/test/fixtures/entrants.yml +14 -0
  192. data/test/fixtures/fk_test_has_fk.yml +3 -0
  193. data/test/fixtures/fk_test_has_pk.yml +2 -0
  194. data/test/fixtures/flowers.jpg +0 -0
  195. data/test/fixtures/funny_jokes.yml +10 -0
  196. data/test/fixtures/joke.rb +6 -0
  197. data/test/fixtures/keyboard.rb +3 -0
  198. data/test/fixtures/legacy_thing.rb +3 -0
  199. data/test/fixtures/legacy_things.yml +3 -0
  200. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  201. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  202. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  203. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  204. data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
  205. data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
  206. data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
  207. data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
  208. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  209. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  210. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  211. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  212. data/test/fixtures/mixed_case_monkey.rb +3 -0
  213. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  214. data/test/fixtures/mixin.rb +63 -0
  215. data/test/fixtures/mixins.yml +127 -0
  216. data/test/fixtures/movie.rb +5 -0
  217. data/test/fixtures/movies.yml +7 -0
  218. data/test/fixtures/naked/csv/accounts.csv +1 -0
  219. data/test/fixtures/naked/yml/accounts.yml +1 -0
  220. data/test/fixtures/naked/yml/companies.yml +1 -0
  221. data/test/fixtures/naked/yml/courses.yml +1 -0
  222. data/test/fixtures/order.rb +4 -0
  223. data/test/fixtures/people.yml +3 -0
  224. data/test/fixtures/person.rb +4 -0
  225. data/test/fixtures/post.rb +58 -0
  226. data/test/fixtures/posts.yml +48 -0
  227. data/test/fixtures/project.rb +27 -0
  228. data/test/fixtures/projects.yml +7 -0
  229. data/test/fixtures/reader.rb +4 -0
  230. data/test/fixtures/readers.yml +4 -0
  231. data/test/fixtures/reply.rb +37 -0
  232. data/test/fixtures/subject.rb +4 -0
  233. data/test/fixtures/subscriber.rb +6 -0
  234. data/test/fixtures/subscribers/first +2 -0
  235. data/test/fixtures/subscribers/second +2 -0
  236. data/test/fixtures/tag.rb +7 -0
  237. data/test/fixtures/tagging.rb +6 -0
  238. data/test/fixtures/taggings.yml +18 -0
  239. data/test/fixtures/tags.yml +7 -0
  240. data/test/fixtures/task.rb +3 -0
  241. data/test/fixtures/tasks.yml +7 -0
  242. data/test/fixtures/topic.rb +25 -0
  243. data/test/fixtures/topics.yml +22 -0
  244. data/test/fixtures/vertex.rb +9 -0
  245. data/test/fixtures/vertices.yml +4 -0
  246. data/test/fixtures_test.rb +401 -0
  247. data/test/inheritance_test.rb +205 -0
  248. data/test/lifecycle_test.rb +137 -0
  249. data/test/locking_test.rb +190 -0
  250. data/test/method_scoping_test.rb +416 -0
  251. data/test/migration_test.rb +768 -0
  252. data/test/migration_test_firebird.rb +124 -0
  253. data/test/mixin_nested_set_test.rb +196 -0
  254. data/test/mixin_test.rb +550 -0
  255. data/test/modules_test.rb +34 -0
  256. data/test/multiple_db_test.rb +60 -0
  257. data/test/pk_test.rb +104 -0
  258. data/test/readonly_test.rb +107 -0
  259. data/test/reflection_test.rb +159 -0
  260. data/test/schema_authorization_test_postgresql.rb +75 -0
  261. data/test/schema_dumper_test.rb +96 -0
  262. data/test/schema_test_postgresql.rb +64 -0
  263. data/test/synonym_test_oracle.rb +17 -0
  264. data/test/table_name_test_sqlserver.rb +23 -0
  265. data/test/threaded_connections_test.rb +48 -0
  266. data/test/transactions_test.rb +230 -0
  267. data/test/unconnected_test.rb +32 -0
  268. data/test/validations_test.rb +1097 -0
  269. data/test/xml_serialization_test.rb +125 -0
  270. metadata +365 -0
@@ -0,0 +1,106 @@
1
+ module ActiveRecord
2
+ module Locking
3
+ # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
4
+ # record increments the lock_version column and the locking facilities ensure that records instantiated twice
5
+ # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
6
+ #
7
+ # p1 = Person.find(1)
8
+ # p2 = Person.find(1)
9
+ #
10
+ # p1.first_name = "Michael"
11
+ # p1.save
12
+ #
13
+ # p2.first_name = "should fail"
14
+ # p2.save # Raises a ActiveRecord::StaleObjectError
15
+ #
16
+ # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
17
+ # or otherwise apply the business logic needed to resolve the conflict.
18
+ #
19
+ # You must ensure that your database schema defaults the lock_version column to 0.
20
+ #
21
+ # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
22
+ # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
23
+ # This method uses the same syntax as <tt>set_table_name</tt>
24
+ module Optimistic
25
+ def self.included(base) #:nodoc:
26
+ super
27
+ base.extend ClassMethods
28
+
29
+ base.cattr_accessor :lock_optimistically, :instance_writer => false
30
+ base.lock_optimistically = true
31
+
32
+ base.alias_method_chain :update, :lock
33
+ base.alias_method_chain :attributes_from_column_definition, :lock
34
+
35
+ class << base
36
+ alias_method :locking_column=, :set_locking_column
37
+ end
38
+ end
39
+
40
+ def locking_enabled? #:nodoc:
41
+ lock_optimistically && respond_to?(self.class.locking_column)
42
+ end
43
+
44
+ def attributes_from_column_definition_with_lock
45
+ result = attributes_from_column_definition_without_lock
46
+
47
+ # If the locking column has no default value set,
48
+ # start the lock version at zero. Note we can't use
49
+ # locking_enabled? at this point as @attributes may
50
+ # not have been initialized yet
51
+
52
+ if lock_optimistically && result.include?(self.class.locking_column)
53
+ result[self.class.locking_column] ||= 0
54
+ end
55
+
56
+ return result
57
+ end
58
+
59
+ def update_with_lock #:nodoc:
60
+ return update_without_lock unless locking_enabled?
61
+
62
+ lock_col = self.class.locking_column
63
+ previous_value = send(lock_col)
64
+ send(lock_col + '=', previous_value + 1)
65
+
66
+ affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
67
+ UPDATE #{self.class.table_name}
68
+ SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
69
+ WHERE #{self.class.primary_key} = #{quote_value(id)}
70
+ AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
71
+ end_sql
72
+
73
+ unless affected_rows == 1
74
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
75
+ end
76
+
77
+ return true
78
+ end
79
+
80
+ module ClassMethods
81
+ DEFAULT_LOCKING_COLUMN = 'lock_version'
82
+
83
+ # Set the column to use for optimistic locking. Defaults to lock_version.
84
+ def set_locking_column(value = nil, &block)
85
+ define_attr_method :locking_column, value, &block
86
+ value
87
+ end
88
+
89
+ # The version column used for optimistic locking. Defaults to lock_version.
90
+ def locking_column
91
+ reset_locking_column
92
+ end
93
+
94
+ # Quote the column name used for optimistic locking.
95
+ def quoted_locking_column
96
+ connection.quote_column_name(locking_column)
97
+ end
98
+
99
+ # Reset the column used for optimistic locking back to the lock_version default.
100
+ def reset_locking_column
101
+ set_locking_column DEFAULT_LOCKING_COLUMN
102
+ end
103
+ end
104
+ end
105
+ end
106
+ 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,394 @@
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
+ # Migrations can manage the evolution of a schema used by several physical databases. It's a solution
12
+ # to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
13
+ # push that change to other developers and to the production server. With migrations, you can describe the transformations
14
+ # in self-contained classes that can be checked into version control systems and executed against another database that
15
+ # might be one, two, or five versions behind.
16
+ #
17
+ # Example of a simple migration:
18
+ #
19
+ # class AddSsl < ActiveRecord::Migration
20
+ # def self.up
21
+ # add_column :accounts, :ssl_enabled, :boolean, :default => 1
22
+ # end
23
+ #
24
+ # def self.down
25
+ # remove_column :accounts, :ssl_enabled
26
+ # end
27
+ # end
28
+ #
29
+ # This migration will add a boolean flag to the accounts table and remove it again, if you're backing out of the migration.
30
+ # It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
31
+ # or remove the migration. These methods can consist of both the migration specific methods, like add_column and remove_column,
32
+ # but may also contain regular Ruby code for generating data needed for the transformations.
33
+ #
34
+ # Example of a more complex migration that also needs to initialize data:
35
+ #
36
+ # class AddSystemSettings < ActiveRecord::Migration
37
+ # def self.up
38
+ # create_table :system_settings do |t|
39
+ # t.column :name, :string
40
+ # t.column :label, :string
41
+ # t.column :value, :text
42
+ # t.column :type, :string
43
+ # t.column :position, :integer
44
+ # end
45
+ #
46
+ # SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
47
+ # end
48
+ #
49
+ # def self.down
50
+ # drop_table :system_settings
51
+ # end
52
+ # end
53
+ #
54
+ # This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
55
+ # that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
56
+ # in one block call.
57
+ #
58
+ # == Available transformations
59
+ #
60
+ # * <tt>create_table(name, options)</tt> Creates a table called +name+ and makes the table object available to a block
61
+ # that can then add columns to it, following the same format as add_column. See example above. The options hash is for
62
+ # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
63
+ # * <tt>drop_table(name)</tt>: Drops the table called +name+.
64
+ # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+.
65
+ # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
66
+ # named +column_name+ specified to be one of the following types:
67
+ # :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time,
68
+ # :date, :binary, :boolean. A default value can be specified by passing an
69
+ # +options+ hash like { :default => 11 }. Other options include :limit and :null (e.g. { :limit => 50, :null => false })
70
+ # -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
71
+ # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
72
+ # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
73
+ # parameters as add_column.
74
+ # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
75
+ # * <tt>add_index(table_name, column_names, index_type, index_name)</tt>: Add a new index with the name of the column, or +index_name+ (if specified) on the column(s). Specify an optional +index_type+ (e.g. UNIQUE).
76
+ # * <tt>remove_index(table_name, index_name)</tt>: Remove the index specified by +index_name+.
77
+ #
78
+ # == Irreversible transformations
79
+ #
80
+ # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
81
+ # an <tt>IrreversibleMigration</tt> exception in their +down+ method.
82
+ #
83
+ # == Running migrations from within Rails
84
+ #
85
+ # The Rails package has several tools to help create and apply migrations.
86
+ #
87
+ # To generate a new migration, use <tt>script/generate migration MyNewMigration</tt>
88
+ # where MyNewMigration is the name of your migration. The generator will
89
+ # create a file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
90
+ # directory, where <tt>nnn</tt> is the next largest migration number.
91
+ # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
92
+ # n MyNewMigration.
93
+ #
94
+ # To run migrations against the currently configured database, use
95
+ # <tt>rake migrate</tt>. This will update the database by running all of the
96
+ # pending migrations, creating the <tt>schema_info</tt> table if missing.
97
+ #
98
+ # To roll the database back to a previous migration version, use
99
+ # <tt>rake migrate VERSION=X</tt> where <tt>X</tt> is the version to which
100
+ # you wish to downgrade. If any of the migrations throw an
101
+ # <tt>IrreversibleMigration</tt> exception, that step will fail and you'll
102
+ # have some manual work to do.
103
+ #
104
+ # == Database support
105
+ #
106
+ # Migrations are currently supported in MySQL, PostgreSQL, SQLite,
107
+ # SQL Server, Sybase, and Oracle (all supported databases except DB2).
108
+ #
109
+ # == More examples
110
+ #
111
+ # Not all migrations change the schema. Some just fix the data:
112
+ #
113
+ # class RemoveEmptyTags < ActiveRecord::Migration
114
+ # def self.up
115
+ # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
116
+ # end
117
+ #
118
+ # def self.down
119
+ # # not much we can do to restore deleted data
120
+ # raise IrreversibleMigration
121
+ # end
122
+ # end
123
+ #
124
+ # Others remove columns when they migrate up instead of down:
125
+ #
126
+ # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
127
+ # def self.up
128
+ # remove_column :items, :incomplete_items_count
129
+ # remove_column :items, :completed_items_count
130
+ # end
131
+ #
132
+ # def self.down
133
+ # add_column :items, :incomplete_items_count
134
+ # add_column :items, :completed_items_count
135
+ # end
136
+ # end
137
+ #
138
+ # And sometimes you need to do something in SQL not abstracted directly by migrations:
139
+ #
140
+ # class MakeJoinUnique < ActiveRecord::Migration
141
+ # def self.up
142
+ # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
143
+ # end
144
+ #
145
+ # def self.down
146
+ # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
147
+ # end
148
+ # end
149
+ #
150
+ # == Using a model after changing its table
151
+ #
152
+ # Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need
153
+ # to make a call to Base#reset_column_information in order to ensure that the model has the latest column data from
154
+ # after the new column was added. Example:
155
+ #
156
+ # class AddPeopleSalary < ActiveRecord::Migration
157
+ # def self.up
158
+ # add_column :people, :salary, :integer
159
+ # Person.reset_column_information
160
+ # Person.find(:all).each do |p|
161
+ # p.update_attribute :salary, SalaryCalculator.compute(p)
162
+ # end
163
+ # end
164
+ # end
165
+ #
166
+ # == Controlling verbosity
167
+ #
168
+ # By default, migrations will describe the actions they are taking, writing
169
+ # them to the console as they happen, along with benchmarks describing how
170
+ # long each step took.
171
+ #
172
+ # You can quiet them down by setting ActiveRecord::Migration.verbose = false.
173
+ #
174
+ # You can also insert your own messages and benchmarks by using the #say_with_time
175
+ # method:
176
+ #
177
+ # def self.up
178
+ # ...
179
+ # say_with_time "Updating salaries..." do
180
+ # Person.find(:all).each do |p|
181
+ # p.update_attribute :salary, SalaryCalculator.compute(p)
182
+ # end
183
+ # end
184
+ # ...
185
+ # end
186
+ #
187
+ # The phrase "Updating salaries..." would then be printed, along with the
188
+ # benchmark for the block when the block completes.
189
+ class Migration
190
+ @@verbose = true
191
+ cattr_accessor :verbose
192
+
193
+ class << self
194
+ def up_using_benchmarks #:nodoc:
195
+ migrate(:up)
196
+ end
197
+
198
+ def down_using_benchmarks #:nodoc:
199
+ migrate(:down)
200
+ end
201
+
202
+ # Execute this migration in the named direction
203
+ def migrate(direction)
204
+ return unless respond_to?(direction)
205
+
206
+ case direction
207
+ when :up then announce "migrating"
208
+ when :down then announce "reverting"
209
+ end
210
+
211
+ result = nil
212
+ time = Benchmark.measure { result = send("real_#{direction}") }
213
+
214
+ case direction
215
+ when :up then announce "migrated (%.4fs)" % time.real; write
216
+ when :down then announce "reverted (%.4fs)" % time.real; write
217
+ end
218
+
219
+ result
220
+ end
221
+
222
+ # Because the method added may do an alias_method, it can be invoked
223
+ # recursively. We use @ignore_new_methods as a guard to indicate whether
224
+ # it is safe for the call to proceed.
225
+ def singleton_method_added(sym) #:nodoc:
226
+ return if @ignore_new_methods
227
+
228
+ begin
229
+ @ignore_new_methods = true
230
+
231
+ case sym
232
+ when :up, :down
233
+ klass = (class << self; self; end)
234
+ klass.send(:alias_method, "real_#{sym}", sym)
235
+ klass.send(:alias_method, sym, "#{sym}_using_benchmarks")
236
+ end
237
+ ensure
238
+ @ignore_new_methods = false
239
+ end
240
+ end
241
+
242
+ def write(text="")
243
+ puts(text) if verbose
244
+ end
245
+
246
+ def announce(message)
247
+ text = "#{name}: #{message}"
248
+ length = [0, 75 - text.length].max
249
+ write "== %s %s" % [text, "=" * length]
250
+ end
251
+
252
+ def say(message, subitem=false)
253
+ write "#{subitem ? " ->" : "--"} #{message}"
254
+ end
255
+
256
+ def say_with_time(message)
257
+ say(message)
258
+ result = nil
259
+ time = Benchmark.measure { result = yield }
260
+ say "%.4fs" % time.real, :subitem
261
+ result
262
+ end
263
+
264
+ def suppress_messages
265
+ save = verbose
266
+ self.verbose = false
267
+ yield
268
+ ensure
269
+ self.verbose = save
270
+ end
271
+
272
+ def method_missing(method, *arguments, &block)
273
+ say_with_time "#{method}(#{arguments.map { |a| a.inspect }.join(", ")})" do
274
+ arguments[0] = Migrator.proper_table_name(arguments.first) unless arguments.empty? || method == :execute
275
+ ActiveRecord::Base.connection.send(method, *arguments, &block)
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ class Migrator#:nodoc:
282
+ class << self
283
+ def migrate(migrations_path, target_version = nil)
284
+ Base.connection.initialize_schema_information
285
+
286
+ case
287
+ when target_version.nil?, current_version < target_version
288
+ up(migrations_path, target_version)
289
+ when current_version > target_version
290
+ down(migrations_path, target_version)
291
+ when current_version == target_version
292
+ return # You're on the right version
293
+ end
294
+ end
295
+
296
+ def up(migrations_path, target_version = nil)
297
+ self.new(:up, migrations_path, target_version).migrate
298
+ end
299
+
300
+ def down(migrations_path, target_version = nil)
301
+ self.new(:down, migrations_path, target_version).migrate
302
+ end
303
+
304
+ def schema_info_table_name
305
+ Base.table_name_prefix + "schema_info" + Base.table_name_suffix
306
+ end
307
+
308
+ def current_version
309
+ (Base.connection.select_one("SELECT version FROM #{schema_info_table_name}") || {"version" => 0})["version"].to_i
310
+ end
311
+
312
+ def proper_table_name(name)
313
+ # Use the ActiveRecord objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
314
+ name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
315
+ end
316
+
317
+ end
318
+
319
+ def initialize(direction, migrations_path, target_version = nil)
320
+ raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
321
+ @direction, @migrations_path, @target_version = direction, migrations_path, target_version
322
+ Base.connection.initialize_schema_information
323
+ end
324
+
325
+ def current_version
326
+ self.class.current_version
327
+ end
328
+
329
+ def migrate
330
+ migration_classes.each do |(version, migration_class)|
331
+ Base.logger.info("Reached target version: #{@target_version}") and break if reached_target_version?(version)
332
+ next if irrelevant_migration?(version)
333
+
334
+ Base.logger.info "Migrating to #{migration_class} (#{version})"
335
+ migration_class.migrate(@direction)
336
+ set_schema_version(version)
337
+ end
338
+ end
339
+
340
+ private
341
+ def migration_classes
342
+ migrations = migration_files.inject([]) do |migrations, migration_file|
343
+ load(migration_file)
344
+ version, name = migration_version_and_name(migration_file)
345
+ assert_unique_migration_version(migrations, version.to_i)
346
+ migrations << [ version.to_i, migration_class(name) ]
347
+ end
348
+
349
+ down? ? migrations.sort.reverse : migrations.sort
350
+ end
351
+
352
+ def assert_unique_migration_version(migrations, version)
353
+ if !migrations.empty? && migrations.transpose.first.include?(version)
354
+ raise DuplicateMigrationVersionError.new(version)
355
+ end
356
+ end
357
+
358
+ def migration_files
359
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f|
360
+ migration_version_and_name(f).first.to_i
361
+ end
362
+ down? ? files.reverse : files
363
+ end
364
+
365
+ def migration_class(migration_name)
366
+ migration_name.camelize.constantize
367
+ end
368
+
369
+ def migration_version_and_name(migration_file)
370
+ return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
371
+ end
372
+
373
+ def set_schema_version(version)
374
+ Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}")
375
+ end
376
+
377
+ def up?
378
+ @direction == :up
379
+ end
380
+
381
+ def down?
382
+ @direction == :down
383
+ end
384
+
385
+ def reached_target_version?(version)
386
+ return false if @target_version == nil
387
+ (up? && version.to_i - 1 >= @target_version) || (down? && version.to_i <= @target_version)
388
+ end
389
+
390
+ def irrelevant_migration?(version)
391
+ (up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
392
+ end
393
+ end
394
+ end