composite_primary_keys 3.1.11 → 4.0.0.beta1

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 (62) hide show
  1. data/History.txt +6 -8
  2. data/lib/composite_primary_keys.rb +53 -36
  3. data/lib/composite_primary_keys/associations/association.rb +23 -0
  4. data/lib/composite_primary_keys/associations/association_scope.rb +67 -0
  5. data/lib/composite_primary_keys/associations/has_and_belongs_to_many_association.rb +31 -121
  6. data/lib/composite_primary_keys/associations/has_many_association.rb +27 -66
  7. data/lib/composite_primary_keys/associations/join_dependency/join_association.rb +22 -0
  8. data/lib/composite_primary_keys/associations/join_dependency/join_part.rb +39 -0
  9. data/lib/composite_primary_keys/associations/preloader/association.rb +61 -0
  10. data/lib/composite_primary_keys/associations/preloader/belongs_to.rb +13 -0
  11. data/lib/composite_primary_keys/associations/preloader/has_and_belongs_to_many.rb +46 -0
  12. data/lib/composite_primary_keys/attribute_methods/dirty.rb +30 -0
  13. data/lib/composite_primary_keys/attribute_methods/read.rb +88 -0
  14. data/lib/composite_primary_keys/attribute_methods/write.rb +33 -0
  15. data/lib/composite_primary_keys/base.rb +18 -70
  16. data/lib/composite_primary_keys/composite_predicates.rb +53 -0
  17. data/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb +6 -4
  18. data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +19 -41
  19. data/lib/composite_primary_keys/fixtures.rb +19 -6
  20. data/lib/composite_primary_keys/persistence.rb +32 -13
  21. data/lib/composite_primary_keys/relation.rb +23 -16
  22. data/lib/composite_primary_keys/relation/calculations.rb +48 -0
  23. data/lib/composite_primary_keys/relation/finder_methods.rb +117 -0
  24. data/lib/composite_primary_keys/relation/query_methods.rb +24 -0
  25. data/lib/composite_primary_keys/validations/uniqueness.rb +19 -23
  26. data/lib/composite_primary_keys/version.rb +5 -5
  27. data/test/connections/native_mysql/connection.rb +1 -1
  28. data/test/fixtures/articles.yml +1 -0
  29. data/test/fixtures/products.yml +2 -4
  30. data/test/fixtures/readings.yml +1 -0
  31. data/test/fixtures/suburbs.yml +1 -4
  32. data/test/fixtures/users.yml +1 -0
  33. data/test/test_associations.rb +61 -63
  34. data/test/test_attributes.rb +16 -21
  35. data/test/test_create.rb +3 -3
  36. data/test/test_delete.rb +87 -84
  37. data/test/{test_clone.rb → test_dup.rb} +8 -5
  38. data/test/test_exists.rb +22 -10
  39. data/test/test_habtm.rb +0 -74
  40. data/test/test_ids.rb +2 -1
  41. data/test/test_miscellaneous.rb +2 -2
  42. data/test/test_polymorphic.rb +1 -1
  43. data/test/test_suite.rb +1 -1
  44. data/test/test_update.rb +3 -3
  45. metadata +76 -75
  46. data/lib/composite_primary_keys/association_preload.rb +0 -158
  47. data/lib/composite_primary_keys/associations.rb +0 -155
  48. data/lib/composite_primary_keys/associations/association_proxy.rb +0 -33
  49. data/lib/composite_primary_keys/associations/has_one_association.rb +0 -27
  50. data/lib/composite_primary_keys/associations/through_association_scope.rb +0 -103
  51. data/lib/composite_primary_keys/attribute_methods.rb +0 -84
  52. data/lib/composite_primary_keys/calculations.rb +0 -31
  53. data/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb +0 -21
  54. data/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb +0 -15
  55. data/lib/composite_primary_keys/connection_adapters/oracle_enhanced_adapter.rb +0 -17
  56. data/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb +0 -15
  57. data/lib/composite_primary_keys/finder_methods.rb +0 -123
  58. data/lib/composite_primary_keys/primary_key.rb +0 -19
  59. data/lib/composite_primary_keys/query_methods.rb +0 -24
  60. data/lib/composite_primary_keys/read.rb +0 -25
  61. data/lib/composite_primary_keys/reflection.rb +0 -37
  62. data/lib/composite_primary_keys/write.rb +0 -18
@@ -0,0 +1,53 @@
1
+ module CompositePrimaryKeys
2
+ module Predicates
3
+ def cpk_and_predicate(predicates)
4
+ if predicates.length == 1
5
+ predicates.first
6
+ else
7
+ Arel::Nodes::And.new(predicates)
8
+ end
9
+ end
10
+
11
+ def cpk_or_predicate(predicates)
12
+ result = predicates.shift
13
+ predicates.each do |predicate|
14
+ result = result.or(predicate)
15
+ end
16
+ result
17
+ end
18
+
19
+ def cpk_id_predicate(table, keys, values)
20
+ eq_predicates = keys.zip(values).map do |key, value|
21
+ table[key].eq(value)
22
+ end
23
+ cpk_and_predicate(eq_predicates)
24
+ end
25
+
26
+ def cpk_join_predicate(table1, key1, table2, key2)
27
+ key1_fields = Array(key1).map {|key| table1[key]}
28
+ key2_fields = Array(key2).map {|key| table2[key]}
29
+
30
+ eq_predicates = key1_fields.zip(key2_fields).map do |key_field1, key_field2|
31
+ key_field1.eq(key_field2)
32
+ end
33
+ cpk_and_predicate(eq_predicates)
34
+ end
35
+
36
+ def cpk_in_predicate(table, primary_keys, ids)
37
+ and_predicates = ids.map do |id_set|
38
+ eq_predicates = Array(primary_keys).zip(Array(id_set)).map do |primary_key, value|
39
+ table[primary_key].eq(value)
40
+ end
41
+ cpk_and_predicate(eq_predicates)
42
+ end
43
+
44
+ cpk_or_predicate(and_predicates)
45
+ end
46
+ end
47
+ end
48
+
49
+ ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
50
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation.send(:include, CompositePrimaryKeys::Predicates)
51
+ ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
52
+ ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
53
+ ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
@@ -1,9 +1,11 @@
1
1
  module ActiveRecord
2
- module ConnectionAdapters # :nodoc:
2
+ module ConnectionAdapters
3
3
  class AbstractAdapter
4
- def concat(*columns)
5
- "CONCAT(#{columns.join(',')})"
4
+ def quote_column_names(name)
5
+ Array(name).map do |col|
6
+ quote_column_name(col.to_s)
7
+ end.join(CompositePrimaryKeys::ID_SEP)
6
8
  end
7
9
  end
8
10
  end
9
- end
11
+ end
@@ -1,52 +1,30 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
- class PostgreSQLAdapter < AbstractAdapter
4
-
5
- # This mightn't be in Core, but count(distinct x,y) doesn't work for me
6
- def supports_count_distinct? #:nodoc:
7
- false
8
- end
9
-
10
- def concat(*columns)
11
- columns = columns.map { |c| "CAST(#{c} AS varchar)" }
12
- "(#{columns.join('||')})"
13
- end
14
-
15
- # Executes an INSERT query and returns the new record's ID
16
- def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
3
+ class PostgreSQLAdapter
4
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
17
5
  # Extract the table from the insert sql. Yuck.
18
- table = sql.split(" ", 4)[2].gsub('"', '')
6
+ _, table = extract_schema_and_table(sql.split(" ", 4)[2])
19
7
 
20
- # Try an insert with 'returning id' if available (PG >= 8.2)
21
- if supports_insert_with_returning?
22
- pk, sequence_name = *pk_and_sequence_for(table) unless pk
23
- if pk
24
- quoted_pk = if pk.is_a?(Array)
25
- pk.map { |col| quote_column_name(col) }.join(CompositePrimaryKeys::ID_SEP)
26
- else
27
- quote_column_name(pk)
28
- end
29
- id = select_value("#{sql} RETURNING #{quoted_pk}")
30
- clear_query_cache
31
- return id
32
- end
33
- end
8
+ pk ||= primary_key(table)
34
9
 
35
- # Otherwise, insert then grab last_insert_id.
36
- if insert_id = super
37
- insert_id
10
+ if pk
11
+ select_value("#{sql} RETURNING #{quote_column_names(pk)}")
38
12
  else
39
- # If neither pk nor sequence name is given, look them up.
40
- unless pk || sequence_name
41
- pk, sequence_name = *pk_and_sequence_for(table)
42
- end
13
+ super
14
+ end
15
+ end
16
+ alias :create :insert
17
+
18
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
19
+ unless pk
20
+ _, table = extract_schema_and_table(sql.split(" ", 4)[2])
43
21
 
44
- # If a pk is given, fallback to default sequence name.
45
- # Don't fetch last insert id for a table without a pk.
46
- if pk && sequence_name ||= default_sequence_name(table, pk)
47
- last_insert_id(table, sequence_name)
48
- end
22
+ pk = primary_key(table)
49
23
  end
24
+
25
+ sql = "#{sql} RETURNING #{quote_column_names(pk)}" if pk
26
+
27
+ [sql, binds]
50
28
  end
51
29
  end
52
30
  end
@@ -1,9 +1,22 @@
1
- class Fixture
2
- def [](key)
3
- if key.is_a? Array
4
- key.map { |a_key| self[a_key.to_s] }
5
- else
6
- @fixture[key]
1
+ module ActiveRecord
2
+ class Fixture
3
+ def find
4
+ if model_class
5
+ # CPK
6
+ # model_class.find(fixture[model_class.primary_key])
7
+ ids = self.ids(model_class.primary_key)
8
+ model_class.find(ids)
9
+ else
10
+ raise FixtureClassNotFound, "No class attached to find."
11
+ end
12
+ end
13
+
14
+ def ids(key)
15
+ if key.is_a? Array
16
+ key.map {|a_key| fixture[a_key.to_s] }
17
+ else
18
+ fixture[key]
19
+ end
7
20
  end
8
21
  end
9
22
  end
@@ -1,17 +1,30 @@
1
1
  module ActiveRecord
2
2
  module Persistence
3
- def cpk_conditions
4
- if self.composite?
5
- ids_hash
6
- else
7
- self.class.arel_table[self.class.primary_key].eq(id)
8
- end
9
- end
10
-
11
3
  def destroy
12
4
  if persisted?
13
- # self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all
14
- self.class.unscoped.where(cpk_conditions).delete_all
5
+ ::ActiveRecord::IdentityMap.remove(self) if ::ActiveRecord::IdentityMap.enabled?
6
+
7
+ # CPK
8
+ #pk = self.class.primary_key
9
+ #column = self.class.columns_hash[pk]
10
+ #substitute = connection.substitute_at(column, 0)
11
+ primary_keys = Array(self.class.primary_key)
12
+ bind_values = Array.new
13
+ eq_predicates = Array.new
14
+ primary_keys.each_with_index do |key, i|
15
+ column = self.class.columns_hash[key.to_s]
16
+ bind_values << [column, self[key]]
17
+ substitute = connection.substitute_at(column, i)
18
+ eq_predicates << self.class.arel_table[key].eq(substitute)
19
+ end
20
+ predicate = Arel::Nodes::And.new(eq_predicates)
21
+ relation = self.class.unscoped.where(predicate)
22
+
23
+ # CPK
24
+ #relation.bind_values = [[column, id]]
25
+ relation.bind_values = bind_values
26
+
27
+ relation.delete_all
15
28
  end
16
29
 
17
30
  @destroyed = true
@@ -21,9 +34,15 @@ module ActiveRecord
21
34
  def update(attribute_names = @attributes.keys)
22
35
  attributes_with_values = arel_attributes_values(false, false, attribute_names)
23
36
  return 0 if attributes_with_values.empty?
37
+ klass = self.class
24
38
  # CPK
25
- # self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
26
- self.class.unscoped.where(cpk_conditions).arel.update(attributes_with_values)
39
+ if !self.composite?
40
+ stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
41
+ else
42
+ # CPK
43
+ stmt = klass.unscoped.where(ids_hash).arel.compile_update(attributes_with_values)
44
+ end
45
+ klass.connection.update stmt.to_sql
27
46
  end
28
47
  end
29
- end
48
+ end
@@ -1,20 +1,15 @@
1
- module CompositePrimaryKeys
2
- module ActiveRecord
3
- module Relation
4
- module InstanceMethods
5
- def where_cpk_id(id)
6
- relation = clone
7
-
8
- predicates = self.primary_keys.zip(Array(id)).map do |key, value|
9
- table[key].eq(value)
10
- end
11
- relation.where_values += predicates
12
- relation
13
- end
1
+ module ActiveRecord
2
+ class Relation
3
+ def add_cpk_support
4
+ class << self
5
+ include CompositePrimaryKeys::ActiveRecord::Calculations
6
+ include CompositePrimaryKeys::ActiveRecord::FinderMethods
7
+ include CompositePrimaryKeys::ActiveRecord::QueryMethods
14
8
 
15
9
  def delete(id_or_array)
10
+ ::ActiveRecord::IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if ::ActiveRecord::IdentityMap.enabled?
16
11
  # CPK
17
- # where(@klass.primary_key => id_or_array).delete_all
12
+ # where(primary_key => id_or_array).delete_all
18
13
 
19
14
  id_or_array = if id_or_array.kind_of?(CompositePrimaryKeys::CompositeKeys)
20
15
  [id_or_array]
@@ -23,7 +18,7 @@ module CompositePrimaryKeys
23
18
  end
24
19
 
25
20
  id_or_array.each do |id|
26
- where_cpk_id(id).delete_all
21
+ where(cpk_id_predicate(table, self.primary_key, id)).delete_all
27
22
  end
28
23
  end
29
24
 
@@ -41,12 +36,24 @@ module CompositePrimaryKeys
41
36
  end
42
37
 
43
38
  id_or_array.each do |id|
44
- where_cpk_id(id).each do |record|
39
+ where(cpk_id_predicate(table, self.primary_key, id)).each do |record|
45
40
  record.destroy
46
41
  end
47
42
  end
48
43
  end
49
44
  end
50
45
  end
46
+
47
+ alias :initialize_cpk :initialize
48
+ def initialize(klass, table)
49
+ initialize_cpk(klass, table)
50
+ add_cpk_support if klass.composite?
51
+ end
52
+
53
+ alias :initialize_copy_cpk :initialize_copy
54
+ def initialize_copy(other)
55
+ initialize_copy_cpk(other)
56
+ add_cpk_support if klass.composite?
57
+ end
51
58
  end
52
59
  end
@@ -0,0 +1,48 @@
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module Calculations
4
+ def execute_simple_calculation(operation, column_name, distinct)
5
+ # Postgresql doesn't like ORDER BY when there are no GROUP BY
6
+ relation = reorder(nil)
7
+
8
+ # CPK
9
+ #if operation == "count" && (relation.limit_value || relation.offset_value)
10
+ if operation == "count"
11
+ # Shortcut when limit is zero.
12
+ return 0 if relation.limit_value == 0
13
+
14
+ query_builder = build_count_subquery(relation, column_name, distinct)
15
+ else
16
+ column = aggregate_column(column_name)
17
+
18
+ select_value = operation_over_aggregate_column(column, operation, distinct)
19
+
20
+ relation.select_values = [select_value]
21
+
22
+ query_builder = relation.arel
23
+ end
24
+
25
+ type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
26
+ end
27
+
28
+ def build_count_subquery(relation, column_name, distinct)
29
+ # CPK
30
+ # column_alias = Arel.sql('count_column')
31
+ subquery_alias = Arel.sql('subquery_for_count')
32
+
33
+ # CPK
34
+ # aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
35
+ # relation.select_values = [aliased_column]
36
+ column = aggregate_column(column_name)
37
+ relation.select_values = ["DISTINCT #{column.to_s}"]
38
+ subquery = relation.arel.as(subquery_alias)
39
+
40
+ sm = Arel::SelectManager.new relation.engine
41
+ # CPK
42
+ # select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
43
+ select_value = operation_over_aggregate_column(Arel.sql("*"), 'count', false)
44
+ sm.project(select_value).from(subquery)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,117 @@
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module FinderMethods
4
+ def construct_limited_ids_condition(relation)
5
+ orders = relation.order_values
6
+ # CPK
7
+ # values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
8
+ keys = @klass.primary_keys.map do |key|
9
+ "#{@klass.connection.quote_table_name @klass.table_name}.#{key}"
10
+ end
11
+ values = @klass.connection.distinct(keys.join(', '), orders)
12
+
13
+ relation = relation.dup
14
+
15
+ ids_array = relation.select(values).collect {|row| row[primary_key]}
16
+ # CPK
17
+ # ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
18
+
19
+ # OR together each and expression (key=value and key=value) that matches an id set
20
+ # since we only need to match 0 or more records
21
+ or_expressions = ids_array.map do |id_set|
22
+ # AND together "key=value" exprssios to match each id set
23
+ and_expressions = [self.primary_keys, id_set].transpose.map do |key, id|
24
+ table[key].eq(id)
25
+ end
26
+ Arel::Nodes::And.new(and_expressions)
27
+ end
28
+
29
+ first = or_expressions.shift
30
+ Arel::Nodes::Grouping.new(or_expressions.inject(first) do |memo, expr|
31
+ Arel::Nodes::Or.new(memo, expr)
32
+ end)
33
+ end
34
+
35
+ def exists?(id = nil)
36
+ # ID can be:
37
+ # Array - ['department_id = ? and location_id = ?', 1, 1]
38
+ # Array -> [1,2]
39
+ # CompositeKeys -> [1,2]
40
+
41
+ id = id.id if ::ActiveRecord::Base === id
42
+
43
+ join_dependency = construct_join_dependency_for_association_find
44
+ relation = construct_relation_for_association_find(join_dependency)
45
+ relation = relation.except(:select).select("1").limit(1)
46
+
47
+ # CPK
48
+ #case id
49
+ #when Array, Hash
50
+ # relation = relation.where(id)
51
+ #else
52
+ # relation = relation.where(table[primary_key].eq(id)) if id
53
+ #end
54
+
55
+ case id
56
+ when CompositePrimaryKeys::CompositeKeys
57
+ relation = relation.where(cpk_id_predicate(table, primary_key, id))
58
+ when Array
59
+ if !id.first.kind_of?(String)
60
+ return self.exists?(id.to_composite_keys)
61
+ else
62
+ relation = relation.where(id)
63
+ end
64
+ when Hash
65
+ relation = relation.where(id)
66
+ end
67
+ connection.select_value(relation.to_sql) ? true : false
68
+ end
69
+
70
+ def find_with_ids(*ids, &block)
71
+ return to_a.find { |*block_args| yield(*block_args) } if block_given?
72
+
73
+ # Supports:
74
+ # find('1,2') -> ['1,2']
75
+ # find(1,2) -> [1,2]
76
+ # find([1,2]) -> [['1,2']]
77
+ # find([1,2], [3,4]) -> [[1,2],[3,4]]
78
+ #
79
+ # Does *not* support:
80
+ # find('1,2', '3,4') -> ['1,2','3,4']
81
+
82
+ # Normalize incoming data. Note the last arg can be nil. Happens
83
+ # when find is called with nil options like the reload method does.
84
+ ids.compact!
85
+ ids = [ids] unless ids.first.kind_of?(Array)
86
+
87
+ results = ids.map do |cpk_ids|
88
+ cpk_ids = if cpk_ids.length == 1
89
+ cpk_ids.first.split(CompositePrimaryKeys::ID_SEP).to_composite_keys
90
+ else
91
+ cpk_ids.to_composite_keys
92
+ end
93
+
94
+ unless cpk_ids.length == @klass.primary_keys.length
95
+ raise "#{cpk_ids.inspect}: Incorrect number of primary keys for #{@klass.name}: #{@klass.primary_keys.inspect}"
96
+ end
97
+
98
+ new_relation = clone
99
+ [@klass.primary_keys, cpk_ids].transpose.map do |key, id|
100
+ new_relation = new_relation.where(key => id)
101
+ end
102
+
103
+ records = new_relation.to_a
104
+
105
+ if records.empty?
106
+ conditions = new_relation.arel.where_sql
107
+ raise(::ActiveRecord::RecordNotFound,
108
+ "Couldn't find #{@klass.name} with ID=#{cpk_ids} #{conditions}")
109
+ end
110
+ records
111
+ end.flatten
112
+
113
+ ids.length == 1 ? results.first : results
114
+ end
115
+ end
116
+ end
117
+ end