globe-composite_primary_keys 3.0.1

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 (150) hide show
  1. data/History.txt +203 -0
  2. data/Manifest.txt +121 -0
  3. data/README.txt +41 -0
  4. data/README_DB2.txt +33 -0
  5. data/Rakefile +30 -0
  6. data/composite_primary_keys.gemspec +17 -0
  7. data/lib/adapter_helper/base.rb +63 -0
  8. data/lib/adapter_helper/mysql.rb +13 -0
  9. data/lib/adapter_helper/oracle.rb +12 -0
  10. data/lib/adapter_helper/oracle_enhanced.rb +12 -0
  11. data/lib/adapter_helper/postgresql.rb +13 -0
  12. data/lib/adapter_helper/sqlite3.rb +13 -0
  13. data/lib/composite_primary_keys.rb +63 -0
  14. data/lib/composite_primary_keys/association_preload.rb +162 -0
  15. data/lib/composite_primary_keys/associations.rb +159 -0
  16. data/lib/composite_primary_keys/attribute_methods.rb +84 -0
  17. data/lib/composite_primary_keys/base.rb +200 -0
  18. data/lib/composite_primary_keys/composite_arrays.rb +29 -0
  19. data/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb +9 -0
  20. data/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb +21 -0
  21. data/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb +15 -0
  22. data/lib/composite_primary_keys/connection_adapters/oracle_enhanced_adapter.rb +17 -0
  23. data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +53 -0
  24. data/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb +15 -0
  25. data/lib/composite_primary_keys/finder_methods.rb +68 -0
  26. data/lib/composite_primary_keys/fixtures.rb +8 -0
  27. data/lib/composite_primary_keys/read.rb +25 -0
  28. data/lib/composite_primary_keys/reflection.rb +39 -0
  29. data/lib/composite_primary_keys/relation.rb +31 -0
  30. data/lib/composite_primary_keys/through_association_scope.rb +212 -0
  31. data/lib/composite_primary_keys/validations/uniqueness.rb +118 -0
  32. data/lib/composite_primary_keys/version.rb +9 -0
  33. data/loader.rb +24 -0
  34. data/local/database_connections.rb.sample +12 -0
  35. data/local/paths.rb.sample +2 -0
  36. data/local/tasks.rb.sample +2 -0
  37. data/scripts/console.rb +48 -0
  38. data/scripts/txt2html +67 -0
  39. data/scripts/txt2js +59 -0
  40. data/tasks/activerecord_selection.rake +43 -0
  41. data/tasks/databases.rake +12 -0
  42. data/tasks/databases/mysql.rake +30 -0
  43. data/tasks/databases/oracle.rake +25 -0
  44. data/tasks/databases/postgresql.rake +25 -0
  45. data/tasks/databases/sqlite3.rake +28 -0
  46. data/tasks/deployment.rake +22 -0
  47. data/tasks/local_setup.rake +13 -0
  48. data/tasks/website.rake +18 -0
  49. data/test/README_tests.txt +67 -0
  50. data/test/abstract_unit.rb +103 -0
  51. data/test/connections/native_ibm_db/connection.rb +23 -0
  52. data/test/connections/native_mysql/connection.rb +13 -0
  53. data/test/connections/native_oracle/connection.rb +14 -0
  54. data/test/connections/native_oracle_enhanced/connection.rb +20 -0
  55. data/test/connections/native_postgresql/connection.rb +8 -0
  56. data/test/connections/native_sqlite/connection.rb +9 -0
  57. data/test/fixtures/article.rb +5 -0
  58. data/test/fixtures/article_group.rb +4 -0
  59. data/test/fixtures/article_groups.yml +7 -0
  60. data/test/fixtures/articles.yml +6 -0
  61. data/test/fixtures/comment.rb +6 -0
  62. data/test/fixtures/comments.yml +16 -0
  63. data/test/fixtures/db_definitions/db2-create-tables.sql +113 -0
  64. data/test/fixtures/db_definitions/db2-drop-tables.sql +16 -0
  65. data/test/fixtures/db_definitions/mysql.sql +181 -0
  66. data/test/fixtures/db_definitions/oracle.drop.sql +39 -0
  67. data/test/fixtures/db_definitions/oracle.sql +188 -0
  68. data/test/fixtures/db_definitions/postgresql.sql +206 -0
  69. data/test/fixtures/db_definitions/sqlite.sql +166 -0
  70. data/test/fixtures/department.rb +5 -0
  71. data/test/fixtures/departments.yml +3 -0
  72. data/test/fixtures/dorm.rb +3 -0
  73. data/test/fixtures/dorms.yml +2 -0
  74. data/test/fixtures/employee.rb +4 -0
  75. data/test/fixtures/employees.yml +9 -0
  76. data/test/fixtures/group.rb +3 -0
  77. data/test/fixtures/groups.yml +3 -0
  78. data/test/fixtures/hack.rb +6 -0
  79. data/test/fixtures/hacks.yml +2 -0
  80. data/test/fixtures/kitchen_sink.rb +3 -0
  81. data/test/fixtures/kitchen_sinks.yml +5 -0
  82. data/test/fixtures/membership.rb +10 -0
  83. data/test/fixtures/membership_status.rb +3 -0
  84. data/test/fixtures/membership_statuses.yml +10 -0
  85. data/test/fixtures/memberships.yml +6 -0
  86. data/test/fixtures/product.rb +7 -0
  87. data/test/fixtures/product_tariff.rb +5 -0
  88. data/test/fixtures/product_tariffs.yml +12 -0
  89. data/test/fixtures/products.yml +6 -0
  90. data/test/fixtures/reading.rb +4 -0
  91. data/test/fixtures/readings.yml +10 -0
  92. data/test/fixtures/reference_code.rb +7 -0
  93. data/test/fixtures/reference_codes.yml +28 -0
  94. data/test/fixtures/reference_type.rb +7 -0
  95. data/test/fixtures/reference_types.yml +9 -0
  96. data/test/fixtures/restaurant.rb +6 -0
  97. data/test/fixtures/restaurants.yml +5 -0
  98. data/test/fixtures/restaurants_suburbs.yml +11 -0
  99. data/test/fixtures/room.rb +10 -0
  100. data/test/fixtures/room_assignment.rb +4 -0
  101. data/test/fixtures/room_assignments.yml +4 -0
  102. data/test/fixtures/room_attribute.rb +3 -0
  103. data/test/fixtures/room_attribute_assignment.rb +5 -0
  104. data/test/fixtures/room_attribute_assignments.yml +4 -0
  105. data/test/fixtures/room_attributes.yml +3 -0
  106. data/test/fixtures/rooms.yml +3 -0
  107. data/test/fixtures/seat.rb +5 -0
  108. data/test/fixtures/seats.yml +4 -0
  109. data/test/fixtures/street.rb +3 -0
  110. data/test/fixtures/streets.yml +15 -0
  111. data/test/fixtures/student.rb +4 -0
  112. data/test/fixtures/students.yml +2 -0
  113. data/test/fixtures/suburb.rb +6 -0
  114. data/test/fixtures/suburbs.yml +9 -0
  115. data/test/fixtures/tariff.rb +6 -0
  116. data/test/fixtures/tariffs.yml +13 -0
  117. data/test/fixtures/user.rb +10 -0
  118. data/test/fixtures/users.yml +6 -0
  119. data/test/hash_tricks.rb +34 -0
  120. data/test/plugins/pagination.rb +405 -0
  121. data/test/plugins/pagination_helper.rb +135 -0
  122. data/test/test_associations.rb +178 -0
  123. data/test/test_attribute_methods.rb +22 -0
  124. data/test/test_attributes.rb +80 -0
  125. data/test/test_clone.rb +34 -0
  126. data/test/test_composite_arrays.rb +32 -0
  127. data/test/test_create.rb +68 -0
  128. data/test/test_delete.rb +83 -0
  129. data/test/test_exists.rb +25 -0
  130. data/test/test_find.rb +73 -0
  131. data/test/test_ids.rb +90 -0
  132. data/test/test_miscellaneous.rb +39 -0
  133. data/test/test_pagination.rb +38 -0
  134. data/test/test_polymorphic.rb +32 -0
  135. data/test/test_santiago.rb +27 -0
  136. data/test/test_suite.rb +19 -0
  137. data/test/test_tutorial_example.rb +26 -0
  138. data/test/test_update.rb +40 -0
  139. data/test/test_validations.rb +11 -0
  140. data/website/index.html +195 -0
  141. data/website/index.txt +159 -0
  142. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  143. data/website/stylesheets/screen.css +126 -0
  144. data/website/template.js +3 -0
  145. data/website/template.rhtml +53 -0
  146. data/website/version-raw.js +3 -0
  147. data/website/version-raw.txt +2 -0
  148. data/website/version.js +4 -0
  149. data/website/version.txt +3 -0
  150. metadata +339 -0
@@ -0,0 +1,68 @@
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module FinderMethods
4
+ module InstanceMethods
5
+ def exists?(id = nil)
6
+ case id
7
+ when Array
8
+ # CPK
9
+ if id.first.is_a?(String) and id.first.match(/\?/)
10
+ where(id).exists?
11
+ else
12
+ where(ids_predicate(id)).exists?
13
+ end
14
+ when Hash
15
+ where(id).exists?
16
+ else
17
+ relation = select(primary_key).limit(1)
18
+ # CPK
19
+ #relation = relation.where(primary_key.eq(id)) if id
20
+ relation = relation.where(ids_predicate(id)) if id
21
+ relation.first ? true : false
22
+ end
23
+ end
24
+
25
+ def find_with_ids(*ids, &block)
26
+ return to_a.find(&block) if block_given?
27
+
28
+ ids.pop if ids.last.nil?
29
+
30
+ # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
31
+ # if ids is list of lists, then each inner list must follow rule above
32
+ if ids.first.is_a? String
33
+ # find '2,1' -> ids = ['2,1']
34
+ # find '2,1;7,3' -> ids = ['2,1;7,3']
35
+ match = ids.first.match(/^\[(.*)\]$/)
36
+ ids = (match ? match[1] : ids.first).split(ID_SET_SEP).map {|id_set| id_set.split(CompositePrimaryKeys::ID_SEP).to_composite_ids}
37
+ # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
38
+ end
39
+
40
+ ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
41
+ ids.each do |id_set|
42
+ unless id_set.is_a?(Array)
43
+ raise "Ids must be in an Array, instead received: #{id_set.inspect}"
44
+ end
45
+ unless id_set.length == @klass.primary_keys.length
46
+ raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
47
+ end
48
+ end
49
+
50
+ new_relation = clone
51
+ ids.each do |id_set|
52
+ [@klass.primary_keys, id_set].transpose.map do |key, id|
53
+ new_relation = new_relation.where(key => id)
54
+ end
55
+ end
56
+
57
+ result = new_relation.to_a
58
+
59
+ if result.size == ids.size
60
+ ids.size == 1 ? result[0] : result
61
+ else
62
+ raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{@klass.name} with IDs (#{ids.inspect})"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ class Fixture #:nodoc:
2
+ def [](key)
3
+ if key.is_a? Array
4
+ return key.map { |a_key| self[a_key.to_s] }.to_composite_ids
5
+ end
6
+ @fixture[key]
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module Read
4
+ def read_attribute(attr_name)
5
+ attr_name = attr_name.to_s
6
+ # CPK
7
+ # attr_name = self.class.primary_key if attr_name == 'id'
8
+ attr_name = self.class.primary_key if (attr_name == 'id' and !self.composite?)
9
+ if !(value = @attributes[attr_name]).nil?
10
+ if column = column_for_attribute(attr_name)
11
+ if unserializable_attribute?(attr_name, column)
12
+ unserialize_attribute(attr_name)
13
+ else
14
+ column.type_cast(value)
15
+ end
16
+ else
17
+ value
18
+ end
19
+ else
20
+ nil
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ module ActiveRecord
2
+ module Reflection
3
+ class AssociationReflection
4
+ def derive_primary_key
5
+ result = if options[:foreign_key]
6
+ options[:foreign_key]
7
+ elsif belongs_to?
8
+ #CPK
9
+ #"#{name}_id"
10
+ class_name.foreign_key
11
+ elsif options[:as]
12
+ options[:as]
13
+ else
14
+ active_record.name.foreign_key
15
+ end
16
+ end
17
+
18
+ def cpk_primary_key
19
+ # Make sure the returned key(s) are an array
20
+ @cpk_primary_key ||= [derive_primary_key].flatten
21
+ end
22
+
23
+ def primary_key_name
24
+ @primary_key_name ||= derive_primary_key_name
25
+ end
26
+
27
+ def derive_primary_key_name
28
+ result = derive_primary_key
29
+
30
+ # CPK
31
+ if result.is_a?(Array)
32
+ result.to_composite_keys.to_s
33
+ else
34
+ result
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module Relation
4
+ module InstanceMethods
5
+ def ids_predicate(id)
6
+ predicate = nil
7
+ self.primary_key.zip(id).each do |key, value|
8
+ eq = key.eq(value)
9
+ predicate = predicate ? predicate.and(eq) : eq
10
+ end
11
+ predicate
12
+ end
13
+
14
+ def delete(id_or_array)
15
+ # CPK
16
+ # where(@klass.primary_key => id_or_array).delete_all
17
+ where(ids_predicate(id_or_array)).delete_all
18
+ end
19
+
20
+ def destroy(id)
21
+ # CPK
22
+ #if id.is_a?(Array)
23
+ # id.map { |one_id| destroy(one_id) }
24
+ #else
25
+ find(id).destroy
26
+ #end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,212 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ module ThroughAssociationScope
4
+ def composite_join_clause(table1, keys1, table2, keys2)
5
+ predicates = composite_join_predicates(table1, keys1, table2, keys2)
6
+
7
+ join_clause = predicates.map do |predicate|
8
+ predicate.to_sql
9
+ end.join(" AND ")
10
+
11
+ "(#{join_clause})"
12
+ end
13
+
14
+ def composite_join_predicates(table1, keys1, table2, keys2)
15
+ attributes1 = [keys1].flatten.map do |key|
16
+ table1[key]
17
+ end
18
+
19
+ attributes2 = [keys2].flatten.map do |key|
20
+ table2[key]
21
+ end
22
+
23
+ [attributes1, attributes2].transpose.map do |attribute1, attribute2|
24
+ attribute1.eq(attribute2)
25
+ end
26
+ end
27
+
28
+ def composite_ids_hash(keys, ids)
29
+ [keys].flatten.zip([ids].flatten).inject(Hash.new) do |hash, (key, value)|
30
+ hash[key] = value
31
+ hash
32
+ end
33
+ end
34
+
35
+ def construct_quoted_owner_attributes(reflection)
36
+ if as = reflection.options[:as]
37
+ { "#{as}_id" => owner_quoted_id,
38
+ "#{as}_type" => reflection.klass.quote_value(
39
+ @owner.class.base_class.name.to_s,
40
+ reflection.klass.columns_hash["#{as}_type"]) }
41
+ elsif reflection.macro == :belongs_to
42
+ # CPK
43
+ # { reflection.klass.primary_key => @owner[reflection.primary_key_name] }
44
+ composite_ids_hash(reflection.klass.primary_key, @owner.quoted_id)
45
+ else
46
+ # CPK
47
+ #{ reflection.primary_key_name => owner_quoted_id }
48
+ composite_ids_hash(reflection.cpk_primary_key, @owner.quoted_id)
49
+ end
50
+ end
51
+
52
+ # Construct attributes for associate pointing to owner.
53
+ def construct_owner_attributes(reflection)
54
+ if as = reflection.options[:as]
55
+ { "#{as}_id" => @owner.id,
56
+ "#{as}_type" => @owner.class.base_class.name.to_s }
57
+ else
58
+ # CPK
59
+ # { reflection.primary_key_name => @owner.id }
60
+ composite_ids_hash(reflection.cpk_primary_key, @owner.id)
61
+ end
62
+ end
63
+
64
+ def construct_joins(custom_joins = nil)
65
+ polymorphic_join = nil
66
+ if @reflection.source_reflection.macro == :belongs_to
67
+ reflection_primary_key = @reflection.klass.primary_key
68
+ source_primary_key = @reflection.source_reflection.cpk_primary_key
69
+ if @reflection.options[:source_type]
70
+ polymorphic_join = "AND %s.%s = %s" % [
71
+ @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
72
+ @owner.class.quote_value(@reflection.options[:source_type])
73
+ ]
74
+ end
75
+ else
76
+ reflection_primary_key = @reflection.source_reflection.cpk_primary_key
77
+ source_primary_key = @reflection.through_reflection.klass.primary_key
78
+ if @reflection.source_reflection.options[:as]
79
+ polymorphic_join = "AND %s.%s = %s" % [
80
+ @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
81
+ @owner.class.quote_value(@reflection.through_reflection.klass.name)
82
+ ]
83
+ end
84
+ end
85
+
86
+ # CPK
87
+ # "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
88
+ # @reflection.through_reflection.quoted_table_name,
89
+ # @reflection.quoted_table_name, reflection_primary_key,
90
+ # @reflection.through_reflection.quoted_table_name, source_primary_key,
91
+ # polymorphic_join
92
+ # ]
93
+
94
+ "INNER JOIN %s ON %s %s #{@reflection.options[:joins]} #{custom_joins}" % [
95
+ @reflection.through_reflection.quoted_table_name,
96
+ composite_join_clause(@reflection.klass.arel_table, reflection_primary_key,
97
+ @reflection.through_reflection.klass.arel_table, source_primary_key),
98
+ polymorphic_join
99
+ ]
100
+ end
101
+ end
102
+
103
+ module ClassMethods
104
+ class JoinAssociation
105
+ # Ugly to include this twice, but I couldn't figure out how to make this
106
+ # work via a module
107
+ def composite_join_predicates(table1, keys1, table2, keys2)
108
+ attributes1 = [keys1].flatten.map do |key|
109
+ table1[key]
110
+ end
111
+
112
+ attributes2 = [keys2].flatten.map do |key|
113
+ table2[key]
114
+ end
115
+
116
+ [attributes1, attributes2].transpose.map do |attribute1, attribute2|
117
+ attribute1.eq(attribute2)
118
+ end
119
+ end
120
+
121
+ def association_join
122
+ return @join if @join
123
+
124
+ aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine)
125
+ parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => arel_engine)
126
+
127
+ @join = case reflection.macro
128
+ when :has_and_belongs_to_many
129
+ join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine)
130
+ fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
131
+ klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key
132
+
133
+ [
134
+ join_table[fk].eq(parent_table[reflection.active_record.primary_key]),
135
+ aliased_table[klass.primary_key].eq(join_table[klass_fk])
136
+ ]
137
+ when :has_many, :has_one
138
+ if reflection.options[:through]
139
+ join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine)
140
+ jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
141
+ first_key = second_key = as_extra = nil
142
+
143
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
144
+ jt_foreign_key = through_reflection.options[:as].to_s + '_id'
145
+ jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].eq(parent.active_record.base_class.name)
146
+ else
147
+ jt_foreign_key = through_reflection.primary_key_name
148
+ end
149
+
150
+ case source_reflection.macro
151
+ when :has_many
152
+ if source_reflection.options[:as]
153
+ first_key = "#{source_reflection.options[:as]}_id"
154
+ second_key = options[:foreign_key] || primary_key
155
+ as_extra = aliased_table["#{source_reflection.options[:as]}_type"].eq(source_reflection.active_record.base_class.name)
156
+ else
157
+ first_key = through_reflection.klass.base_class.to_s.foreign_key
158
+ second_key = options[:foreign_key] || primary_key
159
+ end
160
+
161
+ unless through_reflection.klass.descends_from_active_record?
162
+ jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name)
163
+ end
164
+ when :belongs_to
165
+ first_key = primary_key
166
+ if reflection.options[:source_type]
167
+ second_key = source_reflection.association_foreign_key
168
+ jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type])
169
+ else
170
+ second_key = source_reflection.primary_key_name
171
+ end
172
+ end
173
+
174
+ [
175
+ [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].reject{|x| x.blank? },
176
+ aliased_table[first_key].eq(join_table[second_key])
177
+ ]
178
+ elsif reflection.options[:as]
179
+ id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key])
180
+ type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name)
181
+ [id_rel, type_rel]
182
+ else
183
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
184
+ # CPK
185
+ # [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])]
186
+ composite_join_predicates(aliased_table, foreign_key,
187
+ parent_table, reflection.options[:primary_key] || parent.primary_key)
188
+ end
189
+ when :belongs_to
190
+ [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.cpk_primary_key])]
191
+ end
192
+
193
+ unless klass.descends_from_active_record?
194
+ sti_column = aliased_table[klass.inheritance_column]
195
+ sti_condition = sti_column.eq(klass.sti_name)
196
+ klass.send(:subclasses).each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
197
+
198
+ @join << sti_condition
199
+ end
200
+
201
+ [through_reflection, reflection].each do |ref|
202
+ if ref && ref.options[:conditions]
203
+ @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))
204
+ end
205
+ end
206
+
207
+ @join
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,118 @@
1
+ module ActiveRecord
2
+ module Validations
3
+ class UniquenessValidator
4
+ def validate_each(record, attribute, value)
5
+ finder_class = find_finder_class_for(record)
6
+ table = finder_class.unscoped
7
+
8
+ table_name = record.class.quoted_table_name
9
+ sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
10
+
11
+ relation = table.where(sql, *params)
12
+
13
+ Array.wrap(options[:scope]).each do |scope_item|
14
+ scope_value = record.send(scope_item)
15
+ relation = relation.where(scope_item => scope_value)
16
+ end
17
+
18
+ unless record.new_record?
19
+ # CPK
20
+ # TODO : This should be in Arel
21
+ #relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
22
+ if record.composite?
23
+ predicate = nil
24
+ record.ids_hash.each do |key, value|
25
+ neq = relation.table[key].not_eq(value)
26
+ predicate = predicate ? predicate.and(neq) : neq
27
+ end
28
+ relation = relation.where(predicate)
29
+ else
30
+ relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
31
+ end
32
+ end
33
+
34
+ if relation.first
35
+ record.errors.add(attribute, :taken, :default => options[:message], :value => value)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ #module CompositePrimaryKeys
43
+ # module ActiveRecord
44
+ # module Validations
45
+ # module Uniqueness
46
+ # module ClassMethods
47
+ # def validates_uniqueness_of(*attr_names)
48
+ # configuration = { :case_sensitive => true }
49
+ # configuration.update(attr_names.extract_options!)
50
+ #
51
+ # validates_each(attr_names,configuration) do |record, attr_name, value|
52
+ # # The check for an existing value should be run from a class that
53
+ # # isn't abstract. This means working down from the current class
54
+ # # (self), to the first non-abstract class. Since classes don't know
55
+ # # their subclasses, we have to build the hierarchy between self and
56
+ # # the record's class.
57
+ # class_hierarchy = [record.class]
58
+ # while class_hierarchy.first != self
59
+ # class_hierarchy.insert(0, class_hierarchy.first.superclass)
60
+ # end
61
+ #
62
+ # # Now we can work our way down the tree to the first non-abstract
63
+ # # class (which has a database table to query from).
64
+ # finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
65
+ #
66
+ # column = finder_class.columns_hash[attr_name.to_s]
67
+ #
68
+ # if value.nil?
69
+ # comparison_operator = "IS ?"
70
+ # elsif column.text?
71
+ # comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
72
+ # value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
73
+ # else
74
+ # comparison_operator = "= ?"
75
+ # end
76
+ #
77
+ # sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
78
+ #
79
+ # if value.nil? || (configuration[:case_sensitive] || !column.text?)
80
+ # condition_sql = "#{sql_attribute} #{comparison_operator}"
81
+ # condition_params = [value]
82
+ # else
83
+ # condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
84
+ # condition_params = [value.mb_chars.downcase]
85
+ # end
86
+ #
87
+ # if scope = configuration[:scope]
88
+ # Array(scope).map do |scope_item|
89
+ # scope_value = record.send(scope_item)
90
+ # condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
91
+ # condition_params << scope_value
92
+ # end
93
+ # end
94
+ #
95
+ # unless record.new_record?
96
+ # if record.class.composite?
97
+ # record.class.primary_keys.each do |key|
98
+ # condition_sql << " AND #{record.class.quoted_table_name}.#{key} <> ?"
99
+ # condition_params << record.send(key)
100
+ # end
101
+ # else
102
+ # condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
103
+ # condition_params << record.send(:id)
104
+ # end
105
+ # end
106
+ #
107
+ # finder_class.with_exclusive_scope do
108
+ # if finder_class.exists?([condition_sql, *condition_params])
109
+ # record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
110
+ # end
111
+ # end
112
+ # end
113
+ # end
114
+ # end
115
+ # end
116
+ # end
117
+ # end
118
+ #end