Skip to content

Commit 6447293

Browse files
authored
Merge pull request #1165 from JesseChavez/rails_71_fixes
More Postgres fixes to support AR 7.1
2 parents 494a733 + bd6ecd5 commit 6447293

File tree

4 files changed

+190
-69
lines changed

4 files changed

+190
-69
lines changed

Diff for: lib/arjdbc/abstract/connection_management.rb

+5-6
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,12 @@ def connect
4646
end
4747

4848
def reconnect
49-
if active?
50-
@raw_connection.rollback rescue nil
51-
else
52-
connect
53-
end
54-
end
49+
@raw_connection&.close
50+
51+
@raw_connection = nil
5552

53+
connect
54+
end
5655
end
5756
end
5857
end

Diff for: lib/arjdbc/postgresql/adapter.rb

+158-63
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
require 'arjdbc/postgresql/base/array_decoder'
2323
require 'arjdbc/postgresql/base/array_encoder'
2424
require 'arjdbc/postgresql/name'
25+
require 'arjdbc/postgresql/database_statements'
2526
require 'arjdbc/postgresql/schema_statements'
2627

2728
require 'active_model'
@@ -120,7 +121,8 @@ def configure_connection
120121
citext: { name: 'citext' },
121122
date: { name: 'date' },
122123
daterange: { name: 'daterange' },
123-
datetime: { name: 'timestamp' },
124+
datetime: {}, # set dynamically based on datetime_type
125+
timestamptz: { name: 'timestamptz' },
124126
decimal: { name: 'decimal' }, # :limit => 1000
125127
float: { name: 'float' },
126128
hstore: { name: 'hstore' },
@@ -150,17 +152,10 @@ def configure_connection
150152
tstzrange: { name: 'tstzrange' },
151153
tsvector: { name: 'tsvector' },
152154
uuid: { name: 'uuid' },
153-
xml: { name: 'xml' }
155+
xml: { name: 'xml' },
156+
enum: {} # special type https://www.postgresql.org/docs/current/datatype-enum.html
154157
}
155158

156-
def native_database_types
157-
NATIVE_DATABASE_TYPES
158-
end
159-
160-
def valid_type?(type)
161-
!native_database_types[type].nil?
162-
end
163-
164159
def set_standard_conforming_strings
165160
execute("SET standard_conforming_strings = on", "SCHEMA")
166161
end
@@ -232,10 +227,18 @@ def supports_insert_on_conflict?
232227
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
233228
alias supports_insert_conflict_target? supports_insert_on_conflict?
234229

230+
def supports_virtual_columns?
231+
database_version >= 12_00_00 # >= 12.0
232+
end
233+
235234
def supports_identity_columns? # :nodoc:
236235
database_version >= 10_00_00 # >= 10.0
237236
end
238237

238+
def supports_nulls_not_distinct?
239+
database_version >= 15_00_00 # >= 15.0
240+
end
241+
239242
def index_algorithms
240243
{ concurrently: 'CONCURRENTLY' }
241244
end
@@ -335,33 +338,100 @@ def extensions
335338
# Returns a list of defined enum types, and their values.
336339
def enum_types
337340
query = <<~SQL
338-
SELECT
339-
type.typname AS name,
340-
string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value
341-
FROM pg_enum AS enum
342-
JOIN pg_type AS type
343-
ON (type.oid = enum.enumtypid)
344-
GROUP BY type.typname;
341+
SELECT
342+
type.typname AS name,
343+
type.OID AS oid,
344+
n.nspname AS schema,
345+
string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value
346+
FROM pg_enum AS enum
347+
JOIN pg_type AS type ON (type.oid = enum.enumtypid)
348+
JOIN pg_namespace n ON type.typnamespace = n.oid
349+
WHERE n.nspname = ANY (current_schemas(false))
350+
GROUP BY type.OID, n.nspname, type.typname;
345351
SQL
346-
exec_query(query, "SCHEMA").cast_values
352+
353+
internal_exec_query(query, "SCHEMA", allow_retry: true, materialize_transactions: false).cast_values.each_with_object({}) do |row, memo|
354+
name, schema = row[0], row[2]
355+
schema = nil if schema == current_schema
356+
full_name = [schema, name].compact.join(".")
357+
memo[full_name] = row.last
358+
end.to_a
347359
end
348360

349361
# Given a name and an array of values, creates an enum type.
350-
def create_enum(name, values)
351-
sql_values = values.map { |s| "'#{s}'" }.join(", ")
362+
def create_enum(name, values, **options)
363+
sql_values = values.map { |s| quote(s) }.join(", ")
364+
scope = quoted_scope(name)
365+
query = <<~SQL
366+
DO $$
367+
BEGIN
368+
IF NOT EXISTS (
369+
SELECT 1
370+
FROM pg_type t
371+
JOIN pg_namespace n ON t.typnamespace = n.oid
372+
WHERE t.typname = #{scope[:name]}
373+
AND n.nspname = #{scope[:schema]}
374+
) THEN
375+
CREATE TYPE #{quote_table_name(name)} AS ENUM (#{sql_values});
376+
END IF;
377+
END
378+
$$;
379+
SQL
380+
381+
internal_exec_query(query).tap { reload_type_map }
382+
end
383+
384+
# Drops an enum type.
385+
#
386+
# If the <tt>if_exists: true</tt> option is provided, the enum is dropped
387+
# only if it exists. Otherwise, if the enum doesn't exist, an error is
388+
# raised.
389+
#
390+
# The +values+ parameter will be ignored if present. It can be helpful
391+
# to provide this in a migration's +change+ method so it can be reverted.
392+
# In that case, +values+ will be used by #create_enum.
393+
def drop_enum(name, values = nil, **options)
352394
query = <<~SQL
353-
DO $$
354-
BEGIN
355-
IF NOT EXISTS (
356-
SELECT 1 FROM pg_type t
357-
WHERE t.typname = '#{name}'
358-
) THEN
359-
CREATE TYPE \"#{name}\" AS ENUM (#{sql_values});
360-
END IF;
361-
END
362-
$$;
395+
DROP TYPE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(name)};
363396
SQL
364-
exec_query(query)
397+
internal_exec_query(query).tap { reload_type_map }
398+
end
399+
400+
# Rename an existing enum type to something else.
401+
def rename_enum(name, options = {})
402+
to = options.fetch(:to) { raise ArgumentError, ":to is required" }
403+
404+
exec_query("ALTER TYPE #{quote_table_name(name)} RENAME TO #{to}").tap { reload_type_map }
405+
end
406+
407+
# Add enum value to an existing enum type.
408+
def add_enum_value(type_name, value, options = {})
409+
before, after = options.values_at(:before, :after)
410+
sql = +"ALTER TYPE #{quote_table_name(type_name)} ADD VALUE '#{value}'"
411+
412+
if before && after
413+
raise ArgumentError, "Cannot have both :before and :after at the same time"
414+
elsif before
415+
sql << " BEFORE '#{before}'"
416+
elsif after
417+
sql << " AFTER '#{after}'"
418+
end
419+
420+
execute(sql).tap { reload_type_map }
421+
end
422+
423+
# Rename enum value on an existing enum type.
424+
def rename_enum_value(type_name, options = {})
425+
unless database_version >= 10_00_00 # >= 10.0
426+
raise ArgumentError, "Renaming enum values is only supported in PostgreSQL 10 or later"
427+
end
428+
429+
from = options.fetch(:from) { raise ArgumentError, ":from is required" }
430+
to = options.fetch(:to) { raise ArgumentError, ":to is required" }
431+
432+
execute("ALTER TYPE #{quote_table_name(type_name)} RENAME VALUE '#{from}' TO '#{to}'").tap {
433+
reload_type_map
434+
}
365435
end
366436

367437
# Returns the configured supported identifier length supported by PostgreSQL
@@ -455,11 +525,6 @@ def execute_batch(statements, name = nil)
455525
execute(combine_multi_statements(statements), name)
456526
end
457527

458-
def explain(arel, binds = [])
459-
sql, binds = to_sql_and_binds(arel, binds)
460-
ActiveRecord::ConnectionAdapters::PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query("EXPLAIN #{sql}", 'EXPLAIN', binds))
461-
end
462-
463528
# from ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements
464529
READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
465530
:close, :declare, :fetch, :move, :set, :show
@@ -493,6 +558,16 @@ def reset!
493558
end
494559
end
495560

561+
# Disconnects from the database if already connected. Otherwise, this
562+
# method does nothing.
563+
def disconnect!
564+
@lock.synchronize do
565+
super
566+
@raw_connection&.close
567+
@raw_connection = nil
568+
end
569+
end
570+
496571
def default_sequence_name(table_name, pk = "id") #:nodoc:
497572
serial_sequence(table_name, pk)
498573
rescue ActiveRecord::StatementInvalid
@@ -608,17 +683,19 @@ def column_name_for_operation(operation, node)
608683
# - format_type includes the column size constraint, e.g. varchar(50)
609684
# - ::regclass is a function that gives the id for a table name
610685
def column_definitions(table_name)
611-
select_rows(<<~SQL, 'SCHEMA')
612-
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
613-
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
614-
c.collname, col_description(a.attrelid, a.attnum) AS comment
615-
FROM pg_attribute a
616-
LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
617-
LEFT JOIN pg_type t ON a.atttypid = t.oid
618-
LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
619-
WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
620-
AND a.attnum > 0 AND NOT a.attisdropped
621-
ORDER BY a.attnum
686+
query(<<~SQL, "SCHEMA")
687+
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
688+
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
689+
c.collname, col_description(a.attrelid, a.attnum) AS comment,
690+
#{supports_identity_columns? ? 'attidentity' : quote('')} AS identity,
691+
#{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated
692+
FROM pg_attribute a
693+
LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
694+
LEFT JOIN pg_type t ON a.atttypid = t.oid
695+
LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
696+
WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
697+
AND a.attnum > 0 AND NOT a.attisdropped
698+
ORDER BY a.attnum
622699
SQL
623700
end
624701

@@ -633,22 +710,27 @@ def arel_visitor
633710

634711
# Pulled from ActiveRecord's Postgres adapter and modified to use execute
635712
def can_perform_case_insensitive_comparison_for?(column)
636-
@case_insensitive_cache ||= {}
637-
@case_insensitive_cache[column.sql_type] ||= begin
638-
sql = <<~SQL
639-
SELECT exists(
640-
SELECT * FROM pg_proc
641-
WHERE proname = 'lower'
642-
AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
643-
) OR exists(
644-
SELECT * FROM pg_proc
645-
INNER JOIN pg_cast
646-
ON ARRAY[casttarget]::oidvector = proargtypes
647-
WHERE proname = 'lower'
648-
AND castsource = #{quote column.sql_type}::regtype
649-
)
650-
SQL
651-
select_value(sql, 'SCHEMA')
713+
# NOTE: citext is an exception. It is possible to perform a
714+
# case-insensitive comparison using `LOWER()`, but it is
715+
# unnecessary, as `citext` is case-insensitive by definition.
716+
@case_insensitive_cache ||= { "citext" => false }
717+
@case_insensitive_cache.fetch(column.sql_type) do
718+
@case_insensitive_cache[column.sql_type] = begin
719+
sql = <<~SQL
720+
SELECT exists(
721+
SELECT * FROM pg_proc
722+
WHERE proname = 'lower'
723+
AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
724+
) OR exists(
725+
SELECT * FROM pg_proc
726+
INNER JOIN pg_cast
727+
ON ARRAY[casttarget]::oidvector = proargtypes
728+
WHERE proname = 'lower'
729+
AND castsource = #{quote column.sql_type}::regtype
730+
)
731+
SQL
732+
select_value(sql, 'SCHEMA')
733+
end
652734
end
653735
end
654736

@@ -770,6 +852,7 @@ class PostgreSQLAdapter < AbstractAdapter
770852

771853
require 'arjdbc/postgresql/oid_types'
772854
include ::ArJdbc::PostgreSQL::OIDTypes
855+
include ::ArJdbc::PostgreSQL::DatabaseStatements
773856
include ::ArJdbc::PostgreSQL::SchemaStatements
774857

775858
include ::ArJdbc::PostgreSQL::ColumnHelpers
@@ -841,6 +924,18 @@ def self.database_exists?(config)
841924
public :sql_for_insert
842925
alias :postgresql_version :database_version
843926

927+
def native_database_types # :nodoc:
928+
self.class.native_database_types
929+
end
930+
931+
def self.native_database_types # :nodoc:
932+
@native_database_types ||= begin
933+
types = NATIVE_DATABASE_TYPES.dup
934+
types[:datetime] = types[datetime_type]
935+
types
936+
end
937+
end
938+
844939
private
845940

846941
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:

Diff for: lib/arjdbc/postgresql/database_statements.rb

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module ArJdbc
4+
module PostgreSQL
5+
module DatabaseStatements
6+
def explain(arel, binds = [], options = [])
7+
sql = build_explain_clause(options) + " " + to_sql(arel, binds)
8+
result = internal_exec_query(sql, "EXPLAIN", binds)
9+
ActiveRecord::ConnectionAdapters::PostgreSQL::ExplainPrettyPrinter.new.pp(result)
10+
end
11+
12+
def build_explain_clause(options = [])
13+
return "EXPLAIN" if options.empty?
14+
15+
"EXPLAIN (#{options.join(", ").upcase})"
16+
end
17+
end
18+
end
19+
end

Diff for: lib/arjdbc/sqlite3/adapter.rb

+8
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,14 @@ def build_statement_pool
669669
StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
670670
end
671671

672+
def reconnect
673+
if active?
674+
@raw_connection.rollback rescue nil
675+
else
676+
connect
677+
end
678+
end
679+
672680
def configure_connection
673681
if @config[:timeout] && @config[:retries]
674682
raise ArgumentError, "Cannot specify both timeout and retries arguments"

0 commit comments

Comments
 (0)