22
22
require 'arjdbc/postgresql/base/array_decoder'
23
23
require 'arjdbc/postgresql/base/array_encoder'
24
24
require 'arjdbc/postgresql/name'
25
+ require 'arjdbc/postgresql/database_statements'
25
26
require 'arjdbc/postgresql/schema_statements'
26
27
27
28
require 'active_model'
@@ -120,7 +121,8 @@ def configure_connection
120
121
citext : { name : 'citext' } ,
121
122
date : { name : 'date' } ,
122
123
daterange : { name : 'daterange' } ,
123
- datetime : { name : 'timestamp' } ,
124
+ datetime : { } , # set dynamically based on datetime_type
125
+ timestamptz : { name : 'timestamptz' } ,
124
126
decimal : { name : 'decimal' } , # :limit => 1000
125
127
float : { name : 'float' } ,
126
128
hstore : { name : 'hstore' } ,
@@ -150,17 +152,10 @@ def configure_connection
150
152
tstzrange : { name : 'tstzrange' } ,
151
153
tsvector : { name : 'tsvector' } ,
152
154
uuid : { name : 'uuid' } ,
153
- xml : { name : 'xml' }
155
+ xml : { name : 'xml' } ,
156
+ enum : { } # special type https://www.postgresql.org/docs/current/datatype-enum.html
154
157
}
155
158
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
-
164
159
def set_standard_conforming_strings
165
160
execute ( "SET standard_conforming_strings = on" , "SCHEMA" )
166
161
end
@@ -232,10 +227,18 @@ def supports_insert_on_conflict?
232
227
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
233
228
alias supports_insert_conflict_target? supports_insert_on_conflict?
234
229
230
+ def supports_virtual_columns?
231
+ database_version >= 12_00_00 # >= 12.0
232
+ end
233
+
235
234
def supports_identity_columns? # :nodoc:
236
235
database_version >= 10_00_00 # >= 10.0
237
236
end
238
237
238
+ def supports_nulls_not_distinct?
239
+ database_version >= 15_00_00 # >= 15.0
240
+ end
241
+
239
242
def index_algorithms
240
243
{ concurrently : 'CONCURRENTLY' }
241
244
end
@@ -335,33 +338,100 @@ def extensions
335
338
# Returns a list of defined enum types, and their values.
336
339
def enum_types
337
340
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;
345
351
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
347
359
end
348
360
349
361
# 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 )
352
394
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 ) } ;
363
396
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
+ }
365
435
end
366
436
367
437
# Returns the configured supported identifier length supported by PostgreSQL
@@ -455,11 +525,6 @@ def execute_batch(statements, name = nil)
455
525
execute ( combine_multi_statements ( statements ) , name )
456
526
end
457
527
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
-
463
528
# from ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements
464
529
READ_QUERY = ActiveRecord ::ConnectionAdapters ::AbstractAdapter . build_read_query_regexp (
465
530
:close , :declare , :fetch , :move , :set , :show
@@ -493,6 +558,16 @@ def reset!
493
558
end
494
559
end
495
560
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
+
496
571
def default_sequence_name ( table_name , pk = "id" ) #:nodoc:
497
572
serial_sequence ( table_name , pk )
498
573
rescue ActiveRecord ::StatementInvalid
@@ -608,17 +683,19 @@ def column_name_for_operation(operation, node)
608
683
# - format_type includes the column size constraint, e.g. varchar(50)
609
684
# - ::regclass is a function that gives the id for a table name
610
685
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
622
699
SQL
623
700
end
624
701
@@ -633,22 +710,27 @@ def arel_visitor
633
710
634
711
# Pulled from ActiveRecord's Postgres adapter and modified to use execute
635
712
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
652
734
end
653
735
end
654
736
@@ -770,6 +852,7 @@ class PostgreSQLAdapter < AbstractAdapter
770
852
771
853
require 'arjdbc/postgresql/oid_types'
772
854
include ::ArJdbc ::PostgreSQL ::OIDTypes
855
+ include ::ArJdbc ::PostgreSQL ::DatabaseStatements
773
856
include ::ArJdbc ::PostgreSQL ::SchemaStatements
774
857
775
858
include ::ArJdbc ::PostgreSQL ::ColumnHelpers
@@ -841,6 +924,18 @@ def self.database_exists?(config)
841
924
public :sql_for_insert
842
925
alias :postgresql_version :database_version
843
926
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
+
844
939
private
845
940
846
941
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
0 commit comments