#!/usr/bin/env php
<?php

/*
 * Copyright 2012 Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';

phutil_require_module('phutil', 'console');
phutil_require_module('phabricator', 'infrastructure/setup/sql');

define('SCHEMA_VERSION_TABLE_NAME', 'schema_version');

// TODO: getopt() is super terrible, move to something less terrible.
$options = getopt('fhdv:u:p:m:') + array(
  'v' => null, // Upgrade from specific version
  'u' => null, // Override MySQL User
  'p' => null, // Override MySQL Pass
  'm' => null, // Specify max version to upgrade to
);

foreach (array('h', 'f', 'd') as $key) {
  // By default, these keys are set to 'false' to indicate that the flag was
  // passed.
  if (array_key_exists($key, $options)) {
    $options[$key] = true;
  }
}

if (!empty($options['h']) || ($options['v'] && !is_numeric($options['v']))
    || ($options['m'] && !is_numeric($options['m']))) {
  usage();
}

if (empty($options['f']) && empty($options['d'])) {
  echo phutil_console_wrap(
    "Before running this script, you should take down the Phabricator web ".
    "interface and stop any running Phabricator daemons.");

  if (!phutil_console_confirm('Are you ready to continue?')) {
    echo "Cancelled.\n";
    exit(1);
  }
}

// Use always the version from the commandline if it is defined
$next_version = isset($options['v']) ? (int)$options['v'] : null;
$max_version = isset($options['m']) ? (int)$options['m'] : null;

$conf = DatabaseConfigurationProvider::getConfiguration();

if ($options['u']) {
  $conn_user = $options['u'];
  $conn_pass = $options['p'];
} else {
  $conn_user = $conf->getUser();
  $conn_pass = $conf->getPassword();
}
$conn_host = $conf->getHost();

// Split out port information, since the command-line client requires a
// separate flag for the port.
$uri = new PhutilURI('mysql://'.$conn_host);
if ($uri->getPort()) {
  $conn_port = $uri->getPort();
  $conn_bare_hostname = $uri->getDomain();
} else {
  $conn_port = null;
  $conn_bare_hostname = $conn_host;
}

$conn = new AphrontMySQLDatabaseConnection(
  array(
    'user'      => $conn_user,
    'pass'      => $conn_pass,
    'host'      => $conn_host,
    'database'  => null,
  ));

try {

  $create_sql = <<<END
  CREATE DATABASE IF NOT EXISTS `phabricator_meta_data`;
END;
  queryfx($conn, $create_sql);

  $create_sql = <<<END
  CREATE TABLE IF NOT EXISTS phabricator_meta_data.`schema_version` (
    `version` INTEGER not null
  );
END;
  queryfx($conn, $create_sql);

  // Get the version only if commandline argument wasn't given
  if ($next_version === null) {
    $version = queryfx_one(
      $conn,
      'SELECT * FROM phabricator_meta_data.%T',
      SCHEMA_VERSION_TABLE_NAME);

    if (!$version) {
      print "*** No version information in the database ***\n";
      print "*** Give the first patch version which to  ***\n";
      print "*** apply as the command line argument     ***\n";
      exit(-1);
    }

    $next_version = $version['version'] + 1;
  }

  $patches = PhabricatorSQLPatchList::getPatchList();

  $patch_applied = false;
  foreach ($patches as $patch) {
    if ($patch['version'] < $next_version) {
      continue;
    }

    if ($max_version && $patch['version'] > $max_version) {
      continue;
    }

    $short_name = basename($patch['path']);
    print "Applying patch {$short_name}...\n";

    if (!empty($options['d'])) {
      $patch_applied = true;
      continue;
    }

    if ($conn_port) {
      $port = '--port='.(int)$conn_port;
    } else {
      $port = null;
    }

    if (preg_match('/\.php$/', $patch['path'])) {
      $schema_conn = $conn;
      require_once $patch['path'];
    } else {
      list($stdout, $stderr) = execx(
        "mysql --user=%s --password=%s --host=%s {$port} ".
        "--default-character-set=utf8 < %s",
        $conn_user,
        $conn_pass,
        $conn_bare_hostname,
        $patch['path']);

      if ($stderr) {
        print $stderr;
        exit(-1);
      }
    }

    // Patch was successful, update the db with the latest applied patch version
    // 'DELETE' and 'INSERT' instead of update, because the table might be empty
    queryfx(
      $conn,
      'DELETE FROM phabricator_meta_data.%T',
      SCHEMA_VERSION_TABLE_NAME);
    queryfx(
      $conn,
      'INSERT INTO phabricator_meta_data.%T VALUES (%d)',
      SCHEMA_VERSION_TABLE_NAME,
      $patch['version']);

    $patch_applied = true;
  }

  if (!$patch_applied) {
    print "Your database is already up-to-date.\n";
  }

} catch (AphrontQueryAccessDeniedException $ex) {
  echo
    "ACCESS DENIED\n".
    "The user '{$conn_user}' does not have sufficient MySQL privileges to\n".
    "execute the schema upgrade. Use the -u and -p flags to run as a user\n".
    "with more privileges (e.g., root).".
    "\n\n".
    "EXCEPTION:\n".
    $ex->getMessage().
    "\n\n";
  exit(1);
}

function usage() {
  echo
    "usage: upgrade_schema.php [-v version] [-u user -p pass] [-f] [-h]".
    "\n\n".
    "Run 'upgrade_schema.php -u root -p hunter2' to override the configured ".
    "default user.\n".
    "Run 'upgrade_schema.php -v 12' to apply all patches starting from ".
    "version 12. It is very unlikely you need to do this.\n".
    "Run 'upgrade_schema.php -m 110' to apply all patches up to and ".
    "including version 110 (but nothing past).\n".
    "Use the -f flag to upgrade noninteractively, without prompting.\n".
    "Use the -d flag to do a dry run - patches that would be applied ".
    "will be listed, but not applied.\n".
    "Use the -h flag to show this help.\n";
  exit(1);
}