composite_primary_keys 6.0.8 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +2 -8
  3. data/lib/composite_primary_keys.rb +9 -11
  4. data/lib/composite_primary_keys/active_model/dirty.rb +14 -0
  5. data/lib/composite_primary_keys/associations/association_scope.rb +30 -32
  6. data/lib/composite_primary_keys/associations/has_and_belongs_to_many_association.rb +1 -2
  7. data/lib/composite_primary_keys/associations/has_many_association.rb +13 -14
  8. data/lib/composite_primary_keys/associations/has_many_through_association.rb +88 -0
  9. data/lib/composite_primary_keys/associations/join_dependency.rb +23 -15
  10. data/lib/composite_primary_keys/associations/join_dependency/join_association.rb +3 -3
  11. data/lib/composite_primary_keys/associations/preloader/association.rb +37 -21
  12. data/lib/composite_primary_keys/associations/preloader/belongs_to.rb +9 -3
  13. data/lib/composite_primary_keys/base.rb +0 -1
  14. data/lib/composite_primary_keys/composite_predicates.rb +11 -34
  15. data/lib/composite_primary_keys/connection_adapters/abstract/connection_specification_changes.rb +6 -5
  16. data/lib/composite_primary_keys/core.rb +28 -16
  17. data/lib/composite_primary_keys/model_schema.rb +15 -0
  18. data/lib/composite_primary_keys/nested_attributes.rb +8 -10
  19. data/lib/composite_primary_keys/persistence.rb +28 -29
  20. data/lib/composite_primary_keys/relation.rb +86 -16
  21. data/lib/composite_primary_keys/relation/finder_methods.rb +113 -59
  22. data/lib/composite_primary_keys/version.rb +2 -2
  23. data/test/abstract_unit.rb +7 -8
  24. data/test/fixtures/db_definitions/db2-create-tables.sql +13 -20
  25. data/test/fixtures/db_definitions/db2-drop-tables.sql +13 -14
  26. data/test/fixtures/db_definitions/mysql.sql +0 -7
  27. data/test/fixtures/db_definitions/oracle.drop.sql +0 -1
  28. data/test/fixtures/db_definitions/oracle.sql +0 -6
  29. data/test/fixtures/db_definitions/postgresql.sql +0 -7
  30. data/test/fixtures/db_definitions/sqlite.sql +24 -30
  31. data/test/fixtures/db_definitions/sqlserver.drop.sql +1 -4
  32. data/test/fixtures/db_definitions/sqlserver.sql +7 -16
  33. data/test/fixtures/dorm.rb +2 -2
  34. data/test/fixtures/suburb.rb +2 -2
  35. data/test/test_associations.rb +1 -2
  36. data/test/test_attribute_methods.rb +3 -3
  37. data/test/test_delete.rb +3 -5
  38. data/test/test_equal.rb +5 -5
  39. data/test/test_find.rb +2 -14
  40. data/test/test_predicates.rb +2 -2
  41. data/test/test_santiago.rb +1 -1
  42. data/test/test_serialize.rb +1 -1
  43. data/test/test_suite.rb +0 -1
  44. data/test/test_tutorial_example.rb +3 -3
  45. metadata +9 -32
  46. data/lib/composite_primary_keys/attribute_methods/primary_key.rb +0 -17
  47. data/lib/composite_primary_keys/composite_relation.rb +0 -44
  48. data/lib/composite_primary_keys/locking/optimistic.rb +0 -51
  49. data/test/fixtures/model_with_callback.rb +0 -39
  50. data/test/fixtures/model_with_callbacks.yml +0 -3
  51. data/test/test_callbacks.rb +0 -36
  52. data/test/test_dumpable.rb +0 -15
  53. data/test/test_optimistic.rb +0 -18
@@ -2,14 +2,19 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class Association
5
- def records_for(ids)
5
+ def query_scope(ids)
6
6
  # CPK
7
7
  # scope.where(association_key.in(ids))
8
- predicate = cpk_in_predicate(table, reflection.foreign_key, ids)
9
- scope.where(predicate)
8
+
9
+ if reflection.foreign_key.is_a?(Array)
10
+ predicate = cpk_in_predicate(table, reflection.foreign_key, ids)
11
+ scope.where(predicate)
12
+ else
13
+ scope.where(association_key.in(ids))
14
+ end
10
15
  end
11
-
12
- def associated_records_by_owner
16
+
17
+ def associated_records_by_owner(preloader)
13
18
  # CPK
14
19
  owners_map = owners_by_key
15
20
  #owner_keys = owners_map.keys.compact
@@ -19,29 +24,40 @@ module ActiveRecord
19
24
  end
20
25
  end.compact.uniq
21
26
 
22
- if klass.nil? || owner_keys.empty?
23
- records = []
24
- else
27
+ # Each record may have multiple owners, and vice-versa
28
+ records_by_owner = owners.each_with_object({}) do |owner,h|
29
+ h[owner] = []
30
+ end
31
+
32
+ if owner_keys.any?
25
33
  # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
26
34
  # Make several smaller queries if necessary or make one query if the adapter supports it
27
- sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
28
- records = sliced.map { |slice| records_for(slice) }.flatten
35
+ sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
36
+
37
+ records = load_slices sliced
38
+ records.each do |record, owner_key|
39
+ owners_map[owner_key].each do |owner|
40
+ records_by_owner[owner] << record
41
+ end
42
+ end
29
43
  end
30
44
 
31
- # Each record may have multiple owners, and vice-versa
32
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
33
- records.each do |record|
45
+ records_by_owner
46
+ end
47
+
48
+ def load_slices(slices)
49
+ @preloaded_records = slices.flat_map { |slice|
50
+ records_for(slice)
51
+ }
52
+
53
+ @preloaded_records.map { |record|
34
54
  # CPK
35
- # owner_key = record[association_key_name].to_s
55
+ #[record, record[association_key_name]]
36
56
  owner_key = Array(association_key_name).map do |key_name|
37
57
  record[key_name]
38
58
  end.join(CompositePrimaryKeys::ID_SEP)
39
-
40
- owners_map[owner_key].each do |owner|
41
- records_by_owner[owner] << record
42
- end
43
- end
44
- records_by_owner
59
+ [record, owner_key]
60
+ }
45
61
  end
46
62
 
47
63
  def owners_by_key
@@ -59,4 +75,4 @@ module ActiveRecord
59
75
  end
60
76
  end
61
77
  end
62
- end
78
+ end
@@ -2,10 +2,16 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class BelongsTo
5
- def records_for(ids)
5
+ def query_scope(ids)
6
6
  # CPK
7
- predicate = cpk_in_predicate(table, association_key_name, ids)
8
- scope.where(predicate)
7
+ # scope.where(association_key.in(ids))
8
+
9
+ if association_key_name.is_a?(Array)
10
+ predicate = cpk_in_predicate(table, association_key_name, ids)
11
+ scope.where(predicate)
12
+ else
13
+ scope.where(association_key.in(ids))
14
+ end
9
15
  end
10
16
  end
11
17
  end
@@ -4,7 +4,6 @@ module ActiveRecord
4
4
 
5
5
  class Base
6
6
  include CompositePrimaryKeys::ActiveRecord::Persistence
7
- include CompositePrimaryKeys::ActiveRecord::Locking::Optimistic
8
7
 
9
8
  INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
10
9
  NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
@@ -8,27 +8,10 @@ module CompositePrimaryKeys
8
8
  end
9
9
  end
10
10
 
11
- def figure_engine(table)
12
- case table
13
- when Arel::Nodes::TableAlias
14
- table.left.engine
15
- when Arel::Table
16
- table.engine
17
- when ::ActiveRecord::Base
18
- table
19
- else
20
- nil
21
- end
22
- end
23
-
24
- def cpk_or_predicate(predicates, table = nil)
25
- engine = figure_engine(table)
26
- predicates = predicates.map do |predicate|
27
- predicate_sql = engine ? predicate.to_sql(engine) : predicate.to_sql
28
- "(#{predicate_sql})"
29
- end
30
- predicates = "(#{predicates.join(" OR ")})"
31
- Arel::Nodes::SqlLiteral.new(predicates)
11
+ def cpk_or_predicate(predicates)
12
+ ::Arel::Nodes::Grouping.new(predicates.inject { |memo,node|
13
+ ::Arel::Nodes::Or.new(memo, node)
14
+ })
32
15
  end
33
16
 
34
17
  def cpk_id_predicate(table, keys, values)
@@ -49,25 +32,19 @@ module CompositePrimaryKeys
49
32
  end
50
33
 
51
34
  def cpk_in_predicate(table, primary_keys, ids)
52
- primary_keys = Array(primary_keys)
53
- if primary_keys.length > 1
54
- and_predicates = ids.map do |id_set|
55
- eq_predicates = Array(primary_keys).zip(Array(id_set)).map do |primary_key, value|
56
- table[primary_key].eq(value)
57
- end
58
- cpk_and_predicate(eq_predicates)
59
- end
60
-
61
- cpk_or_predicate(and_predicates, table)
62
- else
63
- table[primary_keys.first].in(ids.flatten)
35
+ and_predicates = ids.map do |id|
36
+ cpk_id_predicate(table, primary_keys, id)
64
37
  end
38
+ cpk_or_predicate(and_predicates)
65
39
  end
66
40
  end
67
41
  end
68
42
 
69
43
  ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
70
- ActiveRecord::Associations::HasAndBelongsToManyAssociation.send(:include, CompositePrimaryKeys::Predicates)
71
44
  ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
72
45
  ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
46
+ ActiveRecord::Associations::HasManyThroughAssociation.send(:include, CompositePrimaryKeys::Predicates)
73
47
  ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
48
+
49
+
50
+
@@ -5,14 +5,15 @@ module ActiveRecord
5
5
  require "composite_primary_keys/connection_adapters/postgresql_adapter.rb"
6
6
  end
7
7
  end
8
-
8
+
9
9
  def self.establish_connection(spec = ENV["DATABASE_URL"])
10
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
11
- spec = resolver.spec
12
-
10
+ spec ||= ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_sym
11
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
12
+ spec = resolver.spec(spec)
13
+
13
14
  # CPK
14
15
  load_cpk_adapter(spec.config[:adapter])
15
-
16
+
16
17
  unless respond_to?(spec.adapter_method)
17
18
  raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
18
19
  end
@@ -1,26 +1,39 @@
1
1
  module ActiveRecord
2
2
  module Core
3
- def init_internals_with_cpk
4
- init_internals_without_cpk
5
- # Remove cpk array from attributes, fixes to_json
6
- @attributes.delete(self.class.primary_key) if self.composite?
3
+ def init_internals
4
+ pk = self.class.primary_key
5
+
6
+ # CPK
7
+ #@attributes[pk] = nil unless @attributes.key?(pk)
8
+ unless self.composite?
9
+ @attributes[pk] = nil unless @attributes.key?(pk)
10
+ end
11
+
12
+ @aggregation_cache = {}
13
+ @association_cache = {}
14
+ @attributes_cache = {}
15
+ @readonly = false
16
+ @destroyed = false
17
+ @marked_for_destruction = false
18
+ @destroyed_by_association = nil
19
+ @new_record = true
20
+ @txn = nil
21
+ @_start_transaction_state = {}
22
+ @transaction_state = nil
23
+ @reflects_state = [false]
7
24
  end
8
- alias_method_chain :init_internals, :cpk
9
25
 
10
- def initialize_dup(other)
26
+ def initialize_dup(other) # :nodoc:
11
27
  cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
12
28
  self.class.initialize_attributes(cloned_attributes, :serialized => false)
13
- # CPK
14
- # cloned_attributes.delete(self.class.primary_key)
15
- Array(self.class.primary_key).each {|key| cloned_attributes.delete(key.to_s)}
29
+
16
30
  @attributes = cloned_attributes
17
-
18
- run_callbacks(:initialize) unless _initialize_callbacks.empty?
19
31
 
20
- @changed_attributes = {}
21
- self.class.column_defaults.each do |attr, orig_value|
22
- @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
23
- end
32
+ # CPK
33
+ #@attributes[self.class.primary_key] = nil
34
+ Array(self.class.primary_key).each {|key| @attributes[key] = nil}
35
+
36
+ run_callbacks(:initialize) unless _initialize_callbacks.empty?
24
37
 
25
38
  @aggregation_cache = {}
26
39
  @association_cache = {}
@@ -28,7 +41,6 @@ module ActiveRecord
28
41
 
29
42
  @new_record = true
30
43
 
31
- ensure_proper_type
32
44
  super
33
45
  end
34
46
  end
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ module ModelSchema
3
+ module ClassMethods
4
+ def columns
5
+ @columns ||= connection.schema_cache.columns(table_name).map do |col|
6
+ col = col.dup
7
+ # CPK
8
+ #col.primary = (col.name == primary_key)
9
+ col.primary = Array(primary_key).include?(col.name)
10
+ col
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,10 +12,10 @@ module ActiveRecord
12
12
  if attributes_collection.is_a? Hash
13
13
  keys = attributes_collection.keys
14
14
  attributes_collection = if keys.include?('id') || keys.include?(:id)
15
- [attributes_collection]
16
- else
17
- attributes_collection.values
18
- end
15
+ [attributes_collection]
16
+ else
17
+ attributes_collection.values
18
+ end
19
19
  end
20
20
 
21
21
  association = association(association_name)
@@ -50,18 +50,16 @@ module ActiveRecord
50
50
  end
51
51
  elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
52
52
  unless call_reject_if(association_name, attributes)
53
- # Make sure we are operatingon the actual object which is in the assocaition's
53
+ # Make sure we are operating on the actual object which is in the association's
54
54
  # proxy_target array (either by finding it, or adding it if not found)
55
- target_record = association.target.detect { |record| record == existing_record }
56
-
55
+ # Take into account that the proxy_target may have changed due to callbacks
56
+ target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
57
57
  if target_record
58
58
  existing_record = target_record
59
59
  else
60
- association.add_to_target(existing_record)
60
+ association.add_to_target(existing_record, :skip_callbacks)
61
61
  end
62
- end
63
62
 
64
- if !call_reject_if(association_name, attributes)
65
63
  assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
66
64
  end
67
65
  else
@@ -2,24 +2,29 @@ module CompositePrimaryKeys
2
2
  module ActiveRecord
3
3
  module Persistence
4
4
  def relation_for_destroy
5
- return super unless composite?
6
-
7
- where_hash = {}
8
- primary_keys = Array(self.class.primary_key)
5
+ # CPK
6
+ #pk = self.class.primary_key
7
+ #column = self.class.columns_hash[pk]
8
+ #substitute = self.class.connection.substitute_at(column, 0)
9
+ #relation = self.class.unscoped.where(
10
+ # self.class.arel_table[pk].eq(substitute))
11
+ #relation.bind_values = [[column, id]]
9
12
 
10
- if primary_keys.empty?
11
- raise ActiveRecord::CompositeKeyError, "No primary key(s) defined for #{self.class.name}"
12
- end
13
+ relation = self.class.unscoped
13
14
 
14
- primary_keys.each do |key|
15
- where_hash[key.to_s] = self[key]
15
+ Array(self.class.primary_key).each_with_index do |key, index|
16
+ column = self.class.columns_hash[key]
17
+ substitute = self.class.connection.substitute_at(column, index)
18
+ relation = relation.where(self.class.arel_table[key].eq(substitute))
19
+ relation.bind_values += [[column, self[key]]]
16
20
  end
17
21
 
18
- relation = self.class.unscoped.where(where_hash)
22
+ relation
19
23
  end
20
-
21
24
 
22
25
  def touch(name = nil)
26
+ raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
27
+
23
28
  attributes = timestamp_attributes_for_update_in_model
24
29
  attributes << name if name
25
30
 
@@ -34,35 +39,29 @@ module CompositePrimaryKeys
34
39
 
35
40
  changes[self.class.locking_column] = increment_lock if locking_enabled?
36
41
 
37
- @changed_attributes.except!(*changes.keys)
42
+ changed_attributes.except!(*changes.keys)
38
43
 
39
44
  relation = self.class.send(:relation)
40
45
  arel_table = self.class.arel_table
41
46
  primary_key = self.class.primary_key
42
47
 
48
+ # CPK
49
+ #self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
43
50
  primary_key_predicate = relation.cpk_id_predicate(arel_table, Array(primary_key), Array(id))
44
-
45
51
  self.class.unscoped.where(primary_key_predicate).update_all(changes) == 1
52
+ else
53
+ true
46
54
  end
47
55
  end
48
56
 
49
- def _update_record(attribute_names = @attributes.keys)
50
- return super(attribute_names) unless composite?
51
-
52
- klass = self.class
53
-
54
- attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
55
- return 0 if attributes_with_values.empty?
57
+ def create_record(attribute_names = @attributes.keys)
58
+ attributes_values = arel_attributes_with_values_for_create(attribute_names)
56
59
 
57
- if !can_change_primary_key? and primary_key_changed?
58
- raise ActiveRecord::CompositeKeyError, "Cannot update primary key values without ActiveModel::Dirty"
59
- elsif primary_key_changed?
60
- stmt = klass.unscoped.where(primary_key_was).arel.compile_update(attributes_with_values)
61
- else
62
- stmt = klass.unscoped.where(ids_hash).arel.compile_update(attributes_with_values)
63
- end
64
-
65
- klass.connection.update stmt.to_sql
60
+ new_id = self.class.unscoped.insert attributes_values
61
+ self.id ||= new_id if self.class.primary_key
62
+
63
+ @new_record = false
64
+ id
66
65
  end
67
66
  end
68
67
  end
@@ -1,25 +1,10 @@
1
1
  module ActiveRecord
2
2
  class Relation
3
- def add_cpk_support
4
- extend CompositePrimaryKeys::CompositeRelation
5
- end
6
-
7
- alias :where_values_hash_without_cpk :where_values_hash
8
- def where_values_hash(relation_table_name = table_name)
9
- # CPK adds this so that it finds the Equality nodes beneath the And node:
10
- nodes_from_and = with_default_scope.where_values.grep(Arel::Nodes::And).map {|and_node| and_node.children.grep(Arel::Nodes::Equality) }.flatten
11
-
12
- equalities = (nodes_from_and + with_default_scope.where_values.grep(Arel::Nodes::Equality)).find_all { |node|
13
- node.left.relation.name == relation_table_name
14
- }
15
-
16
- Hash[equalities.map { |where| [where.left.name, where.right] }]
17
- end
18
-
19
3
  alias :initialize_without_cpk :initialize
20
4
  def initialize(klass, table, values = {})
21
5
  initialize_without_cpk(klass, table, values)
22
6
  add_cpk_support if klass && klass.composite?
7
+ add_cpk_where_values_hash
23
8
  end
24
9
 
25
10
  alias :initialize_copy_without_cpk :initialize_copy
@@ -27,5 +12,90 @@ module ActiveRecord
27
12
  initialize_copy_without_cpk(other)
28
13
  add_cpk_support if klass.composite?
29
14
  end
15
+
16
+ def add_cpk_support
17
+ class << self
18
+ include CompositePrimaryKeys::ActiveRecord::Batches
19
+ include CompositePrimaryKeys::ActiveRecord::Calculations
20
+ include CompositePrimaryKeys::ActiveRecord::FinderMethods
21
+ include CompositePrimaryKeys::ActiveRecord::QueryMethods
22
+
23
+ def delete(id_or_array)
24
+ # Without CPK:
25
+ # where(primary_key => id_or_array).delete_all
26
+
27
+ id_or_array = if id_or_array.kind_of?(CompositePrimaryKeys::CompositeKeys)
28
+ [id_or_array]
29
+ else
30
+ Array(id_or_array)
31
+ end
32
+
33
+ id_or_array.each do |id|
34
+ where(cpk_id_predicate(table, self.primary_key, id)).delete_all
35
+ end
36
+ end
37
+
38
+ def destroy(id_or_array)
39
+ # Without CPK:
40
+ #if id.is_a?(Array)
41
+ # id.map { |one_id| destroy(one_id) }
42
+ #else
43
+ # find(id).destroy
44
+ #end
45
+
46
+ id_or_array = if id_or_array.kind_of?(CompositePrimaryKeys::CompositeKeys)
47
+ [id_or_array]
48
+ else
49
+ Array(id_or_array)
50
+ end
51
+
52
+ id_or_array.each do |id|
53
+ where(cpk_id_predicate(table, self.primary_key, id)).each do |record|
54
+ record.destroy
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def add_cpk_where_values_hash
62
+ class << self
63
+ def where_values_hash
64
+ # CPK adds this so that it finds the Equality nodes beneath the And node:
65
+ #equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
66
+ # node.left.relation.name == table_name
67
+ # }
68
+ nodes_from_and = where_values.grep(Arel::Nodes::And).map {|and_node| and_node.children.grep(Arel::Nodes::Equality) }.flatten
69
+
70
+ equalities = (nodes_from_and + where_values.grep(Arel::Nodes::Equality)).find_all { |node|
71
+ node.left.relation.name == table_name
72
+ }
73
+
74
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
75
+
76
+ Hash[equalities.map { |where|
77
+ name = where.left.name
78
+ [name, binds.fetch(name.to_s) { where.right }]
79
+ }]
80
+ end
81
+ end
82
+ end
83
+
84
+ def update_record(values, id, id_was) # :nodoc:
85
+ substitutes, binds = substitute_values values
86
+
87
+ # CPK
88
+ um = if self.composite?
89
+ relation = @klass.unscoped.where(cpk_id_predicate(@klass.arel_table, @klass.primary_key, id_was || id))
90
+ relation.arel.compile_update(substitutes, @klass.primary_key)
91
+ else
92
+ @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
93
+ end
94
+
95
+ @klass.connection.update(
96
+ um,
97
+ 'SQL',
98
+ binds)
99
+ end
30
100
  end
31
101
  end