<commit_before>class Heroku::Command::Pg < Heroku::Command::Base
  # pg:copy source target
  #
  # Copy all data from source database to target. At least one of
  # these must be a Heroku Postgres database.
  def copy
    source_db = shift_argument
    target_db = shift_argument

    validate_arguments!

    source = resolve_db_or_url(source_db)
    target = resolve_db_or_url(target_db)

    if source.url == target.url
      abort("Cannot copy database to itself")
    end

    attachment = target.attachment || source.attachment

    xfer = hpg_client(attachment).pg_copy(source.name, source.url,
                                          target.name, target.url)
    poll_transfer('copy', xfer[:uuid])
  end

  # pg:backups [subcommand]
  #
  # Interact with built-in backups. Without a subcommand, it lists all
  # available backups. The subcommands available are:
  #
  #  info BACKUP_ID                 # get information about a specific backup
  #  capture DATABASE               # capture a new backup
  #  restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL)
  #  public-url BACKUP_ID           # get secret but publicly accessible URL for BACKUP_ID to download it
  #  cancel                         # cancel an in-progress backup
  #  delete BACKUP_ID               # delete an existing backup
  #  schedule DATABASE              # schedule nightly backups for given database
  #    --at '<hour>:00 <timezone>'  #   at a specific (24h clock) hour in the given timezone
  #  unschedule DATABASE            # stop nightly backup for database
  #  schedules                      # list backup schedule
  def backups
    if args.count == 0
      list_backups
    else
      command = shift_argument
      case command
      when 'list' then list_backups
      when 'info' then backup_status
      when 'capture' then capture_backup
      when 'restore' then restore_backup
      when 'public-url' then public_url
      when 'cancel' then cancel_backup
      when 'delete' then delete_backup
      when 'schedule' then schedule_backups
      when 'unschedule' then unschedule_backups
      when 'schedules' then list_schedules
      else abort "Unknown pg:backups command: #{command}"
      end
    end
  end

  private

  MaybeAttachment = Struct.new(:name, :url, :attachment)

  def url_name(uri)
    "Database #{uri.path[1..-1]} on #{uri.host}:#{uri.port || 5432}"
  end

  def resolve_db_or_url(name_or_url, default=nil)
    if name_or_url =~ %r{postgres://}
      url = name_or_url
      uri = URI.parse(url)
      name = url_name(uri)
      MaybeAttachment.new(name, url, nil)
    else
      attachment = generate_resolver.resolve(name_or_url, default)
      name = attachment.config_var.sub(/^HEROKU_POSTGRESQL_/, '').sub(/_URL$/, '')
      MaybeAttachment.new(name, attachment.url, attachment)
    end
  end

  def arbitrary_app_db
    generate_resolver.all_databases.values.first
  end

  def transfer_name(backup_num, prefix='b')
    "#{prefix}#{format("%03d", backup_num)}"
  end

  def backup_num(transfer_name)
    /b(\d+)/.match(transfer_name) && $1
  end

  def transfer_status(t)
    if t[:finished_at]
      "Finished #{t[:finished_at]}"
    elsif t[:started_at]
      "Running (processed #{size_pretty(t[:processed_bytes])})"
    else
      "Pending"
    end
  end

  def size_pretty(bytes)
    suffixes = {
      'B'  => 1,
      'kB' => 1_000,
      'MB' => 1_000_000,
      'GB' => 1_000_000_000,
      'TB' => 1_000_000_000_000 # (ohdear)
    }
    suffix, multiplier = suffixes.find do |k,v|
      normalized = bytes / v.to_f
      normalized >= 0 && normalized < 1_000
    end
    if suffix.nil?
      return bytes
    end
    normalized = bytes / multiplier.to_f
    num_digits = case<commit_msg>Don't display failed backups as Finished; display as Failed<commit_after>class Heroku::Command::Pg < Heroku::Command::Base
  # pg:copy source target
  #
  # Copy all data from source database to target. At least one of
  # these must be a Heroku Postgres database.
  def copy
    source_db = shift_argument
    target_db = shift_argument

    validate_arguments!

    source = resolve_db_or_url(source_db)
    target = resolve_db_or_url(target_db)

    if source.url == target.url
      abort("Cannot copy database to itself")
    end

    attachment = target.attachment || source.attachment

    xfer = hpg_client(attachment).pg_copy(source.name, source.url,
                                          target.name, target.url)
    poll_transfer('copy', xfer[:uuid])
  end

  # pg:backups [subcommand]
  #
  # Interact with built-in backups. Without a subcommand, it lists all
  # available backups. The subcommands available are:
  #
  #  info BACKUP_ID                 # get information about a specific backup
  #  capture DATABASE               # capture a new backup
  #  restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL)
  #  public-url BACKUP_ID           # get secret but publicly accessible URL for BACKUP_ID to download it
  #  cancel                         # cancel an in-progress backup
  #  delete BACKUP_ID               # delete an existing backup
  #  schedule DATABASE              # schedule nightly backups for given database
  #    --at '<hour>:00 <timezone>'  #   at a specific (24h clock) hour in the given timezone
  #  unschedule DATABASE            # stop nightly backup for database
  #  schedules                      # list backup schedule
  def backups
    if args.count == 0
      list_backups
    else
      command = shift_argument
      case command
      when 'list' then list_backups
      when 'info' then backup_status
      when 'capture' then capture_backup
      when 'restore' then restore_backup
      when 'public-url' then public_url
      when 'cancel' then cancel_backup
      when 'delete' then delete_backup
      when 'schedule' then schedule_backups
      when 'unschedule' then unschedule_backups
      when 'schedules' then list_schedules
      else abort "Unknown pg:backups command: #{command}"
      end
    end
  end

  private

  MaybeAttachment = Struct.new(:name, :url, :attachment)

  def url_name(uri)
    "Database #{uri.path[1..-1]} on #{uri.host}:#{uri.port || 5432}"
  end

  def resolve_db_or_url(name_or_url, default=nil)
    if name_or_url =~ %r{postgres://}
      url = name_or_url
      uri = URI.parse(url)
      name = url_name(uri)
      MaybeAttachment.new(name, url, nil)
    else
      attachment = generate_resolver.resolve(name_or_url, default)
      name = attachment.config_var.sub(/^HEROKU_POSTGRESQL_/, '').sub(/_URL$/, '')
      MaybeAttachment.new(name, attachment.url, attachment)
    end
  end

  def arbitrary_app_db
    generate_resolver.all_databases.values.first
  end

  def transfer_name(backup_num, prefix='b')
    "#{prefix}#{format("%03d", backup_num)}"
  end

  def backup_num(transfer_name)
    /b(\d+)/.match(transfer_name) && $1
  end

  def transfer_status(t)
    if t[:finished_at] && t[:succeeded]
      "Finished #{t[:finished_at]}"
    elsif t[:finished_at] && !t[:succeeded]
      "Failed #{t[:finished_at]}"
    elsif t[:started_at]
      "Running (processed #{size_pretty(t[:processed_bytes])})"
    else
      "Pending"
    end
  end

  def size_pretty(bytes)
    suffixes = {
      'B'  => 1,
      'kB' => 1_000,
      'MB' => 1_000_000,
      'GB' => 1_000_000_000,
      'TB' => 1_000_000_000_000 # (ohdear)
    }
    suffix, multiplier = suffixes.find do |k,v|
      normalized = bytes / v.to_f
      normalized >= 0 && normalized < 1_000
    end
    if suffix.nil?
      return bytes
    end
    normalized = bytes / multiplier.to_f
    num_digits = case
{'fraction_non_alphanumeric': 0.09554718940672466, 'fraction_numerical': 0.014020511489030248, 'mean_word_length': 2.6254117647058823, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5427813', 'n_tokens_mistral': 2514, 'n_tokens_neox': 2387, 'n_words': 661}