activerecord-pg-extensions 0.7.1 → 0.8.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.
- checksums.yaml +4 -4
- data/lib/active_record/pg_extensions/command_recorder.rb +119 -0
- data/lib/active_record/pg_extensions/pessimistic_migrations.rb +116 -9
- data/lib/active_record/pg_extensions/postgresql_adapter.rb +24 -0
- data/lib/active_record/pg_extensions/railtie.rb +1 -1
- data/lib/active_record/pg_extensions/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b9b21a9400229a1c720957b77284901a9be4a0628fa0b9caa031fc30fe5125a
|
|
4
|
+
data.tar.gz: b256aa3c4299d2d5ecce29956b313e6199d94567160308f86598e6ec90645399
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cdc0365c9ac099cf176f6fe58278f8798f0d37b8035efa5df0e00988ca2ba198c5991050a4bfdc89e89dd24bdfdff1b875b10109a41b98a656e7492287df264
|
|
7
|
+
data.tar.gz: 1a5145a392d36fa89bf904346e237380befe419d6702941d73a7c2ab454da99ced1ecc5fc78f0dcc152a15147316bbfadb730d37586f3364ac26df927efb018d
|
|
@@ -8,6 +8,36 @@ module ActiveRecord
|
|
|
8
8
|
record(:rename_constraint, [table_name, old_name, new_name, options])
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def change_constraint(table, constraint, **options)
|
|
12
|
+
record(:change_constraint, [table, constraint, options])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def change_check_constraint(table, **options)
|
|
16
|
+
record(:change_check_constraint, [table, options])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def change_index(table, **options)
|
|
20
|
+
record(:change_index, [table, options])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def invert_change_index(args)
|
|
26
|
+
table, options = args
|
|
27
|
+
options ||= {}
|
|
28
|
+
# reverse the change by swapping :from and :to, leaving everything else intact
|
|
29
|
+
inverted = options.merge(from: options[:to], to: options[:from])
|
|
30
|
+
[:change_index, [table, Hash.ruby2_keywords_hash(inverted)]]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def invert_change_check_constraint(args)
|
|
34
|
+
table, options = args
|
|
35
|
+
options ||= {}
|
|
36
|
+
# reverse the change by swapping :from and :to, leaving everything else intact
|
|
37
|
+
inverted = options.merge(from: options[:to], to: options[:from])
|
|
38
|
+
[:change_check_constraint, [table, Hash.ruby2_keywords_hash(inverted)]]
|
|
39
|
+
end
|
|
40
|
+
|
|
11
41
|
def invert_rename_constraint(args)
|
|
12
42
|
table_name, old_name, new_name, options = args
|
|
13
43
|
options ||= {}
|
|
@@ -15,6 +45,95 @@ module ActiveRecord
|
|
|
15
45
|
# `rename_constraint(..., if_exists:)` rather than a positional hash
|
|
16
46
|
[:rename_constraint, [table_name, new_name, old_name, Hash.ruby2_keywords_hash(options)]]
|
|
17
47
|
end
|
|
48
|
+
|
|
49
|
+
def invert_change_constraint(args)
|
|
50
|
+
table, constraint, options = args
|
|
51
|
+
options ||= {}
|
|
52
|
+
inverted = options.to_h do |key, value|
|
|
53
|
+
[key, invert_change_constraint_option(key, value)]
|
|
54
|
+
end
|
|
55
|
+
[:change_constraint, [table, constraint, Hash.ruby2_keywords_hash(inverted)]]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# stock Rails carries :if_not_exists through to the inverse (remove_*) command
|
|
59
|
+
# unchanged, but the remove side only understands :if_exists (and vice versa);
|
|
60
|
+
# rewrite the existence option so the reverse migration stays idempotent
|
|
61
|
+
def invert_add_index(args)
|
|
62
|
+
change_option(args, from: :if_not_exists, to: :if_exists)
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def invert_remove_index(args)
|
|
67
|
+
change_option(args, from: :if_exists, to: :if_not_exists)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def invert_add_column(args)
|
|
72
|
+
change_option(args, from: :if_not_exists, to: :if_exists)
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def invert_remove_column(args)
|
|
77
|
+
change_option(args, from: :if_exists, to: :if_not_exists)
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def invert_add_foreign_key(args)
|
|
82
|
+
change_option(args, from: :if_not_exists, to: :if_exists)
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def invert_remove_foreign_key(args)
|
|
87
|
+
change_option(args, from: :if_exists, to: :if_not_exists)
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def invert_add_reference(args)
|
|
92
|
+
change_reference_option(args, from: :if_not_exists, to: :if_exists)
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
alias_method :invert_add_belongs_to, :invert_add_reference
|
|
96
|
+
|
|
97
|
+
def invert_remove_reference(args)
|
|
98
|
+
change_reference_option(args, from: :if_exists, to: :if_not_exists)
|
|
99
|
+
super
|
|
100
|
+
end
|
|
101
|
+
alias_method :invert_remove_belongs_to, :invert_remove_reference
|
|
102
|
+
|
|
103
|
+
# renames an option on the trailing options hash in place, e.g. so an inverted
|
|
104
|
+
# command receives :if_exists where the original had :if_not_exists
|
|
105
|
+
def change_option(args, from:, to:)
|
|
106
|
+
options = args.last
|
|
107
|
+
return unless options.is_a?(Hash) && options.key?(from)
|
|
108
|
+
|
|
109
|
+
options[to] = options.delete(from)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# like change_option, but also rewrites the option nested inside a reference's
|
|
113
|
+
# `index:` hash (e.g. `add_reference(..., index: { if_not_exists: true })`)
|
|
114
|
+
def change_reference_option(args, from:, to:)
|
|
115
|
+
change_option(args, from:, to:)
|
|
116
|
+
options = args.last
|
|
117
|
+
change_option([options[:index]], from:, to:) if options.is_a?(Hash) && options[:index].is_a?(Hash)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def invert_change_constraint_option(key, value)
|
|
121
|
+
return value if value.nil?
|
|
122
|
+
|
|
123
|
+
case key
|
|
124
|
+
when :deferrable, :enforced, :inherit
|
|
125
|
+
return !value if [true, false].include?(value)
|
|
126
|
+
|
|
127
|
+
raise ArgumentError, "#{key} must be true or false"
|
|
128
|
+
when :initially
|
|
129
|
+
return :immediate if value == :deferred
|
|
130
|
+
return :deferred if value == :immediate
|
|
131
|
+
|
|
132
|
+
raise ArgumentError, "initially must be :deferred or :immediate"
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError, "unknown change_constraint option: #{key.inspect}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
18
137
|
end
|
|
19
138
|
end
|
|
20
139
|
end
|
|
@@ -89,20 +89,127 @@ module ActiveRecord
|
|
|
89
89
|
super
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
# Replace one check constraint with another
|
|
93
|
+
#
|
|
94
|
+
# @param table [Symbol] The table name
|
|
95
|
+
# @param from [String, Hash] The check constraint to be replaced.
|
|
96
|
+
# If a Hash is provided, it must contain an :expression key with the check constraint expression.
|
|
97
|
+
# Other options are merged with `options` and passed through.
|
|
98
|
+
# @param to [String, Hash] The new check constraint to be added.
|
|
99
|
+
# If a Hash is provided, it must contain an :expression key with the check constraint expression.
|
|
100
|
+
# Other options are merged with `options` and passed through.
|
|
101
|
+
# @param if_exists [true, false] If true, the entire operation should be idempotent (only
|
|
102
|
+
# dropping/renaming the old constraint if it exists, only adding the new constraint if it doesn't exist.)
|
|
103
|
+
# @param delay_validation [true, false] If true, the new check constraint will be added as NOT VALID and
|
|
104
|
+
# validated before removing the old check constraint.
|
|
105
|
+
def change_check_constraint(table, from:, to:, if_exists: false, delay_validation: false, **options)
|
|
106
|
+
delay_validation = false if open_transactions.positive?
|
|
107
|
+
options[:validate] = false if delay_validation
|
|
95
108
|
|
|
96
|
-
|
|
109
|
+
from_expression, from_options = split_arg(from, :expression, options)
|
|
110
|
+
to_expression, to_options = split_arg(to, :expression, options)
|
|
111
|
+
|
|
112
|
+
new_constraint_name = check_constraint_name(table, expression: to_expression, **to_options)
|
|
113
|
+
# when delaying validation, we don't drop the old constraint until after the new one is validated,
|
|
114
|
+
# so we need to rename it if the names are the same
|
|
115
|
+
if delay_validation
|
|
116
|
+
old_constraint_name = check_constraint_name(table, expression: from_expression, **from_options)
|
|
117
|
+
if old_constraint_name == new_constraint_name
|
|
118
|
+
new_old_constraint_name = temporary_name(old_constraint_name)
|
|
119
|
+
unless check_constraint_exists?(table, name: new_old_constraint_name)
|
|
120
|
+
rename_constraint(table, old_constraint_name, new_old_constraint_name, if_exists:)
|
|
121
|
+
end
|
|
122
|
+
old_constraint_name = new_old_constraint_name
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
remove_check_constraint(table, from_expression, if_exists:, **from_options)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
add_check_constraint(table,
|
|
129
|
+
to_expression,
|
|
130
|
+
**to_options,
|
|
131
|
+
if_not_exists: if_exists || delay_validation,
|
|
132
|
+
validate: !delay_validation)
|
|
133
|
+
|
|
134
|
+
return unless delay_validation
|
|
135
|
+
|
|
136
|
+
validate_constraint(table, new_constraint_name)
|
|
137
|
+
remove_check_constraint(table, name: old_constraint_name, if_exists: true)
|
|
97
138
|
end
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
# Replace one index with another
|
|
141
|
+
#
|
|
142
|
+
# @param table [Symbol] The table name
|
|
143
|
+
# @param from [String, Symbol, Array, Hash] The index to be replaced.
|
|
144
|
+
# If a Hash is provided, it must contain a :column key with the indexed column(s).
|
|
145
|
+
# Other options are merged with `options` and passed through.
|
|
146
|
+
# @param to [String, Symbol, Array, Hash] The new index to be added.
|
|
147
|
+
# If a Hash is provided, it must contain a :column key with the indexed column(s).
|
|
148
|
+
# Other options are merged with `options` and passed through.
|
|
149
|
+
# @param if_exists [true, false] If true, the entire operation should be idempotent (only
|
|
150
|
+
# dropping/renaming the old index if it exists, only adding the new index if it doesn't exist.)
|
|
151
|
+
# @param algorithm [Symbol, nil] If :concurrently, the new index is created concurrently and the
|
|
152
|
+
# old index is only removed after the new one exists.
|
|
153
|
+
def change_index(table, from:, to:, if_exists: false, algorithm: nil, **options)
|
|
154
|
+
algorithm = nil if open_transactions.positive?
|
|
155
|
+
concurrently = algorithm == :concurrently
|
|
103
156
|
|
|
104
|
-
|
|
157
|
+
from_column, from_options = split_arg(from, :column, options)
|
|
158
|
+
to_column, to_options = split_arg(to, :column, options)
|
|
159
|
+
|
|
160
|
+
new_index_name = to_options[:name]&.to_s || index_name(table, column: to_column)
|
|
161
|
+
# when creating concurrently, we don't drop the old index until after the new one is created,
|
|
162
|
+
# so we need to rename it if the names are the same
|
|
163
|
+
if concurrently
|
|
164
|
+
old_index_name = from_options[:name]&.to_s || index_name(table, column: from_column)
|
|
165
|
+
if old_index_name == new_index_name
|
|
166
|
+
new_old_index_name = temporary_name(old_index_name)
|
|
167
|
+
# rename_index has no if_exists option, so guard it: skip if the old index is missing
|
|
168
|
+
# (only relevant when if_exists makes the whole operation idempotent) or the temp name is taken
|
|
169
|
+
old_missing = if_exists && !index_name_exists?(table, old_index_name)
|
|
170
|
+
unless old_missing || index_name_exists?(table, new_old_index_name)
|
|
171
|
+
rename_index(table, old_index_name, new_old_index_name)
|
|
172
|
+
end
|
|
173
|
+
old_index_name = new_old_index_name
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
remove_index(table, from_column, if_exists:, **from_options)
|
|
105
177
|
end
|
|
178
|
+
|
|
179
|
+
add_index(table,
|
|
180
|
+
to_column,
|
|
181
|
+
**to_options,
|
|
182
|
+
if_not_exists: if_exists || concurrently,
|
|
183
|
+
algorithm:)
|
|
184
|
+
|
|
185
|
+
return unless concurrently
|
|
186
|
+
|
|
187
|
+
remove_index(table, name: old_index_name, if_exists: true, algorithm: :concurrently)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# splits a from/to argument into its subject and options. If a Hash is given, the given key
|
|
193
|
+
# (e.g. :column or :expression) is extracted and the rest is merged with options; otherwise
|
|
194
|
+
# the argument is treated as the subject itself.
|
|
195
|
+
def split_arg(arg, key, options)
|
|
196
|
+
return [arg, options] unless arg.is_a?(Hash)
|
|
197
|
+
|
|
198
|
+
arg_options = arg.merge(options)
|
|
199
|
+
[arg_options.delete(key), arg_options]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# derives a temporary name from +name+ to rename an existing index/constraint out of the way.
|
|
203
|
+
# Prefers a readable "<name>_to_be_replaced", but falls back to a hashed (still deterministic,
|
|
204
|
+
# still unique to +name+) form when that would exceed the database's identifier length limit,
|
|
205
|
+
# following the same scheme Rails uses for long index names.
|
|
206
|
+
def temporary_name(name)
|
|
207
|
+
suffixed = "#{name}_to_be_replaced"
|
|
208
|
+
return suffixed if suffixed.bytesize <= max_identifier_length
|
|
209
|
+
|
|
210
|
+
hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name.to_s).first(10)}"
|
|
211
|
+
short_name = name.to_s.truncate_bytes(max_identifier_length - hashed_identifier.bytesize, omission: nil)
|
|
212
|
+
"#{short_name}#{hashed_identifier}"
|
|
106
213
|
end
|
|
107
214
|
end
|
|
108
215
|
end
|
|
@@ -38,6 +38,30 @@ module ActiveRecord
|
|
|
38
38
|
SQL
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# alters the attributes of an existing constraint on a table
|
|
42
|
+
# see https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DESC-ALTER-CONSTRAINT
|
|
43
|
+
def change_constraint(table, constraint, deferrable: nil, enforced: nil, initially: nil, inherit: nil)
|
|
44
|
+
if deferrable.nil? && initially.nil? && enforced.nil? && inherit.nil?
|
|
45
|
+
raise ArgumentError, "at least one of :deferrable, :initially, :enforced, or :inherit must be specified"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
options = []
|
|
49
|
+
options << (deferrable ? "DEFERRABLE" : "NOT DEFERRABLE") unless deferrable.nil?
|
|
50
|
+
unless initially.nil?
|
|
51
|
+
case initially
|
|
52
|
+
when :deferred then options << "INITIALLY DEFERRED"
|
|
53
|
+
when :immediate then options << "INITIALLY IMMEDIATE"
|
|
54
|
+
else raise ArgumentError, "initially must be :deferred or :immediate"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
options << (enforced ? "ENFORCED" : "NOT ENFORCED") unless enforced.nil?
|
|
58
|
+
|
|
59
|
+
alter_table = "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{quote_column_name(constraint)}"
|
|
60
|
+
execute("#{alter_table} #{options.join(" ")}") unless options.empty?
|
|
61
|
+
# INHERIT/NO INHERIT cannot be combined with other options
|
|
62
|
+
execute("#{alter_table} #{inherit ? "INHERIT" : "NO INHERIT"}") unless inherit.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
41
65
|
# see https://www.postgresql.org/docs/current/sql-altertable.html#SQL-CREATETABLE-REPLICA-IDENTITY
|
|
42
66
|
def set_replica_identity(table, identity = :default)
|
|
43
67
|
identity_clause = case identity
|
|
@@ -14,7 +14,7 @@ module ActiveRecord
|
|
|
14
14
|
require "active_record/pg_extensions/transaction"
|
|
15
15
|
|
|
16
16
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PostgreSQLAdapter)
|
|
17
|
-
::ActiveRecord::Migration::CommandRecorder.
|
|
17
|
+
::ActiveRecord::Migration::CommandRecorder.prepend(CommandRecorder)
|
|
18
18
|
::ActiveRecord::ConnectionAdapters::NullTransaction.prepend(NullTransaction)
|
|
19
19
|
::ActiveRecord::ConnectionAdapters::Transaction.prepend(Transaction)
|
|
20
20
|
# if they've already require 'all', then inject now
|