activerecord-pg-extensions 0.2.3 → 0.3.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/errors.rb +5 -0
- data/lib/active_record/pg_extensions/pessimistic_migrations.rb +25 -9
- data/lib/active_record/pg_extensions/postgresql_adapter.rb +27 -0
- data/lib/active_record/pg_extensions/railtie.rb +1 -0
- data/lib/active_record/pg_extensions/version.rb +1 -1
- data/spec/pessimistic_migrations_spec.rb +47 -18
- data/spec/postgresql_adapter_spec.rb +103 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b240f7e9306ba57d7f52add8cef37ebe735e2f3d900d328d7cfb2e22fe1fcc6
|
4
|
+
data.tar.gz: 04b62ecf9b4c250e2f664f1a0be0c71baac1b6f5d117dc8d45f08bb7b7100459
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5a1ae9925eb093ea81424574d409186c85ef2474b1cc2a38f388757ff4e94a8b3893ca7e582e5010e8dbe8915a2e499d1600103c12936169d0447703b5c8d68
|
7
|
+
data.tar.gz: 256b8740a417e1bcc84a7edd23663da83543aa412a7221ae2a818721dd04a16b7ccf3f06915cb625d50f95ecb55ccd0719e1d8204441294dd9860ba25cb54b6a
|
@@ -6,20 +6,36 @@ module ActiveRecord
|
|
6
6
|
# to executing, in order to reduce the amount of time the actual DDL takes to
|
7
7
|
# execute (and thus how long it needs the lock)
|
8
8
|
module PessimisticMigrations
|
9
|
-
#
|
9
|
+
# adds a temporary check constraint to reduce locking when changing to NOT NULL, and we're not in a transaction
|
10
10
|
def change_column_null(table, column, nullness, default = nil)
|
11
|
-
# no point in
|
11
|
+
# no point in doing extra work to avoid locking if we're already in a transaction
|
12
12
|
return super if nullness != false || open_transactions.positive?
|
13
|
+
return if columns(table).find { |c| c.name == column.to_s }&.null == false
|
14
|
+
|
15
|
+
temp_constraint_name = "chk_rails_#{table}_#{column}_not_null"
|
16
|
+
scope = quoted_scope(temp_constraint_name)
|
17
|
+
# check for temp constraint
|
18
|
+
valid = select_value(<<~SQL, "SCHEMA")
|
19
|
+
SELECT convalidated FROM pg_constraint INNER JOIN pg_namespace ON pg_namespace.oid=connamespace WHERE conname=#{scope[:name]} AND nspname=#{scope[:schema]}
|
20
|
+
SQL
|
21
|
+
if valid.nil?
|
22
|
+
add_check_constraint(table,
|
23
|
+
"#{quote_column_name(column)} IS NOT NULL",
|
24
|
+
name: temp_constraint_name,
|
25
|
+
validate: false)
|
26
|
+
end
|
27
|
+
begin
|
28
|
+
validate_constraint(table, temp_constraint_name)
|
29
|
+
rescue ActiveRecord::StatementInvalid => e
|
30
|
+
raise ActiveRecord::NotNullViolation.new(sql: e.sql, binds: e.binds) if e.cause.is_a?(PG::CheckViolation)
|
31
|
+
|
32
|
+
raise
|
33
|
+
end
|
13
34
|
|
14
35
|
transaction do
|
15
|
-
|
16
|
-
|
17
|
-
execute("SET LOCAL enable_indexscan=off")
|
18
|
-
execute("SET LOCAL enable_bitmapscan=off")
|
19
|
-
execute("SELECT COUNT(*) FROM #{quote_table_name(table)} WHERE #{quote_column_name(column)} IS NULL")
|
20
|
-
raise ActiveRecord::Rollback
|
36
|
+
super
|
37
|
+
remove_check_constraint(table, name: temp_constraint_name)
|
21
38
|
end
|
22
|
-
super
|
23
39
|
end
|
24
40
|
|
25
41
|
# several improvements:
|
@@ -47,6 +47,7 @@ module ActiveRecord
|
|
47
47
|
sql << " VERSION #{quote(version)}" if version
|
48
48
|
sql << " CASCADE" if cascade
|
49
49
|
execute(sql)
|
50
|
+
reload_type_map
|
50
51
|
@extensions&.delete(extension.to_s)
|
51
52
|
end
|
52
53
|
|
@@ -61,6 +62,7 @@ module ActiveRecord
|
|
61
62
|
sql << " TO #{quote(version)}" if version && version != true
|
62
63
|
sql << " SET SCHEMA #{schema}" if schema
|
63
64
|
execute(sql)
|
65
|
+
reload_type_map
|
64
66
|
@extensions&.delete(extension.to_s)
|
65
67
|
end
|
66
68
|
|
@@ -73,6 +75,7 @@ module ActiveRecord
|
|
73
75
|
sql << extensions.join(", ")
|
74
76
|
sql << " CASCADE" if cascade
|
75
77
|
execute(sql)
|
78
|
+
reload_type_map
|
76
79
|
@extensions&.except!(*extensions.map(&:to_s))
|
77
80
|
end
|
78
81
|
|
@@ -233,6 +236,30 @@ module ActiveRecord
|
|
233
236
|
select_value("SELECT pg_is_in_recovery()")
|
234
237
|
end
|
235
238
|
|
239
|
+
def with_statement_timeout(timeout = nil)
|
240
|
+
timeout = 30 if timeout.nil? || timeout == true
|
241
|
+
transaction do
|
242
|
+
execute("SET LOCAL statement_timeout=#{(timeout * 1000).to_i}")
|
243
|
+
yield
|
244
|
+
rescue ActiveRecord::StatementInvalid => e
|
245
|
+
raise ActiveRecord::QueryTimeout.new(sql: e.sql, binds: e.binds) if e.cause.is_a?(PG::QueryCanceled)
|
246
|
+
|
247
|
+
raise
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
unless ::Rails.version >= "6.1"
|
252
|
+
def add_check_constraint(table_name, expression, name:, validate: true)
|
253
|
+
sql = +"ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{quote_column_name(name)} CHECK (#{expression})" # rubocop:disable Layout/LineLength
|
254
|
+
sql << " NOT VALID" unless validate
|
255
|
+
execute(sql)
|
256
|
+
end
|
257
|
+
|
258
|
+
def remove_check_constraint(table_name, name:)
|
259
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(name)}")
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
236
263
|
private
|
237
264
|
|
238
265
|
if ::Rails.version < "6.1"
|
@@ -8,6 +8,7 @@ module ActiveRecord
|
|
8
8
|
class Railtie < Rails::Railtie
|
9
9
|
initializer "pg_extensions.extend_ar", after: "active_record.initialize_database" do
|
10
10
|
ActiveSupport.on_load(:active_record) do
|
11
|
+
require "active_record/pg_extensions/errors"
|
11
12
|
require "active_record/pg_extensions/postgresql_adapter"
|
12
13
|
|
13
14
|
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PostgreSQLAdapter)
|
@@ -6,32 +6,61 @@ describe ActiveRecord::PGExtensions::PessimisticMigrations do
|
|
6
6
|
end
|
7
7
|
|
8
8
|
describe "#change_column_null" do
|
9
|
+
# Rails 6.1 doesn't quote the constraint name when adding a check constraint??
|
10
|
+
def quote_constraint_name(name)
|
11
|
+
Rails.version >= "6.1" ? name : connection.quote_column_name(name)
|
12
|
+
end
|
13
|
+
|
9
14
|
it "does nothing extra when changing a column to nullable" do
|
10
15
|
connection.change_column_null(:table, :column, true)
|
11
16
|
expect(connection.executed_statements).to eq ['ALTER TABLE "table" ALTER COLUMN "column" DROP NOT NULL']
|
12
17
|
end
|
13
18
|
|
14
|
-
it "
|
19
|
+
it "does nothing if we're in a transaction" do
|
20
|
+
connection.transaction do
|
21
|
+
connection.change_column_null(:table, :column, true)
|
22
|
+
end
|
23
|
+
expect(connection.executed_statements).to eq ["BEGIN",
|
24
|
+
'ALTER TABLE "table" ALTER COLUMN "column" DROP NOT NULL',
|
25
|
+
"COMMIT"]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "skips entirely if the column is already NOT NULL" do
|
29
|
+
expect(connection).to receive(:columns).with(:table).and_return([double(name: "column", null: false)])
|
15
30
|
connection.change_column_null(:table, :column, false)
|
16
|
-
expect(connection.executed_statements).to eq(
|
17
|
-
["BEGIN",
|
18
|
-
"SET LOCAL enable_indexscan=off",
|
19
|
-
"SET LOCAL enable_bitmapscan=off",
|
20
|
-
'SELECT COUNT(*) FROM "table" WHERE "column" IS NULL',
|
21
|
-
"ROLLBACK",
|
22
|
-
'ALTER TABLE "table" ALTER COLUMN "column" SET NOT NULL']
|
23
|
-
)
|
31
|
+
expect(connection.executed_statements).to eq([])
|
24
32
|
end
|
25
33
|
|
26
|
-
it "
|
27
|
-
connection.
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
it "adds and removes a check constraint" do
|
35
|
+
expect(connection).to receive(:columns).and_return([])
|
36
|
+
allow(connection).to receive(:check_constraint_for!).and_return(double(name: "chk_rails_table_column_not_null"))
|
37
|
+
connection.change_column_null(:table, :column, false)
|
38
|
+
|
39
|
+
expect(connection.executed_statements).to eq [
|
40
|
+
"SELECT convalidated FROM pg_constraint INNER JOIN pg_namespace ON pg_namespace.oid=connamespace WHERE conname='chk_rails_table_column_not_null' AND nspname=ANY (current_schemas(false))\n", # rubocop:disable Layout/LineLength
|
41
|
+
%{ALTER TABLE "table" ADD CONSTRAINT #{quote_constraint_name('chk_rails_table_column_not_null')} CHECK ("column" IS NOT NULL) NOT VALID}, # rubocop:disable Layout/LineLength
|
42
|
+
'ALTER TABLE "table" VALIDATE CONSTRAINT "chk_rails_table_column_not_null"',
|
43
|
+
"BEGIN",
|
44
|
+
'ALTER TABLE "table" ALTER COLUMN "column" SET NOT NULL',
|
45
|
+
'ALTER TABLE "table" DROP CONSTRAINT "chk_rails_table_column_not_null"',
|
46
|
+
"COMMIT"
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
it "verifies an existing check constraint" do
|
51
|
+
expect(connection).to receive(:columns).and_return([])
|
52
|
+
allow(connection).to receive(:check_constraint_for!).and_return(double(name: "chk_rails_table_column_not_null"))
|
53
|
+
expect(connection).to receive(:select_value).and_return(false)
|
54
|
+
connection.change_column_null(:table, :column, false)
|
55
|
+
|
56
|
+
expect(connection.executed_statements).to eq [
|
57
|
+
# stubbed out <check constraint valid>
|
58
|
+
'ALTER TABLE "table" VALIDATE CONSTRAINT "chk_rails_table_column_not_null"',
|
59
|
+
"BEGIN",
|
60
|
+
'ALTER TABLE "table" ALTER COLUMN "column" SET NOT NULL',
|
61
|
+
'ALTER TABLE "table" DROP CONSTRAINT "chk_rails_table_column_not_null"',
|
62
|
+
"COMMIT"
|
63
|
+
]
|
35
64
|
end
|
36
65
|
end
|
37
66
|
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
describe ActiveRecord::ConnectionAdapters::PostgreSQLAdapter do
|
4
|
+
before do
|
5
|
+
allow(connection).to receive(:reload_type_map)
|
6
|
+
end
|
7
|
+
|
4
8
|
describe "#set_constraints" do
|
5
9
|
around do |example|
|
6
10
|
connection.dont_execute(&example)
|
@@ -264,4 +268,103 @@ describe ActiveRecord::ConnectionAdapters::PostgreSQLAdapter do
|
|
264
268
|
end
|
265
269
|
end
|
266
270
|
end
|
271
|
+
|
272
|
+
describe "#with_statement_timeout" do
|
273
|
+
it "stops long-running queries" do
|
274
|
+
expect do
|
275
|
+
connection.with_statement_timeout(0.01) do
|
276
|
+
connection.execute("SELECT pg_sleep(3)")
|
277
|
+
end
|
278
|
+
end.to raise_error(ActiveRecord::QueryTimeout)
|
279
|
+
end
|
280
|
+
|
281
|
+
it "re-raises other errors" do
|
282
|
+
expect do
|
283
|
+
connection.with_statement_timeout(1) do
|
284
|
+
connection.execute("bad sql")
|
285
|
+
end
|
286
|
+
end.to raise_error(ActiveRecord::StatementInvalid)
|
287
|
+
end
|
288
|
+
|
289
|
+
context "without executing" do
|
290
|
+
it "converts integer to ms" do
|
291
|
+
connection.with_statement_timeout(30) { nil }
|
292
|
+
expect(connection.executed_statements).to eq(
|
293
|
+
[
|
294
|
+
"BEGIN",
|
295
|
+
"SET LOCAL statement_timeout=30000",
|
296
|
+
"COMMIT"
|
297
|
+
]
|
298
|
+
)
|
299
|
+
end
|
300
|
+
|
301
|
+
it "converts float to ms" do
|
302
|
+
connection.with_statement_timeout(5.5) { nil }
|
303
|
+
expect(connection.executed_statements).to eq(
|
304
|
+
[
|
305
|
+
"BEGIN",
|
306
|
+
"SET LOCAL statement_timeout=5500",
|
307
|
+
"COMMIT"
|
308
|
+
]
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
it "converts ActiveSupport::Duration to ms" do
|
313
|
+
connection.with_statement_timeout(5.seconds) { nil }
|
314
|
+
expect(connection.executed_statements).to eq(
|
315
|
+
[
|
316
|
+
"BEGIN",
|
317
|
+
"SET LOCAL statement_timeout=5000",
|
318
|
+
"COMMIT"
|
319
|
+
]
|
320
|
+
)
|
321
|
+
end
|
322
|
+
|
323
|
+
it "allows true" do
|
324
|
+
connection.with_statement_timeout(true) { nil }
|
325
|
+
expect(connection.executed_statements).to eq(
|
326
|
+
[
|
327
|
+
"BEGIN",
|
328
|
+
"SET LOCAL statement_timeout=30000",
|
329
|
+
"COMMIT"
|
330
|
+
]
|
331
|
+
)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
unless Rails.version >= "6.1"
|
337
|
+
describe "#add_check_constraint" do
|
338
|
+
around do |example|
|
339
|
+
connection.dont_execute(&example)
|
340
|
+
end
|
341
|
+
|
342
|
+
it "works" do
|
343
|
+
connection.add_check_constraint(:table, "column IS NOT NULL", name: :my_constraint)
|
344
|
+
expect(connection.executed_statements).to eq(
|
345
|
+
['ALTER TABLE "table" ADD CONSTRAINT "my_constraint" CHECK (column IS NOT NULL)']
|
346
|
+
)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "defers validation" do
|
350
|
+
connection.add_check_constraint(:table, "column IS NOT NULL", name: :my_constraint, validate: false)
|
351
|
+
expect(connection.executed_statements).to eq(
|
352
|
+
['ALTER TABLE "table" ADD CONSTRAINT "my_constraint" CHECK (column IS NOT NULL) NOT VALID']
|
353
|
+
)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
describe "#remove_check_constraint" do
|
358
|
+
around do |example|
|
359
|
+
connection.dont_execute(&example)
|
360
|
+
end
|
361
|
+
|
362
|
+
it "works" do
|
363
|
+
connection.remove_check_constraint(:table, name: :my_constraint)
|
364
|
+
expect(connection.executed_statements).to eq(
|
365
|
+
['ALTER TABLE "table" DROP CONSTRAINT "my_constraint"']
|
366
|
+
)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
267
370
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-pg-extensions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -175,6 +175,7 @@ files:
|
|
175
175
|
- config/database.yml
|
176
176
|
- lib/active_record/pg_extensions.rb
|
177
177
|
- lib/active_record/pg_extensions/all.rb
|
178
|
+
- lib/active_record/pg_extensions/errors.rb
|
178
179
|
- lib/active_record/pg_extensions/extension.rb
|
179
180
|
- lib/active_record/pg_extensions/pessimistic_migrations.rb
|
180
181
|
- lib/active_record/pg_extensions/postgresql_adapter.rb
|
@@ -204,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
205
|
- !ruby/object:Gem::Version
|
205
206
|
version: '0'
|
206
207
|
requirements: []
|
207
|
-
rubygems_version: 3.
|
208
|
+
rubygems_version: 3.2.24
|
208
209
|
signing_key:
|
209
210
|
specification_version: 4
|
210
211
|
summary: Several extensions to ActiveRecord's PostgreSQLAdapter.
|