activerecord 1.0.0 → 4.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 (255) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +213 -0
  5. data/examples/performance.rb +172 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record/aggregations.rb +180 -84
  8. data/lib/active_record/associations/alias_tracker.rb +76 -0
  9. data/lib/active_record/associations/association.rb +248 -0
  10. data/lib/active_record/associations/association_scope.rb +135 -0
  11. data/lib/active_record/associations/belongs_to_association.rb +92 -0
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +35 -0
  13. data/lib/active_record/associations/builder/association.rb +108 -0
  14. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  15. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  17. data/lib/active_record/associations/builder/has_many.rb +15 -0
  18. data/lib/active_record/associations/builder/has_one.rb +25 -0
  19. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  20. data/lib/active_record/associations/collection_association.rb +608 -0
  21. data/lib/active_record/associations/collection_proxy.rb +986 -0
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +58 -39
  23. data/lib/active_record/associations/has_many_association.rb +116 -85
  24. data/lib/active_record/associations/has_many_through_association.rb +197 -0
  25. data/lib/active_record/associations/has_one_association.rb +102 -0
  26. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_dependency.rb +235 -0
  31. data/lib/active_record/associations/join_helper.rb +45 -0
  32. data/lib/active_record/associations/preloader/association.rb +121 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  42. data/lib/active_record/associations/preloader.rb +178 -0
  43. data/lib/active_record/associations/singular_association.rb +64 -0
  44. data/lib/active_record/associations/through_association.rb +87 -0
  45. data/lib/active_record/associations.rb +1437 -431
  46. data/lib/active_record/attribute_assignment.rb +201 -0
  47. data/lib/active_record/attribute_methods/before_type_cast.rb +70 -0
  48. data/lib/active_record/attribute_methods/dirty.rb +118 -0
  49. data/lib/active_record/attribute_methods/primary_key.rb +122 -0
  50. data/lib/active_record/attribute_methods/query.rb +40 -0
  51. data/lib/active_record/attribute_methods/read.rb +107 -0
  52. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  53. data/lib/active_record/attribute_methods/time_zone_conversion.rb +59 -0
  54. data/lib/active_record/attribute_methods/write.rb +63 -0
  55. data/lib/active_record/attribute_methods.rb +393 -0
  56. data/lib/active_record/autosave_association.rb +426 -0
  57. data/lib/active_record/base.rb +268 -930
  58. data/lib/active_record/callbacks.rb +203 -230
  59. data/lib/active_record/coders/yaml_column.rb +38 -0
  60. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +638 -0
  61. data/lib/active_record/connection_adapters/abstract/database_limits.rb +67 -0
  62. data/lib/active_record/connection_adapters/abstract/database_statements.rb +390 -0
  63. data/lib/active_record/connection_adapters/abstract/query_cache.rb +95 -0
  64. data/lib/active_record/connection_adapters/abstract/quoting.rb +129 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +501 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  67. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +873 -0
  68. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  69. data/lib/active_record/connection_adapters/abstract_adapter.rb +389 -275
  70. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  71. data/lib/active_record/connection_adapters/column.rb +318 -0
  72. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +517 -90
  75. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  76. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  77. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  78. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  79. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  80. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  81. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  82. data/lib/active_record/connection_adapters/postgresql_adapter.rb +911 -138
  83. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  84. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +624 -0
  85. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  86. data/lib/active_record/connection_handling.rb +98 -0
  87. data/lib/active_record/core.rb +463 -0
  88. data/lib/active_record/counter_cache.rb +122 -0
  89. data/lib/active_record/dynamic_matchers.rb +131 -0
  90. data/lib/active_record/errors.rb +213 -0
  91. data/lib/active_record/explain.rb +38 -0
  92. data/lib/active_record/explain_registry.rb +30 -0
  93. data/lib/active_record/explain_subscriber.rb +29 -0
  94. data/lib/active_record/fixture_set/file.rb +55 -0
  95. data/lib/active_record/fixtures.rb +892 -138
  96. data/lib/active_record/inheritance.rb +200 -0
  97. data/lib/active_record/integration.rb +60 -0
  98. data/lib/active_record/locale/en.yml +47 -0
  99. data/lib/active_record/locking/optimistic.rb +181 -0
  100. data/lib/active_record/locking/pessimistic.rb +77 -0
  101. data/lib/active_record/log_subscriber.rb +82 -0
  102. data/lib/active_record/migration/command_recorder.rb +164 -0
  103. data/lib/active_record/migration/join_table.rb +15 -0
  104. data/lib/active_record/migration.rb +1015 -0
  105. data/lib/active_record/model_schema.rb +345 -0
  106. data/lib/active_record/nested_attributes.rb +546 -0
  107. data/lib/active_record/null_relation.rb +65 -0
  108. data/lib/active_record/persistence.rb +509 -0
  109. data/lib/active_record/query_cache.rb +56 -0
  110. data/lib/active_record/querying.rb +62 -0
  111. data/lib/active_record/railtie.rb +205 -0
  112. data/lib/active_record/railties/console_sandbox.rb +5 -0
  113. data/lib/active_record/railties/controller_runtime.rb +50 -0
  114. data/lib/active_record/railties/databases.rake +402 -0
  115. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  116. data/lib/active_record/readonly_attributes.rb +30 -0
  117. data/lib/active_record/reflection.rb +544 -87
  118. data/lib/active_record/relation/batches.rb +93 -0
  119. data/lib/active_record/relation/calculations.rb +399 -0
  120. data/lib/active_record/relation/delegation.rb +125 -0
  121. data/lib/active_record/relation/finder_methods.rb +349 -0
  122. data/lib/active_record/relation/merger.rb +161 -0
  123. data/lib/active_record/relation/predicate_builder.rb +106 -0
  124. data/lib/active_record/relation/query_methods.rb +1044 -0
  125. data/lib/active_record/relation/spawn_methods.rb +73 -0
  126. data/lib/active_record/relation.rb +655 -0
  127. data/lib/active_record/result.rb +67 -0
  128. data/lib/active_record/runtime_registry.rb +17 -0
  129. data/lib/active_record/sanitization.rb +168 -0
  130. data/lib/active_record/schema.rb +65 -0
  131. data/lib/active_record/schema_dumper.rb +204 -0
  132. data/lib/active_record/schema_migration.rb +39 -0
  133. data/lib/active_record/scoping/default.rb +146 -0
  134. data/lib/active_record/scoping/named.rb +175 -0
  135. data/lib/active_record/scoping.rb +82 -0
  136. data/lib/active_record/serialization.rb +22 -0
  137. data/lib/active_record/serializers/xml_serializer.rb +197 -0
  138. data/lib/active_record/statement_cache.rb +26 -0
  139. data/lib/active_record/store.rb +156 -0
  140. data/lib/active_record/tasks/database_tasks.rb +203 -0
  141. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  142. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  143. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  144. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  145. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  146. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  147. data/lib/active_record/test_case.rb +96 -0
  148. data/lib/active_record/timestamp.rb +119 -0
  149. data/lib/active_record/transactions.rb +366 -69
  150. data/lib/active_record/translation.rb +22 -0
  151. data/lib/active_record/validations/associated.rb +49 -0
  152. data/lib/active_record/validations/presence.rb +65 -0
  153. data/lib/active_record/validations/uniqueness.rb +225 -0
  154. data/lib/active_record/validations.rb +64 -185
  155. data/lib/active_record/version.rb +11 -0
  156. data/lib/active_record.rb +149 -24
  157. data/lib/rails/generators/active_record/migration/migration_generator.rb +62 -0
  158. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  159. data/lib/rails/generators/active_record/migration/templates/migration.rb +39 -0
  160. data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
  161. data/lib/rails/generators/active_record/model/templates/model.rb +10 -0
  162. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  163. data/lib/rails/generators/active_record.rb +23 -0
  164. metadata +261 -161
  165. data/CHANGELOG +0 -581
  166. data/README +0 -361
  167. data/RUNNING_UNIT_TESTS +0 -36
  168. data/dev-utils/eval_debugger.rb +0 -9
  169. data/examples/associations.png +0 -0
  170. data/examples/associations.rb +0 -87
  171. data/examples/shared_setup.rb +0 -15
  172. data/examples/validation.rb +0 -88
  173. data/install.rb +0 -60
  174. data/lib/active_record/associations/association_collection.rb +0 -70
  175. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -107
  176. data/lib/active_record/deprecated_associations.rb +0 -70
  177. data/lib/active_record/observer.rb +0 -71
  178. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  179. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  180. data/lib/active_record/support/clean_logger.rb +0 -10
  181. data/lib/active_record/support/inflector.rb +0 -70
  182. data/lib/active_record/vendor/mysql.rb +0 -1117
  183. data/lib/active_record/vendor/simple.rb +0 -702
  184. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  185. data/lib/active_record/wrappings.rb +0 -59
  186. data/rakefile +0 -122
  187. data/test/abstract_unit.rb +0 -16
  188. data/test/aggregations_test.rb +0 -34
  189. data/test/all.sh +0 -8
  190. data/test/associations_test.rb +0 -477
  191. data/test/base_test.rb +0 -513
  192. data/test/class_inheritable_attributes_test.rb +0 -33
  193. data/test/connections/native_mysql/connection.rb +0 -24
  194. data/test/connections/native_postgresql/connection.rb +0 -24
  195. data/test/connections/native_sqlite/connection.rb +0 -24
  196. data/test/deprecated_associations_test.rb +0 -336
  197. data/test/finder_test.rb +0 -67
  198. data/test/fixtures/accounts/signals37 +0 -3
  199. data/test/fixtures/accounts/unknown +0 -2
  200. data/test/fixtures/auto_id.rb +0 -4
  201. data/test/fixtures/column_name.rb +0 -3
  202. data/test/fixtures/companies/first_client +0 -6
  203. data/test/fixtures/companies/first_firm +0 -4
  204. data/test/fixtures/companies/second_client +0 -6
  205. data/test/fixtures/company.rb +0 -37
  206. data/test/fixtures/company_in_module.rb +0 -33
  207. data/test/fixtures/course.rb +0 -3
  208. data/test/fixtures/courses/java +0 -2
  209. data/test/fixtures/courses/ruby +0 -2
  210. data/test/fixtures/customer.rb +0 -30
  211. data/test/fixtures/customers/david +0 -6
  212. data/test/fixtures/db_definitions/mysql.sql +0 -96
  213. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  214. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  215. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  216. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  217. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  218. data/test/fixtures/default.rb +0 -2
  219. data/test/fixtures/developer.rb +0 -8
  220. data/test/fixtures/developers/david +0 -2
  221. data/test/fixtures/developers/jamis +0 -2
  222. data/test/fixtures/developers_projects/david_action_controller +0 -2
  223. data/test/fixtures/developers_projects/david_active_record +0 -2
  224. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  225. data/test/fixtures/entrant.rb +0 -3
  226. data/test/fixtures/entrants/first +0 -3
  227. data/test/fixtures/entrants/second +0 -3
  228. data/test/fixtures/entrants/third +0 -3
  229. data/test/fixtures/fixture_database.sqlite +0 -0
  230. data/test/fixtures/fixture_database_2.sqlite +0 -0
  231. data/test/fixtures/movie.rb +0 -5
  232. data/test/fixtures/movies/first +0 -2
  233. data/test/fixtures/movies/second +0 -2
  234. data/test/fixtures/project.rb +0 -3
  235. data/test/fixtures/projects/action_controller +0 -2
  236. data/test/fixtures/projects/active_record +0 -2
  237. data/test/fixtures/reply.rb +0 -21
  238. data/test/fixtures/subscriber.rb +0 -5
  239. data/test/fixtures/subscribers/first +0 -2
  240. data/test/fixtures/subscribers/second +0 -2
  241. data/test/fixtures/topic.rb +0 -20
  242. data/test/fixtures/topics/first +0 -9
  243. data/test/fixtures/topics/second +0 -8
  244. data/test/fixtures_test.rb +0 -20
  245. data/test/inflector_test.rb +0 -104
  246. data/test/inheritance_test.rb +0 -125
  247. data/test/lifecycle_test.rb +0 -110
  248. data/test/modules_test.rb +0 -21
  249. data/test/multiple_db_test.rb +0 -46
  250. data/test/pk_test.rb +0 -57
  251. data/test/reflection_test.rb +0 -78
  252. data/test/thread_safety_test.rb +0 -33
  253. data/test/transactions_test.rb +0 -83
  254. data/test/unconnected_test.rb +0 -24
  255. data/test/validations_test.rb +0 -126
@@ -0,0 +1,225 @@
1
+ module ActiveRecord
2
+ module Validations
3
+ class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
4
+ def initialize(options)
5
+ if options[:conditions] && !options[:conditions].respond_to?(:call)
6
+ raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
7
+ "Pass a callable instead: `conditions: -> { where(approved: true) }`"
8
+ end
9
+ super({ case_sensitive: true }.merge!(options))
10
+ end
11
+
12
+ # Unfortunately, we have to tie Uniqueness validators to a class.
13
+ def setup(klass)
14
+ @klass = klass
15
+ end
16
+
17
+ def validate_each(record, attribute, value)
18
+ finder_class = find_finder_class_for(record)
19
+ table = finder_class.arel_table
20
+ value = deserialize_attribute(record, attribute, value)
21
+
22
+ relation = build_relation(finder_class, table, attribute, value)
23
+ relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted?
24
+ relation = scope_relation(record, table, relation)
25
+ relation = finder_class.unscoped.where(relation)
26
+ relation = relation.merge(options[:conditions]) if options[:conditions]
27
+
28
+ if relation.exists?
29
+ error_options = options.except(:case_sensitive, :scope, :conditions)
30
+ error_options[:value] = value
31
+
32
+ record.errors.add(attribute, :taken, error_options)
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ # The check for an existing value should be run from a class that
39
+ # isn't abstract. This means working down from the current class
40
+ # (self), to the first non-abstract class. Since classes don't know
41
+ # their subclasses, we have to build the hierarchy between self and
42
+ # the record's class.
43
+ def find_finder_class_for(record) #:nodoc:
44
+ class_hierarchy = [record.class]
45
+
46
+ while class_hierarchy.first != @klass
47
+ class_hierarchy.unshift(class_hierarchy.first.superclass)
48
+ end
49
+
50
+ class_hierarchy.detect { |klass| !klass.abstract_class? }
51
+ end
52
+
53
+ def build_relation(klass, table, attribute, value) #:nodoc:
54
+ if reflection = klass.reflect_on_association(attribute)
55
+ attribute = reflection.foreign_key
56
+ value = value.attributes[reflection.primary_key_column.name]
57
+ end
58
+
59
+ column = klass.columns_hash[attribute.to_s]
60
+ value = klass.connection.type_cast(value, column)
61
+ value = value.to_s[0, column.limit] if value && column.limit && column.text?
62
+
63
+ if !options[:case_sensitive] && value && column.text?
64
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
65
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
66
+ else
67
+ value = klass.connection.case_sensitive_modifier(value) unless value.nil?
68
+ table[attribute].eq(value)
69
+ end
70
+ end
71
+
72
+ def scope_relation(record, table, relation)
73
+ Array(options[:scope]).each do |scope_item|
74
+ if reflection = record.class.reflect_on_association(scope_item)
75
+ scope_value = record.send(reflection.foreign_key)
76
+ scope_item = reflection.foreign_key
77
+ else
78
+ scope_value = record.read_attribute(scope_item)
79
+ end
80
+ relation = relation.and(table[scope_item].eq(scope_value))
81
+ end
82
+
83
+ relation
84
+ end
85
+
86
+ def deserialize_attribute(record, attribute, value)
87
+ coder = record.class.serialized_attributes[attribute.to_s]
88
+ value = coder.dump value if value && coder
89
+ value
90
+ end
91
+ end
92
+
93
+ module ClassMethods
94
+ # Validates whether the value of the specified attributes are unique
95
+ # across the system. Useful for making sure that only one user
96
+ # can be named "davidhh".
97
+ #
98
+ # class Person < ActiveRecord::Base
99
+ # validates_uniqueness_of :user_name
100
+ # end
101
+ #
102
+ # It can also validate whether the value of the specified attributes are
103
+ # unique based on a <tt>:scope</tt> parameter:
104
+ #
105
+ # class Person < ActiveRecord::Base
106
+ # validates_uniqueness_of :user_name, scope: :account_id
107
+ # end
108
+ #
109
+ # Or even multiple scope parameters. For example, making sure that a
110
+ # teacher can only be on the schedule once per semester for a particular
111
+ # class.
112
+ #
113
+ # class TeacherSchedule < ActiveRecord::Base
114
+ # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
115
+ # end
116
+ #
117
+ # It is also possible to limit the uniqueness constraint to a set of
118
+ # records matching certain conditions. In this example archived articles
119
+ # are not being taken into consideration when validating uniqueness
120
+ # of the title attribute:
121
+ #
122
+ # class Article < ActiveRecord::Base
123
+ # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
124
+ # end
125
+ #
126
+ # When the record is created, a check is performed to make sure that no
127
+ # record exists in the database with the given value for the specified
128
+ # attribute (that maps to a column). When the record is updated,
129
+ # the same check is made but disregarding the record itself.
130
+ #
131
+ # Configuration options:
132
+ #
133
+ # * <tt>:message</tt> - Specifies a custom error message (default is:
134
+ # "has already been taken").
135
+ # * <tt>:scope</tt> - One or more columns by which to limit the scope of
136
+ # the uniqueness constraint.
137
+ # * <tt>:conditions</tt> - Specify the conditions to be included as a
138
+ # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
139
+ # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
140
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
141
+ # non-text columns (+true+ by default).
142
+ # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
143
+ # attribute is +nil+ (default is +false+).
144
+ # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
145
+ # attribute is blank (default is +false+).
146
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
147
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
148
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
149
+ # proc or string should return or evaluate to a +true+ or +false+ value.
150
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
151
+ # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>,
152
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
153
+ # method, proc or string should return or evaluate to a +true+ or +false+
154
+ # value.
155
+ #
156
+ # === Concurrency and integrity
157
+ #
158
+ # Using this validation method in conjunction with ActiveRecord::Base#save
159
+ # does not guarantee the absence of duplicate record insertions, because
160
+ # uniqueness checks on the application level are inherently prone to race
161
+ # conditions. For example, suppose that two users try to post a Comment at
162
+ # the same time, and a Comment's title must be unique. At the database-level,
163
+ # the actions performed by these users could be interleaved in the following manner:
164
+ #
165
+ # User 1 | User 2
166
+ # ------------------------------------+--------------------------------------
167
+ # # User 1 checks whether there's |
168
+ # # already a comment with the title |
169
+ # # 'My Post'. This is not the case. |
170
+ # SELECT * FROM comments |
171
+ # WHERE title = 'My Post' |
172
+ # |
173
+ # | # User 2 does the same thing and also
174
+ # | # infers that his title is unique.
175
+ # | SELECT * FROM comments
176
+ # | WHERE title = 'My Post'
177
+ # |
178
+ # # User 1 inserts his comment. |
179
+ # INSERT INTO comments |
180
+ # (title, content) VALUES |
181
+ # ('My Post', 'hi!') |
182
+ # |
183
+ # | # User 2 does the same thing.
184
+ # | INSERT INTO comments
185
+ # | (title, content) VALUES
186
+ # | ('My Post', 'hello!')
187
+ # |
188
+ # | # ^^^^^^
189
+ # | # Boom! We now have a duplicate
190
+ # | # title!
191
+ #
192
+ # This could even happen if you use transactions with the 'serializable'
193
+ # isolation level. The best way to work around this problem is to add a unique
194
+ # index to the database table using
195
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
196
+ # rare case that a race condition occurs, the database will guarantee
197
+ # the field's uniqueness.
198
+ #
199
+ # When the database catches such a duplicate insertion,
200
+ # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
201
+ # exception. You can either choose to let this error propagate (which
202
+ # will result in the default Rails exception page being shown), or you
203
+ # can catch it and restart the transaction (e.g. by telling the user
204
+ # that the title already exists, and asking him to re-enter the title).
205
+ # This technique is also known as optimistic concurrency control:
206
+ # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
207
+ #
208
+ # The bundled ActiveRecord::ConnectionAdapters distinguish unique index
209
+ # constraint errors from other types of database errors by throwing an
210
+ # ActiveRecord::RecordNotUnique exception. For other adapters you will
211
+ # have to parse the (database-specific) exception message to detect such
212
+ # a case.
213
+ #
214
+ # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
215
+ #
216
+ # * ActiveRecord::ConnectionAdapters::MysqlAdapter.
217
+ # * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
218
+ # * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
219
+ # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
220
+ def validates_uniqueness_of(*attr_names)
221
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
222
+ end
223
+ end
224
+ end
225
+ end
@@ -1,205 +1,84 @@
1
1
  module ActiveRecord
2
- # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
3
- # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
4
- # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
2
+ # = Active Record RecordInvalid
5
3
  #
6
- # Example:
4
+ # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
5
+ # +record+ method to retrieve the record which did not validate.
7
6
  #
8
- # class Person < ActiveRecord::Base
9
- # protected
10
- # def validate
11
- # errors.add_on_empty %w( first_name last_name )
12
- # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
13
- # end
14
- #
15
- # def validate_on_create # is only run the first time a new object is saved
16
- # unless valid_discount?(membership_discount)
17
- # errors.add("membership_discount", "has expired")
18
- # end
19
- # end
20
- #
21
- # def validate_on_update
22
- # errors.add_to_base("No changes have occured") if unchanged_attributes?
23
- # end
7
+ # begin
8
+ # complex_operation_that_calls_save!_internally
9
+ # rescue ActiveRecord::RecordInvalid => invalid
10
+ # puts invalid.record.errors
24
11
  # end
25
- #
26
- # person = Person.new("first_name" => "David", "phone_number" => "what?")
27
- # person.save # => false (and doesn't do the save)
28
- # person.errors.empty? # => false
29
- # person.count # => 2
30
- # person.errors.on "last_name" # => "can't be empty"
31
- # person.errors.on "phone_number" # => "has invalid format"
32
- # person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
33
- # "Phone number has invalid format"
34
- #
35
- # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
36
- # person.save # => true (and person is now saved in the database)
37
- #
38
- # An +Errors+ object is automatically created for every Active Record.
39
- module Validations
40
- def self.append_features(base) # :nodoc:
41
- super
42
-
43
- base.class_eval do
44
- alias_method :save_without_validation, :save
45
- alias_method :save, :save_with_validation
46
-
47
- alias_method :update_attribute_without_validation_skipping, :update_attribute
48
- alias_method :update_attribute, :update_attribute_with_validation_skipping
49
- end
50
- end
51
-
52
- # The validation process on save can be skipped by passing false. The regular Base#save method is
53
- # replaced with this when the validations module is mixed in, which it is by default.
54
- def save_with_validation(perform_validation = true)
55
- if perform_validation && valid? || !perform_validation then save_without_validation else false end
12
+ class RecordInvalid < ActiveRecordError
13
+ attr_reader :record # :nodoc:
14
+ def initialize(record) # :nodoc:
15
+ @record = record
16
+ errors = @record.errors.full_messages.join(", ")
17
+ super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
56
18
  end
57
-
58
- # Updates a single attribute and saves the record without going through the normal validation procedure.
59
- # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
60
- # in Base is replaced with this when the validations module is mixed in, which it is by default.
61
- def update_attribute_with_validation_skipping(name, value)
62
- @attributes[name] = value
63
- save(false)
64
- end
65
-
66
- # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
67
- def valid?
68
- errors.clear
69
- validate
70
- if new_record? then validate_on_create else validate_on_update end
71
- errors.empty?
72
- end
73
-
74
- # Returns the Errors object that holds all information about attribute error messages.
75
- def errors
76
- @errors = Errors.new(self) if @errors.nil?
77
- @errors
78
- end
79
-
80
- protected
81
- # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
82
- def validate #:doc:
83
- end
84
-
85
- # Overwrite this method for validation checks used only on creation.
86
- def validate_on_create #:doc:
87
- end
88
-
89
- # Overwrite this method for validation checks used only on updates.
90
- def validate_on_update # :doc:
91
- end
92
19
  end
93
20
 
94
- # Active Record validation is reported to and from this object, which is used by Base#save to
95
- # determine whether the object in a valid state to be saved. See usage example in Validations.
96
- class Errors
97
- def initialize(base) # :nodoc:
98
- @base, @errors = base, {}
99
- end
100
-
101
- # Adds an error to the base object instead of any particular attribute. This is used
102
- # to report errors that doesn't tie to any specific attribute, but rather to the object
103
- # as a whole. These error messages doesn't get prepended with any field name when iterating
104
- # with each_full, so they should be complete sentences.
105
- def add_to_base(msg)
106
- add(:base, msg)
107
- end
108
-
109
- # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
110
- # for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
111
- # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
112
- # If no +msg+ is supplied, "invalid" is assumed.
113
- def add(attribute, msg = "invalid")
114
- @errors[attribute] = [] if @errors[attribute].nil?
115
- @errors[attribute] << msg
116
- end
117
-
118
- # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
119
- def add_on_empty(attributes, msg = "can't be empty")
120
- [attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr) }
121
- end
122
-
123
- # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
124
- # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
125
- def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
126
- for attr in [attributes].flatten
127
- add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr) && @base.send(attr).length < range.begin
128
- add(attr, too_long_msg % range.end) if @base.attribute_present?(attr) && @base.send(attr).length > range.end
21
+ # = Active Record Validations
22
+ #
23
+ # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt>
24
+ # all of which accept the <tt>:on</tt> argument to define the context where the
25
+ # validations are active. Active Record will always supply either the context of
26
+ # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
27
+ # <tt>new_record?</tt>.
28
+ module Validations
29
+ extend ActiveSupport::Concern
30
+ include ActiveModel::Validations
31
+
32
+ module ClassMethods
33
+ # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
34
+ # so an exception is raised if the record is invalid.
35
+ def create!(attributes = nil, &block)
36
+ if attributes.is_a?(Array)
37
+ attributes.collect { |attr| create!(attr, &block) }
38
+ else
39
+ object = new(attributes)
40
+ yield(object) if block_given?
41
+ object.save!
42
+ object
43
+ end
129
44
  end
130
45
  end
131
46
 
132
- alias :add_on_boundry_breaking :add_on_boundary_breaking
133
-
134
- # Returns true if the specified +attribute+ has errors associated with it.
135
- def invalid?(attribute)
136
- !@errors[attribute].nil?
47
+ # The validation process on save can be skipped by passing <tt>validate: false</tt>.
48
+ # The regular Base#save method is replaced with this when the validations
49
+ # module is mixed in, which it is by default.
50
+ def save(options={})
51
+ perform_validations(options) ? super : false
137
52
  end
138
53
 
139
- # * Returns nil, if no errors are associated with the specified +attribute+.
140
- # * Returns the error message, if one error is associated with the specified +attribute+.
141
- # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
142
- def on(attribute)
143
- if @errors[attribute].nil?
144
- nil
145
- elsif @errors[attribute].length == 1
146
- @errors[attribute].first
147
- else
148
- @errors[attribute]
149
- end
54
+ # Attempts to save the record just like Base#save but will raise a +RecordInvalid+
55
+ # exception instead of returning +false+ if the record is not valid.
56
+ def save!(options={})
57
+ perform_validations(options) ? super : raise(RecordInvalid.new(self))
150
58
  end
151
59
 
152
- alias :[] :on
153
-
154
- # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
155
- def on_base
156
- on(:base)
157
- end
158
-
159
- # Yields each attribute and associated message per error added.
160
- def each
161
- @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
162
- end
163
-
164
- # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
165
- # through iteration as "First name can't be empty".
166
- def each_full
167
- full_messages.each { |msg| yield msg }
60
+ # Runs all the validations within the specified context. Returns +true+ if
61
+ # no errors are found, +false+ otherwise.
62
+ #
63
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
64
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
65
+ #
66
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
67
+ # some <tt>:on</tt> option will only run in the specified context.
68
+ def valid?(context = nil)
69
+ context ||= (new_record? ? :create : :update)
70
+ output = super(context)
71
+ errors.empty? && output
168
72
  end
169
73
 
170
- # Returns all the full error messages in an array.
171
- def full_messages
172
- full_messages = []
173
-
174
- @errors.each_key do |attr|
175
- @errors[attr].each do |msg|
176
- if attr == :base
177
- full_messages << msg
178
- else
179
- full_messages << @base.class.human_attribute_name(attr) + " " + msg
180
- end
181
- end
182
- end
183
-
184
- return full_messages
185
- end
74
+ protected
186
75
 
187
- # Returns true if no errors have been added.
188
- def empty?
189
- return @errors.empty?
190
- end
191
-
192
- # Removes all the errors that have been added.
193
- def clear
194
- @errors = {}
195
- end
196
-
197
- # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
198
- # with this as well.
199
- def count
200
- error_count = 0
201
- @errors.each_value { |attribute| error_count += attribute.length }
202
- error_count
76
+ def perform_validations(options={}) # :nodoc:
77
+ options[:validate] == false || valid?(options[:context])
203
78
  end
204
79
  end
205
80
  end
81
+
82
+ require "active_record/validations/associated"
83
+ require "active_record/validations/uniqueness"
84
+ require "active_record/validations/presence"
@@ -0,0 +1,11 @@
1
+ module ActiveRecord
2
+ # Returns the version of the currently loaded ActiveRecord as a Gem::Version
3
+ def self.version
4
+ Gem::Version.new "4.0.0"
5
+ end
6
+
7
+ module VERSION #:nodoc:
8
+ MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments
9
+ STRING = ActiveRecord.version.to_s
10
+ end
11
+ end