composite_primary_keys 6.0.8 → 7.0.0

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 (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