composite_primary_keys 3.1.11 → 4.0.0.beta1

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