pg 1.0.0 → 1.2.3

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +0 -6595
  5. data/History.rdoc +156 -0
  6. data/Manifest.txt +8 -2
  7. data/README-Windows.rdoc +4 -4
  8. data/README.ja.rdoc +1 -2
  9. data/README.rdoc +55 -9
  10. data/Rakefile +9 -7
  11. data/Rakefile.cross +58 -57
  12. data/ext/errorcodes.def +68 -0
  13. data/ext/errorcodes.rb +1 -1
  14. data/ext/errorcodes.txt +19 -2
  15. data/ext/extconf.rb +7 -5
  16. data/ext/pg.c +141 -98
  17. data/ext/pg.h +64 -21
  18. data/ext/pg_binary_decoder.c +82 -15
  19. data/ext/pg_binary_encoder.c +13 -12
  20. data/ext/pg_coder.c +73 -12
  21. data/ext/pg_connection.c +625 -346
  22. data/ext/pg_copy_coder.c +16 -8
  23. data/ext/pg_record_coder.c +491 -0
  24. data/ext/pg_result.c +571 -191
  25. data/ext/pg_text_decoder.c +606 -40
  26. data/ext/pg_text_encoder.c +185 -54
  27. data/ext/pg_tuple.c +549 -0
  28. data/ext/pg_type_map.c +1 -1
  29. data/ext/pg_type_map_all_strings.c +4 -4
  30. data/ext/pg_type_map_by_class.c +9 -4
  31. data/ext/pg_type_map_by_column.c +7 -6
  32. data/ext/pg_type_map_by_mri_type.c +1 -1
  33. data/ext/pg_type_map_by_oid.c +3 -2
  34. data/ext/pg_type_map_in_ruby.c +1 -1
  35. data/ext/{util.c → pg_util.c} +10 -10
  36. data/ext/{util.h → pg_util.h} +2 -2
  37. data/lib/pg.rb +8 -6
  38. data/lib/pg/basic_type_mapping.rb +121 -25
  39. data/lib/pg/binary_decoder.rb +23 -0
  40. data/lib/pg/coder.rb +23 -2
  41. data/lib/pg/connection.rb +22 -3
  42. data/lib/pg/constants.rb +2 -1
  43. data/lib/pg/exceptions.rb +2 -1
  44. data/lib/pg/result.rb +14 -2
  45. data/lib/pg/text_decoder.rb +21 -26
  46. data/lib/pg/text_encoder.rb +32 -8
  47. data/lib/pg/tuple.rb +30 -0
  48. data/lib/pg/type_map_by_column.rb +3 -2
  49. data/spec/helpers.rb +52 -20
  50. data/spec/pg/basic_type_mapping_spec.rb +362 -37
  51. data/spec/pg/connection_spec.rb +376 -146
  52. data/spec/pg/connection_sync_spec.rb +41 -0
  53. data/spec/pg/result_spec.rb +240 -15
  54. data/spec/pg/tuple_spec.rb +333 -0
  55. data/spec/pg/type_map_by_class_spec.rb +2 -2
  56. data/spec/pg/type_map_by_column_spec.rb +6 -2
  57. data/spec/pg/type_map_by_mri_type_spec.rb +1 -1
  58. data/spec/pg/type_map_by_oid_spec.rb +3 -3
  59. data/spec/pg/type_map_in_ruby_spec.rb +1 -1
  60. data/spec/pg/type_map_spec.rb +1 -1
  61. data/spec/pg/type_spec.rb +363 -17
  62. data/spec/pg_spec.rb +1 -1
  63. metadata +47 -47
  64. metadata.gz.sig +0 -0
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ module PG
5
+ module BinaryDecoder
6
+ # Convenience classes for timezone options
7
+ class TimestampUtc < Timestamp
8
+ def initialize(params={})
9
+ super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC))
10
+ end
11
+ end
12
+ class TimestampUtcToLocal < Timestamp
13
+ def initialize(params={})
14
+ super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_LOCAL))
15
+ end
16
+ end
17
+ class TimestampLocal < Timestamp
18
+ def initialize(params={})
19
+ super(params.merge(flags: PG::Coder::TIMESTAMP_DB_LOCAL | PG::Coder::TIMESTAMP_APP_LOCAL))
20
+ end
21
+ end
22
+ end
23
+ end # module PG
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  module PG
4
5
 
@@ -28,6 +29,7 @@ module PG
28
29
  {
29
30
  oid: oid,
30
31
  format: format,
32
+ flags: flags,
31
33
  name: name,
32
34
  }
33
35
  end
@@ -52,6 +54,18 @@ module PG
52
54
  str[-1,0] = "#{name_str} #{oid_str}#{format_str}"
53
55
  str
54
56
  end
57
+
58
+ def inspect_short
59
+ str = case format
60
+ when 0 then "T"
61
+ when 1 then "B"
62
+ else format.to_s
63
+ end
64
+ str += "E" if respond_to?(:encode)
65
+ str += "D" if respond_to?(:decode)
66
+
67
+ "#{name || self.class.name}:#{str}"
68
+ end
55
69
  end
56
70
 
57
71
  class CompositeCoder < Coder
@@ -79,5 +93,12 @@ module PG
79
93
  })
80
94
  end
81
95
  end
82
- end # module PG
83
96
 
97
+ class RecordCoder < Coder
98
+ def to_h
99
+ super.merge!({
100
+ type_map: type_map,
101
+ })
102
+ end
103
+ end
104
+ end # module PG
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pg' unless defined?( PG )
4
5
  require 'uri'
@@ -47,7 +48,7 @@ class PG::Connection
47
48
 
48
49
  if args.length == 1
49
50
  case args.first
50
- when URI, /\A#{URI.regexp}\z/
51
+ when URI, /\A#{URI::ABS_URI_REF}\z/
51
52
  uri = URI(args.first)
52
53
  options.merge!( Hash[URI.decode_www_form( uri.query )] ) if uri.query
53
54
  when /=/
@@ -268,5 +269,23 @@ class PG::Connection
268
269
  end
269
270
  end
270
271
 
271
- end # class PG::Connection
272
+ REDIRECT_METHODS = {
273
+ :exec => [:async_exec, :sync_exec],
274
+ :query => [:async_exec, :sync_exec],
275
+ :exec_params => [:async_exec_params, :sync_exec_params],
276
+ :prepare => [:async_prepare, :sync_prepare],
277
+ :exec_prepared => [:async_exec_prepared, :sync_exec_prepared],
278
+ :describe_portal => [:async_describe_portal, :sync_describe_portal],
279
+ :describe_prepared => [:async_describe_prepared, :sync_describe_prepared],
280
+ }
272
281
 
282
+ def self.async_api=(enable)
283
+ REDIRECT_METHODS.each do |ali, (async, sync)|
284
+ remove_method(ali) if method_defined?(ali)
285
+ alias_method( ali, enable ? async : sync )
286
+ end
287
+ end
288
+
289
+ # pg-1.1.0+ defaults to libpq's async API for query related blocking methods
290
+ self.async_api = true
291
+ end # class PG::Connection
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pg' unless defined?( PG )
4
5
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pg' unless defined?( PG )
4
5
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pg' unless defined?( PG )
4
5
 
@@ -9,12 +10,23 @@ class PG::Result
9
10
  #
10
11
  # +type_map+: a PG::TypeMap instance.
11
12
  #
12
- # See PG::BasicTypeMapForResults
13
+ # This method is equal to #type_map= , but returns self, so that calls can be chained.
14
+ #
15
+ # See also PG::BasicTypeMapForResults
13
16
  def map_types!(type_map)
14
17
  self.type_map = type_map
15
18
  return self
16
19
  end
17
20
 
21
+ # Set the data type for all field name returning methods.
22
+ #
23
+ # +type+: a Symbol defining the field name type.
24
+ #
25
+ # This method is equal to #field_name_type= , but returns self, so that calls can be chained.
26
+ def field_names_as(type)
27
+ self.field_name_type = type
28
+ return self
29
+ end
18
30
 
19
31
  ### Return a String representation of the object suitable for debugging.
20
32
  def inspect
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'date'
4
5
  require 'json'
@@ -6,10 +7,8 @@ require 'json'
6
7
  module PG
7
8
  module TextDecoder
8
9
  class Date < SimpleDecoder
9
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
10
-
11
10
  def decode(string, tuple=nil, field=nil)
12
- if string =~ ISO_DATE
11
+ if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/
13
12
  ::Date.new $1.to_i, $2.to_i, $3.to_i
14
13
  else
15
14
  string
@@ -17,35 +16,31 @@ module PG
17
16
  end
18
17
  end
19
18
 
20
- class TimestampWithoutTimeZone < SimpleDecoder
21
- ISO_DATETIME_WITHOUT_TIMEZONE = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
22
-
19
+ class JSON < SimpleDecoder
23
20
  def decode(string, tuple=nil, field=nil)
24
- if string =~ ISO_DATETIME_WITHOUT_TIMEZONE
25
- Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r
26
- else
27
- string
28
- end
21
+ ::JSON.parse(string, quirks_mode: true)
29
22
  end
30
23
  end
31
24
 
32
- class TimestampWithTimeZone < SimpleDecoder
33
- ISO_DATETIME_WITH_TIMEZONE = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?([-\+]\d\d):?(\d\d)?:?(\d\d)?\z/
34
-
35
- def decode(string, tuple=nil, field=nil)
36
- if string =~ ISO_DATETIME_WITH_TIMEZONE
37
- Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r, "#{$8}:#{$9 || '00'}:#{$10 || '00'}"
38
- else
39
- string
40
- end
25
+ # Convenience classes for timezone options
26
+ class TimestampUtc < Timestamp
27
+ def initialize(params={})
28
+ super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC))
41
29
  end
42
30
  end
43
-
44
- class JSON < SimpleDecoder
45
- def decode(string, tuple=nil, field=nil)
46
- ::JSON.parse(string, quirks_mode: true)
31
+ class TimestampUtcToLocal < Timestamp
32
+ def initialize(params={})
33
+ super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_LOCAL))
34
+ end
35
+ end
36
+ class TimestampLocal < Timestamp
37
+ def initialize(params={})
38
+ super(params.merge(flags: PG::Coder::TIMESTAMP_DB_LOCAL | PG::Coder::TIMESTAMP_APP_LOCAL))
47
39
  end
48
40
  end
41
+
42
+ # For backward compatibility:
43
+ TimestampWithoutTimeZone = TimestampLocal
44
+ TimestampWithTimeZone = Timestamp
49
45
  end
50
46
  end # module PG
51
-
@@ -1,27 +1,32 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'json'
5
+ require 'ipaddr'
4
6
 
5
7
  module PG
6
8
  module TextEncoder
7
9
  class Date < SimpleEncoder
8
- STRFTIME_ISO_DATE = "%Y-%m-%d".freeze
9
10
  def encode(value)
10
- value.respond_to?(:strftime) ? value.strftime(STRFTIME_ISO_DATE) : value
11
+ value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value
11
12
  end
12
13
  end
13
14
 
14
15
  class TimestampWithoutTimeZone < SimpleEncoder
15
- STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N".freeze
16
16
  def encode(value)
17
- value.respond_to?(:strftime) ? value.strftime(STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE) : value
17
+ value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d %H:%M:%S.%N") : value
18
+ end
19
+ end
20
+
21
+ class TimestampUtc < SimpleEncoder
22
+ def encode(value)
23
+ value.respond_to?(:utc) ? value.utc.strftime("%Y-%m-%d %H:%M:%S.%N") : value
18
24
  end
19
25
  end
20
26
 
21
27
  class TimestampWithTimeZone < SimpleEncoder
22
- STRFTIME_ISO_DATETIME_WITH_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N %:z".freeze
23
28
  def encode(value)
24
- value.respond_to?(:strftime) ? value.strftime(STRFTIME_ISO_DATETIME_WITH_TIMEZONE) : value
29
+ value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d %H:%M:%S.%N %:z") : value
25
30
  end
26
31
  end
27
32
 
@@ -30,6 +35,25 @@ module PG
30
35
  ::JSON.generate(value, quirks_mode: true)
31
36
  end
32
37
  end
38
+
39
+ class Inet < SimpleEncoder
40
+ def encode(value)
41
+ case value
42
+ when IPAddr
43
+ default_prefix = (value.family == Socket::AF_INET ? 32 : 128)
44
+ s = value.to_s
45
+ if value.respond_to?(:prefix)
46
+ prefix = value.prefix
47
+ else
48
+ range = value.to_range
49
+ prefix = default_prefix - Math.log(((range.end.to_i - range.begin.to_i) + 1), 2).to_i
50
+ end
51
+ s << "/" << prefix.to_s if prefix != default_prefix
52
+ s
53
+ else
54
+ value
55
+ end
56
+ end
57
+ end
33
58
  end
34
59
  end # module PG
35
-
@@ -0,0 +1,30 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'pg' unless defined?( PG )
5
+
6
+
7
+ class PG::Tuple
8
+
9
+ ### Return a String representation of the object suitable for debugging.
10
+ def inspect
11
+ "#<#{self.class} #{self.map{|k,v| "#{k}: #{v.inspect}" }.join(", ") }>"
12
+ end
13
+
14
+ def has_key?(key)
15
+ field_map.has_key?(key)
16
+ end
17
+ alias key? has_key?
18
+
19
+ def keys
20
+ field_names || field_map.keys.freeze
21
+ end
22
+
23
+ def each_key(&block)
24
+ if fn=field_names
25
+ fn.each(&block)
26
+ else
27
+ field_map.each_key(&block)
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pg' unless defined?( PG )
4
5
 
@@ -9,7 +10,7 @@ class PG::TypeMapByColumn
9
10
  end
10
11
 
11
12
  def inspect
12
- type_strings = coders.map{|c| c ? "#{c.name}:#{c.format}" : 'nil' }
13
+ type_strings = coders.map{|c| c ? c.inspect_short : 'nil' }
13
14
  "#<#{self.class} #{type_strings.join(' ')}>"
14
15
  end
15
16
  end
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ # -*- ruby -*-
2
2
 
3
3
  require 'pathname'
4
4
  require 'rspec'
@@ -22,12 +22,11 @@ module PG::TestingHelpers
22
22
 
23
23
  mod.around( :each ) do |example|
24
24
  begin
25
+ @conn.set_default_encoding
25
26
  @conn.exec( 'BEGIN' ) unless example.metadata[:without_transaction]
26
- if PG.respond_to?( :library_version )
27
- desc = example.source_location.join(':')
28
- @conn.exec_params %Q{SET application_name TO '%s'} %
29
- [@conn.escape_string(desc.slice(-60))]
30
- end
27
+ desc = example.source_location.join(':')
28
+ @conn.exec %Q{SET application_name TO '%s'} %
29
+ [@conn.escape_string(desc.slice(-60))]
31
30
  example.run
32
31
  ensure
33
32
  @conn.exec( 'ROLLBACK' ) unless example.metadata[:without_transaction]
@@ -173,18 +172,18 @@ module PG::TestingHelpers
173
172
  datadir = testdir + 'data'
174
173
  pidfile = datadir + 'postmaster.pid'
175
174
  if pidfile.exist? && pid = pidfile.read.chomp.to_i
176
- $stderr.puts "pidfile (%p) exists: %d" % [ pidfile, pid ]
175
+ trace "pidfile (%p) exists: %d" % [ pidfile, pid ]
177
176
  begin
178
177
  Process.kill( 0, pid )
179
178
  rescue Errno::ESRCH
180
- $stderr.puts "No postmaster running for %s" % [ datadir ]
179
+ trace "No postmaster running for %s" % [ datadir ]
181
180
  # Process isn't alive, so don't try to stop it
182
181
  else
183
- $stderr.puts "Stopping lingering database at PID %d" % [ pid ]
182
+ trace "Stopping lingering database at PID %d" % [ pid ]
184
183
  run 'pg_ctl', '-D', datadir.to_s, '-m', 'fast', 'stop'
185
184
  end
186
185
  else
187
- $stderr.puts "No pidfile (%p)" % [ pidfile ]
186
+ trace "No pidfile (%p)" % [ pidfile ]
188
187
  end
189
188
  end
190
189
  end
@@ -195,12 +194,12 @@ module PG::TestingHelpers
195
194
  require 'pg'
196
195
  stop_existing_postmasters()
197
196
 
198
- puts "Setting up test database for #{description}"
197
+ trace "Setting up test database for #{description}"
199
198
  @test_pgdata = TEST_DIRECTORY + 'data'
200
199
  @test_pgdata.mkpath
201
200
 
202
- @port = 54321
203
- ENV['PGPORT'] = @port.to_s
201
+ ENV['PGPORT'] ||= "54321"
202
+ @port = ENV['PGPORT'].to_i
204
203
  ENV['PGHOST'] = 'localhost'
205
204
  @conninfo = "host=localhost port=#{@port} dbname=test"
206
205
 
@@ -210,7 +209,7 @@ module PG::TestingHelpers
210
209
  begin
211
210
  unless (@test_pgdata+"postgresql.conf").exist?
212
211
  FileUtils.rm_rf( @test_pgdata, :verbose => $DEBUG )
213
- $stderr.puts "Running initdb"
212
+ trace "Running initdb"
214
213
  log_and_run @logfile, 'initdb', '-E', 'UTF8', '--no-locale', '-D', @test_pgdata.to_s
215
214
  end
216
215
 
@@ -219,14 +218,14 @@ module PG::TestingHelpers
219
218
  '-D', @test_pgdata.to_s, 'start'
220
219
  sleep 2
221
220
 
222
- $stderr.puts "Creating the test DB"
221
+ trace "Creating the test DB"
223
222
  log_and_run @logfile, 'psql', '-e', '-c', 'DROP DATABASE IF EXISTS test', 'postgres'
224
223
  log_and_run @logfile, 'createdb', '-e', 'test'
225
224
 
226
225
  rescue => err
227
226
  $stderr.puts "%p during test setup: %s" % [ err.class, err.message ]
228
227
  $stderr.puts "See #{@logfile} for details."
229
- $stderr.puts *err.backtrace if $DEBUG
228
+ $stderr.puts err.backtrace if $DEBUG
230
229
  fail
231
230
  end
232
231
 
@@ -240,7 +239,7 @@ module PG::TestingHelpers
240
239
 
241
240
 
242
241
  def teardown_testing_db( conn )
243
- puts "Tearing down test database"
242
+ trace "Tearing down test database"
244
243
 
245
244
  if conn
246
245
  check_for_lingering_connections( conn )
@@ -266,7 +265,7 @@ module PG::TestingHelpers
266
265
 
267
266
  # Retrieve the names of the column types of a given result set.
268
267
  def result_typenames(res)
269
- @conn.exec( "SELECT " + res.nfields.times.map{|i| "format_type($#{i*2+1},$#{i*2+2})"}.join(","),
268
+ @conn.exec_params( "SELECT " + res.nfields.times.map{|i| "format_type($#{i*2+1},$#{i*2+2})"}.join(","),
270
269
  res.nfields.times.map{|i| [res.ftype(i), res.fmod(i)] }.flatten ).
271
270
  values[0]
272
271
  end
@@ -320,6 +319,39 @@ module PG::TestingHelpers
320
319
  return ConnStillUsableMatcher.new
321
320
  end
322
321
 
322
+ def wait_for_polling_ok(conn, meth = :connect_poll)
323
+ status = conn.send(meth)
324
+
325
+ while status != PG::PGRES_POLLING_OK
326
+ if status == PG::PGRES_POLLING_READING
327
+ select( [conn.socket_io], [], [], 5.0 ) or
328
+ raise "Asynchronous connection timed out!"
329
+
330
+ elsif status == PG::PGRES_POLLING_WRITING
331
+ select( [], [conn.socket_io], [], 5.0 ) or
332
+ raise "Asynchronous connection timed out!"
333
+ end
334
+ status = conn.send(meth)
335
+ end
336
+ end
337
+
338
+ def wait_for_query_result(conn)
339
+ result = nil
340
+ loop do
341
+ # Buffer any incoming data on the socket until a full result is ready.
342
+ conn.consume_input
343
+ while conn.is_busy
344
+ select( [conn.socket_io], nil, nil, 5.0 ) or
345
+ raise "Timeout waiting for query response."
346
+ conn.consume_input
347
+ end
348
+
349
+ # Fetch the next result. If there isn't one, the query is finished
350
+ result = conn.get_result || break
351
+ end
352
+ result
353
+ end
354
+
323
355
  end
324
356
 
325
357
 
@@ -338,11 +370,11 @@ RSpec.configure do |config|
338
370
  else
339
371
  config.filter_run_excluding :windows
340
372
  end
341
- config.filter_run_excluding :socket_io unless
342
- PG::Connection.instance_methods.map( &:to_sym ).include?( :socket_io )
343
373
 
344
374
  config.filter_run_excluding( :postgresql_93 ) if PG.library_version < 90300
345
375
  config.filter_run_excluding( :postgresql_94 ) if PG.library_version < 90400
346
376
  config.filter_run_excluding( :postgresql_95 ) if PG.library_version < 90500
377
+ config.filter_run_excluding( :postgresql_96 ) if PG.library_version < 90600
347
378
  config.filter_run_excluding( :postgresql_10 ) if PG.library_version < 100000
379
+ config.filter_run_excluding( :postgresql_12 ) if PG.library_version < 120000
348
380
  end