composite_primary_keys 2.3.5.1 → 3.0.0.b2

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 (103) hide show
  1. data/History.txt +26 -0
  2. data/README.txt +1 -1
  3. data/Rakefile +41 -51
  4. data/lib/composite_primary_keys.rb +19 -7
  5. data/lib/composite_primary_keys/association_preload.rb +75 -170
  6. data/lib/composite_primary_keys/associations.rb +98 -400
  7. data/lib/composite_primary_keys/associations/association_proxy.rb +32 -0
  8. data/lib/composite_primary_keys/associations/has_and_belongs_to_many_association.rb +30 -0
  9. data/lib/composite_primary_keys/associations/has_many_association.rb +72 -0
  10. data/lib/composite_primary_keys/associations/has_one_association.rb +19 -0
  11. data/lib/composite_primary_keys/associations/through_association_scope.rb +103 -0
  12. data/lib/composite_primary_keys/base.rb +148 -305
  13. data/lib/composite_primary_keys/calculations.rb +22 -64
  14. data/lib/composite_primary_keys/composite_arrays.rb +3 -10
  15. data/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb +9 -0
  16. data/lib/composite_primary_keys/connection_adapters/oracle_enhanced_adapter.rb +17 -0
  17. data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +1 -1
  18. data/lib/composite_primary_keys/finder_methods.rb +71 -0
  19. data/lib/composite_primary_keys/fixtures.rb +1 -1
  20. data/lib/composite_primary_keys/read.rb +25 -0
  21. data/lib/composite_primary_keys/reflection.rb +30 -10
  22. data/lib/composite_primary_keys/relation.rb +31 -0
  23. data/lib/composite_primary_keys/validations/uniqueness.rb +106 -66
  24. data/lib/composite_primary_keys/version.rb +4 -4
  25. data/scripts/console.rb +1 -1
  26. data/tasks/Rakefile.rb +13 -0
  27. data/tasks/databases/mysql.rake +11 -15
  28. data/tasks/databases/oracle.rake +10 -11
  29. data/tasks/databases/postgresql.rake +10 -13
  30. data/tasks/databases/sqlite3.rake +9 -9
  31. data/test/README_tests.txt +1 -45
  32. data/test/abstract_unit.rb +17 -14
  33. data/test/connections/connection_spec.rb +19 -0
  34. data/test/connections/databases.example.yml +11 -0
  35. data/test/connections/databases.yml +13 -0
  36. data/test/connections/native_mysql/connection.rb +10 -2
  37. data/test/connections/native_oracle/connection.rb +7 -4
  38. data/test/connections/native_oracle_enhanced/connection.rb +23 -0
  39. data/test/connections/native_postgresql/connection.rb +13 -5
  40. data/test/connections/native_sqlite/connection.rb +7 -3
  41. data/test/fixtures/article_group.rb +4 -0
  42. data/test/fixtures/article_groups.yml +7 -0
  43. data/test/fixtures/db_definitions/postgresql.sql +2 -1
  44. data/test/fixtures/debug.log +133 -0
  45. data/test/fixtures/dorm.rb +3 -0
  46. data/test/fixtures/dorms.yml +2 -0
  47. data/test/fixtures/kitchen_sink.rb +3 -0
  48. data/test/fixtures/kitchen_sinks.yml +5 -0
  49. data/test/fixtures/reference_codes.yml +2 -0
  50. data/test/fixtures/reference_type.rb +1 -1
  51. data/test/fixtures/restaurant.rb +6 -0
  52. data/test/fixtures/restaurants.yml +5 -0
  53. data/test/fixtures/restaurants_suburbs.yml +11 -0
  54. data/test/fixtures/room.rb +10 -0
  55. data/test/fixtures/room_assignment.rb +4 -0
  56. data/test/fixtures/room_assignments.yml +4 -0
  57. data/test/fixtures/room_attribute.rb +3 -0
  58. data/test/fixtures/room_attribute_assignment.rb +5 -0
  59. data/test/fixtures/room_attribute_assignments.yml +4 -0
  60. data/test/fixtures/room_attributes.yml +3 -0
  61. data/test/fixtures/rooms.yml +3 -0
  62. data/test/fixtures/seat.rb +5 -0
  63. data/test/fixtures/seats.yml +4 -0
  64. data/test/fixtures/student.rb +4 -0
  65. data/test/fixtures/students.yml +2 -0
  66. data/test/test_associations.rb +27 -50
  67. data/test/test_attributes.rb +15 -19
  68. data/test/test_composite_arrays.rb +2 -21
  69. data/test/test_create.rb +3 -3
  70. data/test/test_delete.rb +7 -20
  71. data/test/test_exists.rb +3 -7
  72. data/test/test_find.rb +0 -8
  73. data/test/test_ids.rb +3 -17
  74. data/test/test_polymorphic.rb +5 -4
  75. data/test/test_suite.rb +19 -0
  76. data/test/{test_tutorial_examle.rb → test_tutorial_example.rb} +0 -0
  77. metadata +110 -72
  78. data/Manifest.txt +0 -123
  79. data/lib/adapter_helper/base.rb +0 -63
  80. data/lib/adapter_helper/mysql.rb +0 -13
  81. data/lib/adapter_helper/oracle.rb +0 -12
  82. data/lib/adapter_helper/postgresql.rb +0 -13
  83. data/lib/adapter_helper/sqlite3.rb +0 -13
  84. data/lib/composite_primary_keys/migration.rb +0 -20
  85. data/local/database_connections.rb.sample +0 -12
  86. data/local/paths.rb.sample +0 -2
  87. data/local/tasks.rb.sample +0 -2
  88. data/tasks/activerecord_selection.rake +0 -43
  89. data/tasks/databases.rake +0 -12
  90. data/tasks/deployment.rake +0 -22
  91. data/tasks/local_setup.rake +0 -13
  92. data/test/test_dummy.rb +0 -28
  93. data/tmp/test.db +0 -0
  94. data/website/index.html +0 -195
  95. data/website/index.txt +0 -159
  96. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  97. data/website/stylesheets/screen.css +0 -126
  98. data/website/template.js +0 -3
  99. data/website/template.rhtml +0 -53
  100. data/website/version-raw.js +0 -3
  101. data/website/version-raw.txt +0 -2
  102. data/website/version.js +0 -4
  103. data/website/version.txt +0 -3
@@ -0,0 +1,32 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class AssociationProxy
4
+ def full_columns_equals(table_name, keys, quoted_ids)
5
+ quoted_table_name = @owner.connection.quote_table_name(table_name)
6
+
7
+ keys = [keys].flatten
8
+ ids = [quoted_ids].flatten
9
+
10
+ [keys,ids].transpose.map do |key, id|
11
+ "(#{quoted_table_name}.#{@owner.connection.quote_column_name(key)} = #{id})"
12
+ end.join(' AND ')
13
+ end
14
+
15
+ def set_belongs_to_association_for(record)
16
+ if @reflection.options[:as]
17
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
18
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
19
+ else
20
+ unless @owner.new_record?
21
+ primary_key = @reflection.options[:primary_key] || :id
22
+ # CPK
23
+ # record[@reflection.primary_key_name] = @owner.send(primary_key)
24
+ values = [@owner.send(primary_key)].flatten
25
+ key_values = @reflection.cpk_primary_key.zip(values)
26
+ key_values.each {|key, value| record[key] = value}
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasAndBelongsToManyAssociation
4
+ def construct_sql
5
+ if @reflection.options[:finder_sql]
6
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
7
+ else
8
+ # CPK
9
+ # @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
10
+ @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.cpk_primary_key, owner_quoted_id)
11
+ @finder_sql << " AND (#{conditions})" if conditions
12
+ end
13
+
14
+ join_condition = if composite?
15
+ conditions = Array.new
16
+ primary_keys.length.times do |i|
17
+ conditions << "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key[i]} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key[i]}"
18
+ end
19
+ conditions.join(' AND ')
20
+ else
21
+ "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
22
+ end
23
+ #@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
24
+ @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON (#{join_condition})"
25
+
26
+ construct_counter_sql
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,72 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasManyAssociation
4
+ def construct_sql
5
+ case
6
+ when @reflection.options[:finder_sql]
7
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
8
+
9
+ when @reflection.options[:as]
10
+ @finder_sql =
11
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
12
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
13
+ @finder_sql << " AND (#{conditions})" if conditions
14
+
15
+ else
16
+ # CPK
17
+ # @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
18
+ @finder_sql = full_columns_equals(@reflection.quoted_table_name, @reflection.cpk_primary_key, owner_quoted_id)
19
+ @finder_sql << " AND (#{conditions})" if conditions
20
+ end
21
+
22
+ construct_counter_sql
23
+ end
24
+
25
+ # Deletes the records according to the <tt>:dependent</tt> option.
26
+ def delete_records(records)
27
+ case @reflection.options[:dependent]
28
+ when :destroy
29
+ records.each { |r| r.destroy }
30
+ when :delete_all
31
+ @reflection.klass.delete(records.map { |record| record.id })
32
+ else
33
+ relation = Arel::Table.new(@reflection.table_name)
34
+ # CPK
35
+ # relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
36
+ # and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id)))
37
+ # ).update(relation[@reflection.primary_key_name] => nil)
38
+ id_predicate = nil
39
+ owner_key_values = @reflection.cpk_primary_key.zip([@owner.id].flatten)
40
+ owner_key_values.each do |key, value|
41
+ eq = relation[key].eq(value)
42
+ id_predicate = id_predicate ? id_predicate.and(eq) : eq
43
+ end
44
+
45
+ record_predicates = nil
46
+ records.each do |record|
47
+ keys = [@reflection.klass.primary_key].flatten
48
+ values = [record.id].flatten
49
+
50
+ record_predicate = nil
51
+ keys.zip(values).each do |key, value|
52
+ eq = relation[key].eq(value)
53
+ record_predicate = record_predicate ? record_predicate.and(eq) : eq
54
+ end
55
+ record_predicates = record_predicates ? record_predicates.or(record_predicate) : record_predicate
56
+ end
57
+
58
+ relation = relation.where(id_predicate.and(record_predicates))
59
+
60
+ nullify = @reflection.cpk_primary_key.inject(Hash.new) do |hash, key|
61
+ hash[relation[key]] = nil
62
+ hash
63
+ end
64
+
65
+ relation.update(nullify)
66
+
67
+ @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasOneAssociation
4
+ def construct_sql
5
+ case
6
+ when @reflection.options[:as]
7
+ @finder_sql =
8
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
9
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
10
+ else
11
+ # CPK
12
+ #@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
13
+ @finder_sql = full_columns_equals(@reflection.quoted_table_name, @reflection.cpk_primary_key, owner_quoted_id)
14
+ end
15
+ @finder_sql << " AND (#{conditions})" if conditions
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,103 @@
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
+ end
103
+ end
@@ -1,343 +1,186 @@
1
- module CompositePrimaryKeys
2
- module ActiveRecord #:nodoc:
3
- class CompositeKeyError < StandardError #:nodoc:
4
- end
5
-
6
- module Base #:nodoc:
7
-
8
- INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
9
- NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
1
+ module ActiveRecord
2
+ class CompositeKeyError < StandardError #:nodoc:
3
+ end
10
4
 
11
- def self.append_features(base)
12
- super
13
- base.send(:include, InstanceMethods)
14
- base.extend(ClassMethods)
15
- end
5
+ class Base
6
+ INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
7
+ NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
16
8
 
17
- module ClassMethods
18
- def set_primary_keys(*keys)
19
- keys = keys.first if keys.first.is_a?(Array)
20
- keys = keys.map { |k| k.to_sym }
21
- cattr_accessor :primary_keys
22
- self.primary_keys = keys.to_composite_keys
9
+ class << self
10
+ def set_primary_keys(*keys)
11
+ keys = keys.first if keys.first.is_a?(Array)
23
12
 
24
- class_eval <<-EOV
25
- extend CompositeClassMethods
26
- include CompositeInstanceMethods
13
+ if keys.length == 1
14
+ set_primary_key(keys.first)
15
+ return
16
+ end
27
17
 
28
- include CompositePrimaryKeys::ActiveRecord::Associations
29
- include CompositePrimaryKeys::ActiveRecord::AssociationPreload
30
- include CompositePrimaryKeys::ActiveRecord::Calculations
31
- include CompositePrimaryKeys::ActiveRecord::AttributeMethods
18
+ cattr_accessor :primary_keys
19
+ self.primary_keys = keys.map { |k| k.to_sym }
32
20
 
33
- extend CompositePrimaryKeys::ActiveRecord::Validations::Uniqueness::ClassMethods
34
- EOV
35
- end
21
+ class_eval <<-EOV
22
+ extend CompositeClassMethods
23
+ include CompositeInstanceMethods
24
+ include CompositePrimaryKeys::ActiveRecord::AssociationPreload
25
+ EOV
36
26
 
37
- def composite?
38
- false
27
+ class << unscoped
28
+ include CompositePrimaryKeys::ActiveRecord::FinderMethods::InstanceMethods
29
+ include CompositePrimaryKeys::ActiveRecord::Relation::InstanceMethods
39
30
  end
40
31
  end
41
32
 
42
- module InstanceMethods
43
- def composite?; self.class.composite?; end
33
+ def composite?
34
+ false
44
35
  end
36
+ end
45
37
 
46
- module CompositeInstanceMethods
47
-
48
- # A model instance's primary keys is always available as model.ids
49
- # whether you name it the default 'id' or set it to something else.
50
- def id
51
- attr_names = self.class.primary_keys
52
- CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) })
53
- end
54
- alias_method :ids, :id
55
-
56
- def to_param
57
- id.to_s
58
- end
59
-
60
- def id_before_type_cast #:nodoc:
61
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET
62
- end
63
-
64
- def quoted_id #:nodoc:
65
- [self.class.primary_keys, ids].
66
- transpose.
67
- map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.
68
- to_composite_ids
69
- end
70
-
71
- # Sets the primary ID.
72
- def id=(ids)
73
- ids = ids.split(ID_SEP) if ids.is_a?(String)
74
- ids.flatten!
75
- unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length
76
- raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"
77
- end
78
- [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}
79
- id
80
- end
81
-
82
- # Returns a clone of the record that hasn't been assigned an id yet and
83
- # is treated as a new record. Note that this is a "shallow" clone:
84
- # it copies the object's attributes only, not its associations.
85
- # The extent of a "deep" clone is application-specific and is therefore
86
- # left to the application to implement according to its need.
87
- def clone
88
- attrs = self.attributes_before_type_cast
89
- self.class.primary_keys.each {|key| attrs.delete(key.to_s)}
90
- self.class.new do |record|
91
- record.send :instance_variable_set, '@attributes', attrs
92
- end
93
- end
94
-
95
-
96
- private
97
- # The xx_without_callbacks methods are overwritten as that is the end of the alias chain
98
-
99
- # Creates a new record with values matching those of the instance attributes.
100
- def create_without_callbacks
101
- unless self.id
102
- raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"
103
- end
104
- attributes_minus_pks = attributes_with_quotes(false)
105
- quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) }
106
- cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns
107
- vals = attributes_minus_pks.values << quoted_id
108
- connection.insert(
109
- "INSERT INTO #{self.class.quoted_table_name} " +
110
- "(#{cols.join(', ')}) " +
111
- "VALUES (#{vals.join(', ')})",
112
- "#{self.class.name} Create",
113
- self.class.primary_key,
114
- self.id
115
- )
116
- @new_record = false
117
- return true
118
- end
119
-
120
- # Updates the associated record with values matching those of the instance attributes.
121
- def update_without_callbacks
122
- where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
123
- "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
124
- end
125
- where_clause = where_clause_terms.join(" AND ")
126
- connection.update(
127
- "UPDATE #{self.class.quoted_table_name} " +
128
- "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
129
- "WHERE #{where_clause}",
130
- "#{self.class.name} Update"
131
- )
132
- return true
133
- end
38
+ def composite?
39
+ self.class.composite?
40
+ end
134
41
 
135
- # Deletes the record in the database and freezes this instance to reflect that no changes should
136
- # be made (since they can't be persisted).
137
- def destroy_without_callbacks
138
- where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
139
- "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
140
- end
141
- where_clause = where_clause_terms.join(" AND ")
142
- unless new_record?
143
- connection.delete(
144
- "DELETE FROM #{self.class.quoted_table_name} " +
145
- "WHERE #{where_clause}",
146
- "#{self.class.name} Destroy"
147
- )
148
- end
149
- freeze
150
- end
42
+ def [](attr_name)
43
+ # CPK
44
+ if attr_name.is_a?(String) and attr_name != attr_name.split(CompositePrimaryKeys::ID_SEP).first
45
+ attr_name = attr_name.split(CompositePrimaryKeys::ID_SEP)
151
46
  end
152
47
 
153
- module CompositeClassMethods
154
- def primary_key; primary_keys; end
155
- def primary_key=(keys); primary_keys = keys; end
48
+ # CPK
49
+ if attr_name.is_a?(Array)
50
+ values = attr_name.map {|name| read_attribute(name)}
51
+ CompositePrimaryKeys::CompositeKeys.new(values)
52
+ else
53
+ read_attribute(attr_name)
54
+ end
55
+ end
156
56
 
157
- def composite?
158
- true
159
- end
57
+ def []=(attr_name, value)
58
+ # CPK
59
+ if attr_name.is_a?(String) and attr_name != attr_name.split(CompositePrimaryKeys::ID_SEP).first
60
+ attr_name = attr_name.split(CompositePrimaryKeys::ID_SEP)
61
+ end
160
62
 
161
- #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
162
- #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
163
- def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
164
- many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)
165
- end
166
-
167
- # Creates WHERE condition from list of composited ids
168
- # User.update_all({:role => 'admin'}, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> UPDATE admins SET admin.role='admin' WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)
169
- # User.find(:all, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> SELECT * FROM admins WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)
170
- def composite_where_clause(ids)
171
- if ids.is_a?(String)
172
- ids = [[ids]]
173
- elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
174
- ids = [ids.to_composite_ids]
175
- end
176
-
177
- ids.map do |id_set|
178
- [primary_keys, id_set].transpose.map do |key, id|
179
- "#{table_name}.#{key.to_s}=#{sanitize(id)}"
180
- end.join(" AND ")
181
- end.join(") OR (")
63
+ if attr_name.is_a? Array
64
+ unless value.length == attr_name.length
65
+ raise "Number of attr_names and values do not match"
182
66
  end
67
+ [attr_name, value].transpose.map {|name,val| write_attribute(name, val)}
68
+ value
69
+ else
70
+ write_attribute(attr_name, value)
71
+ end
72
+ end
73
+
74
+ module CompositeClassMethods
75
+ def primary_key
76
+ primary_keys
77
+ end
183
78
 
184
- # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
185
- # Example:
186
- # Person.exists?(5,7)
187
- def exists?(ids)
188
- if ids.is_a?(Array) && ids.first.is_a?(String)
189
- count(:conditions => ids) > 0
190
- else
191
- obj = find(ids) rescue false
192
- !obj.nil? and obj.is_a?(self)
193
- end
194
- end
79
+ def primary_key=(keys)
80
+ primary_keys = keys
81
+ end
195
82
 
196
- # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
197
- # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
198
- # are deleted.
199
- def delete(*ids)
200
- unless ids.is_a?(Array); raise "*ids must be an Array"; end
201
- ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
202
- where_clause = ids.map do |id_set|
203
- [primary_keys, id_set].transpose.map do |key, id|
204
- "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}"
205
- end.join(" AND ")
206
- end.join(") OR (")
207
- delete_all([ "(#{where_clause})" ])
208
- end
83
+ def composite?
84
+ true
85
+ end
209
86
 
210
- # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
211
- # If an array of ids is provided, all of them are destroyed.
212
- def destroy(*ids)
213
- unless ids.is_a?(Array); raise "*ids must be an Array"; end
214
- if ids.first.is_a?(Array)
215
- ids = ids.map{|compids| compids.to_composite_ids}
216
- else
217
- ids = ids.to_composite_ids
218
- end
219
- ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
220
- end
87
+ #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
88
+ #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
89
+ def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
90
+ many_ids.map {|ids| "#{left_bracket}#{CompositePrimaryKeys::CompositeKeys.new(ids)}#{right_bracket}"}.join(list_sep)
91
+ end
92
+ end
221
93
 
222
- # Returns an array of column objects for the table associated with this class.
223
- # Each column that matches to one of the primary keys has its
224
- # primary attribute set to true
225
- def columns
226
- unless @columns
227
- @columns = connection.columns(table_name, "#{name} Columns")
228
- @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
229
- end
230
- @columns
231
- end
94
+ module CompositeInstanceMethods
95
+ # A model instance's primary keys is always available as model.ids
96
+ # whether you name it the default 'id' or set it to something else.
97
+ def id
98
+ attr_names = self.class.primary_keys
99
+ Array.new(attr_names.map { |attr_name| read_attribute(attr_name) })
100
+ end
101
+ alias_method :ids, :id
232
102
 
233
- ## DEACTIVATED METHODS ##
234
- public
235
- # Lazy-set the sequence name to the connection's default. This method
236
- # is only ever called once since set_sequence_name overrides it.
237
- def sequence_name #:nodoc:
238
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
103
+ def ids_hash
104
+ self.class.primary_key.zip(ids).inject(Hash.new) do |hash, (key, value)|
105
+ hash[key] = value
106
+ hash
239
107
  end
108
+ end
240
109
 
241
- def reset_sequence_name #:nodoc:
242
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
243
- end
110
+ def to_param
111
+ id.join(CompositePrimaryKeys::ID_SEP)
112
+ end
244
113
 
245
- def set_primary_key(value = nil, &block)
246
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
247
- end
114
+ def id_before_type_cast #:nodoc:
115
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET
116
+ end
248
117
 
249
- private
250
- def find_one(id, options)
251
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
252
- end
118
+ def quoted_id #:nodoc:
119
+ [self.class.primary_keys, ids].
120
+ transpose.
121
+ map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.
122
+ to_composite_ids
123
+ end
253
124
 
254
- def find_some(ids, options)
255
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
125
+ # Sets the primary ID.
126
+ def id=(ids)
127
+ ids = ids.split(CompositePrimaryKeys::ID_SEP) if ids.is_a?(String)
128
+ ids.flatten!
129
+ unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length
130
+ raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"
256
131
  end
132
+ [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}
133
+ id
134
+ end
257
135
 
258
- def find_from_ids(ids, options)
259
- ids = ids.first if ids.last == nil
260
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
261
- # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
262
- # if ids is list of lists, then each inner list must follow rule above
263
- if ids.first.is_a? String
264
- # find '2,1' -> ids = ['2,1']
265
- # find '2,1;7,3' -> ids = ['2,1;7,3']
266
- ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}
267
- # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
268
- end
269
- ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
270
- ids.each do |id_set|
271
- unless id_set.is_a?(Array)
272
- raise "Ids must be in an Array, instead received: #{id_set.inspect}"
273
- end
274
- unless id_set.length == primary_keys.length
275
- raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
276
- end
277
- end
278
-
279
- # Let keys = [:a, :b]
280
- # If ids = [[10, 50], [11, 51]], then :conditions =>
281
- # "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))"
282
-
283
- conditions = ids.map do |id_set|
284
- [primary_keys, id_set].transpose.map do |key, id|
285
- col = columns_hash[key.to_s]
286
- val = quote_value(id, col)
287
- "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}"
288
- end.join(" AND ")
289
- end.join(") OR (")
290
-
291
- options.update :conditions => "(#{conditions})"
292
-
293
- result = find_every(options)
294
-
295
- if result.size == ids.size
296
- ids.size == 1 ? result[0] : result
297
- else
298
- raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"
299
- end
136
+ # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone
137
+ # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is
138
+ # application specific and is therefore left to the application to implement according to its need.
139
+ def initialize_copy(other)
140
+ # Think the assertion which fails if the after_initialize callback goes at the end of the method is wrong. The
141
+ # deleted clone method called new which therefore called the after_initialize callback. It then went on to copy
142
+ # over the attributes. But if it's copying the attributes afterwards then it hasn't finished initializing right?
143
+ # For example in the test suite the topic model's after_initialize method sets the author_email_address to
144
+ # test@test.com. I would have thought this would mean that all cloned models would have an author email address
145
+ # of test@test.com. However the test_clone test method seems to test that this is not the case. As a result the
146
+ # after_initialize callback has to be run *before* the copying of the atrributes rather than afterwards in order
147
+ # for all tests to pass. This makes no sense to me.
148
+ callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
149
+ cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
150
+ # CPK
151
+ #cloned_attributes.delete(self.class.primary_key)
152
+ self.class.primary_key.each {|key| cloned_attributes.delete(key.to_s)}
153
+
154
+ @attributes = cloned_attributes
155
+ clear_aggregation_cache
156
+ @attributes_cache = {}
157
+ @new_record = true
158
+ ensure_proper_type
159
+
160
+ if scope = self.class.send(:current_scoped_methods)
161
+ create_with = scope.scope_for_create
162
+ create_with.each { |att,value| self.send("#{att}=", value) } if create_with
300
163
  end
301
164
  end
302
- end
303
- end
304
- end
305
165
 
166
+ def destroy
167
+ if persisted?
168
+ # CPK
169
+ # self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all
170
+ self.class.unscoped.where(ids_hash).delete_all
171
+ end
306
172
 
307
- module ActiveRecord
308
- ID_SEP = ','
309
- ID_SET_SEP = ';'
310
-
311
- class Base
312
- # Allows +attr_name+ to be the list of primary_keys, and returns the id
313
- # of the object
314
- # e.g. @object[@object.class.primary_key] => [1,1]
315
- def [](attr_name)
316
- if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
317
- attr_name = attr_name.split(ID_SEP)
318
- end
319
- attr_name.is_a?(Array) ?
320
- attr_name.map {|name| read_attribute(name)} :
321
- read_attribute(attr_name)
322
- end
323
-
324
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
325
- # (Alias for the protected write_attribute method).
326
- def []=(attr_name, value)
327
- if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
328
- attr_name = attr_name.split(ID_SEP)
173
+ @destroyed = true
174
+ freeze
329
175
  end
330
176
 
331
- if attr_name.is_a? Array
332
- value = value.split(ID_SEP) if value.is_a? String
333
- unless value.length == attr_name.length
334
- raise "Number of attr_names and values do not match"
335
- end
336
- #breakpoint
337
- [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
338
- else
339
- write_attribute(attr_name, value)
177
+ def update(attribute_names = @attributes.keys)
178
+ attributes_with_values = arel_attributes_values(false, false, attribute_names)
179
+ return 0 if attributes_with_values.empty?
180
+ # CPK
181
+ # self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
182
+ self.class.unscoped.where(ids_hash).arel.update(attributes_with_values)
340
183
  end
341
184
  end
342
185
  end
343
- end
186
+ end