activerecord-yugabytedb-adapter 7.0.4.1 → 7.1.3.4
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +21 -9
- data/README.md +8 -2
- data/activerecord-yugabytedb-adapter.gemspec +2 -2
- data/lib/active_record/connection_adapters/yugabytedb/column.rb +16 -3
- data/lib/active_record/connection_adapters/yugabytedb/database_statements.rb +75 -45
- data/lib/active_record/connection_adapters/yugabytedb/oid/array.rb +3 -3
- data/lib/active_record/connection_adapters/yugabytedb/oid/bytea.rb +1 -1
- data/lib/active_record/connection_adapters/yugabytedb/oid/money.rb +3 -2
- data/lib/active_record/connection_adapters/yugabytedb/oid/range.rb +11 -2
- data/lib/active_record/connection_adapters/yugabytedb/oid/timestamp_with_time_zone.rb +2 -2
- data/lib/active_record/connection_adapters/yugabytedb/quoting.rb +42 -9
- data/lib/active_record/connection_adapters/yugabytedb/referential_integrity.rb +3 -9
- data/lib/active_record/connection_adapters/yugabytedb/schema_creation.rb +76 -6
- data/lib/active_record/connection_adapters/yugabytedb/schema_definitions.rb +131 -2
- data/lib/active_record/connection_adapters/yugabytedb/schema_dumper.rb +53 -0
- data/lib/active_record/connection_adapters/yugabytedb/schema_statements.rb +362 -60
- data/lib/active_record/connection_adapters/yugabytedb/utils.rb +11 -12
- data/lib/active_record/connection_adapters/yugabytedb_adapter.rb +625 -464
- data/lib/arel/visitors/yugabytedb.rb +12 -1
- data/lib/version.rb +1 -1
- metadata +5 -5
| @@ -74,11 +74,11 @@ module ActiveRecord | |
| 74 74 | 
             
                        FROM pg_class t
         | 
| 75 75 | 
             
                        INNER JOIN pg_index d ON t.oid = d.indrelid
         | 
| 76 76 | 
             
                        INNER JOIN pg_class i ON d.indexrelid = i.oid
         | 
| 77 | 
            -
                        LEFT JOIN pg_namespace n ON n.oid =  | 
| 77 | 
            +
                        LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
         | 
| 78 78 | 
             
                        WHERE i.relkind IN ('i', 'I')
         | 
| 79 79 | 
             
                          AND i.relname = #{index[:name]}
         | 
| 80 80 | 
             
                          AND t.relname = #{table[:name]}
         | 
| 81 | 
            -
                          AND n.nspname = #{ | 
| 81 | 
            +
                          AND n.nspname = #{table[:schema]}
         | 
| 82 82 | 
             
                      SQL
         | 
| 83 83 | 
             
                    end
         | 
| 84 84 |  | 
| @@ -88,11 +88,11 @@ module ActiveRecord | |
| 88 88 |  | 
| 89 89 | 
             
                      result = query(<<~SQL, "SCHEMA")
         | 
| 90 90 | 
             
                        SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
         | 
| 91 | 
            -
                                        pg_catalog.obj_description(i.oid, 'pg_class') AS comment
         | 
| 91 | 
            +
                                        pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid
         | 
| 92 92 | 
             
                        FROM pg_class t
         | 
| 93 93 | 
             
                        INNER JOIN pg_index d ON t.oid = d.indrelid
         | 
| 94 94 | 
             
                        INNER JOIN pg_class i ON d.indexrelid = i.oid
         | 
| 95 | 
            -
                        LEFT JOIN pg_namespace n ON n.oid =  | 
| 95 | 
            +
                        LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
         | 
| 96 96 | 
             
                        WHERE i.relkind IN ('i', 'I')
         | 
| 97 97 | 
             
                          AND d.indisprimary = 'f'
         | 
| 98 98 | 
             
                          AND t.relname = #{scope[:name]}
         | 
| @@ -107,25 +107,24 @@ module ActiveRecord | |
| 107 107 | 
             
                        inddef = row[3]
         | 
| 108 108 | 
             
                        oid = row[4]
         | 
| 109 109 | 
             
                        comment = row[5]
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                        using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
         | 
| 110 | 
            +
                        valid = row[6]
         | 
| 111 | 
            +
                        using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten
         | 
| 112 112 |  | 
| 113 113 | 
             
                        orders = {}
         | 
| 114 114 | 
             
                        opclasses = {}
         | 
| 115 | 
            +
                        include_columns = include ? include.split(",").map(&:strip) : []
         | 
| 115 116 |  | 
| 116 117 | 
             
                        if indkey.include?(0)
         | 
| 117 118 | 
             
                          columns = expressions
         | 
| 118 119 | 
             
                        else
         | 
| 119 | 
            -
                          columns =  | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
                            AND a.attnum IN (#{indkey.join(",")})
         | 
| 124 | 
            -
                          SQL
         | 
| 120 | 
            +
                          columns = column_names_from_column_numbers(oid, indkey)
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                          # prevent INCLUDE columns from being matched
         | 
| 123 | 
            +
                          columns.reject! { |c| include_columns.include?(c) }
         | 
| 125 124 |  | 
| 126 125 | 
             
                          # add info on sort order (only desc order is explicitly specified, asc is the default)
         | 
| 127 126 | 
             
                          # and non-default opclasses
         | 
| 128 | 
            -
                          expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
         | 
| 127 | 
            +
                          expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
         | 
| 129 128 | 
             
                            opclasses[column] = opclass.to_sym if opclass
         | 
| 130 129 | 
             
                            if nulls
         | 
| 131 130 | 
             
                              orders[column] = [desc, nulls].compact.join(" ")
         | 
| @@ -144,7 +143,10 @@ module ActiveRecord | |
| 144 143 | 
             
                          opclasses: opclasses,
         | 
| 145 144 | 
             
                          where: where,
         | 
| 146 145 | 
             
                          using: using.to_sym,
         | 
| 147 | 
            -
                           | 
| 146 | 
            +
                          include: include_columns.presence,
         | 
| 147 | 
            +
                          nulls_not_distinct: nulls_not_distinct.present?,
         | 
| 148 | 
            +
                          comment: comment.presence,
         | 
| 149 | 
            +
                          valid: valid
         | 
| 148 150 | 
             
                        )
         | 
| 149 151 | 
             
                      end
         | 
| 150 152 | 
             
                    end
         | 
| @@ -223,7 +225,7 @@ module ActiveRecord | |
| 223 225 | 
             
                    # This should be not be called manually but set in database.yml.
         | 
| 224 226 | 
             
                    def schema_search_path=(schema_csv)
         | 
| 225 227 | 
             
                      if schema_csv
         | 
| 226 | 
            -
                         | 
| 228 | 
            +
                        internal_execute("SET search_path TO #{schema_csv}")
         | 
| 227 229 | 
             
                        @schema_search_path = schema_csv
         | 
| 228 230 | 
             
                      end
         | 
| 229 231 | 
             
                    end
         | 
| @@ -240,7 +242,7 @@ module ActiveRecord | |
| 240 242 |  | 
| 241 243 | 
             
                    # Set the client message level.
         | 
| 242 244 | 
             
                    def client_min_messages=(level)
         | 
| 243 | 
            -
                       | 
| 245 | 
            +
                      internal_execute("SET client_min_messages TO '#{level}'")
         | 
| 244 246 | 
             
                    end
         | 
| 245 247 |  | 
| 246 248 | 
             
                    # Returns the sequence name for a table's primary key or some other specified key.
         | 
| @@ -288,14 +290,14 @@ module ActiveRecord | |
| 288 290 | 
             
                        quoted_sequence = quote_table_name(sequence)
         | 
| 289 291 | 
             
                        max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
         | 
| 290 292 | 
             
                        if max_pk.nil?
         | 
| 291 | 
            -
                          if database_version >=  | 
| 293 | 
            +
                          if database_version >= 10_00_00
         | 
| 292 294 | 
             
                            minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
         | 
| 293 295 | 
             
                          else
         | 
| 294 296 | 
             
                            minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
         | 
| 295 297 | 
             
                          end
         | 
| 296 298 | 
             
                        end
         | 
| 297 299 |  | 
| 298 | 
            -
                        query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk  | 
| 300 | 
            +
                        query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
         | 
| 299 301 | 
             
                      end
         | 
| 300 302 | 
             
                    end
         | 
| 301 303 |  | 
| @@ -339,7 +341,7 @@ module ActiveRecord | |
| 339 341 | 
             
                          JOIN pg_namespace   nsp  ON (t.relnamespace = nsp.oid)
         | 
| 340 342 | 
             
                          WHERE t.oid = #{quote(quote_table_name(table))}::regclass
         | 
| 341 343 | 
             
                            AND cons.contype = 'p'
         | 
| 342 | 
            -
                            AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
         | 
| 344 | 
            +
                            AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate|gen_random_uuid'
         | 
| 343 345 | 
             
                        SQL
         | 
| 344 346 | 
             
                      end
         | 
| 345 347 |  | 
| @@ -375,18 +377,26 @@ module ActiveRecord | |
| 375 377 | 
             
                    #
         | 
| 376 378 | 
             
                    # Example:
         | 
| 377 379 | 
             
                    #   rename_table('octopuses', 'octopi')
         | 
| 378 | 
            -
                    def rename_table(table_name, new_name)
         | 
| 380 | 
            +
                    def rename_table(table_name, new_name, **options)
         | 
| 381 | 
            +
                      validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
         | 
| 379 382 | 
             
                      clear_cache!
         | 
| 380 383 | 
             
                      schema_cache.clear_data_source_cache!(table_name.to_s)
         | 
| 381 384 | 
             
                      schema_cache.clear_data_source_cache!(new_name.to_s)
         | 
| 382 385 | 
             
                      execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
         | 
| 383 386 | 
             
                      pk, seq = pk_and_sequence_for(new_name)
         | 
| 384 387 | 
             
                      if pk
         | 
| 385 | 
            -
                         | 
| 386 | 
            -
                         | 
| 388 | 
            +
                        # PostgreSQL automatically creates an index for PRIMARY KEY with name consisting of
         | 
| 389 | 
            +
                        # truncated table name and "_pkey" suffix fitting into max_identifier_length number of characters.
         | 
| 390 | 
            +
                        max_pkey_prefix = max_identifier_length - "_pkey".size
         | 
| 391 | 
            +
                        idx = "#{table_name[0, max_pkey_prefix]}_pkey"
         | 
| 392 | 
            +
                        new_idx = "#{new_name[0, max_pkey_prefix]}_pkey"
         | 
| 387 393 | 
             
                        execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
         | 
| 388 | 
            -
             | 
| 389 | 
            -
             | 
| 394 | 
            +
             | 
| 395 | 
            +
                        # PostgreSQL automatically creates a sequence for PRIMARY KEY with name consisting of
         | 
| 396 | 
            +
                        # truncated table name and "#{primary_key}_seq" suffix fitting into max_identifier_length number of characters.
         | 
| 397 | 
            +
                        max_seq_prefix = max_identifier_length - "_#{pk}_seq".size
         | 
| 398 | 
            +
                        if seq && seq.identifier == "#{table_name[0, max_seq_prefix]}_#{pk}_seq"
         | 
| 399 | 
            +
                          new_seq = "#{new_name[0, max_seq_prefix]}_#{pk}_seq"
         | 
| 390 400 | 
             
                          execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
         | 
| 391 401 | 
             
                        end
         | 
| 392 402 | 
             
                      end
         | 
| @@ -406,18 +416,39 @@ module ActiveRecord | |
| 406 416 | 
             
                      procs.each(&:call)
         | 
| 407 417 | 
             
                    end
         | 
| 408 418 |  | 
| 419 | 
            +
                    # Builds a ChangeColumnDefinition object.
         | 
| 420 | 
            +
                    #
         | 
| 421 | 
            +
                    # This definition object contains information about the column change that would occur
         | 
| 422 | 
            +
                    # if the same arguments were passed to #change_column. See #change_column for information about
         | 
| 423 | 
            +
                    # passing a +table_name+, +column_name+, +type+ and other options that can be passed.
         | 
| 424 | 
            +
                    def build_change_column_definition(table_name, column_name, type, **options) # :nodoc:
         | 
| 425 | 
            +
                      td = create_table_definition(table_name)
         | 
| 426 | 
            +
                      cd = td.new_column_definition(column_name, type, **options)
         | 
| 427 | 
            +
                      ChangeColumnDefinition.new(cd, column_name)
         | 
| 428 | 
            +
                    end
         | 
| 429 | 
            +
             | 
| 409 430 | 
             
                    # Changes the default value of a table column.
         | 
| 410 431 | 
             
                    def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
         | 
| 411 432 | 
             
                      execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
         | 
| 412 433 | 
             
                    end
         | 
| 413 434 |  | 
| 435 | 
            +
                    def build_change_column_default_definition(table_name, column_name, default_or_changes) # :nodoc:
         | 
| 436 | 
            +
                      column = column_for(table_name, column_name)
         | 
| 437 | 
            +
                      return unless column
         | 
| 438 | 
            +
             | 
| 439 | 
            +
                      default = extract_new_default_value(default_or_changes)
         | 
| 440 | 
            +
                      ChangeColumnDefaultDefinition.new(column, default)
         | 
| 441 | 
            +
                    end
         | 
| 442 | 
            +
             | 
| 414 443 | 
             
                    def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
         | 
| 444 | 
            +
                      validate_change_column_null_argument!(null)
         | 
| 445 | 
            +
             | 
| 415 446 | 
             
                      clear_cache!
         | 
| 416 447 | 
             
                      unless null || default.nil?
         | 
| 417 448 | 
             
                        column = column_for(table_name, column_name)
         | 
| 418 449 | 
             
                        execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column
         | 
| 419 450 | 
             
                      end
         | 
| 420 | 
            -
                      execute "ALTER TABLE #{quote_table_name(table_name)} #{ | 
| 451 | 
            +
                      execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
         | 
| 421 452 | 
             
                    end
         | 
| 422 453 |  | 
| 423 454 | 
             
                    # Adds comment for given table column or drops it if +comment+ is a +nil+
         | 
| @@ -442,15 +473,19 @@ module ActiveRecord | |
| 442 473 | 
             
                    end
         | 
| 443 474 |  | 
| 444 475 | 
             
                    def add_index(table_name, column_name, **options) # :nodoc:
         | 
| 445 | 
            -
                       | 
| 446 | 
            -
             | 
| 447 | 
            -
                      create_index = CreateIndexDefinition.new(index, algorithm, if_not_exists)
         | 
| 476 | 
            +
                      create_index = build_create_index_definition(table_name, column_name, **options)
         | 
| 448 477 | 
             
                      result = execute schema_creation.accept(create_index)
         | 
| 449 478 |  | 
| 479 | 
            +
                      index = create_index.index
         | 
| 450 480 | 
             
                      execute "COMMENT ON INDEX #{quote_column_name(index.name)} IS #{quote(index.comment)}" if index.comment
         | 
| 451 481 | 
             
                      result
         | 
| 452 482 | 
             
                    end
         | 
| 453 483 |  | 
| 484 | 
            +
                    def build_create_index_definition(table_name, column_name, **options) # :nodoc:
         | 
| 485 | 
            +
                      index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
         | 
| 486 | 
            +
                      CreateIndexDefinition.new(index, algorithm, if_not_exists)
         | 
| 487 | 
            +
                    end
         | 
| 488 | 
            +
             | 
| 454 489 | 
             
                    def remove_index(table_name, column_name = nil, **options) # :nodoc:
         | 
| 455 490 | 
             
                      table = Utils.extract_schema_qualified_name(table_name.to_s)
         | 
| 456 491 |  | 
| @@ -477,13 +512,33 @@ module ActiveRecord | |
| 477 512 | 
             
                    def rename_index(table_name, old_name, new_name)
         | 
| 478 513 | 
             
                      validate_index_length!(table_name, new_name)
         | 
| 479 514 |  | 
| 480 | 
            -
                       | 
| 515 | 
            +
                      schema, = extract_schema_qualified_name(table_name)
         | 
| 516 | 
            +
                      execute "ALTER INDEX #{quote_table_name(schema) + '.' if schema}#{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
         | 
| 517 | 
            +
                    end
         | 
| 518 | 
            +
             | 
| 519 | 
            +
                    def index_name(table_name, options) # :nodoc:
         | 
| 520 | 
            +
                      _schema, table_name = extract_schema_qualified_name(table_name.to_s)
         | 
| 521 | 
            +
                      super
         | 
| 522 | 
            +
                    end
         | 
| 523 | 
            +
             | 
| 524 | 
            +
                    def add_foreign_key(from_table, to_table, **options)
         | 
| 525 | 
            +
                      if options[:deferrable] == true
         | 
| 526 | 
            +
                        ActiveRecord.deprecator.warn(<<~MSG)
         | 
| 527 | 
            +
                          `deferrable: true` is deprecated in favor of `deferrable: :immediate`, and will be removed in Rails 7.2.
         | 
| 528 | 
            +
                        MSG
         | 
| 529 | 
            +
             | 
| 530 | 
            +
                        options[:deferrable] = :immediate
         | 
| 531 | 
            +
                      end
         | 
| 532 | 
            +
             | 
| 533 | 
            +
                      assert_valid_deferrable(options[:deferrable])
         | 
| 534 | 
            +
             | 
| 535 | 
            +
                      super
         | 
| 481 536 | 
             
                    end
         | 
| 482 537 |  | 
| 483 538 | 
             
                    def foreign_keys(table_name)
         | 
| 484 539 | 
             
                      scope = quoted_scope(table_name)
         | 
| 485 | 
            -
                      fk_info =  | 
| 486 | 
            -
                        SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred
         | 
| 540 | 
            +
                      fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
         | 
| 541 | 
            +
                        SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid
         | 
| 487 542 | 
             
                        FROM pg_constraint c
         | 
| 488 543 | 
             
                        JOIN pg_class t1 ON c.conrelid = t1.oid
         | 
| 489 544 | 
             
                        JOIN pg_class t2 ON c.confrelid = t2.oid
         | 
| @@ -497,19 +552,31 @@ module ActiveRecord | |
| 497 552 | 
             
                      SQL
         | 
| 498 553 |  | 
| 499 554 | 
             
                      fk_info.map do |row|
         | 
| 555 | 
            +
                        to_table = Utils.unquote_identifier(row["to_table"])
         | 
| 556 | 
            +
                        conkey = row["conkey"].scan(/\d+/).map(&:to_i)
         | 
| 557 | 
            +
                        confkey = row["confkey"].scan(/\d+/).map(&:to_i)
         | 
| 558 | 
            +
             | 
| 559 | 
            +
                        if conkey.size > 1
         | 
| 560 | 
            +
                          column = column_names_from_column_numbers(row["conrelid"], conkey)
         | 
| 561 | 
            +
                          primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
         | 
| 562 | 
            +
                        else
         | 
| 563 | 
            +
                          column = Utils.unquote_identifier(row["column"])
         | 
| 564 | 
            +
                          primary_key = row["primary_key"]
         | 
| 565 | 
            +
                        end
         | 
| 566 | 
            +
             | 
| 500 567 | 
             
                        options = {
         | 
| 501 | 
            -
                          column:  | 
| 568 | 
            +
                          column: column,
         | 
| 502 569 | 
             
                          name: row["name"],
         | 
| 503 | 
            -
                          primary_key:  | 
| 570 | 
            +
                          primary_key: primary_key
         | 
| 504 571 | 
             
                        }
         | 
| 505 572 |  | 
| 506 573 | 
             
                        options[:on_delete] = extract_foreign_key_action(row["on_delete"])
         | 
| 507 574 | 
             
                        options[:on_update] = extract_foreign_key_action(row["on_update"])
         | 
| 508 | 
            -
                        options[:deferrable] =  | 
| 575 | 
            +
                        options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
         | 
| 509 576 |  | 
| 510 577 | 
             
                        options[:validate] = row["valid"]
         | 
| 511 578 |  | 
| 512 | 
            -
                        ForeignKeyDefinition.new(table_name,  | 
| 579 | 
            +
                        ForeignKeyDefinition.new(table_name, to_table, options)
         | 
| 513 580 | 
             
                      end
         | 
| 514 581 | 
             
                    end
         | 
| 515 582 |  | 
| @@ -524,7 +591,7 @@ module ActiveRecord | |
| 524 591 | 
             
                    def check_constraints(table_name) # :nodoc:
         | 
| 525 592 | 
             
                      scope = quoted_scope(table_name)
         | 
| 526 593 |  | 
| 527 | 
            -
                      check_info =  | 
| 594 | 
            +
                      check_info = internal_exec_query(<<-SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
         | 
| 528 595 | 
             
                        SELECT conname, pg_get_constraintdef(c.oid, true) AS constraintdef, c.convalidated AS valid
         | 
| 529 596 | 
             
                        FROM pg_constraint c
         | 
| 530 597 | 
             
                        JOIN pg_class t ON c.conrelid = t.oid
         | 
| @@ -545,6 +612,171 @@ module ActiveRecord | |
| 545 612 | 
             
                      end
         | 
| 546 613 | 
             
                    end
         | 
| 547 614 |  | 
| 615 | 
            +
                    # Returns an array of exclusion constraints for the given table.
         | 
| 616 | 
            +
                    # The exclusion constraints are represented as ExclusionConstraintDefinition objects.
         | 
| 617 | 
            +
                    def exclusion_constraints(table_name)
         | 
| 618 | 
            +
                      scope = quoted_scope(table_name)
         | 
| 619 | 
            +
             | 
| 620 | 
            +
                      exclusion_info = internal_exec_query(<<-SQL, "SCHEMA")
         | 
| 621 | 
            +
                        SELECT conname, pg_get_constraintdef(c.oid) AS constraintdef, c.condeferrable, c.condeferred
         | 
| 622 | 
            +
                        FROM pg_constraint c
         | 
| 623 | 
            +
                        JOIN pg_class t ON c.conrelid = t.oid
         | 
| 624 | 
            +
                        JOIN pg_namespace n ON n.oid = c.connamespace
         | 
| 625 | 
            +
                        WHERE c.contype = 'x'
         | 
| 626 | 
            +
                          AND t.relname = #{scope[:name]}
         | 
| 627 | 
            +
                          AND n.nspname = #{scope[:schema]}
         | 
| 628 | 
            +
                      SQL
         | 
| 629 | 
            +
             | 
| 630 | 
            +
                      exclusion_info.map do |row|
         | 
| 631 | 
            +
                        method_and_elements, predicate = row["constraintdef"].split(" WHERE ")
         | 
| 632 | 
            +
                        method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/)
         | 
| 633 | 
            +
                        predicate.remove!(/ DEFERRABLE(?: INITIALLY (?:IMMEDIATE|DEFERRED))?/) if predicate
         | 
| 634 | 
            +
                        predicate = predicate.from(2).to(-3) if predicate # strip 2 opening and closing parentheses
         | 
| 635 | 
            +
             | 
| 636 | 
            +
                        deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
         | 
| 637 | 
            +
             | 
| 638 | 
            +
                        options = {
         | 
| 639 | 
            +
                          name: row["conname"],
         | 
| 640 | 
            +
                          using: method_and_elements_parts["using"].to_sym,
         | 
| 641 | 
            +
                          where: predicate,
         | 
| 642 | 
            +
                          deferrable: deferrable
         | 
| 643 | 
            +
                        }
         | 
| 644 | 
            +
             | 
| 645 | 
            +
                        ExclusionConstraintDefinition.new(table_name, method_and_elements_parts["expression"], options)
         | 
| 646 | 
            +
                      end
         | 
| 647 | 
            +
                    end
         | 
| 648 | 
            +
             | 
| 649 | 
            +
                    # Returns an array of unique constraints for the given table.
         | 
| 650 | 
            +
                    # The unique constraints are represented as UniqueConstraintDefinition objects.
         | 
| 651 | 
            +
                    def unique_constraints(table_name)
         | 
| 652 | 
            +
                      scope = quoted_scope(table_name)
         | 
| 653 | 
            +
             | 
| 654 | 
            +
                      unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
         | 
| 655 | 
            +
                        SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred
         | 
| 656 | 
            +
                        FROM pg_constraint c
         | 
| 657 | 
            +
                        JOIN pg_class t ON c.conrelid = t.oid
         | 
| 658 | 
            +
                        JOIN pg_namespace n ON n.oid = c.connamespace
         | 
| 659 | 
            +
                        WHERE c.contype = 'u'
         | 
| 660 | 
            +
                          AND t.relname = #{scope[:name]}
         | 
| 661 | 
            +
                          AND n.nspname = #{scope[:schema]}
         | 
| 662 | 
            +
                      SQL
         | 
| 663 | 
            +
             | 
| 664 | 
            +
                      unique_info.map do |row|
         | 
| 665 | 
            +
                        conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
         | 
| 666 | 
            +
                        columns = column_names_from_column_numbers(row["conrelid"], conkey)
         | 
| 667 | 
            +
             | 
| 668 | 
            +
                        deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
         | 
| 669 | 
            +
             | 
| 670 | 
            +
                        options = {
         | 
| 671 | 
            +
                          name: row["conname"],
         | 
| 672 | 
            +
                          deferrable: deferrable
         | 
| 673 | 
            +
                        }
         | 
| 674 | 
            +
             | 
| 675 | 
            +
                        UniqueConstraintDefinition.new(table_name, columns, options)
         | 
| 676 | 
            +
                      end
         | 
| 677 | 
            +
                    end
         | 
| 678 | 
            +
             | 
| 679 | 
            +
                    # Adds a new exclusion constraint to the table. +expression+ is a String
         | 
| 680 | 
            +
                    # representation of a list of exclusion elements and operators.
         | 
| 681 | 
            +
                    #
         | 
| 682 | 
            +
                    #   add_exclusion_constraint :products, "price WITH =, availability_range WITH &&", using: :gist, name: "price_check"
         | 
| 683 | 
            +
                    #
         | 
| 684 | 
            +
                    # generates:
         | 
| 685 | 
            +
                    #
         | 
| 686 | 
            +
                    #   ALTER TABLE "products" ADD CONSTRAINT price_check EXCLUDE USING gist (price WITH =, availability_range WITH &&)
         | 
| 687 | 
            +
                    #
         | 
| 688 | 
            +
                    # The +options+ hash can include the following keys:
         | 
| 689 | 
            +
                    # [<tt>:name</tt>]
         | 
| 690 | 
            +
                    #   The constraint name. Defaults to <tt>excl_rails_<identifier></tt>.
         | 
| 691 | 
            +
                    # [<tt>:deferrable</tt>]
         | 
| 692 | 
            +
                    #   Specify whether or not the exclusion constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
         | 
| 693 | 
            +
                    def add_exclusion_constraint(table_name, expression, **options)
         | 
| 694 | 
            +
                      options = exclusion_constraint_options(table_name, expression, options)
         | 
| 695 | 
            +
                      at = create_alter_table(table_name)
         | 
| 696 | 
            +
                      at.add_exclusion_constraint(expression, options)
         | 
| 697 | 
            +
             | 
| 698 | 
            +
                      execute schema_creation.accept(at)
         | 
| 699 | 
            +
                    end
         | 
| 700 | 
            +
             | 
| 701 | 
            +
                    def exclusion_constraint_options(table_name, expression, options) # :nodoc:
         | 
| 702 | 
            +
                      assert_valid_deferrable(options[:deferrable])
         | 
| 703 | 
            +
             | 
| 704 | 
            +
                      options = options.dup
         | 
| 705 | 
            +
                      options[:name] ||= exclusion_constraint_name(table_name, expression: expression, **options)
         | 
| 706 | 
            +
                      options
         | 
| 707 | 
            +
                    end
         | 
| 708 | 
            +
             | 
| 709 | 
            +
                    # Removes the given exclusion constraint from the table.
         | 
| 710 | 
            +
                    #
         | 
| 711 | 
            +
                    #   remove_exclusion_constraint :products, name: "price_check"
         | 
| 712 | 
            +
                    #
         | 
| 713 | 
            +
                    # The +expression+ parameter will be ignored if present. It can be helpful
         | 
| 714 | 
            +
                    # to provide this in a migration's +change+ method so it can be reverted.
         | 
| 715 | 
            +
                    # In that case, +expression+ will be used by #add_exclusion_constraint.
         | 
| 716 | 
            +
                    def remove_exclusion_constraint(table_name, expression = nil, **options)
         | 
| 717 | 
            +
                      excl_name_to_delete = exclusion_constraint_for!(table_name, expression: expression, **options).name
         | 
| 718 | 
            +
             | 
| 719 | 
            +
                      at = create_alter_table(table_name)
         | 
| 720 | 
            +
                      at.drop_exclusion_constraint(excl_name_to_delete)
         | 
| 721 | 
            +
             | 
| 722 | 
            +
                      execute schema_creation.accept(at)
         | 
| 723 | 
            +
                    end
         | 
| 724 | 
            +
             | 
| 725 | 
            +
                    # Adds a new unique constraint to the table.
         | 
| 726 | 
            +
                    #
         | 
| 727 | 
            +
                    #   add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position"
         | 
| 728 | 
            +
                    #
         | 
| 729 | 
            +
                    # generates:
         | 
| 730 | 
            +
                    #
         | 
| 731 | 
            +
                    #   ALTER TABLE "sections" ADD CONSTRAINT unique_position UNIQUE (position) DEFERRABLE INITIALLY DEFERRED
         | 
| 732 | 
            +
                    #
         | 
| 733 | 
            +
                    # If you want to change an existing unique index to deferrable, you can use :using_index to create deferrable unique constraints.
         | 
| 734 | 
            +
                    #
         | 
| 735 | 
            +
                    #   add_unique_constraint :sections, deferrable: :deferred, name: "unique_position", using_index: "index_sections_on_position"
         | 
| 736 | 
            +
                    #
         | 
| 737 | 
            +
                    # The +options+ hash can include the following keys:
         | 
| 738 | 
            +
                    # [<tt>:name</tt>]
         | 
| 739 | 
            +
                    #   The constraint name. Defaults to <tt>uniq_rails_<identifier></tt>.
         | 
| 740 | 
            +
                    # [<tt>:deferrable</tt>]
         | 
| 741 | 
            +
                    #   Specify whether or not the unique constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
         | 
| 742 | 
            +
                    # [<tt>:using_index</tt>]
         | 
| 743 | 
            +
                    #   To specify an existing unique index name. Defaults to +nil+.
         | 
| 744 | 
            +
                    def add_unique_constraint(table_name, column_name = nil, **options)
         | 
| 745 | 
            +
                      options = unique_constraint_options(table_name, column_name, options)
         | 
| 746 | 
            +
                      at = create_alter_table(table_name)
         | 
| 747 | 
            +
                      at.add_unique_constraint(column_name, options)
         | 
| 748 | 
            +
             | 
| 749 | 
            +
                      execute schema_creation.accept(at)
         | 
| 750 | 
            +
                    end
         | 
| 751 | 
            +
             | 
| 752 | 
            +
                    def unique_constraint_options(table_name, column_name, options) # :nodoc:
         | 
| 753 | 
            +
                      assert_valid_deferrable(options[:deferrable])
         | 
| 754 | 
            +
             | 
| 755 | 
            +
                      if column_name && options[:using_index]
         | 
| 756 | 
            +
                        raise ArgumentError, "Cannot specify both column_name and :using_index options."
         | 
| 757 | 
            +
                      end
         | 
| 758 | 
            +
             | 
| 759 | 
            +
                      options = options.dup
         | 
| 760 | 
            +
                      options[:name] ||= unique_constraint_name(table_name, column: column_name, **options)
         | 
| 761 | 
            +
                      options
         | 
| 762 | 
            +
                    end
         | 
| 763 | 
            +
             | 
| 764 | 
            +
                    # Removes the given unique constraint from the table.
         | 
| 765 | 
            +
                    #
         | 
| 766 | 
            +
                    #   remove_unique_constraint :sections, name: "unique_position"
         | 
| 767 | 
            +
                    #
         | 
| 768 | 
            +
                    # The +column_name+ parameter will be ignored if present. It can be helpful
         | 
| 769 | 
            +
                    # to provide this in a migration's +change+ method so it can be reverted.
         | 
| 770 | 
            +
                    # In that case, +column_name+ will be used by #add_unique_constraint.
         | 
| 771 | 
            +
                    def remove_unique_constraint(table_name, column_name = nil, **options)
         | 
| 772 | 
            +
                      unique_name_to_delete = unique_constraint_for!(table_name, column: column_name, **options).name
         | 
| 773 | 
            +
             | 
| 774 | 
            +
                      at = create_alter_table(table_name)
         | 
| 775 | 
            +
                      at.drop_unique_constraint(unique_name_to_delete)
         | 
| 776 | 
            +
             | 
| 777 | 
            +
                      execute schema_creation.accept(at)
         | 
| 778 | 
            +
                    end
         | 
| 779 | 
            +
             | 
| 548 780 | 
             
                    # Maps logical Rails types to PostgreSQL-specific data types.
         | 
| 549 781 | 
             
                    def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, enum_type: nil, **) # :nodoc:
         | 
| 550 782 | 
             
                      sql = \
         | 
| @@ -648,11 +880,32 @@ module ActiveRecord | |
| 648 880 | 
             
                      validate_constraint table_name, chk_name_to_validate
         | 
| 649 881 | 
             
                    end
         | 
| 650 882 |  | 
| 651 | 
            -
                     | 
| 652 | 
            -
                       | 
| 653 | 
            -
             | 
| 883 | 
            +
                    def foreign_key_column_for(table_name, column_name) # :nodoc:
         | 
| 884 | 
            +
                      _schema, table_name = extract_schema_qualified_name(table_name)
         | 
| 885 | 
            +
                      super
         | 
| 886 | 
            +
                    end
         | 
| 887 | 
            +
             | 
| 888 | 
            +
                    def add_index_options(table_name, column_name, **options) # :nodoc:
         | 
| 889 | 
            +
                      if (where = options[:where]) && table_exists?(table_name) && column_exists?(table_name, where)
         | 
| 890 | 
            +
                        options[:where] = quote_column_name(where)
         | 
| 891 | 
            +
                      end
         | 
| 892 | 
            +
                      super
         | 
| 893 | 
            +
                    end
         | 
| 894 | 
            +
             | 
| 895 | 
            +
                    def quoted_include_columns_for_index(column_names) # :nodoc:
         | 
| 896 | 
            +
                      return quote_column_name(column_names) if column_names.is_a?(Symbol)
         | 
| 897 | 
            +
             | 
| 898 | 
            +
                      quoted_columns = column_names.each_with_object({}) do |name, result|
         | 
| 899 | 
            +
                        result[name.to_sym] = quote_column_name(name).dup
         | 
| 654 900 | 
             
                      end
         | 
| 901 | 
            +
                      add_options_for_index_columns(quoted_columns).values.join(", ")
         | 
| 902 | 
            +
                    end
         | 
| 903 | 
            +
             | 
| 904 | 
            +
                    def schema_creation  # :nodoc:
         | 
| 905 | 
            +
                      YugabyteDB::SchemaCreation.new(self)
         | 
| 906 | 
            +
                    end
         | 
| 655 907 |  | 
| 908 | 
            +
                    private
         | 
| 656 909 | 
             
                      def create_table_definition(name, **options)
         | 
| 657 910 | 
             
                        YugabyteDB::TableDefinition.new(self, name, **options)
         | 
| 658 911 | 
             
                      end
         | 
| @@ -661,8 +914,8 @@ module ActiveRecord | |
| 661 914 | 
             
                        YugabyteDB::AlterTable.new create_table_definition(name)
         | 
| 662 915 | 
             
                      end
         | 
| 663 916 |  | 
| 664 | 
            -
                      def new_column_from_field(table_name, field)
         | 
| 665 | 
            -
                        column_name, type, default, notnull, oid, fmod, collation, comment, attgenerated = field
         | 
| 917 | 
            +
                      def new_column_from_field(table_name, field, _definitions)
         | 
| 918 | 
            +
                        column_name, type, default, notnull, oid, fmod, collation, comment, identity, attgenerated = field
         | 
| 666 919 | 
             
                        type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
         | 
| 667 920 | 
             
                        default_value = extract_value_from_default(default)
         | 
| 668 921 |  | 
| @@ -685,6 +938,7 @@ module ActiveRecord | |
| 685 938 | 
             
                          collation: collation,
         | 
| 686 939 | 
             
                          comment: comment.presence,
         | 
| 687 940 | 
             
                          serial: serial,
         | 
| 941 | 
            +
                          identity: identity.presence,
         | 
| 688 942 | 
             
                          generated: attgenerated
         | 
| 689 943 | 
             
                        )
         | 
| 690 944 | 
             
                      end
         | 
| @@ -725,8 +979,19 @@ module ActiveRecord | |
| 725 979 | 
             
                        end
         | 
| 726 980 | 
             
                      end
         | 
| 727 981 |  | 
| 728 | 
            -
                      def  | 
| 729 | 
            -
                        deferrable  | 
| 982 | 
            +
                      def assert_valid_deferrable(deferrable)
         | 
| 983 | 
            +
                        return if !deferrable || %i(immediate deferred).include?(deferrable)
         | 
| 984 | 
            +
             | 
| 985 | 
            +
                        raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
         | 
| 986 | 
            +
                      end
         | 
| 987 | 
            +
             | 
| 988 | 
            +
                      def extract_constraint_deferrable(deferrable, deferred)
         | 
| 989 | 
            +
                        deferrable && (deferred ? :deferred : :immediate)
         | 
| 990 | 
            +
                      end
         | 
| 991 | 
            +
             | 
| 992 | 
            +
                      def reference_name_for_table(table_name)
         | 
| 993 | 
            +
                        _schema, table_name = extract_schema_qualified_name(table_name.to_s)
         | 
| 994 | 
            +
                        table_name.singularize
         | 
| 730 995 | 
             
                      end
         | 
| 731 996 |  | 
| 732 997 | 
             
                      def add_column_for_alter(table_name, column_name, type, **options)
         | 
| @@ -735,32 +1000,20 @@ module ActiveRecord | |
| 735 1000 | 
             
                      end
         | 
| 736 1001 |  | 
| 737 1002 | 
             
                      def change_column_for_alter(table_name, column_name, type, **options)
         | 
| 738 | 
            -
                         | 
| 739 | 
            -
                         | 
| 740 | 
            -
                        sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))]
         | 
| 1003 | 
            +
                        change_col_def = build_change_column_definition(table_name, column_name, type, **options)
         | 
| 1004 | 
            +
                        sqls = [schema_creation.accept(change_col_def)]
         | 
| 741 1005 | 
             
                        sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
         | 
| 742 1006 | 
             
                        sqls
         | 
| 743 1007 | 
             
                      end
         | 
| 744 1008 |  | 
| 745 | 
            -
                      def  | 
| 746 | 
            -
                        column = column_for(table_name, column_name)
         | 
| 747 | 
            -
                        return unless column
         | 
| 748 | 
            -
             | 
| 749 | 
            -
                        default = extract_new_default_value(default_or_changes)
         | 
| 750 | 
            -
                        alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s"
         | 
| 1009 | 
            +
                      def change_column_null_for_alter(table_name, column_name, null, default = nil)
         | 
| 751 1010 | 
             
                        if default.nil?
         | 
| 752 | 
            -
                           | 
| 753 | 
            -
                          # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
         | 
| 754 | 
            -
                          alter_column_query % "DROP DEFAULT"
         | 
| 1011 | 
            +
                          "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
         | 
| 755 1012 | 
             
                        else
         | 
| 756 | 
            -
                           | 
| 1013 | 
            +
                          Proc.new { change_column_null(table_name, column_name, null, default) }
         | 
| 757 1014 | 
             
                        end
         | 
| 758 1015 | 
             
                      end
         | 
| 759 1016 |  | 
| 760 | 
            -
                      def change_column_null_for_alter(table_name, column_name, null, default = nil)
         | 
| 761 | 
            -
                        "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
         | 
| 762 | 
            -
                      end
         | 
| 763 | 
            -
             | 
| 764 1017 | 
             
                      def add_index_opclass(quoted_columns, **options)
         | 
| 765 1018 | 
             
                        opclasses = options_for_index_columns(options[:opclass])
         | 
| 766 1019 | 
             
                        quoted_columns.each do |name, column|
         | 
| @@ -773,6 +1026,46 @@ module ActiveRecord | |
| 773 1026 | 
             
                        super
         | 
| 774 1027 | 
             
                      end
         | 
| 775 1028 |  | 
| 1029 | 
            +
                      def exclusion_constraint_name(table_name, **options)
         | 
| 1030 | 
            +
                        options.fetch(:name) do
         | 
| 1031 | 
            +
                          expression = options.fetch(:expression)
         | 
| 1032 | 
            +
                          identifier = "#{table_name}_#{expression}_excl"
         | 
| 1033 | 
            +
                          hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
         | 
| 1034 | 
            +
             | 
| 1035 | 
            +
                          "excl_rails_#{hashed_identifier}"
         | 
| 1036 | 
            +
                        end
         | 
| 1037 | 
            +
                      end
         | 
| 1038 | 
            +
             | 
| 1039 | 
            +
                      def exclusion_constraint_for(table_name, **options)
         | 
| 1040 | 
            +
                        excl_name = exclusion_constraint_name(table_name, **options)
         | 
| 1041 | 
            +
                        exclusion_constraints(table_name).detect { |excl| excl.name == excl_name }
         | 
| 1042 | 
            +
                      end
         | 
| 1043 | 
            +
             | 
| 1044 | 
            +
                      def exclusion_constraint_for!(table_name, expression: nil, **options)
         | 
| 1045 | 
            +
                        exclusion_constraint_for(table_name, expression: expression, **options) ||
         | 
| 1046 | 
            +
                          raise(ArgumentError, "Table '#{table_name}' has no exclusion constraint for #{expression || options}")
         | 
| 1047 | 
            +
                      end
         | 
| 1048 | 
            +
             | 
| 1049 | 
            +
                      def unique_constraint_name(table_name, **options)
         | 
| 1050 | 
            +
                        options.fetch(:name) do
         | 
| 1051 | 
            +
                          column_or_index = Array(options[:column] || options[:using_index]).map(&:to_s)
         | 
| 1052 | 
            +
                          identifier = "#{table_name}_#{column_or_index * '_and_'}_unique"
         | 
| 1053 | 
            +
                          hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
         | 
| 1054 | 
            +
             | 
| 1055 | 
            +
                          "uniq_rails_#{hashed_identifier}"
         | 
| 1056 | 
            +
                        end
         | 
| 1057 | 
            +
                      end
         | 
| 1058 | 
            +
             | 
| 1059 | 
            +
                      def unique_constraint_for(table_name, **options)
         | 
| 1060 | 
            +
                        name = unique_constraint_name(table_name, **options) unless options.key?(:column)
         | 
| 1061 | 
            +
                        unique_constraints(table_name).detect { |unique_constraint| unique_constraint.defined_for?(name: name, **options) }
         | 
| 1062 | 
            +
                      end
         | 
| 1063 | 
            +
             | 
| 1064 | 
            +
                      def unique_constraint_for!(table_name, column: nil, **options)
         | 
| 1065 | 
            +
                        unique_constraint_for(table_name, column: column, **options) ||
         | 
| 1066 | 
            +
                          raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column || options}")
         | 
| 1067 | 
            +
                      end
         | 
| 1068 | 
            +
             | 
| 776 1069 | 
             
                      def data_source_sql(name = nil, type: nil)
         | 
| 777 1070 | 
             
                        scope = quoted_scope(name, type: type)
         | 
| 778 1071 | 
             
                        scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
         | 
| @@ -806,6 +1099,15 @@ module ActiveRecord | |
| 806 1099 | 
             
                        name = Utils.extract_schema_qualified_name(string.to_s)
         | 
| 807 1100 | 
             
                        [name.schema, name.identifier]
         | 
| 808 1101 | 
             
                      end
         | 
| 1102 | 
            +
             | 
| 1103 | 
            +
                      def column_names_from_column_numbers(table_oid, column_numbers)
         | 
| 1104 | 
            +
                        Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
         | 
| 1105 | 
            +
                          SELECT a.attnum, a.attname
         | 
| 1106 | 
            +
                          FROM pg_attribute a
         | 
| 1107 | 
            +
                          WHERE a.attrelid = #{table_oid}
         | 
| 1108 | 
            +
                          AND a.attnum IN (#{column_numbers.join(", ")})
         | 
| 1109 | 
            +
                        SQL
         | 
| 1110 | 
            +
                      end
         | 
| 809 1111 | 
             
                  end
         | 
| 810 1112 | 
             
                end
         | 
| 811 1113 | 
             
              end
         |