composite_primary_keys 2.3.5.1 → 3.0.0.b2
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.
- data/History.txt +26 -0
- data/README.txt +1 -1
- data/Rakefile +41 -51
- data/lib/composite_primary_keys.rb +19 -7
- data/lib/composite_primary_keys/association_preload.rb +75 -170
- data/lib/composite_primary_keys/associations.rb +98 -400
- data/lib/composite_primary_keys/associations/association_proxy.rb +32 -0
- data/lib/composite_primary_keys/associations/has_and_belongs_to_many_association.rb +30 -0
- data/lib/composite_primary_keys/associations/has_many_association.rb +72 -0
- data/lib/composite_primary_keys/associations/has_one_association.rb +19 -0
- data/lib/composite_primary_keys/associations/through_association_scope.rb +103 -0
- data/lib/composite_primary_keys/base.rb +148 -305
- data/lib/composite_primary_keys/calculations.rb +22 -64
- data/lib/composite_primary_keys/composite_arrays.rb +3 -10
- data/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb +9 -0
- data/lib/composite_primary_keys/connection_adapters/oracle_enhanced_adapter.rb +17 -0
- data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +1 -1
- data/lib/composite_primary_keys/finder_methods.rb +71 -0
- data/lib/composite_primary_keys/fixtures.rb +1 -1
- data/lib/composite_primary_keys/read.rb +25 -0
- data/lib/composite_primary_keys/reflection.rb +30 -10
- data/lib/composite_primary_keys/relation.rb +31 -0
- data/lib/composite_primary_keys/validations/uniqueness.rb +106 -66
- data/lib/composite_primary_keys/version.rb +4 -4
- data/scripts/console.rb +1 -1
- data/tasks/Rakefile.rb +13 -0
- data/tasks/databases/mysql.rake +11 -15
- data/tasks/databases/oracle.rake +10 -11
- data/tasks/databases/postgresql.rake +10 -13
- data/tasks/databases/sqlite3.rake +9 -9
- data/test/README_tests.txt +1 -45
- data/test/abstract_unit.rb +17 -14
- data/test/connections/connection_spec.rb +19 -0
- data/test/connections/databases.example.yml +11 -0
- data/test/connections/databases.yml +13 -0
- data/test/connections/native_mysql/connection.rb +10 -2
- data/test/connections/native_oracle/connection.rb +7 -4
- data/test/connections/native_oracle_enhanced/connection.rb +23 -0
- data/test/connections/native_postgresql/connection.rb +13 -5
- data/test/connections/native_sqlite/connection.rb +7 -3
- data/test/fixtures/article_group.rb +4 -0
- data/test/fixtures/article_groups.yml +7 -0
- data/test/fixtures/db_definitions/postgresql.sql +2 -1
- data/test/fixtures/debug.log +133 -0
- data/test/fixtures/dorm.rb +3 -0
- data/test/fixtures/dorms.yml +2 -0
- data/test/fixtures/kitchen_sink.rb +3 -0
- data/test/fixtures/kitchen_sinks.yml +5 -0
- data/test/fixtures/reference_codes.yml +2 -0
- data/test/fixtures/reference_type.rb +1 -1
- data/test/fixtures/restaurant.rb +6 -0
- data/test/fixtures/restaurants.yml +5 -0
- data/test/fixtures/restaurants_suburbs.yml +11 -0
- data/test/fixtures/room.rb +10 -0
- data/test/fixtures/room_assignment.rb +4 -0
- data/test/fixtures/room_assignments.yml +4 -0
- data/test/fixtures/room_attribute.rb +3 -0
- data/test/fixtures/room_attribute_assignment.rb +5 -0
- data/test/fixtures/room_attribute_assignments.yml +4 -0
- data/test/fixtures/room_attributes.yml +3 -0
- data/test/fixtures/rooms.yml +3 -0
- data/test/fixtures/seat.rb +5 -0
- data/test/fixtures/seats.yml +4 -0
- data/test/fixtures/student.rb +4 -0
- data/test/fixtures/students.yml +2 -0
- data/test/test_associations.rb +27 -50
- data/test/test_attributes.rb +15 -19
- data/test/test_composite_arrays.rb +2 -21
- data/test/test_create.rb +3 -3
- data/test/test_delete.rb +7 -20
- data/test/test_exists.rb +3 -7
- data/test/test_find.rb +0 -8
- data/test/test_ids.rb +3 -17
- data/test/test_polymorphic.rb +5 -4
- data/test/test_suite.rb +19 -0
- data/test/{test_tutorial_examle.rb → test_tutorial_example.rb} +0 -0
- metadata +110 -72
- data/Manifest.txt +0 -123
- data/lib/adapter_helper/base.rb +0 -63
- data/lib/adapter_helper/mysql.rb +0 -13
- data/lib/adapter_helper/oracle.rb +0 -12
- data/lib/adapter_helper/postgresql.rb +0 -13
- data/lib/adapter_helper/sqlite3.rb +0 -13
- data/lib/composite_primary_keys/migration.rb +0 -20
- data/local/database_connections.rb.sample +0 -12
- data/local/paths.rb.sample +0 -2
- data/local/tasks.rb.sample +0 -2
- data/tasks/activerecord_selection.rake +0 -43
- data/tasks/databases.rake +0 -12
- data/tasks/deployment.rake +0 -22
- data/tasks/local_setup.rake +0 -13
- data/test/test_dummy.rb +0 -28
- data/tmp/test.db +0 -0
- data/website/index.html +0 -195
- data/website/index.txt +0 -159
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -126
- data/website/template.js +0 -3
- data/website/template.rhtml +0 -53
- data/website/version-raw.js +0 -3
- data/website/version-raw.txt +0 -2
- data/website/version.js +0 -4
- 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
|
2
|
-
|
3
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
13
|
+
if keys.length == 1
|
14
|
+
set_primary_key(keys.first)
|
15
|
+
return
|
16
|
+
end
|
27
17
|
|
28
|
-
|
29
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
21
|
+
class_eval <<-EOV
|
22
|
+
extend CompositeClassMethods
|
23
|
+
include CompositeInstanceMethods
|
24
|
+
include CompositePrimaryKeys::ActiveRecord::AssociationPreload
|
25
|
+
EOV
|
36
26
|
|
37
|
-
|
38
|
-
|
27
|
+
class << unscoped
|
28
|
+
include CompositePrimaryKeys::ActiveRecord::FinderMethods::InstanceMethods
|
29
|
+
include CompositePrimaryKeys::ActiveRecord::Relation::InstanceMethods
|
39
30
|
end
|
40
31
|
end
|
41
32
|
|
42
|
-
|
43
|
-
|
33
|
+
def composite?
|
34
|
+
false
|
44
35
|
end
|
36
|
+
end
|
45
37
|
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
110
|
+
def to_param
|
111
|
+
id.join(CompositePrimaryKeys::ID_SEP)
|
112
|
+
end
|
244
113
|
|
245
|
-
|
246
|
-
|
247
|
-
|
114
|
+
def id_before_type_cast #:nodoc:
|
115
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET
|
116
|
+
end
|
248
117
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
-
|
255
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
-
|
308
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|