#!/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.

use strict;
use DBI::Role;
use DBI;

# need ljconfig to set up database connection
use LJ::Config;
LJ::Config->load;

$LJ::DBIRole = new DBI::Role {
    'timeout' => sub {
        my ( $dsn, $user, $pass, $role ) = @_;
        return 0 if $role && $role eq "master";
        return $LJ::DB_TIMEOUT;
    },
    'sources'     => \%LJ::DBINFO,
    'default_db'  => "livejournal",
    'time_check'  => 60,
    'time_report' => \&LJ::DB::dbtime_callback,
};

# tables on user databases (ljlib-local should define @LJ::USER_TABLES_LOCAL)
# this is here and no longer in bin/upgrading/update-db-{general|local}.pl
# so other tools (in particular, the inter-cluster user mover) can verify
# that it knows how to move all types of data before it will proceed.
@LJ::USER_TABLES = (
    "userbio",         "birthdays",            "dudata",              "log2",
    "logtext2",        "logprop2",             "logsec2",             "talk2",
    "talkprop2",       "talktext2",            "talkleft",            "userpicblob2",
    "subs",            "subsprop",             "has_subs",            "ratelog",
    "loginstall",      "sessions",             "sessions_data",       "modlog",
    "modblob",         "userproplite2",        "links",               "userpropblob",
    "clustertrack2",   "reluser2",             "tempanonips",         "inviterecv",
    "invitesent",      "memorable2",           "memkeyword2",         "userkeywords",
    "trust_groups",    "userpicmap2",          "userpic2",            "s2stylelayers2",
    "s2compiled2",     "userlog",              "logtags",             "logtagsrecent",
    "logkwsum",        "usertags",             "pendcomments",        "loginlog",
    "active_user",     "bannotes",             "notifyqueue",         "dbnotes",
    "random_user_set", "poll2",                "pollquestion2",       "pollitem2",
    "pollresult2",     "pollsubmission2",      "vgift_trans",         "embedcontent",
    "usermsg",         "usermsgtext",          "usermsgprop",         "notifyarchive",
    "notifybookmarks", "embedcontent_preview", "logprop_history",     "import_status",
    "externalaccount", "content_filters",      "content_filter_data", "userpicmap3",
    "media",           "collections",          "collection_items",    "logslugs",
    "media_versions",  "media_props",          "user_profile_accts",
);

# keep track of what db locks we have out
%LJ::LOCK_OUT = ();    # {global|user} => caller_with_lock

package LJ::DB;

use Carp qw(croak);    # import croak into package LJ::DB

sub isdb {
    return ref $_[0]
        && ( ref $_[0] eq "DBI::db"
        || ref $_[0] eq "Apache::DBI::db" );
}

sub dbh_by_role {
    return $LJ::DBIRole->get_dbh(@_);
}

sub root_dbh_by_name {
    my $name = shift;
    my $dbh  = dbh_by_role("master")
        or die "Couldn't contact master to find name of '$name'";

    my $fdsn = $dbh->selectrow_array( "SELECT rootfdsn FROM dbinfo WHERE name=?", undef, $name );
    die "No rootfdsn found for db name '$name'\n" unless $fdsn;

    return $LJ::DBIRole->get_dbh_conn($fdsn);
}

sub foreach_cluster {
    my $coderef = shift;
    my $opts    = shift || {};

    foreach my $cluster_id (@LJ::CLUSTERS) {
        my $dbr = LJ::get_cluster_reader($cluster_id);
        $coderef->( $cluster_id, $dbr );
    }
}

# when calling a supported function (currently: LJ::load_user() or LJ::load_userid*)
# ignores in-process request cache, memcache, and selects directly
# from the global master
#
# called as: require_master(sub { block })
sub require_master {
    my $callback = shift;
    croak "invalid code ref passed to require_master"
        unless ref $callback eq 'CODE';

    # run code in the block with local var set
    local $LJ::_PRAGMA_FORCE_MASTER = 1;
    return $callback->();
}

sub no_cache {
    my $sb = shift;
    local $LJ::MemCache::GET_DISABLED = 1;
    return $sb->();
}

sub cond_no_cache {
    my ( $cond, $sb ) = @_;
    return no_cache($sb) if $cond;
    return $sb->();
}

# returns the DBI::Role role name of a cluster master given a clusterid
sub master_role {
    my $id   = shift;
    my $role = "cluster${id}";
    if ( my $ab = $LJ::CLUSTER_PAIR_ACTIVE{$id} ) {
        $ab = lc($ab);

        # master-master cluster
        $role = "cluster${id}${ab}" if $ab eq "a" || $ab eq "b";
    }
    return $role;
}

sub dbtime_callback {
    my ( $dsn, $dbtime, $time ) = @_;
    my $diff = abs( $dbtime - $time );
    if ( $diff > 2 ) {
        $dsn =~ /host=([^:\;\|]*)/;
        my $db = $1;
        print STDERR "Clock skew of $diff seconds between web($LJ::SERVER_NAME) and db($db)\n";
    }
}

# <LJFUNC>
# name: LJ::DB::get_dbirole_dbh
# class: db
# des: Internal function for get_dbh(). Uses the DBIRole to fetch a dbh, with
#      hooks into db stats-generation if that's turned on.
# info:
# args: opts, role
# des-opts: A hashref of options.
# des-role: The database role.
# returns: A dbh.
# </LJFUNC>
sub get_dbirole_dbh {
    my $dbh = $LJ::DBIRole->get_dbh(@_) or return undef;

    return $dbh;
}

# <LJFUNC>
# name: LJ::DB::get_lock
# des: get a MySQL lock on a given key/dbrole combination.
# returns: undef if called improperly, true on success, die() on failure
# args: db, dbrole, lockname, wait_time?
# des-dbrole: the role this lock should be gotten on, either 'global' or 'user'.
# des-lockname: the name to be used for this lock.
# des-wait_time: an optional timeout argument, defaults to 10 seconds.
# </LJFUNC>
sub get_lock {
    my ( $db, $dbrole, $lockname, $wait_time ) = @_;
    return undef unless $db && $lockname;
    return undef unless $dbrole eq 'global' || $dbrole eq 'user';

    my $curr_sub = ( caller 1 )[3];    # caller of current sub

    # die if somebody already has a lock
    die "LOCK ERROR: $curr_sub; can't get lock from: $LJ::LOCK_OUT{$dbrole}\n"
        if exists $LJ::LOCK_OUT{$dbrole};

    # get a lock from mysql
    $wait_time ||= 10;
    $db->do( "SELECT GET_LOCK(?,?)", undef, $lockname, $wait_time )
        or return undef;

    # successfully got a lock
    $LJ::LOCK_OUT{$dbrole} = $curr_sub;
    return 1;
}

# <LJFUNC>
# name: LJ::DB::release_lock
# des: release a MySQL lock on a given key/dbrole combination.
# returns: undef if called improperly, true on success, die() on failure
# args: db, dbrole, lockname
# des-dbrole: role on which to get this lock, either 'global' or 'user'.
# des-lockname: the name to be used for this lock
# </LJFUNC>
sub release_lock {
    my ( $db, $dbrole, $lockname ) = @_;
    return undef unless $db && $lockname;
    return undef unless $dbrole eq 'global' || $dbrole eq 'user';

    # get a lock from mysql
    $db->do( "SELECT RELEASE_LOCK(?)", undef, $lockname );
    delete $LJ::LOCK_OUT{$dbrole};

    return 1;
}

# <LJFUNC>
# name: LJ::DB::disconnect_dbs
# des: Clear cached DB handles
# </LJFUNC>
sub disconnect_dbs {

    # clear cached handles
    $LJ::DBIRole->disconnect_all( { except => [qw(logs)] } );
}

# <LJFUNC>
# name: LJ::DB::use_diff_db
# class:
# des: given two DB roles, returns true only if it is certain the two roles are
#      served by different database servers.
# info: This is useful for, say, the moveusercluster script: You would not want
#       to select something from one DB, copy it into another, and then delete it from the
#       source if they were both the same machine.
# args:
# des-:
# returns:
# </LJFUNC>
sub use_diff_db {
    $LJ::DBIRole->use_diff_db(@_);
}

# <LJFUNC>
# name: LJ::DB::get_cluster_description
# des: Get descriptive text for a cluster id.
# args: clusterid
# des-clusterid: id of cluster to get description of.
# returns: string representing the cluster description
# </LJFUNC>
sub get_cluster_description {
    my ($cid) = @_;
    $cid += 0;
    my $text = LJ::Hooks::run_hook( 'cluster_description', $cid );
    return $text if $text;

    # default behavior just returns clusterid
    return $cid;
}

# <LJFUNC>
# name: LJ::DB::new_account_cluster
# des: Which cluster to put a new account on.  $DEFAULT_CLUSTER if it's
#      a scalar, random element from [ljconfig[default_cluster]] if it's arrayref.
#      also verifies that the database seems to be available.
# returns: clusterid where the new account should be created; 0 on error
#          (such as no clusters available).
# </LJFUNC>
sub new_account_cluster {

    # if it's not an arrayref, put it in an array ref so we can use it below
    my $clusters = ref $LJ::DEFAULT_CLUSTER ? $LJ::DEFAULT_CLUSTER : [ $LJ::DEFAULT_CLUSTER + 0 ];

    # select a random cluster from the set we've chosen in $LJ::DEFAULT_CLUSTER
    return LJ::DB::random_cluster(@$clusters);
}

# returns the clusterid of a random cluster which is up
# -- accepts @clusters as an arg to enforce a subset, otherwise
#    uses @LJ::CLUSTERS
sub random_cluster {
    my @clusters = @_ ? @_ : @LJ::CLUSTERS;

    # iterate through the new clusters from a random point
    my $size  = @clusters;
    my $start = int( rand() * $size );
    foreach ( 1 .. $size ) {
        my $cid = $clusters[ $start++ % $size ];

        # verify that this cluster is in @LJ::CLUSTERS
        my @check = grep { $_ == $cid } @LJ::CLUSTERS;
        next unless scalar(@check) >= 1 && $check[0] == $cid;

        # try this cluster to see if we can use it, return if so
        my $dbcm = LJ::get_cluster_master($cid);
        return $cid if $dbcm;
    }

    # if we get here, we found no clusters that were up...
    return 0;
}

package LJ;

use Carp qw(confess);    # import confess into package LJ

# <LJFUNC>
# name: LJ::get_dbh
# class: db
# des: Given one or more roles, returns a database handle.
# info:
# args:
# des-:
# returns:
# </LJFUNC>
sub get_dbh {
    my $opts = ref $_[0] eq "HASH" ? shift : {};

    unless ( exists $opts->{'max_repl_lag'} ) {

        # for slave or cluster<n>slave roles, don't allow lag
        if ( $_[0] =~ /slave$/ ) {
            $opts->{'max_repl_lag'} = $LJ::MAX_REPL_LAG || 100_000;
        }
    }

    my $nodb = sub {
        my $roles = shift;
        my $err   = LJ::errobj( "Database::Unavailable", roles => $roles );
        return $err->cond_throw;
    };

    foreach my $role (@_) {

        # let site admin turn off global master write access during
        # maintenance
        return $nodb->( [@_] ) if $LJ::DISABLE_MASTER && $role eq "master";
        my $db = LJ::DB::get_dbirole_dbh( $opts, $role );
        return $db if $db;
    }
    return $nodb->( [@_] );
}

sub get_db_reader {
    return LJ::get_dbh("master") if $LJ::_PRAGMA_FORCE_MASTER;
    return LJ::get_dbh( "slave", "master" );
}

sub get_db_writer {
    return LJ::get_dbh("master");
}

# <LJFUNC>
# name: LJ::get_cluster_reader
# class: db
# des: Returns a cluster slave for a user or clusterid, or cluster master if
#      no slaves exist.
# args: uarg
# des-uarg: Either a clusterid scalar or a user object.
# returns: DB handle.  Or undef if all dbs are unavailable.
# </LJFUNC>
sub get_cluster_reader {
    my $arg   = shift;
    my $id    = isu($arg) ? $arg->{'clusterid'} : $arg;
    my @roles = ( "cluster${id}slave", "cluster${id}" );
    if ( my $ab = $LJ::CLUSTER_PAIR_ACTIVE{$id} ) {
        $ab = lc($ab);

        # master-master cluster
        @roles = ("cluster${id}${ab}") if $ab eq "a" || $ab eq "b";
    }
    return LJ::get_dbh(@roles);
}

# <LJFUNC>
# name: LJ::get_cluster_def_reader
# class: db
# des: Returns a definitive cluster reader for a given user or clusterid, used
#      when the caller wants the master handle, but will only
#      use it to read.
# args: uarg
# des-uarg: Either a clusterid scalar or a user object.
# returns: DB handle.  Or undef if definitive reader is unavailable.
# </LJFUNC>
sub get_cluster_def_reader {
    my @dbh_opts = scalar(@_) == 2 ? ( shift @_ ) : ();
    my $arg      = shift;
    my $id       = isu($arg) ? $arg->{'clusterid'} : $arg;
    return LJ::get_cluster_reader( @dbh_opts, $id )
        if $LJ::DEF_READER_ACTUALLY_SLAVE{$id};
    return LJ::get_dbh( @dbh_opts, LJ::DB::master_role($id) );
}

# <LJFUNC>
# name: LJ::get_cluster_master
# class: db
# des: Returns a cluster master for a given user or clusterid, used when the
#      caller might use it to do a write (insert/delete/update/etc...)
# args: uarg
# des-uarg: Either a clusterid scalar or a user object.
# returns: DB handle.  Or undef if master is unavailable.
# </LJFUNC>
sub get_cluster_master {
    my @dbh_opts = scalar(@_) == 2 ? ( shift @_ ) : ();
    my $arg      = shift;
    my $id       = isu($arg) ? $arg->{'clusterid'} : $arg;
    return undef if $LJ::READONLY_CLUSTER{$id};
    return LJ::get_dbh( @dbh_opts, LJ::DB::master_role($id) );
}

# Single-letter domain values are for livejournal-generic code.
#  - 0-9 are reserved for site-local hooks and are mapped from a long
#    (> 1 char) string passed as the $dom to a single digit by the
#    'map_global_counter_domain' hook.
#
# LJ-generic domains:
#  $dom: 'S' == style, 'P' == userpic, 'A' == stock support answer
#        'E' == external user, 'V' == vgifts,
#        'L' == poLL,  'M' == Messaging, 'H' == sHopping cart,
#        'F' == PubSubHubbub subscription id (F for Fred),
#        'K' == sitekeyword, 'I' == shopping cart Item,
#        'X' == sphinX id, 'U' == OAuth ConsUmer, 'N' == seNdmail history
#
sub alloc_global_counter {
    my ( $dom, $recurse ) = @_;
    my $dbh = LJ::get_db_writer();
    return undef unless $dbh;

    # $dom can come as a direct argument or as a string to be mapped via hook
    my $dom_unmod = $dom;
    unless ( $dom =~ /^[ESLPAHCMFKIVXUN]$/ ) {
        $dom = LJ::Hooks::run_hook( 'map_global_counter_domain', $dom );
    }
    return LJ::errobj( "InvalidParameters", params => { dom => $dom_unmod } )->cond_throw
        unless defined $dom;

    my $newmax;
    my $uid = 0;    # userid is not needed, we just use '0'

    my $rs = $dbh->do( "UPDATE counter SET max=LAST_INSERT_ID(max+1) WHERE journalid=? AND area=?",
        undef, $uid, $dom );
    if ( $rs > 0 ) {
        $newmax = $dbh->selectrow_array("SELECT LAST_INSERT_ID()");
        return $newmax;
    }

    return undef if $recurse;

    # no prior counter rows - initialize one.
    if ( $dom eq "S" ) {
        confess 'Tried to allocate S1 counter.';
    }
    elsif ( $dom eq "P" ) {
        $newmax = 0;
        foreach my $cid (@LJ::CLUSTERS) {
            my $dbcm = LJ::get_cluster_master($cid) or return undef;
            my $max  = $dbcm->selectrow_array('SELECT MAX(picid) FROM userpic2') // 0;
            $newmax = $max if $max > $newmax;
        }
    }
    elsif ( $dom eq "E" || $dom eq "M" ) {

        # if there is no extuser or message counter row
        # start at 'ext_1'  - ( the 0 here is incremented after the recurse )
        $newmax = 0;
    }
    elsif ( $dom eq "A" ) {
        $newmax = $dbh->selectrow_array("SELECT MAX(ansid) FROM support_answers");
    }
    elsif ( $dom eq "H" ) {
        $newmax = $dbh->selectrow_array("SELECT MAX(cartid) FROM shop_carts");
    }
    elsif ( $dom eq "L" ) {

        # pick maximum id from pollowner
        $newmax = $dbh->selectrow_array("SELECT MAX(pollid) FROM pollowner");
    }
    elsif ( $dom eq 'F' ) {
        confess 'Tried to allocate PubSubHubbub counter.';
    }
    elsif ( $dom eq 'U' ) {
        $newmax = $dbh->selectrow_array("SELECT MAX(consumer_id) FROM oauth_consumer");
    }
    elsif ( $dom eq 'V' ) {
        $newmax = $dbh->selectrow_array("SELECT MAX(vgiftid) FROM vgift_ids");
    }
    elsif ( $dom eq 'N' ) {
        $newmax = $dbh->selectrow_array("SELECT MAX(msgid) FROM siteadmin_email_history");
    }
    elsif ( $dom eq 'K' ) {

        # pick maximum id from sitekeywords & interests
        my $max_sitekeys  = $dbh->selectrow_array("SELECT MAX(kwid) FROM sitekeywords");
        my $max_interests = $dbh->selectrow_array("SELECT MAX(intid) FROM interests");
        $newmax = ( $max_sitekeys // 0 ) > ( $max_interests // 0 ) ? $max_sitekeys : $max_interests;
    }
    elsif ( $dom eq 'I' ) {

        # if we have no counter, start at 0, as we have no way of determining what
        # the maximum used item id is
        $newmax = 0;
    }
    elsif ( $dom eq 'X' ) {
        my $dbsx = LJ::get_dbh('sphinx_search')
            or die "Unable to allocate counter type X unless Sphinx is configured.\n";
        $newmax = $dbsx->selectrow_array('SELECT MAX(id) FROM items_raw');
    }
    else {
        $newmax = LJ::Hooks::run_hook( 'global_counter_init_value', $dom );
        die "No alloc_global_counter initalizer for domain '$dom'"
            unless defined $newmax;
    }
    $newmax += 0;
    $dbh->do( "INSERT IGNORE INTO counter (journalid, area, max) VALUES (?,?,?)",
        undef, $uid, $dom, $newmax )
        or return LJ::errobj($dbh)->cond_throw;
    return LJ::alloc_global_counter( $dom, 1 );
}

# $dom: 'L' == log, 'T' == talk, 'M' == modlog, 'S' == session,
#       'R' == memory (remembrance), 'K' == keyword id,
#       'C' == pending comment
#       'V' == 'vgift', 'E' == ESN subscription id
#       'Q' == Notification Inbox,
#       'D' == 'moDule embed contents', 'I' == Import data block
#       'Z' == import status item, 'X' == eXternal account
#       'F' == filter id, 'Y' = pic/keYword mapping id
#       'A' == mediA item id, 'O' == cOllection id,
#       'N' == collectioN item id, 'B' == api key id,
#       'P' == Profile account id
#
#       remaining unused letters: G H J U W
#
sub alloc_user_counter {
    my ( $u, $dom, $opts ) = @_;
    $opts ||= {};

    ##################################################################
    # IF YOU UPDATE THIS MAKE SURE YOU ADD INITIALIZATION CODE BELOW #
    return undef unless $dom =~ /^[LTMPSRKCOVEQDIZXFYABN]$/;
    ##################################################################

    my $dbh = LJ::get_db_writer();
    return undef unless $dbh;

    my $newmax;
    my $uid = $u->userid + 0;
    return undef unless $uid;
    my $memkey = [ $uid, "auc:$uid:$dom" ];

    # in a master-master DB cluster we need to be careful that in
    # an automatic failover case where one cluster is slightly behind
    # that the same counter ID isn't handed out twice.  use memcache
    # as a sanity check to record/check latest number handed out.
    my $memmax = int( LJ::MemCache::get($memkey) || 0 );

    my $rs = $dbh->do(
        "UPDATE usercounter SET max=LAST_INSERT_ID(GREATEST(max,$memmax)+1) "
            . "WHERE journalid=? AND area=?",
        undef, $uid, $dom
    );
    if ( $rs > 0 ) {
        $newmax = $dbh->selectrow_array("SELECT LAST_INSERT_ID()");

        # if we've got a supplied callback, lets check the counter
        # number for consistency.  If it fails our test, wipe
        # the counter row and start over, initializing a new one.
        # callbacks should return true to signal 'all is well.'
        if ( $opts->{callback} && ref $opts->{callback} eq 'CODE' ) {
            my $rv = 0;
            eval { $rv = $opts->{callback}->( $u, $newmax ) };
            if ( $@ or !$rv ) {
                $dbh->do( "DELETE FROM usercounter WHERE " . "journalid=? AND area=?",
                    undef, $uid, $dom );
                return LJ::alloc_user_counter( $u, $dom );
            }
        }

        LJ::MemCache::set( $memkey, $newmax );
        return $newmax;
    }

    if ( $opts->{recurse} ) {

        # We shouldn't ever get here if all is right with the world.
        return undef;
    }

    my $qry_map = {

        # for entries:
        'log'         => "SELECT MAX(jitemid) FROM log2     WHERE journalid=?",
        'logtext'     => "SELECT MAX(jitemid) FROM logtext2 WHERE journalid=?",
        'talk_nodeid' => "SELECT MAX(nodeid)  FROM talk2    WHERE nodetype='L' AND journalid=?",

        # for comments:
        'talk'     => "SELECT MAX(jtalkid) FROM talk2     WHERE journalid=?",
        'talktext' => "SELECT MAX(jtalkid) FROM talktext2 WHERE journalid=?",
    };

    my $consider = sub {
        my @tables = @_;
        foreach my $t (@tables) {
            my $res = $u->selectrow_array( $qry_map->{$t}, undef, $uid );
            $newmax = $res if defined $res and $res > $newmax;
        }
    };

    # Make sure the counter table is populated for this uid/dom.
    if ( $dom eq "L" ) {

        # back in the ol' days IDs were reused (because of MyISAM)
        # so now we're extra careful not to reuse a number that has
        # foreign junk "attached".  turns out people like to delete
        # each entry by hand, but we do lazy deletes that are often
        # too lazy and a user can see old stuff come back alive
        $consider->( "log", "logtext", "talk_nodeid" );
    }
    elsif ( $dom eq "T" ) {

        # just paranoia, not as bad as above.  don't think we've ever
        # run into cases of talktext without a talk, but who knows.
        # can't hurt.
        $consider->( "talk", "talktext" );
    }
    elsif ( $dom eq "M" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(modid) FROM modlog WHERE journalid=?", undef, $uid );
    }
    elsif ( $dom eq "S" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(sessid) FROM sessions WHERE userid=?", undef, $uid );
    }
    elsif ( $dom eq "R" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(memid) FROM memorable2 WHERE userid=?", undef, $uid );
    }
    elsif ( $dom eq "K" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(kwid) FROM userkeywords WHERE userid=?", undef, $uid );
    }
    elsif ( $dom eq "C" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(pendcid) FROM pendcomments WHERE jid=?", undef, $uid );
    }
    elsif ( $dom eq "V" ) {
        $newmax = $u->selectrow_array( "SELECT MAX(transid) FROM vgift_trans WHERE rcptid=?",
            undef, $uid );
    }
    elsif ( $dom eq "E" ) {
        $newmax = $u->selectrow_array( "SELECT MAX(subid) FROM subs WHERE userid=?", undef, $uid );
    }
    elsif ( $dom eq "Q" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(qid) FROM notifyqueue WHERE userid=?", undef, $uid );
    }
    elsif ( $dom eq "D" ) {
        $newmax = $u->selectrow_array( "SELECT MAX(moduleid) FROM embedcontent WHERE userid=?",
            undef, $uid );
    }
    elsif ( $dom eq "I" ) {
        $newmax =
            $dbh->selectrow_array( "SELECT MAX(import_data_id) FROM import_data WHERE userid=?",
            undef, $uid );
    }
    elsif ( $dom eq "Z" ) {
        $newmax =
            $dbh->selectrow_array( "SELECT MAX(import_status_id) FROM import_status WHERE userid=?",
            undef, $uid );
    }
    elsif ( $dom eq "X" ) {
        $newmax = $u->selectrow_array( "SELECT MAX(acctid) FROM externalaccount WHERE userid=?",
            undef, $uid );
    }
    elsif ( $dom eq "F" ) {
        $newmax = $u->selectrow_array( "SELECT MAX(filterid) FROM watch_filters WHERE userid=?",
            undef, $uid );
    }
    elsif ( $dom eq "Y" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(mapid) FROM userpicmap3 WHERE userid=?", undef, $uid );
    }
    elsif ( $dom eq "A" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(mediaid) FROM media WHERE userid = ?", undef, $uid );
    }
    elsif ( $dom eq "O" ) {
        $newmax = $u->selectrow_array( "SELECT MAX(colid) FROM collections WHERE userid = ?",
            undef, $uid );
    }
    elsif ( $dom eq "B" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(keyid) FROM api_key WHERE userid = ?", undef, $uid );
    }
    elsif ( $dom eq "N" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(colitemid) FROM collection_items WHERE userid = ?",
            undef, $uid );
    }
    elsif ( $dom eq "P" ) {
        $newmax =
            $u->selectrow_array( "SELECT MAX(account_id) FROM user_profile_accts WHERE userid = ?",
            undef, $uid );
    }
    else {
        die "No user counter initializer defined for area '$dom'.\n";
    }
    $newmax += 0;
    $dbh->do( "INSERT IGNORE INTO usercounter (journalid, area, max) VALUES (?,?,?)",
        undef, $uid, $dom, $newmax )
        or return undef;

    # The 2nd invocation of the alloc_user_counter sub should do the
    # intended incrementing.
    return LJ::alloc_user_counter( $u, $dom, { recurse => 1 } );
}

package LJ::Error::Database::Unavailable;
sub fields { qw(roles) }    # arrayref of roles requested

sub as_string {
    my $self  = shift;
    my $ct    = @{ $self->field('roles') };
    my $clist = join( ", ", @{ $self->field('roles') } );
    return $ct == 1
        ? "Database unavailable for role $clist"
        : "Database unavailable for roles $clist";
}

package LJ::Error::Database::Failure;
sub fields { qw(db) }

sub user_caused { 0 }

sub as_string {
    my $self = shift;
    my $code = $self->err;
    my $txt  = $self->errstr;
    return "Database error code $code: $txt";
}

sub err {
    my $self = shift;
    return $self->field('db')->err;
}

sub errstr {
    my $self = shift;
    return $self->field('db')->errstr;
}

1;
