2222require 'arjdbc/postgresql/base/array_decoder'
2323require 'arjdbc/postgresql/base/array_encoder'
2424require 'arjdbc/postgresql/name'
25+ require 'arjdbc/postgresql/database_statements'
2526require 'arjdbc/postgresql/schema_statements'
2627
2728require '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:
0 commit comments