#!/usr/bin/perl
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
#
# This program will bring your LiveJournal database schema up-to-date
#

 use strict;

BEGIN { $LJ::_T_CONFIG = "$ENV{DW_TEST}"; require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; }
 use Digest::MD5;
 use Getopt::Long;
 use File::Path ();
 use File::Basename qw/ dirname /;
 use File::Copy ();
 use Cwd qw/ abs_path /;
 use Image::Size ();
 use LJ::S2;

my $opt_sql     = 0;
my $opt_drop    = 0;
my $opt_pop     = 0;
my $opt_confirm = "";
my $opt_skip    = "";
my $opt_help    = 0;
my $cluster     = 0;    # by default, upgrade master.
my $opt_listtables;
my $opt_nostyles;
my $opt_forcebuild    = 0;
my $opt_compiletodisk = 0;
my $opt_innodb;
my $opt_poptest  = 0;
my $opt_parallel = 0;

exit 1
    unless GetOptions(
    "runsql"        => \$opt_sql,
    "drop"          => \$opt_drop,
    "populate|p"    => \$opt_pop,
    "confirm=s"     => \$opt_confirm,
    "cluster=s"     => \$cluster,
    "skip=s"        => \$opt_skip,
    "help"          => \$opt_help,
    "listtables"    => \$opt_listtables,
    "nostyles"      => \$opt_nostyles,
    "forcebuild|fb" => \$opt_forcebuild,
    "ctd"           => \$opt_compiletodisk,
    "innodb"        => \$opt_innodb,
    "parallel=i"    => \$opt_parallel,
    );

$opt_nostyles = 1 unless LJ::is_enabled("update_styles");
$opt_nostyles = 1 if $ENV{DW_TEST};
$opt_innodb   = 1;

if ($opt_help) {
    die "Usage: update-db.pl
  -r  --runsql       Actually do the SQL, instead of just showing it.
  -p  --populate     Populate the database with the latest required base data.
  -d  --drop         Drop old unused tables (default is to never)
      --cluster=<n>  Upgrade cluster number <n> (defaut,0 is global cluster)
      --cluster=<n>,<n>,<n>
      --cluster=user Update user clusters
      --cluster=all  Update user clusters, and global
  -l  --listtables   Print used tables, one per line.
      --nostyles     When used in combination with --populate, disables population
                     of style information.
      --innodb       Use InnoDB when creating tables.
      --parallel=<n> Number of parallel processes for S2 compilation (default: 8)
                     Set to 1 to disable parallelization and use original serial behavior.
      --forcebuild   Force recompilation of ALL S2 layers, ignoring MD5 source checks.
                     This will rebuild all system layers from scratch.
";
}

## make sure $LJHOME is set so we can load & run everything
unless ( -d $ENV{'LJHOME'} ) {
    die "LJHOME environment variable is not set, or is not a directory.\n"
        . "You must fix this before you can run this database update script.";
}

die "Can't --populate a cluster" if $opt_pop && ( $cluster && $cluster ne "all" );

my @clusters;
foreach my $cl ( split( /,/, $cluster ) ) {
    die "Invalid cluster spec: $cl\n"
        unless $cl =~ /^\s*((\d+)|all|user)\s*$/;
    if ( $cl eq "all" ) { push @clusters, 0, @LJ::CLUSTERS; }
    elsif ( $cl eq "user" ) { push @clusters, @LJ::CLUSTERS; }
    else                    { push @clusters, $1; }
}
@clusters = (0) unless @clusters;

my $su;                 # system user, not available until populate mode
my %status;             # clusterid -> string
my %clustered_table;    # $table -> 1
my $sth;
my %table_exists;    # $table -> 1
my %table_unknown;   # $table -> 1
my %table_create;    # $table -> $create_sql
my %table_drop;      # $table -> 1
my %table_status;    # $table -> { SHOW TABLE STATUS ... row }
my %post_create;     # $table -> [ [ $action, $what ]* ]
my %coltype;         # $table -> { $col -> $type }
my %coldefault;      # $table -> { $col -> $default }
my %indexname;       # $table -> "INDEX"|"UNIQUE" . ":" . "col1-col2-col3" -> "PRIMARY" | index_name
my @alters;
my $dbh;

CLUSTER: foreach my $cluster (@clusters) {
    print "Updating cluster: $cluster\n" unless $opt_listtables;
    ## make sure we can connect
    $dbh = $cluster ? LJ::get_cluster_master($cluster) : LJ::get_db_writer();
    unless ($dbh) {
        $status{$cluster} =
            "ERROR: Can't connect to the database (clust\#$cluster), so I can't update it. ("
            . DBI->errstr . ")";
        next CLUSTER;
    }

    # reset everything
    %clustered_table  = %table_exists = %table_unknown =
        %table_create = %table_drop   = %post_create = %coltype = %indexname = %table_status = ();
    @alters = ();

    ## figure out what tables already exist (but not details of their structure)
    $sth = $dbh->prepare("SHOW TABLES");
    $sth->execute;
    while ( my ($table) = $sth->fetchrow_array ) {
        next if $table =~ /^(access|errors)\d+$/;
        $table_exists{$table} = 1;
    }
    %table_unknown = %table_exists;    # for now, later we'll delete from table_unknown

    ## very important that local is run first!  (it can define tables that
    ## the site-wide would drop if it didn't know about them already)

    my $load_datfile = sub {
        my $file  = shift;
        my $local = shift;
        return if $local && !-e $file;
        open( F, $file ) or die "Can't find database update file at $file\n";
        my $data;
        {
            local $/ = undef;
            $data = <F>;
        }
        close F;
        eval $data;
        die "Can't run $file: $@\n" if $@;
        return 1;
    };

    foreach my $fn ( LJ::get_all_files("bin/upgrading/update-db-local.pl") ) {
        $load_datfile->( $fn, 1 );
    }
    foreach my $fn ( LJ::get_all_files("bin/upgrading/update-db-general.pl") ) {
        $load_datfile->($fn);
    }

    foreach my $t ( sort keys %table_create ) {
        delete $table_drop{$t} if ( $table_drop{$t} );
        print "$t\n" if $opt_listtables;
    }
    exit if $opt_listtables;

    foreach my $t ( keys %table_drop ) {
        delete $table_unknown{$t};
    }

    foreach my $t ( keys %table_unknown ) {
        print "# Warning: unknown live table: $t\n";
    }

    my $run_alter = $table_exists{dbnotes};

    ## create tables
    foreach my $t ( keys %table_create ) {
        next if $table_exists{$t};
        create_table($t);
    }

    ## drop tables
    foreach my $t ( keys %table_drop ) {
        next unless $table_exists{$t};
        drop_table($t);
    }

    # If dbnotes didn't exist before but was just created, we can now run alters
    if ( !$run_alter && $opt_sql ) {
        my ($exists) = $dbh->selectrow_array("SHOW TABLES LIKE 'dbnotes'");
        if ($exists) {
            print "## dbnotes table was just created, running alters.\n";
            $run_alter = 1;
        }
    }

    if ($run_alter) {
        ## do all the alters
        foreach my $s (@alters) {
            $s->( $dbh, $opt_sql );
        }
    }
    else {
        print
            "## Skipping alters this pass, re-run with -r to create tables and then run alters.\n";
    }

    $status{$cluster} = "OKAY";
}

print "\ncluster: status\n";
foreach my $clid ( sort { $a <=> $b } keys %status ) {
    printf "%7d: %s\n", $clid, $status{$clid};
}
print "\n";

if ($opt_pop) {
    $dbh = LJ::get_db_writer()
        or die "Couldn't get master handle for population.";
    populate_database();
}

print "# Done.\n";

############################################################################

sub populate_database {
    populate_basedata();
    populate_proplists();

    # system user
    my $made_system;
    ( $su, $made_system ) = vivify_system_user();

    populate_moods();
    populate_external_moods();

    # we have a flag to disable population of s1/s2 if the user requests
    unless ($opt_nostyles) {
        populate_s2();
    }

    print
"\nThe system user was created with a random password.\nRun \$LJHOME/bin/upgrading/make_system.pl to change its password and grant the necessary privileges."
        if $made_system;

    print "\nRemember to also run:\n  bin/upgrading/texttool.pl load\n\n"
        if $LJ::IS_DEV_SERVER;

}

sub vivify_system_user {
    my $freshly_made = 0;
    my $su           = LJ::load_user("system");
    unless ($su) {
        print "System user not found. Creating with random password.\n";
        my $pass = LJ::make_auth_code(10);
        $su = LJ::User->create(
            user     => 'system',
            name     => 'System Account',
            password => $pass
        );
        die "Failed to create system user." unless $su;
        $freshly_made = 1;
    }
    return wantarray ? ( $su, $freshly_made ) : $su;
}

sub populate_s2 {

    # S2
    print "Populating public system styles (S2):\n";
    if ($opt_forcebuild) {
        print "*** FORCE REBUILD MODE: All layers will be rebuilt regardless of changes ***\n";
    }
    {
        my $sysid = $su->{'userid'};

        # find existing re-distributed layers that are in the database
        # and their styleids.
        my $existing = LJ::S2::get_public_layers( { force => 1 }, $sysid );

        my %known_id;
        chdir "$ENV{'LJHOME'}/" or die;
        my %layer;    # maps redist_uniq -> { 'type', 'parent' (uniq), 'id' (s2lid) }

        my $has_new_layer = 0;
        my @layers_to_compile;    # Array to store layer compilation jobs

        # If force building, we definitely have "new" layers for cache clearing purposes
        $has_new_layer = 1 if $opt_forcebuild;
        my $compile = sub {
            my ( $base, $type, $parent, $s2source, $LD ) = @_;
            return unless $s2source =~ /\S/;

            my $id = $existing->{$base} ? $existing->{$base}->{'s2lid'} : 0;
            unless ($id) {
                my $parentid = 0;
                $parentid = $layer{$parent}->{'id'} unless $type eq "core";

                # allocate a new one.
                $dbh->do(
                    "INSERT INTO s2layers (s2lid, b2lid, userid, type) "
                        . "VALUES (NULL, $parentid, $sysid, ?)",
                    undef, $type
                );
                die $dbh->errstr if $dbh->err;
                $id = $dbh->{'mysql_insertid'};
                if ($id) {
                    $dbh->do(
                        "INSERT INTO s2info (s2lid, infokey, value) VALUES (?,'redist_uniq',?)",
                        undef, $id, $base );
                }
            }
            die "Can't generate ID for '$base'" unless $id;

            # remember it so we don't delete it later.
            $known_id{$id} = 1;

            $layer{$base} = {
                'type'   => $type,
                'parent' => $parent,
                'id'     => $id,
            };

            my $parid = $layer{$parent}->{'id'};

            # see if source changed
            my $md5_source   = Digest::MD5::md5_hex($s2source);
            my $source_exist = LJ::S2::load_layer_source($id);
            my $md5_exist    = Digest::MD5::md5_hex($source_exist);

            $has_new_layer = 1 unless $source_exist;
            $has_new_layer = 1 if $opt_forcebuild;     # Force cache clearing when force building

         # skip compilation if source is unchanged and parent wasn't rebuilt and not forcing rebuild
            return if $md5_source eq $md5_exist && !$layer{$parent}->{'built'} && !$opt_forcebuild;

            print "$base($id) is $type";
            if ($parid)          { print ", parent = $parent($parid)"; }
            if ($opt_forcebuild) { print " [FORCE REBUILD]"; }
            print "\n";

            # we're going to go ahead and build it.
            $layer{$base}->{'built'} = 1;

            # Store compilation job for parallel processing
            push @layers_to_compile,
                {
                base     => $base,
                id       => $id,
                sysid    => $sysid,
                parid    => $parid,
                type     => $type,
                s2source => $s2source,
                LD       => $LD,
                };
        };

        # Function to compile a single layer (runs in child process)
        my $compile_layer = sub {
            my $job = shift;

            # Since we might fork, we disconnect here and then people can get a new one.
            LJ::DB::disconnect_dbs();

            # Fork out a child so it can compile. This saves us the memory usage.
            if ( my $pid = fork ) {
                return $pid;    # Return the PID to parent
            }
            else {
                # Child process
                $dbh = LJ::get_db_writer();

                # compile!
                my $lay = {
                    's2lid'  => $job->{id},
                    'userid' => $job->{sysid},
                    'b2lid'  => $job->{parid},
                    'type'   => $job->{type},
                };
                my $error = "";
                my $compiled;
                my $info;

                # do this in an eval, so that if the layer_compile call returns an error,
                # we die and pass it up in $@.  but if layer_compile dies, it should pass up
                # an error itself, which we can get.
                eval {
                    die $error
                        unless LJ::S2::layer_compile(
                        $lay,
                        \$error,
                        {
                            's2ref'       => \$job->{s2source},
                            'redist_uniq' => $job->{base},
                            'compiledref' => \$compiled,
                            'layerinfo'   => \$info,
                        }
                        );
                };

                if ($@) {
                    print "S2 compilation failed: $@\n";
                    exit 1;
                }

                if ($opt_compiletodisk) {
                    open( CO, ">$job->{LD}/$job->{base}.pl" ) or die;
                    print CO $compiled;
                    close CO;
                }

                # put raw S2 in database.
                LJ::S2::set_layer_source( $job->{id}, \$job->{s2source} );

                # We are the child, so we can exit here.
                exit;
            }
        };

        my @layerfiles = LJ::get_all_files( "styles/s2layers.dat", home_first => 1 );
        while (@layerfiles) {
            my $file = abs_path( shift @layerfiles );
            next unless -e $file;
            open( SL, $file ) or die;
            my $LD     = dirname($file);
            my $d_file = $file;
            my $d_LD   = $LD;

            $d_file =~ s!^\Q$LJ::HOME\E/*!!;
            $d_LD   =~ s!^\Q$LJ::HOME\E/*!!;

            print "SOURCE: $d_file ( $d_LD )\n";

            while (<SL>) {
                s/\#.*//;
                s/^\s+//;
                s/\s+$//;
                next unless /\S/;
                my ( $base, $type, $parent ) = split;

                if ( $type eq "INCLUDE" ) {
                    unshift @layerfiles, dirname($file) . "/$base";
                    next;
                }

                if ( $type ne "core" && !defined $layer{$parent} ) {
                    die "'$base' references unknown parent '$parent'\n";
                }

                # is the referenced $base file really an aggregation of
                # many smaller layers?  (likely themes, which tend to be small)
                my $multi = ( $type =~ s/\+$// );

                my $s2source;
                open( L, "$LD/$base.s2" ) or die "Can't open file: $base.s2\n";

                unless ($multi) {

      # check if this layer should be mapped to another layer (i.e. exact copy except for layerinfo)
                    if ( $type =~ s/\(([^)]+)\)// )
                    {    # grab the layer in the parentheses and erase it
                        open( my $map_layout, "$LD/$1.s2" ) or die "Can't open file: $1.s2\n";
                        while (<$map_layout>) { $s2source .= $_; }
                    }
                    while (<L>) { $s2source .= $_; }
                    $compile->( $base, $type, $parent, $s2source, $LD );
                }
                else {
                    my $curname;
                    while (<L>) {
                        if (/^\#NEWLAYER:\s*(\S+)/) {
                            my $newname = $1;
                            $compile->( $curname, $type, $parent, $s2source, $LD );
                            $curname  = $newname;
                            $s2source = "";
                        }
                        elsif (/^\#NEWLAYER/) {
                            die "Badly formatted \#NEWLAYER line";
                        }
                        elsif ($curname) {
                            $s2source .= $_;
                        }
                        else {
                            # skip any lines before the first #NEWLAYER section
                        }
                    }
                    $compile->( $curname, $type, $parent, $s2source, $LD );
                }
                close L;
            }
            close SL;
        }

        # Now process all compilation jobs in dependency order with parallelization
        if (@layers_to_compile) {
            print "\nCompiling " . scalar(@layers_to_compile) . " layers in parallel...\n";

            # Build dependency graph and compile in topological order
            my %dependencies;     # base -> [list of dependencies]
            my %rdependencies;    # base -> [list of things that depend on this]
            my %pending_jobs;     # base -> job
            my %completed;        # base -> 1 when done

            # Build dependency mapping
            for my $job (@layers_to_compile) {
                my $base = $job->{base};
                $pending_jobs{$base}  = $job;
                $dependencies{$base}  = [];
                $rdependencies{$base} = [];
            }

            # Now build dependencies after all jobs are in the pending_jobs hash
            for my $job (@layers_to_compile) {
                my $base   = $job->{base};
                my $parent = $layer{$base}{parent};

                # Add dependency if parent exists and is not a core layer
                if ( $parent && $parent ne '-' ) {

                    # Check if parent needs compilation (is in pending jobs)
                    if ( exists $pending_jobs{$parent} ) {
                        push @{ $dependencies{$base} },    $parent;
                        push @{ $rdependencies{$parent} }, $base;
                    }

                    # If parent doesn't need compilation, mark it as completed
                    # This handles the case where parent layers are up-to-date
                    else {
                        $completed{$parent} = 1;
                    }
                }
            }

            # Debug output if verbose
            if ( $ENV{DW_DEBUG_S2} ) {
                print "Dependency graph:\n";
                for my $base ( sort keys %dependencies ) {
                    my $deps =
                        @{ $dependencies{$base} }
                        ? join( ", ", @{ $dependencies{$base} } )
                        : "none";
                    print "  $base depends on: $deps\n";
                }
                print "Pre-completed layers: " . join( ", ", sort keys %completed ) . "\n"
                    if %completed;
            }

            my @running_pids;    # Array of [pid, base] pairs
            my $max_parallel = $opt_parallel || 8;    # Use command line option or default

            # Special case: if parallel=1, use original serial behavior
            if ( $max_parallel == 1 ) {
                print "Using serial compilation (parallel=1)\n";
                for my $job (@layers_to_compile) {
                    my $pid = $compile_layer->($job);
                    waitpid( $pid, 0 );
                    die "S2 compilation failed" if $? >> 8 != 0;
                }
                $dbh = LJ::get_db_writer();
                print "All layer compilations completed successfully!\n";
            }
            else {

                print "Using maximum $max_parallel parallel compilation processes\n";

                while ( %pending_jobs || @running_pids ) {

                    # Wait for any completed jobs first if we have running processes
                    if (@running_pids) {
                        my $finished_pid = waitpid( -1, 0 );    # Wait for any child
                        my $exit_status  = $? >> 8;

                        if ( $exit_status != 0 ) {

                            # Kill remaining children and die
                            kill 'TERM', map { $_->[0] } @running_pids;
                            die "S2 compilation failed with exit status $exit_status";
                        }

                        # Find which job completed and mark it done
                        for my $i ( 0 .. $#running_pids ) {
                            if ( $running_pids[$i][0] == $finished_pid ) {
                                my $completed_base = $running_pids[$i][1];
                                splice @running_pids, $i, 1;
                                $completed{$completed_base} = 1;
                                print "Completed compilation of $completed_base\n";
                                if ( $ENV{DW_DEBUG_S2} ) {
                                    print "Completed layers now: "
                                        . join( ", ", sort keys %completed ) . "\n";
                                    print "Remaining pending jobs: "
                                        . scalar( keys %pending_jobs ) . "\n";
                                }
                                last;
                            }
                        }
                    }

                    # Start new jobs if we have capacity and ready jobs
                    while ( @running_pids < $max_parallel && %pending_jobs ) {

                        # Find a job with all dependencies completed
                        my $ready_job;
                        my @ready_candidates;
                        for my $base ( keys %pending_jobs ) {
                            my $all_deps_done = 1;
                            my @unsatisfied_deps;
                            for my $dep ( @{ $dependencies{$base} } ) {
                                if ( !$completed{$dep} ) {
                                    $all_deps_done = 0;
                                    push @unsatisfied_deps, $dep;
                                    if (   $ENV{DW_DEBUG_S2}
                                        && $dep eq 'core2'
                                        && $base eq 'core2base/layout' )
                                    {
                                        print "DEBUG: $base depends on $dep, completed{$dep} = "
                                            . ( $completed{$dep} // 'undef' ) . "\n";
                                        print "DEBUG: Available completed keys: "
                                            . join( ", ", sort keys %completed ) . "\n";
                                    }
                                }
                            }
                            if ($all_deps_done) {
                                push @ready_candidates, $base;
                            }
                            elsif ( $ENV{DW_DEBUG_S2} && @unsatisfied_deps <= 2 ) {

                                # Debug: show a few jobs that are almost ready
                                print "Almost ready - $base waiting for: "
                                    . join( ", ", @unsatisfied_deps ) . "\n";
                            }
                        }

                        if (@ready_candidates) {
                            $ready_job = $ready_candidates[0];    # Take the first ready job
                            if ( $ENV{DW_DEBUG_S2} ) {
                                print "Ready jobs this round: "
                                    . join( ", ", @ready_candidates ) . "\n";
                            }
                        }

                        last unless $ready_job;                   # No jobs ready to start

                        my $job = delete $pending_jobs{$ready_job};
                        my $pid = $compile_layer->($job);
                        push @running_pids, [ $pid, $ready_job ];
                        if ( $ENV{DW_DEBUG_S2} ) {
                            print "Started compilation of $ready_job (PID $pid)\n";
                        }
                    }

                    # Safety check to prevent infinite loops
                    if ( !@running_pids && %pending_jobs ) {
                        my @remaining = keys %pending_jobs;
                        print "Compilation deadlock detected. Remaining jobs: "
                            . join( ", ", @remaining ) . "\n";
                        print "Dependency analysis:\n";
                        for my $job (@remaining) {
                            my $deps =
                                @{ $dependencies{$job} }
                                ? join( ", ", @{ $dependencies{$job} } )
                                : "none";
                            my $unsatisfied = [];
                            for my $dep ( @{ $dependencies{$job} } ) {
                                push @$unsatisfied, $dep unless $completed{$dep};
                            }
                            my $unsatisfied_str =
                                @$unsatisfied ? join( ", ", @$unsatisfied ) : "none";
                            print "  $job: depends on [$deps], unsatisfied: [$unsatisfied_str]\n";
                        }
                        die "Cannot proceed with compilation.";
                    }
                }

                # Reconnect to database after all forks
                $dbh = LJ::get_db_writer();

                print "All layer compilations completed successfully!\n";
            }
        }

        if ($LJ::IS_DEV_SERVER) {

            # now, delete any system layers that don't below (from previous imports?)
            my @del_ids;
            my $sth =
                $dbh->prepare("SELECT s2lid FROM s2layers WHERE userid=? AND NOT type='user'");
            $sth->execute($sysid);
            while ( my $id = $sth->fetchrow_array ) {
                next if $known_id{$id};
                push @del_ids, $id;
            }

            # if we need to delete things, prompt before blowing away system layers
            if (@del_ids) {
                print
"\nWARNING: The following S2 layer ids are known as system layers but are no longer\n"
                    . "present in the import files.  If this is expected and you really want to DELETE\n"
                    . "these layers, type 'YES' (in all capitals).\n\nType YES to delete layers "
                    . join( ', ', @del_ids ) . ": ";
                my $inp = <STDIN>;
                if ( $inp =~ /^YES$/ ) {
                    print "\nOkay, I am PERMANENTLY DELETING the layers.\n";
                    LJ::S2::delete_layer($_) foreach @del_ids;
                }
                else {
                    print "\nOkay, I am NOT deleting the layers.\n";
                }
            }

            if ($has_new_layer) {
                $LJ::CACHED_PUBLIC_LAYERS = undef;
                LJ::MemCache::delete("s2publayers");

                print "\nCleared styles cache.\n";
            }
        }

    }
}

sub populate_basedata {

    # base data
    foreach my $ffile ( LJ::get_all_files( "bin/upgrading/base-data.sql", home_first => 1 ) ) {
        my $d_file = $ffile;
        $d_file =~ s!^\Q$LJ::HOME\E/*!!;

        print "Populating database with $d_file.\n";
        open( BD, $ffile ) or die "Can't open $d_file file\n";
        while ( my $q = <BD> ) {
            chomp $q;    # remove newline
            next unless ( $q =~ /^(REPLACE|INSERT|UPDATE)/ );
            chop $q;     # remove semicolon
            $dbh->do($q);
            if ( $dbh->err ) {
                print "$q\n";
                die "#  ERROR: " . $dbh->errstr . "\n";
            }
        }
        close(BD);
    }
}

sub populate_proplists {
    foreach my $ffile ( LJ::get_all_files( "bin/upgrading/proplists.dat", home_first => 1 ) ) {
        populate_proplist_file( $ffile, "general" );
    }

    foreach my $ffile ( LJ::get_all_files( "bin/upgrading/proplists-local.dat", home_first => 1 ) )
    {
        populate_proplist_file( $ffile, "local" );
    }
}

sub populate_proplist_file {
    my ( $file, $scope ) = @_;
    open( my $fh, $file ) or die "Failed to open $file: $!";

    my %pk = (
        'userproplist'    => 'name',
        'logproplist'     => 'name',
        'media_prop_list' => 'name',
        'talkproplist'    => 'name',
        'usermsgproplist' => 'name',
    );
    my %id = (
        'userproplist'    => 'upropid',
        'logproplist'     => 'propid',
        'media_prop_list' => 'propid',
        'talkproplist'    => 'tpropid',
        'usermsgproplist' => 'propid',
    );

    my $table;    # table
    my $pk;       # table's primary key name
    my $pkv;      # primary key value
    my %vals;     # hash of column -> value, including primary key

    my %current_props;

    foreach $table ( keys %pk ) {
        $pk = $pk{$table};
        my $id = $id{$table};

        $current_props{$table} = $dbh->selectall_hashref( "SELECT `$id`,`$pk` FROM `$table`", $pk );
    }

    my $insert = sub {
        return unless %vals;
        my $sets = join( ", ", map { "$_=" . $dbh->quote( $vals{$_} ) } keys %vals );
        my $idk  = $id{$table};

        my $rv = 0;
        unless ( $current_props{$table}{$pkv} ) {
            $rv = $dbh->do("INSERT INTO $table SET $sets");
            die $dbh->errstr if $dbh->err;
            $current_props{$table}{$pkv} =
                { name => $pkv, $idk => $dbh->last_insert_id( undef, undef, $table, $idk ) };
        }

        # zero-but-true:  see if row didn't exist before, so above did nothing.
        # in that case, update it.
        if ( $rv < 1 ) {
            $rv = $dbh->do( "UPDATE $table SET $sets WHERE $pk=?", undef, $pkv );
            die $dbh->errstr if $dbh->err;
        }

        $table = undef;
        %vals  = ();
    };
    while (<$fh>) {
        next if /^\#/;

        if (/^(\w+)\.(\w+):/) {
            $insert->();
            ( $table, $pkv ) = ( $1, $2 );
            $pk = $pk{$table} or die "Don't know non-numeric primary key for table '$table'";
            $vals{$pk} = $pkv;
            $vals{"scope"} = $scope;
            next;
        }
        if (/^\s+(\w+)\s*:\s*(.*)/) {
            die "Unexpected line: $_ when not in a block" unless $table;
            $vals{$1} = $2;
            next;
        }
        if (/\S/) {
            die "Unxpected line: $_";
        }
    }
    $insert->();
    close($fh);
}

sub populate_external_moods {
    my $moodfile = "$ENV{'LJHOME'}/bin/upgrading/moods-external.dat";

    if ( open MOODFILE, "<$moodfile" ) {
        print "Populating mood data for external sites.\n";

        # $siteid => { $mood => { siteid => $siteid, mood => $mood, moodid => $moodid } }
        my $moods = $dbh->selectall_hashref( "SELECT siteid, mood, moodid FROM external_site_moods",
            [ 'siteid', 'mood' ] );

        foreach my $line (<MOODFILE>) {
            chomp $line;

            if ( $line =~ /^(\d+)\s+(\d+)\s+(.+)$/ ) {
                my ( $siteid, $moodid, $mood ) = ( $1, $2, $3 );

                unless ( $moods->{$siteid}
                    && $moods->{$siteid}->{$mood}
                    && $moods->{$siteid}->{$mood}->{moodid} eq $moodid )
                {
                    $dbh->do(
"REPLACE INTO external_site_moods ( siteid, mood, moodid ) VALUES ( ?, ?, ? )",
                        undef, $siteid, $mood, $moodid
                    );
                }
            }
        }

        close MOODFILE;
    }
}

sub populate_moods {
    foreach my $moodfile ( LJ::get_all_files( "bin/upgrading/moods.dat", home_first => 1 ) ) {
        if ( open( M, $moodfile ) ) {
            my $file = $moodfile;
            $file =~ s!^\Q$LJ::HOME\E/*!!;

            print "Populating mood data [ $file ].\n";

            my %mood;    # id -> [ mood, parent_id ]
            my $sth = $dbh->prepare("SELECT moodid, mood, parentmood, weight FROM moods");
            $sth->execute;
            while ( @_ = $sth->fetchrow_array ) { $mood{ $_[0] } = [ $_[1], $_[2], $_[3] ]; }

            my %moodtheme;    # name -> [ id, des ]
            $sth =
                $dbh->prepare("SELECT moodthemeid, name, des FROM moodthemes WHERE is_public='Y'");
            $sth->execute;
            while ( @_ = $sth->fetchrow_array ) { $moodtheme{ $_[1] } = [ $_[0], $_[2] ]; }

            my $themeid;      # current themeid (from existing db or just made)
            my %data;         # moodid -> "$url$width$height" (for equality test)

            while (<M>) {
                chomp;
                if (/^MOOD\s+(\d+)\s+(.+)\s+(\d+)\s+(\d+)\s*$/) {
                    my ( $id, $mood, $parid, $weight ) = ( $1, $2, $3, $4 );
                    if (  !$mood{$id}
                        || $mood{$id}->[0] ne $mood
                        || $mood{$id}->[1] ne $parid )
                    {
                        $dbh->do(
"REPLACE INTO moods (moodid, mood, parentmood, weight) VALUES (?,?,?,?)",
                            undef, $id, $mood, $parid, $weight
                        );
                    }
                    elsif ( !defined $mood{$id}->[2] ) {
                        $dbh->do( "UPDATE moods SET weight = ? WHERE moodid = ?",
                            undef, $weight, $id );
                    }
                }

                if (/^MOODTHEME\s+(.+?)\s*:\s*(.+)$/) {
                    my ( $name, $des ) = ( $1, $2 );
                    %data = ();
                    if ( $moodtheme{$name} ) {
                        $themeid = $moodtheme{$name}->[0];
                        if ( $moodtheme{$name}->[1] ne $des ) {
                            $dbh->do( "UPDATE moodthemes SET des=? WHERE moodthemeid=?",
                                undef, $des, $themeid );
                        }
                        $sth = $dbh->prepare( "SELECT moodid, picurl, width, height "
                                . "FROM moodthemedata WHERE moodthemeid=?" );
                        $sth->execute($themeid);
                        while ( @_ = $sth->fetchrow_array ) {
                            $data{ $_[0] } = "$_[1]$_[2]$_[3]";
                        }
                    }
                    else {
                        $dbh->do(
                            "INSERT INTO moodthemes (ownerid, name, des, is_public) "
                                . "VALUES (?,?,?,'Y')",
                            undef, $su->{'userid'}, $name, $des
                        );
                        $themeid = $dbh->{'mysql_insertid'};
                        die "Couldn't generate themeid for theme $name\n" unless $themeid;
                    }
                    next;
                }

                if (/^(\d+)\s+(\S+)\s+(\d+)\s+(\d+)\s*$/) {
                    next unless $themeid;
                    my ( $moodid, $url, $w, $h ) = ( $1, $2, $3, $4 );
                    next if $data{$moodid} eq "$url$w$h";
                    $dbh->do(
                        "REPLACE INTO moodthemedata (moodthemeid, moodid, picurl, width, height) "
                            . "VALUES (?,?,?,?,?)",
                        undef, $themeid, $moodid, $url, $w, $h
                    );
                    LJ::MemCache::delete( [ $themeid, "moodthemedata:$themeid" ] );
                }
            }
            close M;
            LJ::MemCache::delete("moods_public");
        }
    }
}

sub skip_opt {
    return $opt_skip;
}

sub do_sql {
    my $sql = shift;
    chomp $sql;
    my $disp_sql = $sql;
    $disp_sql =~ s/\bIN \(.+\)/IN (...)/g;
    print "$disp_sql;\n";
    if ($opt_sql) {
        print "# Running...\n";
        $dbh->do($sql);
        if ( $dbh->err ) {
            die "#  ERROR: " . $dbh->errstr . "\n";
        }
    }
}

sub do_code {
    my ( $what, $code ) = @_;
    print "Code block: $what\n";
    if ($opt_sql) {
        print "# Running...\n";
        $code->();
    }
}

sub try_sql {
    my $sql = shift;
    print "$sql;\n";
    if ($opt_sql) {
        print "# Non-critical SQL (upgrading only... it might fail)...\n";
        $dbh->do($sql);
        if ( $dbh->err ) {
            print "#  Acceptable failure: " . $dbh->errstr . "\n";
        }
    }
}

sub try_alter {
    my ( $table, $sql ) = @_;
    return if $cluster && !defined $clustered_table{$table};

    try_sql($sql);

    # columns will have changed, so clear cache:
    clear_table_info($table);
}

sub do_alter {
    my ( $table, $sql ) = @_;
    return if $cluster && !defined $clustered_table{$table};

    do_sql($sql);

    # columns will have changed, so clear cache:
    clear_table_info($table);
}

sub create_table {
    my $table = shift;
    return if $cluster && !defined $clustered_table{$table};

    my $create_sql = $table_create{$table};
    if ( $opt_innodb && $create_sql !~ /engine=myisam/i ) {
        $create_sql .= " ENGINE=INNODB";
    }
    do_sql($create_sql);

    foreach my $pc ( @{ $post_create{$table} } ) {
        my @args = @{$pc};
        my $ac   = shift @args;
        if ( $ac eq "sql" ) {
            print "# post-create SQL\n";
            do_sql( $args[0] );
        }
        elsif ( $ac eq "sqltry" ) {
            print "# post-create SQL (necessary if upgrading only)\n";
            try_sql( $args[0] );
        }
        elsif ( $ac eq "code" ) {
            print "# post-create code\n";
            $args[0]->( $dbh, $opt_sql );
        }
        else { print "# don't know how to do \$ac = $ac"; }
    }
}

sub drop_table {
    my $table = shift;

    if ($opt_drop) {
        do_sql("DROP TABLE $table");
    }
    else {
        print "# Not dropping table $table to be paranoid (use --drop)\n";
    }
}

sub mark_clustered {
    foreach (@_) {
        $clustered_table{$_} = 1;
    }
}

sub register_tablecreate {
    my ( $table, $create ) = @_;

    # we now know of it
    delete $table_unknown{$table};

    return if $cluster && !defined $clustered_table{$table};

    $table_create{$table} = $create;
}

sub register_tabledrop {
    my ($table) = @_;
    $table_drop{$table} = 1;
}

sub post_create {
    my $table = shift;
    while ( my ( $type, $what ) = splice( @_, 0, 2 ) ) {
        push @{ $post_create{$table} }, [ $type, $what ];
    }
}

sub register_alter {
    my $sub = shift;
    push @alters, $sub;
}

sub clear_table_info {
    my $table = shift;
    delete $coltype{$table};
    delete $indexname{$table};
    delete $table_status{$table};
}

sub load_table_info {
    my $table = shift;

    clear_table_info($table);

    my $sth = $dbh->prepare("DESCRIBE $table");
    $sth->execute;
    while ( my $row = $sth->fetchrow_hashref ) {
        my $type = $row->{'Type'};
        $type .= " $1" if $row->{'Extra'} =~ /(auto_increment)/i;
        $coltype{$table}->{ $row->{'Field'} }    = lc($type);
        $coldefault{$table}->{ $row->{'Field'} } = $row->{'Default'};
    }

    # current physical table properties
    $table_status{$table} = $dbh->selectrow_hashref("SHOW TABLE STATUS LIKE '$table'");

    $sth = $dbh->prepare("SHOW INDEX FROM $table");
    $sth->execute;
    my %idx_type;     # name -> "UNIQUE"|"INDEX"
    my %idx_parts;    # name -> []
    while ( my $ir = $sth->fetchrow_hashref ) {
        $idx_type{ $ir->{'Key_name'} } = $ir->{'Non_unique'} ? "INDEX" : "UNIQUE";
        push @{ $idx_parts{ $ir->{'Key_name'} } }, $ir->{'Column_name'};
    }

    foreach my $idx ( keys %idx_type ) {
        my $val = "$idx_type{$idx}:" . join( "-", @{ $idx_parts{$idx} } );
        $indexname{$table}->{$val} = $idx;
    }
}

sub index_name {
    my ( $table, $idx ) = @_;    # idx form is:  INDEX:col1-col2-col3
    load_table_info($table) unless $indexname{$table};
    return $indexname{$table}->{$idx} || "";
}

sub table_relevant {
    my $table = shift;
    return 1 unless $cluster;
    return 1 if $clustered_table{$table};
    return 0;
}

sub column_type {
    my ( $table, $col ) = @_;
    load_table_info($table) unless $coltype{$table};
    my $type = $coltype{$table}->{$col};
    $type ||= "";
    return $type;
}

sub column_default {
    my ( $table, $col ) = @_;
    load_table_info($table) unless exists $coldefault{$table};
    return $coldefault{$table}->{$col};
}

sub table_status {
    my ( $table, $col ) = @_;
    load_table_info($table) unless $table_status{$table};

    return $table_status{$table}->{$col} || "";
}

sub ensure_confirm {
    my $area = shift;

    return 1 if (
        $opt_sql
        && (   $opt_confirm eq "all"
            or $opt_confirm eq $area )
    );

    print STDERR "To proceed with the necessary changes, rerun with -r --confirm=$area\n";
    return 0;
}

sub set_dbnote {
    my ( $key, $value ) = @_;
    return unless $opt_sql && $key && $value;

    return $dbh->do( "REPLACE INTO dbnotes (dbnote, value) VALUES (?,?)", undef, $key, $value );
}

sub check_dbnote {
    my $key = shift;

    return $dbh->selectrow_array( "SELECT value FROM dbnotes WHERE dbnote=?", undef, $key );
}
