composite_primary_keys 2.3.5.1 → 3.0.0.b2

Sign up to get free protection for your applications and to get access to all the features.
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