module PgColumnBytePacker::PgDump

Public Class Methods

_sort_table_lines(connection:, qualified_table:, lines:) click to toggle source
# File lib/pg_column_byte_packer/pg_dump.rb, line 52
    def self._sort_table_lines(connection:, qualified_table:, lines:)
      schema, table = qualified_table.match(/([^\.]+)\.([^\.]+)/)[1..-1]

      lines.sort_by do |line|
        line = line.chomp

        # To handle the vagaries of quoted keywords as column names
        # and spaces/special characters in column names, we resort
        # to using PostgreSQL's parsing code itself on a faked
        # CREATE TABLE call with a single column; we could parse
        # the entire CREATE TABLE statement we have in the file,
        # but then we'd have to figure out a way to recreate that
        # query from the parse tree, and therein lies madness.
        line_without_comma = line[-1] == "," ? line[0..-2] : line
        fake_query = "CREATE TABLE t(#{line_without_comma});"
        parsed = begin
           PgQuery.parse(fake_query)
         rescue PgQuery::ParseError
           nil
         end

        # Ignore CONSTRAINT definitions etc.
        if parsed && (column_def = parsed.tree[0]["RawStmt"]["stmt"]["CreateStmt"]["tableElts"][0]["ColumnDef"])
          column = column_def["colname"]

          values = connection.select_rows(<<~SQL, "Column and Type Info").first
            SELECT
              pg_catalog.format_type(attr.atttypid, attr.atttypmod),
              typ_nsp.nspname,
              attr.attnotnull,
              attr.atthasdef,
              EXISTS (
                SELECT 1
                FROM pg_index idx
                WHERE idx.indisprimary
                  AND attr.attnum = ANY(idx.indkey)
                  AND idx.indrelid = attr.attrelid
              )
            FROM pg_catalog.pg_attribute attr
            JOIN pg_catalog.pg_type typ ON typ.oid = attr.atttypid
            JOIN pg_catalog.pg_class cls ON cls.oid = attr.attrelid
            JOIN pg_catalog.pg_namespace attr_nsp ON attr_nsp.oid = cls.relnamespace
            JOIN pg_catalog.pg_namespace typ_nsp ON typ_nsp.oid = typ.typnamespace
            WHERE attr.attname = '#{connection.quote_string(column)}'
              AND attr_nsp.nspname = '#{connection.quote_string(schema)}'
              AND cls.relname = '#{connection.quote_string(table)}'
              AND NOT attr.attisdropped
          SQL
          sql_type, type_schema, not_null, has_default, primary_key = values

          PgColumnBytePacker.ordering_key_for_column(
            connection: connection,
            name: column,
            sql_type: sql_type,
            type_schema: type_schema,
            primary_key: primary_key,
            nullable: !not_null,
            has_default: has_default
          )
        else
          # All non-column lines we want to sort at the end
          # of the table defintion statement.
          [2**32]
        end
      end
    end
sort_columns_for_definition_file(path, connection:) click to toggle source
# File lib/pg_column_byte_packer/pg_dump.rb, line 3
def self.sort_columns_for_definition_file(path, connection:)
  sorted_dump_output = []
  current_block_start = nil
  current_block_body = []
  current_table_name = nil
  block_begin_prefix_pattern = /\ACREATE TABLE ([^\(]+) \(/
  block_end_line_pattern = /\A\);?\n\Z/

  File.foreach(path) do |line|
    if current_block_start.nil? && (create_table_match = block_begin_prefix_pattern.match(line))
      current_block_start = line
      current_block_body = []
      current_table_name = create_table_match[1]
    elsif current_block_start
      if line =~ block_end_line_pattern
        sorted_dump_output << current_block_start
        table_lines = _sort_table_lines(
          connection: connection,
          qualified_table: current_table_name,
          lines: current_block_body
        )
        table_lines = table_lines.map.with_index do |column_line, index|
          has_trailing_comma = column_line =~ /,\s*\Z/
          last_line = index == table_lines.size - 1
          if !last_line && !has_trailing_comma
            column_line.sub(/(.+)(\s*)\Z/, '\1,\2')
          elsif last_line && has_trailing_comma
            column_line.sub(/,(\s*)\Z/, '\1')
          else
            column_line
          end
        end
        sorted_dump_output.concat(table_lines)
        sorted_dump_output << line

        current_block_body = []
        current_block_start = nil
        current_table_name = nil
      else
        current_block_body << line
      end
    else
      sorted_dump_output << line
    end
  end

  File.write(path, sorted_dump_output.join)
end