#!/usr/bin/perl

use strict;
use warnings;

=head1 DESCRIPTION

direct-pt-osc -- скрипт для генерации команд pt-online-schema-change в Директ-подобно устроенных БД

Знает про Директовые шардированные БД, берет актуальных мастеров и порты из db-config.

Можно использовать как подсказку и запустить все вручную, 
а можно передать bash'у, можно велеть запустить все в tmux (--tmux-exec).
      
=head1 OPTIONS

    --query
        исходный запрос (альтер)
        если указан - (дефис) -- читает запрос из stdin
        если не указан ключ --query -- запросом считается первый параметр скрипта

    --db <db>
        название базы данных

    -u, --user <username>
        пользователь для коннекта к mysql
        по умолчанию берется из db-config.json

    -p, --pass <password>
        пароль для коннекта к mysql
        по умолчанию берется из db-config.json
        
    --execute
        запустить сгенерированные команды, иначе команды просто выведутся
        
    --recursion-method
        способ мониторинга реплик
        hosts (по умолчанию), processlist или none

    --osc-dry-run 
        если указан, будет сгенерирована команда pt-osc --dry-run, иначе (по умолчанию) --execute

    --critical-load <param>
        как есть подставляется в --critical-load, подробнее см. man pt-online-schema-change
        По умолчанию Threads_running=1000

    --max-load <param>
        как есть подставляется в --max-load, подробнее см man pt-online-schema-change
        По умолчанию Threads_running=100

    --alter-foreign-keys-method <rebuild_constraints|drop_swap|none|auto>
        как обрабатывать ссылки foreign key на альтеримую таблицу
        как есть передается в pt-osc, подробнее см. man pt-online-schema-change
        по-умолчанию - rebuild_constraints + --set-vars foreign_key_checks=0

    --max-lag <seconds>
        как есть подставляется в --max-lag, подробнее см. man pt-online-schema-change
        по умолчанию 240

    --check-interval <seconds>
        как есть подставляется в --check-interval, подробнее см. pt-online-schema-change
        по умолчанию 1

    --no-auto-finish 
        не переименовывать таблицы после копирования данных
        Полезно для очень долгих альтеров, чтобы не терять многочасовое копирование из-за долгих селектов в таблице.
        Если выполняется такой запрос, после окончания копирования надо: 
        1. проверить, что не висят долгие запросы к таблице (если висят -- дождаться окончания или прибить, если можно ими пожертвовать)
        2. rename table orig to old, new to orig
        3. drop triggers
        4. drop table old
        
    --no-check-alter
       см. man pt-online-schema-change
        

=head1 EXAMPLES

  Лучше запускать из под dbs!!
  
  Самое простое (на примере dev7:ppc:12):
  direct-pt-osc -h ppctest-mysql04i.yandex.ru --port 3352 -u adiuser -p utro --db ppc 'ALTER TABLE clients_options ADD COLUMN non_resident BOOL DEFAULT 0;' --execute

  Сгенерировать dry-run'ы:
  direct-pt-osc -h ppctest-mysql04i.yandex.ru --port 3352 -u adiuser -p utro --db ppc 'ALTER TABLE camp_options ADD COLUMN `is_related_keywords_enabled` tinyint(1) unsigned NOT NULL DEFAULT "0"' --osc-dry-run
  
   С различными параметрами:
   direct-pt-osc -h ppctest-mysql04i.yandex.ru --port 3352 -u adiuser -p utro --db ppc 'ALTER TABLE clients_options ADD COLUMN non_resident BOOL DEFAULT 0;' --recursion-method none --no-check-alter

=head1 TODO

  * --tmux-clean -- удалить файлы со скриптами и логами 

=cut

use Data::Dumper;
use Getopt::Long;
use POSIX qw(strftime);
use YAML;

use Yandex::DBTools;
use Yandex::DBShards;


run() unless caller();

sub run
{
    my %O = %{parse_options()};
    my $query = $O{query};

    $query =~ s/'/"/g;
    $query =~ s/\s+/ /g;
    $query =~ s/[;\s]+$//;
    unless ($query =~ /(?:alter) *table *([^ ]+) *(.*)$/si){
        die "can't parse query";
    }
    my ($table, $short_query) = ($1, $2);

    for ($table) {
        s/^`//;
        s/`$//;
    }

    my $database = $O{db};
    my $host = $O{host};
    my $port = $O{port};
    my $user = $O{mysql_user};
    my $pass = $O{mysql_password};

    for my $s ( $database, $host, $port, $user ){
        die "unexpected symbols in parameter '$s', stop" unless $s =~ /^[a-z0-9\-\.]+$/i;
    }
    
    my $base_osc_query = "pt-online-schema-change t=$table,h=$host,P=$port,u=$user,p=$pass,D=$database --alter '$short_query'";
    my $no_swap_table_params = $O{no_auto_finish} ? "--no-swap-tables --no-drop-new-table --no-drop-triggers" : "";
    my $load_treshold_params = "--critical-load $O{critical_load} --max-load $O{max_load}";
    my $foreign_key_params = $O{alter_foreign_keys_method} 
            ? "--alter-foreign-keys-method $O{alter_foreign_keys_method}"
            : "--alter-foreign-keys-method rebuild_constraints --set-vars foreign_key_checks=0";
    my $check_alter_params = $O{no_check_alter} ? "--no-check-alter" : "";
    my $check_replica_params = "--recurse 1 --recursion-method $O{recursion_method} --nocheck-replication-filters --max-lag $O{max_lag} --check-interval $O{check_interval}";

     my $cmd;
     if ($O{osc_dry_run}) {
         $cmd = "date; $base_osc_query $load_treshold_params $foreign_key_params $no_swap_table_params $check_alter_params $check_replica_params --dry-run; echo \$?; date\n";
     } else {
         $cmd = "date; $base_osc_query $load_treshold_params $foreign_key_params $no_swap_table_params $check_alter_params $check_replica_params --execute; echo \$?; date\n"; 
     }

     my $cmd_for_log = $cmd;
     $cmd_for_log =~ s/p=($pass)/p=***/ig;
    
    if (!$O{execute}) {
        my $no_swap_message = $O{no_auto_finish} ? "\n# ИСПОЛЬЗУЕТСЯ НЕЗАКОНЧЕННЫЙ ЗАПРОС (--no-swap-tables --no-drop-new-table --no-drop-triggers)\n# после окончания копирования данных надо вручную переимновать таблицы и дропнуть триггеры, смотри справку, зови на помощь\n" : "";
        print qq!
# Команды для выполнения альтера через pt-online-schema-change
# Если альтер нельзя выполнять параллельно на разных шардах, надо явно указывать шард, в котором выполнить альтер: --db ppc:2
#
# Если не знаешь, что такое pt-online-schema-change и применим ли он в конкретном случае -- сначала изучи или спроси кого-нибудь. 
# Никаких гарантий, что данные Директа не пострадают, не дается.

$no_swap_message
set -ex

!;
        print "$cmd_for_log\n";
    } else {
        print "$cmd_for_log\n";

        #проверяем текущий guard, если только пользователь на свой страх и риск не отказался от этого
        unless ($O{dangerous_ignore_dbs_guard}){
            my $is_guard_active = 1;
            my $no_dbs_guard_message = "# !!! ERROR !!! Не запущен guard! Команды не будут выполнены. host: $host port: $port";
#            system("direct-query-guard -H $host -P $port -u $user -p $pass --db $database --check") == 0 or $is_guard_active = 0;
            # костыль после отрыва super-привилегий для пользователя direct-sql: https://st.yandex-team.ru/DIRECT-153260
            if (-f "/etc/direct-tokens/mysql_direct-test") {
                system("direct-query-guard -H $host -P $port -u direct-test -p \`cat /etc/direct-tokens/mysql_direct-test\` --db $database --check") == 0 or $is_guard_active = 0;
            } elsif (-f "/etc/direct-tokens/mysql_ppc") {
                system("direct-query-guard -H $host -P $port -u ppc -p \`cat /etc/direct-tokens/mysql_ppc\` --db $database --check") == 0 or $is_guard_active = 0;
            } else {
                die "ошибка: не найден пароль от пользователя, с которым запускается приложение, для проверки запуска guard\n    нужен файл /etc/direct-tokens/mysql_direct-test на непродакшене, /etc/direct-tokens/mysql_ppc в продакшене\n"
            }

            if ($is_guard_active == 0) {
                die "$no_dbs_guard_message\n";
            }
        }

        system($cmd);
        print ( $? >> 8 == 0 ? "...success\n":"...fail\n");
    }
    
    exit 0;
}


sub parse_options
{
    my %O = (
        query => '',
        db => '',
        cmd => 'execute',
        max_load => 'Threads_running=100',
        critical_load => 'Threads_running=1000',
        max_lag => 240,
        check_interval => 1,
        recursion_method => 'hosts',
    );

    GetOptions(
        "help" => sub {
            system("podselect -section NAME -section DESCRIPTION -section OPTIONS -section EXAMPLES $0 | pod2text-utf8"); 
            exit 0;
        },
        "query=s" => \$O{query},
        "db=s" => \$O{db},
        "h|host=s" => \$O{host},
        "P|port=s" => \$O{port},
        "u|user=s" => \$O{mysql_user},
        "p|password=s" => \$O{mysql_password},
        "osc-dry-run" => \$O{osc_dry_run},
        "no-auto-finish" => \$O{no_auto_finish},
        "critical-load=s" => \$O{critical_load},
        "max-load=s" => \$O{max_load},
        "alter-foreign-keys-method=s" => \$O{alter_foreign_keys_method},
        "max-lag=i" => \$O{max_lag},
        "check_interval=i" => \$O{check_interval},
        "execute" => \$O{execute},
        "no-check-alter" => \$O{no_check_alter},
        "recursion-method=s" => \$O{recursion_method},
        "dangerous-ignore-dbs-guard" => \$O{dangerous_ignore_dbs_guard},
    ) || die "can't parse options, stop";


    if ( @ARGV > 2 ){
        die "too many arguments: @ARGV, stop";
    } elsif ( @ARGV == 2 ) {
        die "too many arguments: @ARGV, stop";
    } elsif ( @ARGV == 1 ){
        $O{query} = $ARGV[0];
    } else {
    }

    if ($O{query} eq '-'){
        $O{query} = join ' ', <STDIN>;
    }

    die "empty query, stop" unless $O{query};

    return \%O;
}

