activerecord-pg-extensions 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45b4e3d0cf437ce75246b882697214a96cf89e44b2b28420b5dd215f243f5351
4
- data.tar.gz: c49b15be7227aafe9de557cf78ef4d4938a842b72d28838f2c1bef59d49f1a7f
3
+ metadata.gz: 05fb32b33134833c55a06810bf8015c335d2e7fe85b59cbd6910b90141a88fec
4
+ data.tar.gz: 5663306168a18e60f0e5acd439f35ce439f1108c9074430868d7a346db937e78
5
5
  SHA512:
6
- metadata.gz: 1123584bcfb35e545245c3274e3e1d485ff9080cf46fdcf1788eddbcc7477dcbdc4a3e66c7d3742d3f6f9b701599454b81e994bd8c228eeb77560698c0b2369e
7
- data.tar.gz: 5fda99a4141dee1332a889f7123581ceb6ca5e321e064bff89e5015c63d50d73c1e0cb1c0f775ca1d67d29f4d04ff9a7bc7d98c02ea482efabfd8f28e4cb7c7a
6
+ metadata.gz: 8f3541b26c2071c1176bd27a1978abd3b5972091e0cc36e55524a042589655817c2b55a087d85a4f0a216ee889a629076ccf7fabb242961a913e122c36256926
7
+ data.tar.gz: fc4b5af43db911db62b51d72c1e86e76c2bb2506c81d8a61f8f81b6e47aa735451236f1ddc69c2865932c6ca182224f7629a1900d715ed581e163ef4a3af28c9
@@ -27,10 +27,8 @@ module ActiveRecord
27
27
  end
28
28
  begin
29
29
  validate_constraint(table, temp_constraint_name)
30
- rescue ActiveRecord::StatementInvalid => e
31
- raise ActiveRecord::NotNullViolation.new(sql: e.sql, binds: e.binds) if e.cause.is_a?(PG::CheckViolation)
32
-
33
- raise
30
+ rescue PG::CheckViolation => e
31
+ raise ActiveRecord::NotNullViolation.new(sql: e.sql, binds: e.binds)
34
32
  end
35
33
 
36
34
  transaction do
@@ -66,7 +64,7 @@ module ActiveRecord
66
64
  # will automatically remove a NOT VALID index before trying to add
67
65
  def add_index(table_name, column_name, **options)
68
66
  # catch a concurrent index add that fails because it already exists, and is invalid
69
- if options[:algorithm] == :concurrently || options[:if_not_exists]
67
+ if options[:algorithm] == :concurrently && options[:if_not_exists]
70
68
  column_names = index_column_names(column_name)
71
69
  index_name = options[:name].to_s if options.key?(:name)
72
70
  index_name ||= index_name(table_name, column_names)
@@ -84,16 +82,26 @@ module ActiveRecord
84
82
  AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{index[:schema]} )
85
83
  LIMIT 1
86
84
  SQL
87
- if valid == false && options[:algorithm] == :concurrently
88
- remove_index(table_name, name: index_name,
89
- algorithm: :concurrently)
90
- end
91
- return if options[:if_not_exists] && valid == true
85
+ return if valid == true
86
+
87
+ remove_index(table_name, name: index_name, algorithm: :concurrently) if valid == false
92
88
  end
93
- # Rails.version: can stop doing this in Rails 6.2, when it's natively supported
94
- options.delete(:if_not_exists)
95
89
  super
96
90
  end
91
+
92
+ def add_check_constraint(table_name, expression, if_not_exists: false, **options)
93
+ return if if_not_exists && check_constraint_for(table_name, expression, **options)
94
+
95
+ super
96
+ end
97
+
98
+ if ActiveRecord.version < Gem::Version.new("7.1")
99
+ def remove_check_constraint(table_name, expression = nil, **options)
100
+ return if options[:if_exists] && !check_constraint_for(table_name, expression, **options)
101
+
102
+ super
103
+ end
104
+ end
97
105
  end
98
106
  end
99
107
  end
@@ -9,8 +9,8 @@ module ActiveRecord
9
9
  # set constraint check timing for the current transaction
10
10
  # see https://www.postgresql.org/docs/current/sql-set-constraints.html
11
11
  def set_constraints(deferred, *constraints)
12
- raise ArgumentError, "deferred must be :deferred or :immediate" unless %w[deferred
13
- immediate].include?(deferred.to_s)
12
+ raise ArgumentError, "deferred must be :deferred or :immediate" unless %i[deferred
13
+ immediate].include?(deferred)
14
14
 
15
15
  constraints = constraints.map { |c| quote_table_name(c) }.join(", ")
16
16
  constraints = "ALL" if constraints.empty?
@@ -160,7 +160,7 @@ module ActiveRecord
160
160
  raise ArgumentError, "columns may only be specified if a analyze is specified" unless analyze
161
161
 
162
162
  table.map do |table_name, columns|
163
- "#{quote_table_name(table_name)} (#{Array.wrap(columns).map { |c| quote_column_name(c) }.join(', ')})"
163
+ "#{quote_table_name(table_name)} (#{Array.wrap(columns).map { |c| quote_column_name(c) }.join(", ")})"
164
164
  end.join(", ")
165
165
  else
166
166
  quote_table_name(table)
@@ -182,35 +182,35 @@ module ActiveRecord
182
182
  def current_wal_lsn
183
183
  return nil unless wal?
184
184
 
185
- select_value("SELECT #{pre_pg10_wal_function_name('pg_current_wal_lsn')}()")
185
+ select_value("SELECT #{pre_pg10_wal_function_name("pg_current_wal_lsn")}()")
186
186
  end
187
187
 
188
188
  # see https://www.postgresql.org/docs/current/functions-admin.html#id-1.5.8.33.5.5.2.2.2.1.1.1
189
189
  def current_wal_flush_lsn
190
190
  return nil unless wal?
191
191
 
192
- select_value("SELECT #{pre_pg10_wal_function_name('pg_current_wal_flush_lsn')}()")
192
+ select_value("SELECT #{pre_pg10_wal_function_name("pg_current_wal_flush_lsn")}()")
193
193
  end
194
194
 
195
195
  # see https://www.postgresql.org/docs/current/functions-admin.html#id-1.5.8.33.5.5.2.2.3.1.1.1
196
196
  def current_wal_insert_lsn
197
197
  return nil unless wal?
198
198
 
199
- select_value("SELECT #{pre_pg10_wal_function_name('pg_current_wal_insert_lsn')}()")
199
+ select_value("SELECT #{pre_pg10_wal_function_name("pg_current_wal_insert_lsn")}()")
200
200
  end
201
201
 
202
202
  # https://www.postgresql.org/docs/current/functions-admin.html#id-1.5.8.33.6.3.2.2.2.1.1.1
203
203
  def last_wal_receive_lsn
204
204
  return nil unless wal?
205
205
 
206
- select_value("SELECT #{pre_pg10_wal_function_name('pg_last_wal_receive_lsn')}()")
206
+ select_value("SELECT #{pre_pg10_wal_function_name("pg_last_wal_receive_lsn")}()")
207
207
  end
208
208
 
209
209
  # see https://www.postgresql.org/docs/current/functions-admin.html#id-1.5.8.33.6.3.2.2.3.1.1.1
210
210
  def last_wal_replay_lsn
211
211
  return nil unless wal?
212
212
 
213
- select_value("SELECT #{pre_pg10_wal_function_name('pg_last_wal_replay_lsn')}()")
213
+ select_value("SELECT #{pre_pg10_wal_function_name("pg_last_wal_replay_lsn")}()")
214
214
  end
215
215
 
216
216
  # see https://www.postgresql.org/docs/current/functions-admin.html#id-1.5.8.33.5.5.2.2.4.1.1.1
@@ -229,7 +229,7 @@ module ActiveRecord
229
229
  end
230
230
  end
231
231
 
232
- select_value("SELECT #{pre_pg10_wal_function_name('pg_wal_lsn_diff')}(#{lsns[0]}, #{lsns[1]})")
232
+ select_value("SELECT #{pre_pg10_wal_function_name("pg_wal_lsn_diff")}(#{lsns[0]}, #{lsns[1]})")
233
233
  end
234
234
 
235
235
  def in_recovery?
@@ -238,7 +238,7 @@ module ActiveRecord
238
238
 
239
239
  def set(configuration_parameter, value, local: false)
240
240
  value = value.nil? ? "DEFAULT" : quote(value)
241
- execute("SET#{' LOCAL' if local} #{configuration_parameter} TO #{value}")
241
+ execute("SET#{" LOCAL" if local} #{configuration_parameter} TO #{value}")
242
242
  end
243
243
 
244
244
  def reset(configuration_parameter)
@@ -274,65 +274,8 @@ module ActiveRecord
274
274
  end
275
275
  end
276
276
 
277
- unless ::Rails.version >= "6.1"
278
- def add_check_constraint(table_name, expression, name:, validate: true)
279
- sql = +"ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{quote_column_name(name)} CHECK (#{expression})" # rubocop:disable Layout/LineLength
280
- sql << " NOT VALID" unless validate
281
- execute(sql)
282
- end
283
-
284
- def remove_check_constraint(table_name, name:)
285
- execute("ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(name)}")
286
- end
287
- end
288
-
289
277
  private
290
278
 
291
- if ::Rails.version < "6.1"
292
- # significant change: add PG::TextDecoder::Numeric
293
- def add_pg_decoders
294
- @default_timezone = nil
295
- @timestamp_decoder = nil
296
-
297
- coders_by_name = {
298
- "int2" => PG::TextDecoder::Integer,
299
- "int4" => PG::TextDecoder::Integer,
300
- "int8" => PG::TextDecoder::Integer,
301
- "oid" => PG::TextDecoder::Integer,
302
- "float4" => PG::TextDecoder::Float,
303
- "float8" => PG::TextDecoder::Float,
304
- "bool" => PG::TextDecoder::Boolean,
305
- "numeric" => PG::TextDecoder::Numeric
306
- }
307
-
308
- if defined?(PG::TextDecoder::TimestampUtc)
309
- # Use native PG encoders available since pg-1.1
310
- coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc
311
- coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone
312
- end
313
-
314
- known_coder_types = coders_by_name.keys.map { |n| quote(n) }
315
- query = format(<<~SQL, *known_coder_types.join(", "))
316
- SELECT t.oid, t.typname
317
- FROM pg_type as t
318
- WHERE t.typname IN (%s)
319
- SQL
320
- coders = execute_and_clear(query, "SCHEMA", []) do |result|
321
- result
322
- .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
323
- .compact
324
- end
325
-
326
- map = PG::TypeMapByOid.new
327
- coders.each { |coder| map.add_coder(coder) }
328
- @connection.type_map_for_results = map
329
-
330
- # extract timestamp decoder for use in update_typemap_for_default_timezone
331
- @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
332
- update_typemap_for_default_timezone
333
- end
334
- end
335
-
336
279
  def initialize_type_map(map = type_map)
337
280
  map.register_type "pg_lsn", ActiveRecord::ConnectionAdapters::PostgreSQL::OID::SpecializedString.new(:pg_lsn)
338
281
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module PGExtensions
5
- VERSION = "0.4.4"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,55 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-pg-extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.5.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: 2022-03-18 00:00:00.000000000 Z
11
+ date: 2023-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '6.0'
20
- - - "<"
17
+ - - "~>"
21
18
  - !ruby/object:Gem::Version
22
- version: '7.1'
19
+ version: 7.0.0
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: '6.0'
30
- - - "<"
24
+ - - "~>"
31
25
  - !ruby/object:Gem::Version
32
- version: '7.1'
26
+ version: 7.0.0
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: railties
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '6.0'
40
- - - "<"
31
+ - - "~>"
41
32
  - !ruby/object:Gem::Version
42
- version: '7.1'
33
+ version: 7.0.0
43
34
  type: :runtime
44
35
  prerelease: false
45
36
  version_requirements: !ruby/object:Gem::Requirement
46
37
  requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: '6.0'
50
- - - "<"
38
+ - - "~>"
51
39
  - !ruby/object:Gem::Version
52
- version: '7.1'
40
+ version: 7.0.0
53
41
  - !ruby/object:Gem::Dependency
54
42
  name: appraisal
55
43
  requirement: !ruby/object:Gem::Requirement
@@ -121,19 +109,19 @@ dependencies:
121
109
  - !ruby/object:Gem::Version
122
110
  version: '3.0'
123
111
  - !ruby/object:Gem::Dependency
124
- name: rubocop
112
+ name: rubocop-inst
125
113
  requirement: !ruby/object:Gem::Requirement
126
114
  requirements:
127
115
  - - "~>"
128
116
  - !ruby/object:Gem::Version
129
- version: '1.7'
117
+ version: '1'
130
118
  type: :development
131
119
  prerelease: false
132
120
  version_requirements: !ruby/object:Gem::Requirement
133
121
  requirements:
134
122
  - - "~>"
135
123
  - !ruby/object:Gem::Version
136
- version: '1.7'
124
+ version: '1'
137
125
  - !ruby/object:Gem::Dependency
138
126
  name: rubocop-rake
139
127
  requirement: !ruby/object:Gem::Requirement
@@ -172,7 +160,6 @@ files:
172
160
  - CHANGELOG.md
173
161
  - LICENSE.txt
174
162
  - README.md
175
- - config/database.yml
176
163
  - lib/active_record/pg_extensions.rb
177
164
  - lib/active_record/pg_extensions/all.rb
178
165
  - lib/active_record/pg_extensions/errors.rb
@@ -183,9 +170,6 @@ files:
183
170
  - lib/active_record/pg_extensions/transaction.rb
184
171
  - lib/active_record/pg_extensions/version.rb
185
172
  - lib/activerecord-pg-extensions.rb
186
- - spec/pessimistic_migrations_spec.rb
187
- - spec/postgresql_adapter_spec.rb
188
- - spec/spec_helper.rb
189
173
  homepage: https://github.com/instructure/activerecord-pg-extensions
190
174
  licenses:
191
175
  - MIT
@@ -200,19 +184,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
200
184
  requirements:
201
185
  - - ">="
202
186
  - !ruby/object:Gem::Version
203
- version: 2.6.0
187
+ version: '2.7'
204
188
  required_rubygems_version: !ruby/object:Gem::Requirement
205
189
  requirements:
206
190
  - - ">="
207
191
  - !ruby/object:Gem::Version
208
192
  version: '0'
209
193
  requirements: []
210
- rubygems_version: 3.1.4
194
+ rubygems_version: 3.4.19
211
195
  signing_key:
212
196
  specification_version: 4
213
197
  summary: Several extensions to ActiveRecord's PostgreSQLAdapter.
214
- test_files:
215
- - spec/spec_helper.rb
216
- - spec/postgresql_adapter_spec.rb
217
- - spec/pessimistic_migrations_spec.rb
218
- - config/database.yml
198
+ test_files: []
data/config/database.yml DELETED
@@ -1,3 +0,0 @@
1
- test:
2
- adapter: postgresql
3
- database: arpgextensions
@@ -1,136 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe ActiveRecord::PGExtensions::PessimisticMigrations do
4
- around do |example|
5
- connection.dont_execute(&example)
6
- end
7
-
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
-
14
- it "does nothing extra when changing a column to nullable" do
15
- connection.change_column_null(:table, :column, true)
16
- expect(connection.executed_statements).to eq ['ALTER TABLE "table" ALTER COLUMN "column" DROP NOT NULL']
17
- end
18
-
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)])
30
- connection.change_column_null(:table, :column, false)
31
- expect(connection.executed_statements).to eq([])
32
- end
33
-
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
- ]
64
- end
65
- end
66
-
67
- describe "#add_foreign_key" do
68
- it "does nothing extra if a transaction is already active" do
69
- connection.transaction do
70
- connection.add_foreign_key :emails, :users, delay_validation: true
71
- end
72
- expect(connection.executed_statements).to match(
73
- ["BEGIN",
74
- match(/\AALTER TABLE "emails" ADD CONSTRAINT "fk_rails_[0-9a-f]+".+REFERENCES "users" \("id"\)\s*\z/m),
75
- "COMMIT"]
76
- )
77
- end
78
-
79
- it "delays validation" do
80
- connection.add_foreign_key :emails, :users, delay_validation: true
81
- expect(connection.executed_statements).to match(
82
- [/convalidated/,
83
- match(/\AALTER TABLE "emails" ADD CONSTRAINT "[a-z0-9_]+".+REFERENCES "users" \("id"\)\s+NOT VALID\z/m),
84
- match(/^ALTER TABLE "emails" VALIDATE CONSTRAINT "fk_rails_[0-9a-f]+"/)]
85
- )
86
- end
87
-
88
- it "only validates if the constraint already exists, and is not valid" do
89
- expect(connection).to receive(:select_value).with(/convalidated/, "SCHEMA").and_return(false)
90
- connection.add_foreign_key :emails, :users, delay_validation: true
91
- expect(connection.executed_statements).to match(
92
- [match(/^ALTER TABLE "emails" VALIDATE CONSTRAINT "fk_rails_[0-9a-f]+"/)]
93
- )
94
- end
95
-
96
- it "does nothing if constraint already exists" do
97
- expect(connection).to receive(:select_value).with(/convalidated/, "SCHEMA").and_return(true)
98
- connection.add_foreign_key :emails, :users, if_not_exists: true
99
- expect(connection.executed_statements).to eq []
100
- end
101
-
102
- it "still tries if delay_validation is true but if_not_exists is false and it already exists" do
103
- expect(connection).to receive(:select_value).with(/convalidated/, "SCHEMA").and_return(true)
104
- connection.add_foreign_key :emails, :users, delay_validation: true
105
- expect(connection.executed_statements).to match(
106
- [match(/\AALTER TABLE "emails" ADD CONSTRAINT "[a-z0-9_]+".+REFERENCES "users" \("id"\)\s+NOT VALID\z/m),
107
- match(/^ALTER TABLE "emails" VALIDATE CONSTRAINT "fk_rails_[0-9a-f]+"/)]
108
- )
109
- end
110
-
111
- it "does nothing if_not_exists is true and it is NOT VALID" do
112
- expect(connection).to receive(:select_value).with(/convalidated/, "SCHEMA").and_return(false)
113
- connection.add_foreign_key :emails, :users, if_not_exists: true
114
- expect(connection.executed_statements).to eq []
115
- end
116
- end
117
-
118
- describe "#add_index" do
119
- it "removes a NOT VALID index before re-adding" do
120
- expect(connection).to receive(:select_value).with(/indisvalid/, "SCHEMA").and_return(false)
121
- expect(connection).to receive(:remove_index).with(:users, name: "index_users_on_name", algorithm: :concurrently)
122
-
123
- connection.add_index :users, :name, algorithm: :concurrently
124
- expect(connection.executed_statements).to match(
125
- [match(/\ACREATE +INDEX CONCURRENTLY "index_users_on_name" ON "users" +\("name"\)\z/)]
126
- )
127
- end
128
-
129
- it "does nothing if the index already exists" do
130
- expect(connection).to receive(:select_value).with(/indisvalid/, "SCHEMA").and_return(true)
131
-
132
- connection.add_index :users, :name, if_not_exists: true
133
- expect(connection.executed_statements).to eq []
134
- end
135
- end
136
- end
@@ -1,457 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe ActiveRecord::ConnectionAdapters::PostgreSQLAdapter do
4
- before do
5
- allow(connection).to receive(:reload_type_map)
6
- end
7
-
8
- describe "#set_constraints" do
9
- around do |example|
10
- connection.dont_execute(&example)
11
- end
12
-
13
- it "requires :deferred or :immediate" do
14
- expect { connection.set_constraints("garbage") }.to raise_error(ArgumentError)
15
- end
16
-
17
- it "defaults to all" do
18
- connection.set_constraints(:deferred)
19
- expect(connection.executed_statements).to eq ["SET CONSTRAINTS ALL DEFERRED"]
20
- end
21
-
22
- it "quotes constraints" do
23
- connection.set_constraints(:deferred, :my_fk)
24
- expect(connection.executed_statements).to eq ['SET CONSTRAINTS "my_fk" DEFERRED']
25
- end
26
-
27
- it "quotes multiple constraints" do
28
- connection.set_constraints(:deferred, :my_fk1, :my_fk2)
29
- expect(connection.executed_statements).to eq ['SET CONSTRAINTS "my_fk1", "my_fk2" DEFERRED']
30
- end
31
- end
32
-
33
- describe "#defer_constraints" do
34
- around do |example|
35
- connection.dont_execute(&example)
36
- end
37
-
38
- it "defers and resets" do
39
- block_called = false
40
- connection.defer_constraints do
41
- block_called = true
42
- end
43
- expect(block_called).to eq true
44
- expect(connection.executed_statements).to eq ["SET CONSTRAINTS ALL DEFERRED", "SET CONSTRAINTS ALL IMMEDIATE"]
45
- end
46
- end
47
-
48
- describe "#set_replica_identity" do
49
- around do |example|
50
- connection.dont_execute(&example)
51
- end
52
-
53
- it "resets identity" do
54
- connection.set_replica_identity(:my_table)
55
- expect(connection.executed_statements).to eq ['ALTER TABLE "my_table" REPLICA IDENTITY DEFAULT']
56
- end
57
-
58
- it "sets full identity" do
59
- connection.set_replica_identity(:my_table, :full)
60
- expect(connection.executed_statements).to eq ['ALTER TABLE "my_table" REPLICA IDENTITY FULL']
61
- end
62
-
63
- it "sets an index" do
64
- connection.set_replica_identity(:my_table, :my_index)
65
- expect(connection.executed_statements).to eq ['ALTER TABLE "my_table" REPLICA IDENTITY USING INDEX "my_index"']
66
- end
67
- end
68
-
69
- context "extensions" do
70
- it "creates and drops an extension" do
71
- connection.create_extension(:pg_trgm, schema: "public")
72
- expect(connection.executed_statements).to eq ["CREATE EXTENSION pg_trgm SCHEMA public"]
73
- expect(ext = connection.extension(:pg_trgm)).not_to be_nil
74
- expect(ext.schema).to eq "public"
75
- expect(ext.version).not_to be_nil
76
- expect(ext.name).to eq "pg_trgm"
77
- ensure
78
- connection.executed_statements.clear
79
- connection.drop_extension(:pg_trgm, if_exists: true)
80
- expect(connection.executed_statements).to eq ["DROP EXTENSION IF EXISTS pg_trgm"]
81
- expect(connection.extension(:pg_trgm)).to be_nil
82
- end
83
-
84
- it "doesn't try to recreate" do
85
- connection.create_extension(:pg_trgm, schema: "public")
86
- connection.create_extension(:pg_trgm, schema: "public", if_not_exists: true)
87
- expect(connection.executed_statements).to eq ["CREATE EXTENSION pg_trgm SCHEMA public",
88
- "CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA public"]
89
- ensure
90
- connection.drop_extension(:pg_trgm, if_exists: true)
91
- end
92
-
93
- context "non-executing" do
94
- around do |example|
95
- connection.dont_execute(&example)
96
- end
97
-
98
- it "supports all options on create" do
99
- connection.create_extension(:my_extension, if_not_exists: true, schema: "public", version: "2.0", cascade: true)
100
- expect(connection.executed_statements).to eq(
101
- ["CREATE EXTENSION IF NOT EXISTS my_extension SCHEMA public VERSION '2.0' CASCADE"]
102
- )
103
- end
104
-
105
- it "supports all options on drop" do
106
- connection.drop_extension(:my_extension, if_exists: true, cascade: true)
107
- expect(connection.executed_statements).to eq ["DROP EXTENSION IF EXISTS my_extension CASCADE"]
108
- end
109
-
110
- it "can update an extensions" do
111
- connection.alter_extension(:my_extension, version: true)
112
- expect(connection.executed_statements).to eq ["ALTER EXTENSION my_extension UPDATE"]
113
- end
114
-
115
- it "can update to a specific version" do
116
- connection.alter_extension(:my_extension, version: "2.0")
117
- expect(connection.executed_statements).to eq ["ALTER EXTENSION my_extension UPDATE TO '2.0'"]
118
- end
119
-
120
- it "can change schemas" do
121
- connection.alter_extension(:my_extension, schema: "my_app")
122
- expect(connection.executed_statements).to eq ["ALTER EXTENSION my_extension SET SCHEMA my_app"]
123
- end
124
-
125
- it "cannot change schema and update" do
126
- expect { connection.alter_extension(:my_extension, schema: "my_app", version: "2.0") }
127
- .to raise_error(ArgumentError)
128
- end
129
-
130
- it "can drop multiple extensions" do
131
- connection.drop_extension(:my_extension1, :my_extension2)
132
- expect(connection.executed_statements).to eq ["DROP EXTENSION my_extension1, my_extension2"]
133
- end
134
-
135
- it "does not allow dropping no extensions" do
136
- expect { connection.drop_extension }.to raise_error(ArgumentError)
137
- end
138
-
139
- describe "#extension_available?" do
140
- it "works with no version constraint" do
141
- connection.extension_available?(:postgis)
142
- expect(connection.executed_statements).to eq ["SELECT 1 FROM pg_available_extensions WHERE name='postgis'"]
143
- end
144
-
145
- it "works with a version constraint" do
146
- connection.extension_available?(:postgis, "2.0")
147
- expect(connection.executed_statements).to eq(
148
- ["SELECT 1 FROM pg_available_extension_versions WHERE name='postgis' AND version='2.0'"]
149
- )
150
- end
151
- end
152
- end
153
- end
154
-
155
- describe "#add_schema_to_search_path" do
156
- around do |example|
157
- original_search_path = connection.schema_search_path
158
- connection.schema_search_path = "public"
159
- example.call
160
- ensure
161
- connection.schema_search_path = original_search_path
162
- end
163
-
164
- it "adds a schema to search path" do
165
- connection.add_schema_to_search_path("postgis") do
166
- expect(connection.schema_search_path).to eq "public,postgis"
167
- end
168
- expect(connection.schema_search_path).to eq "public"
169
- end
170
-
171
- it "doesn't duplicate an existing schema" do
172
- connection.add_schema_to_search_path("public") do
173
- expect(connection.schema_search_path).to eq "public"
174
- end
175
- expect(connection.schema_search_path).to eq "public"
176
- end
177
-
178
- it "is cleaned up properly when the transaction rolls back manually" do
179
- expect do
180
- connection.add_schema_to_search_path("postgis") do
181
- raise ActiveRecord::Rollback
182
- end
183
- end.to raise_error(ActiveRecord::Rollback)
184
- expect(connection.schema_search_path).to eq "public"
185
- end
186
-
187
- it "is cleaned up properly when the transaction rolls back" do
188
- expect do
189
- connection.add_schema_to_search_path("postgis") do
190
- connection.execute("gibberish")
191
- end
192
- end.to raise_error(ActiveRecord::StatementInvalid)
193
- expect(connection.schema_search_path).to eq "public"
194
- end
195
- end
196
-
197
- describe "#vacuum" do
198
- it "does a straight vacuum of everything" do
199
- connection.vacuum
200
- expect(connection.executed_statements).to eq ["VACUUM"]
201
- end
202
-
203
- it "supports several options" do
204
- connection.vacuum(analyze: true, verbose: true)
205
- expect(connection.executed_statements).to eq ["VACUUM VERBOSE ANALYZE"]
206
- end
207
-
208
- it "validates parallel option is an integer" do
209
- expect { connection.vacuum(parallel: :garbage) }.to raise_error(ArgumentError)
210
- end
211
-
212
- it "validates parallel option is postive" do
213
- expect { connection.vacuum(parallel: -1) }.to raise_error(ArgumentError)
214
- end
215
-
216
- context "non-executing" do
217
- around do |example|
218
- connection.dont_execute(&example)
219
- end
220
-
221
- it "vacuums a table" do
222
- connection.vacuum(:my_table)
223
- expect(connection.executed_statements).to eq ['VACUUM "my_table"']
224
- end
225
-
226
- it "vacuums multiple tables" do
227
- connection.vacuum(:table1, :table2)
228
- expect(connection.executed_statements).to eq ['VACUUM "table1", "table2"']
229
- end
230
-
231
- it "requires analyze with specific columns" do
232
- expect { connection.vacuum({ my_table: :column1 }) }.to raise_error(ArgumentError)
233
- end
234
-
235
- it "analyzes a specific column" do
236
- connection.vacuum({ my_table: :column }, analyze: true)
237
- expect(connection.executed_statements).to eq ['VACUUM ANALYZE "my_table" ("column")']
238
- end
239
-
240
- it "analyzes multiples columns" do
241
- connection.vacuum({ my_table: %i[column1 column2] }, analyze: true)
242
- expect(connection.executed_statements).to eq ['VACUUM ANALYZE "my_table" ("column1", "column2")']
243
- end
244
-
245
- it "analyzes a mixture of tables and columns" do
246
- connection.vacuum(:table1, { my_table: %i[column1 column2] }, analyze: true)
247
- expect(connection.executed_statements).to eq ['VACUUM ANALYZE "table1", "my_table" ("column1", "column2")']
248
- end
249
- end
250
-
251
- describe "#wal_lsn_diff" do
252
- skip unless connection.wal?
253
-
254
- it "executes" do
255
- expect(connection.wal_lsn_diff(:current, :current)).to eq 0
256
- end
257
- end
258
-
259
- describe "#in_recovery?" do
260
- it "works" do
261
- expect(connection.in_recovery?).to eq false
262
- end
263
- end
264
-
265
- describe "#select_value" do
266
- it "casts numeric types" do
267
- expect(connection.select_value("SELECT factorial(2)")).to eq 2
268
- end
269
- end
270
- end
271
-
272
- describe "#with_statement_timeout" do
273
- around do |example|
274
- # these specs were written before we supported deferring setting timeouts
275
- # until the transaction materializes
276
- connection.disable_lazy_transactions!
277
- example.call
278
- connection.enable_lazy_transactions!
279
- end
280
-
281
- it "stops long-running queries" do
282
- expect do
283
- connection.with_statement_timeout(0.01) do
284
- connection.execute("SELECT pg_sleep(3)")
285
- end
286
- end.to raise_error(ActiveRecord::QueryCanceled)
287
- end
288
-
289
- it "re-raises other errors" do
290
- expect do
291
- connection.with_statement_timeout(1) do
292
- connection.execute("bad sql")
293
- end
294
- end.to(raise_error { |e| expect(e.cause).to be_a(PG::SyntaxError) })
295
- end
296
-
297
- context "without executing" do
298
- around do |example|
299
- connection.dont_execute(&example)
300
- end
301
-
302
- it "converts integer to ms" do
303
- connection.with_statement_timeout(30) { nil }
304
- expect(connection.executed_statements).to eq(
305
- [
306
- "BEGIN",
307
- "SET LOCAL statement_timeout TO '30000ms'",
308
- "COMMIT"
309
- ]
310
- )
311
- end
312
-
313
- it "converts float to ms" do
314
- connection.with_statement_timeout(5.5) { nil }
315
- expect(connection.executed_statements).to eq(
316
- [
317
- "BEGIN",
318
- "SET LOCAL statement_timeout TO '5500ms'",
319
- "COMMIT"
320
- ]
321
- )
322
- end
323
-
324
- it "converts ActiveSupport::Duration to ms" do
325
- connection.with_statement_timeout(5.seconds) { nil }
326
- expect(connection.executed_statements).to eq(
327
- [
328
- "BEGIN",
329
- "SET LOCAL statement_timeout TO '5000ms'",
330
- "COMMIT"
331
- ]
332
- )
333
- end
334
-
335
- it "allows true" do
336
- connection.with_statement_timeout(true) { nil }
337
- expect(connection.executed_statements).to eq(
338
- [
339
- "BEGIN",
340
- "SET LOCAL statement_timeout TO '30000ms'",
341
- "COMMIT"
342
- ]
343
- )
344
- end
345
- end
346
- end
347
-
348
- describe "#statement_timeout=" do
349
- around do |example|
350
- connection.dont_execute(&example)
351
- end
352
-
353
- it "raises if a transaction isn't active" do
354
- expect { connection.statement_timeout = 30 }.to raise_error(ArgumentError)
355
- end
356
-
357
- it "does nothing if the transaction never materializes" do
358
- connection.transaction do
359
- connection.statement_timeout = 30
360
- expect(connection.statement_timeout).to eq 30
361
- end
362
- expect(connection.statement_timeout).to be_nil
363
-
364
- expect(connection.executed_statements).to be_empty
365
- end
366
-
367
- it "sets the timeout if the transaction is materialized" do
368
- connection.transaction do
369
- connection.select_value("SELECT 1")
370
- connection.statement_timeout = 30
371
- expect(connection.statement_timeout).to eq 30
372
- end
373
- expect(connection.statement_timeout).to be_nil
374
-
375
- expect(connection.executed_statements).to eq(
376
- ["BEGIN",
377
- "SELECT 1",
378
- "SET LOCAL statement_timeout TO '30000ms'",
379
- "COMMIT"]
380
- )
381
- end
382
-
383
- it "sets the timeout if the transaction materializes" do
384
- connection.transaction do
385
- connection.statement_timeout = 30
386
- connection.select_value("SELECT 1")
387
- expect(connection.statement_timeout).to eq 30
388
- end
389
- expect(connection.statement_timeout).to be_nil
390
-
391
- expect(connection.executed_statements).to eq(
392
- ["BEGIN",
393
- "SET LOCAL statement_timeout TO '30000ms'",
394
- "SELECT 1",
395
- "COMMIT"]
396
- )
397
- end
398
-
399
- it "works with nested transactions" do
400
- connection.transaction do
401
- connection.statement_timeout = 30
402
- connection.transaction(requires_new: true) do
403
- connection.statement_timeout = 15
404
- connection.select_value("SELECT 1")
405
- expect(connection.statement_timeout).to eq 15
406
- end
407
- expect(connection.statement_timeout).to eq 30
408
- end
409
- expect(connection.statement_timeout).to be_nil
410
-
411
- expect(connection.executed_statements).to eq(
412
- ["BEGIN",
413
- "SET LOCAL statement_timeout TO '30000ms'",
414
- "SAVEPOINT active_record_1",
415
- "SET LOCAL statement_timeout TO '15000ms'",
416
- "SELECT 1",
417
- "RELEASE SAVEPOINT active_record_1",
418
- "COMMIT"]
419
- )
420
- end
421
- end
422
-
423
- unless Rails.version >= "6.1"
424
- describe "#add_check_constraint" do
425
- around do |example|
426
- connection.dont_execute(&example)
427
- end
428
-
429
- it "works" do
430
- connection.add_check_constraint(:table, "column IS NOT NULL", name: :my_constraint)
431
- expect(connection.executed_statements).to eq(
432
- ['ALTER TABLE "table" ADD CONSTRAINT "my_constraint" CHECK (column IS NOT NULL)']
433
- )
434
- end
435
-
436
- it "defers validation" do
437
- connection.add_check_constraint(:table, "column IS NOT NULL", name: :my_constraint, validate: false)
438
- expect(connection.executed_statements).to eq(
439
- ['ALTER TABLE "table" ADD CONSTRAINT "my_constraint" CHECK (column IS NOT NULL) NOT VALID']
440
- )
441
- end
442
- end
443
-
444
- describe "#remove_check_constraint" do
445
- around do |example|
446
- connection.dont_execute(&example)
447
- end
448
-
449
- it "works" do
450
- connection.remove_check_constraint(:table, name: :my_constraint)
451
- expect(connection.executed_statements).to eq(
452
- ['ALTER TABLE "table" DROP CONSTRAINT "my_constraint"']
453
- )
454
- end
455
- end
456
- end
457
- end
data/spec/spec_helper.rb DELETED
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "activerecord-pg-extensions"
4
- require "byebug"
5
- require "active_record/railtie"
6
- require "active_record/pg_extensions/all"
7
-
8
- ActiveRecord::Base # rubocop:disable Lint/Void
9
- Rails.env = "test"
10
-
11
- class Application < Rails::Application
12
- config.eager_load = false
13
- end
14
- Application.initialize!
15
-
16
- ActiveRecord::Tasks::DatabaseTasks.create_all
17
-
18
- module StatementCaptureConnection
19
- def dont_execute
20
- @dont_execute = true
21
- yield
22
- ensure
23
- @dont_execute = false
24
- end
25
-
26
- def executed_statements
27
- @executed_statements ||= []
28
- end
29
-
30
- %w[execute exec_no_cache exec_cache].each do |method|
31
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
32
- def #{method}(statement, *#{Rails.version >= '7.0' ? ', **kwargs' : ''})
33
- materialize_transactions # this still needs to get called, even if we skip actually executing
34
- executed_statements << statement
35
- return empty_pg_result if @dont_execute
36
-
37
- super
38
- end
39
- RUBY
40
- end
41
-
42
- # we can't actually generate a dummy one of these, so we just query the db with something
43
- # that won't return anything
44
- def empty_pg_result
45
- @connection.async_exec("SELECT 0 WHERE FALSE")
46
- end
47
- end
48
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(StatementCaptureConnection)
49
-
50
- RSpec.configure do |config|
51
- config.expect_with :rspec do |c|
52
- c.syntax = :expect
53
- end
54
-
55
- def connection
56
- ActiveRecord::Base.connection
57
- end
58
-
59
- config.before do
60
- connection.executed_statements.clear
61
- end
62
- end