composite_primary_keys 14.0.5 → 14.0.6
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.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76a010358e1d2cacd9b05f6229c1fe4225f20f888fefb57b0bd6a9f62589a720
|
4
|
+
data.tar.gz: 1b7870c774bbed21d4556cf53518054860903cae4a0987f5978e02fc3883597f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 117d7be0b54619f7adcb05ddb200c3c97571cc74bf2fcf33c99034efaec08924cf6d56e998db1135c92bf71497697b30ca531a882d755329d5b6f9192d606013
|
7
|
+
data.tar.gz: a9f330398e5b1a9153a67f229a28e83d65c5bc02334e6e9f3456229c4c1e011a3bf03909fa45e37739d4ab47a37663a3ee6ef09c7d7194721e91849e520c0a01
|
data/History.rdoc
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
== 14.0.6 (2023-02-04)
|
2
|
+
* Port fix for #573 (Charlie Savage)
|
3
|
+
* Port fix for fix #577 (Charlie Savage)
|
4
|
+
|
1
5
|
== 14.0.5 (2023-02-04)
|
2
6
|
* Improve query generation for cpk_in_predicate. This reduces the length of
|
3
7
|
queries when loading many keys and enables Postgres to use index scans
|
@@ -27,6 +31,17 @@ more frequently. (Andrew Kiellor)
|
|
27
31
|
== 14.0.0 (2022-01-9)
|
28
32
|
* Update to ActiveRecord 7.0 (Sammy Larbi)
|
29
33
|
|
34
|
+
== 13.0.7 (2023-02-04)
|
35
|
+
* Fix #573 (Charlie Savage)
|
36
|
+
|
37
|
+
== 13.0.6 (2023-02-04)
|
38
|
+
* Fix #577 (Charlie Savage)
|
39
|
+
|
40
|
+
== 13.0.5 (2023-02-04)
|
41
|
+
* Improve query generation for cpk_in_predicate. This reduces the length of
|
42
|
+
queries when loading many keys and enables Postgres to use index scans
|
43
|
+
more frequently. (Andrew Kiellor)
|
44
|
+
|
30
45
|
== 13.0.4 (2022-12-05)
|
31
46
|
* Fix previously_new_record? not being set to true after create (Akinori MUSHA)
|
32
47
|
|
@@ -46,22 +46,6 @@ module ActiveRecord
|
|
46
46
|
(result[key] ||= []) << owner if key
|
47
47
|
end
|
48
48
|
end
|
49
|
-
|
50
|
-
# TODO: is records_by_owner needed anymore? Rails' implementation has changed significantly
|
51
|
-
def records_by_owner
|
52
|
-
@records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
|
53
|
-
key = if association_key_name.is_a?(Array)
|
54
|
-
Array(record[association_key_name]).map do |key|
|
55
|
-
convert_key(key)
|
56
|
-
end
|
57
|
-
else
|
58
|
-
convert_key(record[association_key_name])
|
59
|
-
end
|
60
|
-
owners_by_key[key].each do |owner|
|
61
|
-
(result[owner] ||= []) << record
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
49
|
end
|
66
50
|
end
|
67
51
|
end
|
@@ -1,197 +1,197 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
class Relation
|
3
|
-
alias :initialize_without_cpk :initialize
|
4
|
-
def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
|
5
|
-
initialize_without_cpk(klass, table: table, predicate_builder: predicate_builder, values: values)
|
6
|
-
add_cpk_support if klass && klass.composite?
|
7
|
-
end
|
8
|
-
|
9
|
-
alias :initialize_copy_without_cpk :initialize_copy
|
10
|
-
def initialize_copy(other)
|
11
|
-
initialize_copy_without_cpk(other)
|
12
|
-
add_cpk_support if klass.composite?
|
13
|
-
end
|
14
|
-
|
15
|
-
def add_cpk_support
|
16
|
-
extend CompositePrimaryKeys::CompositeRelation
|
17
|
-
end
|
18
|
-
|
19
|
-
def update_all(updates)
|
20
|
-
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
|
21
|
-
|
22
|
-
if eager_loading?
|
23
|
-
relation = apply_join_dependency
|
24
|
-
return relation.update_all(updates)
|
25
|
-
end
|
26
|
-
|
27
|
-
stmt = Arel::UpdateManager.new
|
28
|
-
stmt.table(arel.join_sources.empty? ? table : arel.source)
|
29
|
-
stmt.key = table[primary_key]
|
30
|
-
|
31
|
-
# CPK
|
32
|
-
if @klass.composite?
|
33
|
-
stmt = Arel::UpdateManager.new
|
34
|
-
stmt.table(arel_table)
|
35
|
-
cpk_subquery(stmt)
|
36
|
-
else
|
37
|
-
stmt.wheres = arel.constraints
|
38
|
-
end
|
39
|
-
stmt.take(arel.limit)
|
40
|
-
stmt.offset(arel.offset)
|
41
|
-
stmt.order(*arel.orders)
|
42
|
-
|
43
|
-
if updates.is_a?(Hash)
|
44
|
-
if klass.locking_enabled? &&
|
45
|
-
!updates.key?(klass.locking_column) &&
|
46
|
-
!updates.key?(klass.locking_column.to_sym)
|
47
|
-
attr = table[klass.locking_column]
|
48
|
-
updates[attr.name] = _increment_attribute(attr)
|
49
|
-
end
|
50
|
-
stmt.set _substitute_values(updates)
|
51
|
-
else
|
52
|
-
stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
|
53
|
-
end
|
54
|
-
|
55
|
-
@klass.connection.update stmt, "#{@klass} Update All"
|
56
|
-
end
|
57
|
-
|
58
|
-
def delete_all
|
59
|
-
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
|
60
|
-
value = @values[method]
|
61
|
-
method == :distinct ? value : value&.any?
|
62
|
-
end
|
63
|
-
if invalid_methods.any?
|
64
|
-
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
|
65
|
-
end
|
66
|
-
|
67
|
-
if eager_loading?
|
68
|
-
relation = apply_join_dependency
|
69
|
-
return relation.delete_all
|
70
|
-
end
|
71
|
-
|
72
|
-
stmt = Arel::DeleteManager.new
|
73
|
-
stmt.from(arel.join_sources.empty? ? table : arel.source)
|
74
|
-
stmt.key = table[primary_key]
|
75
|
-
|
76
|
-
# CPK
|
77
|
-
if @klass.composite?
|
78
|
-
stmt = Arel::DeleteManager.new
|
79
|
-
stmt.from(arel_table)
|
80
|
-
cpk_subquery(stmt)
|
81
|
-
else
|
82
|
-
stmt.wheres = arel.constraints
|
83
|
-
end
|
84
|
-
|
85
|
-
stmt.take(arel.limit)
|
86
|
-
stmt.offset(arel.offset)
|
87
|
-
stmt.order(*arel.orders)
|
88
|
-
|
89
|
-
affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
|
90
|
-
|
91
|
-
reset
|
92
|
-
affected
|
93
|
-
end
|
94
|
-
|
95
|
-
# CPK
|
96
|
-
def cpk_subquery(stmt)
|
97
|
-
# For update and delete statements we need a way to specify which records should
|
98
|
-
# get updated. By default, Rails creates a nested IN subquery that uses the primary
|
99
|
-
# key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
|
100
|
-
# columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
|
101
|
-
# and MySQL supports obfuscated IN queries. Thus we need to check the type of
|
102
|
-
# database adapter to decide how to proceed.
|
103
|
-
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
104
|
-
cpk_mysql_subquery(stmt)
|
105
|
-
elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
|
106
|
-
cpk_exists_subquery(stmt)
|
107
|
-
else
|
108
|
-
cpk_in_subquery(stmt)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
# Used by postgresql, sqlite, mariadb and oracle. Example query:
|
113
|
-
#
|
114
|
-
# UPDATE reference_codes
|
115
|
-
# SET ...
|
116
|
-
# WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
|
117
|
-
# (SELECT reference_codes.reference_type_id, reference_codes.reference_code
|
118
|
-
# FROM reference_codes)
|
119
|
-
def cpk_in_subquery(stmt)
|
120
|
-
# Setup the subquery
|
121
|
-
subquery = arel.clone
|
122
|
-
subquery.projections = primary_keys.map do |key|
|
123
|
-
arel_table[key]
|
124
|
-
end
|
125
|
-
|
126
|
-
where_fields = primary_keys.map do |key|
|
127
|
-
arel_table[key]
|
128
|
-
end
|
129
|
-
where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
|
130
|
-
stmt.wheres = [where]
|
131
|
-
end
|
132
|
-
|
133
|
-
# CPK. This is an alternative to IN subqueries. It is used by sqlserver.
|
134
|
-
# Example query:
|
135
|
-
#
|
136
|
-
# UPDATE reference_codes
|
137
|
-
# SET ...
|
138
|
-
# WHERE EXISTS
|
139
|
-
# (SELECT 1
|
140
|
-
# FROM reference_codes cpk_child
|
141
|
-
# WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
|
142
|
-
# reference_codes.reference_code = cpk_child.reference_code)
|
143
|
-
def cpk_exists_subquery(stmt)
|
144
|
-
arel_attributes = primary_keys.map do |key|
|
145
|
-
table[key]
|
146
|
-
end.to_composite_keys
|
147
|
-
|
148
|
-
# Clone the query
|
149
|
-
subselect = arel.clone
|
150
|
-
|
151
|
-
# Alias the table - we assume just one table
|
152
|
-
aliased_table = subselect.froms.first
|
153
|
-
aliased_table.table_alias = "cpk_child"
|
154
|
-
|
155
|
-
# Project - really we could just set this to "1"
|
156
|
-
subselect.projections = arel_attributes
|
157
|
-
|
158
|
-
# Setup correlation to the outer query via where clauses
|
159
|
-
primary_keys.map do |key|
|
160
|
-
outer_attribute = arel_table[key]
|
161
|
-
inner_attribute = aliased_table[key]
|
162
|
-
where = outer_attribute.eq(inner_attribute)
|
163
|
-
subselect.where(where)
|
164
|
-
end
|
165
|
-
stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
|
166
|
-
end
|
167
|
-
|
168
|
-
# CPK. This is the old way CPK created subqueries and is used by MySql.
|
169
|
-
# MySQL does not support referencing the same table that is being UPDATEd or
|
170
|
-
# DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
|
171
|
-
#
|
172
|
-
# UPDATE `reference_codes`
|
173
|
-
# SET ...
|
174
|
-
# WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
|
175
|
-
# (SELECT reference_type_id,reference_code
|
176
|
-
# FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
|
177
|
-
# FROM `reference_codes`) __active_record_temp)
|
178
|
-
def cpk_mysql_subquery(stmt)
|
179
|
-
arel_attributes = primary_keys.map do |key|
|
180
|
-
table[key]
|
181
|
-
end.to_composite_keys
|
182
|
-
|
183
|
-
subselect = arel.clone
|
184
|
-
subselect.projections = arel_attributes
|
185
|
-
|
186
|
-
# Materialize subquery by adding distinct
|
187
|
-
# to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
|
188
|
-
subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
|
189
|
-
|
190
|
-
key_name = arel_attributes.map(&:name).join(',')
|
191
|
-
|
192
|
-
manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
|
193
|
-
|
194
|
-
stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
1
|
+
module ActiveRecord
|
2
|
+
class Relation
|
3
|
+
alias :initialize_without_cpk :initialize
|
4
|
+
def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
|
5
|
+
initialize_without_cpk(klass, table: table, predicate_builder: predicate_builder, values: values)
|
6
|
+
add_cpk_support if klass && klass.composite?
|
7
|
+
end
|
8
|
+
|
9
|
+
alias :initialize_copy_without_cpk :initialize_copy
|
10
|
+
def initialize_copy(other)
|
11
|
+
initialize_copy_without_cpk(other)
|
12
|
+
add_cpk_support if klass.composite?
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_cpk_support
|
16
|
+
extend CompositePrimaryKeys::CompositeRelation
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_all(updates)
|
20
|
+
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
|
21
|
+
|
22
|
+
if eager_loading?
|
23
|
+
relation = apply_join_dependency
|
24
|
+
return relation.update_all(updates)
|
25
|
+
end
|
26
|
+
|
27
|
+
stmt = Arel::UpdateManager.new
|
28
|
+
stmt.table(arel.join_sources.empty? ? table : arel.source)
|
29
|
+
stmt.key = table[primary_key]
|
30
|
+
|
31
|
+
# CPK
|
32
|
+
if @klass.composite?
|
33
|
+
stmt = Arel::UpdateManager.new
|
34
|
+
stmt.table(arel_table)
|
35
|
+
cpk_subquery(stmt)
|
36
|
+
else
|
37
|
+
stmt.wheres = arel.constraints
|
38
|
+
end
|
39
|
+
stmt.take(arel.limit)
|
40
|
+
stmt.offset(arel.offset)
|
41
|
+
stmt.order(*arel.orders)
|
42
|
+
|
43
|
+
if updates.is_a?(Hash)
|
44
|
+
if klass.locking_enabled? &&
|
45
|
+
!updates.key?(klass.locking_column) &&
|
46
|
+
!updates.key?(klass.locking_column.to_sym)
|
47
|
+
attr = table[klass.locking_column]
|
48
|
+
updates[attr.name] = _increment_attribute(attr)
|
49
|
+
end
|
50
|
+
stmt.set _substitute_values(updates)
|
51
|
+
else
|
52
|
+
stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
|
53
|
+
end
|
54
|
+
|
55
|
+
@klass.connection.update stmt, "#{@klass} Update All"
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete_all
|
59
|
+
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
|
60
|
+
value = @values[method]
|
61
|
+
method == :distinct ? value : value&.any?
|
62
|
+
end
|
63
|
+
if invalid_methods.any?
|
64
|
+
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
|
65
|
+
end
|
66
|
+
|
67
|
+
if eager_loading?
|
68
|
+
relation = apply_join_dependency
|
69
|
+
return relation.delete_all
|
70
|
+
end
|
71
|
+
|
72
|
+
stmt = Arel::DeleteManager.new
|
73
|
+
stmt.from(arel.join_sources.empty? ? table : arel.source)
|
74
|
+
stmt.key = table[primary_key]
|
75
|
+
|
76
|
+
# CPK
|
77
|
+
if @klass.composite?
|
78
|
+
stmt = Arel::DeleteManager.new
|
79
|
+
stmt.from(arel_table)
|
80
|
+
cpk_subquery(stmt)
|
81
|
+
else
|
82
|
+
stmt.wheres = arel.constraints
|
83
|
+
end
|
84
|
+
|
85
|
+
stmt.take(arel.limit)
|
86
|
+
stmt.offset(arel.offset)
|
87
|
+
stmt.order(*arel.orders)
|
88
|
+
|
89
|
+
affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
|
90
|
+
|
91
|
+
reset
|
92
|
+
affected
|
93
|
+
end
|
94
|
+
|
95
|
+
# CPK
|
96
|
+
def cpk_subquery(stmt)
|
97
|
+
# For update and delete statements we need a way to specify which records should
|
98
|
+
# get updated. By default, Rails creates a nested IN subquery that uses the primary
|
99
|
+
# key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
|
100
|
+
# columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
|
101
|
+
# and MySQL supports obfuscated IN queries. Thus we need to check the type of
|
102
|
+
# database adapter to decide how to proceed.
|
103
|
+
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
104
|
+
cpk_mysql_subquery(stmt)
|
105
|
+
elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
|
106
|
+
cpk_exists_subquery(stmt)
|
107
|
+
else
|
108
|
+
cpk_in_subquery(stmt)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Used by postgresql, sqlite, mariadb and oracle. Example query:
|
113
|
+
#
|
114
|
+
# UPDATE reference_codes
|
115
|
+
# SET ...
|
116
|
+
# WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
|
117
|
+
# (SELECT reference_codes.reference_type_id, reference_codes.reference_code
|
118
|
+
# FROM reference_codes)
|
119
|
+
def cpk_in_subquery(stmt)
|
120
|
+
# Setup the subquery
|
121
|
+
subquery = arel.clone
|
122
|
+
subquery.projections = primary_keys.map do |key|
|
123
|
+
arel_table[key]
|
124
|
+
end
|
125
|
+
|
126
|
+
where_fields = primary_keys.map do |key|
|
127
|
+
arel_table[key]
|
128
|
+
end
|
129
|
+
where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
|
130
|
+
stmt.wheres = [where]
|
131
|
+
end
|
132
|
+
|
133
|
+
# CPK. This is an alternative to IN subqueries. It is used by sqlserver.
|
134
|
+
# Example query:
|
135
|
+
#
|
136
|
+
# UPDATE reference_codes
|
137
|
+
# SET ...
|
138
|
+
# WHERE EXISTS
|
139
|
+
# (SELECT 1
|
140
|
+
# FROM reference_codes cpk_child
|
141
|
+
# WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
|
142
|
+
# reference_codes.reference_code = cpk_child.reference_code)
|
143
|
+
def cpk_exists_subquery(stmt)
|
144
|
+
arel_attributes = primary_keys.map do |key|
|
145
|
+
table[key]
|
146
|
+
end.to_composite_keys
|
147
|
+
|
148
|
+
# Clone the query
|
149
|
+
subselect = arel.clone
|
150
|
+
|
151
|
+
# Alias the table - we assume just one table
|
152
|
+
aliased_table = subselect.froms.first
|
153
|
+
aliased_table.table_alias = "cpk_child"
|
154
|
+
|
155
|
+
# Project - really we could just set this to "1"
|
156
|
+
subselect.projections = arel_attributes
|
157
|
+
|
158
|
+
# Setup correlation to the outer query via where clauses
|
159
|
+
primary_keys.map do |key|
|
160
|
+
outer_attribute = arel_table[key]
|
161
|
+
inner_attribute = aliased_table[key]
|
162
|
+
where = outer_attribute.eq(inner_attribute)
|
163
|
+
subselect.where(where)
|
164
|
+
end
|
165
|
+
stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
|
166
|
+
end
|
167
|
+
|
168
|
+
# CPK. This is the old way CPK created subqueries and is used by MySql.
|
169
|
+
# MySQL does not support referencing the same table that is being UPDATEd or
|
170
|
+
# DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
|
171
|
+
#
|
172
|
+
# UPDATE `reference_codes`
|
173
|
+
# SET ...
|
174
|
+
# WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
|
175
|
+
# (SELECT reference_type_id,reference_code
|
176
|
+
# FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
|
177
|
+
# FROM `reference_codes`) __active_record_temp)
|
178
|
+
def cpk_mysql_subquery(stmt)
|
179
|
+
arel_attributes = primary_keys.map do |key|
|
180
|
+
table[key]
|
181
|
+
end.to_composite_keys
|
182
|
+
|
183
|
+
subselect = arel.clone
|
184
|
+
subselect.projections = arel_attributes
|
185
|
+
|
186
|
+
# Materialize subquery by adding distinct
|
187
|
+
# to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
|
188
|
+
subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
|
189
|
+
|
190
|
+
key_name = arel_attributes.map(&:name).join(',')
|
191
|
+
|
192
|
+
manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
|
193
|
+
|
194
|
+
stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|