Roxen.git / server / etc / modules / DBManager.pmod

version» Context lines:

Roxen.git/server/etc/modules/DBManager.pmod:1:   // Symbolic DB handling.   // - // $Id: DBManager.pmod,v 1.104 2012/03/16 10:06:08 marty Exp $ + // $Id$      //! Manages database aliases and permissions      #include <roxen.h>   #include <config.h>         // FIXME: There should be mutexes here!      
Roxen.git/server/etc/modules/DBManager.pmod:20:      constant READ = 1;   //! Read permission. Used in @[set_permission] and @[get_permission_map]      constant WRITE = 2;   //! Write permission. Used in @[set_permission] and @[get_permission_map]         private   { +  string normalized_server_version; +     mixed query( mixed ... args )    {    return connect_to_my_mysql( 0, "roxen" )->query( @args );    }       Sql.sql_result big_query( mixed ... args )    {    return connect_to_my_mysql( 0, "roxen" )->big_query( @args );    }       string short( string n )    {    return lower_case(sprintf("%s%4x", CN(n)[..6],(hash( n )&65535) ));    }       void clear_sql_caches()    { - #if DBMANAGER_DEBUG -  werror("DBManager: clear_sql_caches():\n" -  " dead_sql_cache: %O\n" -  " sql_cache: %O\n" -  " connection_cache: %O\n", -  dead_sql_cache, -  sql_cache, -  connection_cache); - #endif /* DMBMANAGER_DEBUG */ +     /* Rotate the sql_caches.    *    * Perform the rotation first, to avoid thread-races.    */    sql_url_cache = ([]);    user_db_permissions = ([]);    connection_user_cache = ([]);    restricted_user_cache = ([]);    clear_connect_to_my_mysql_cache();    }
Roxen.git/server/etc/modules/DBManager.pmod:82:    big_query ("SELECT 1 FROM user WHERE Host=%s AND User=%s LIMIT 1",    host, user)->    num_rows();    }       protected void low_ensure_has_users( Sql.Sql db, string short_name,    string host, string|void password )    {    if( password )    { -  // According to the documentation MySQL is 4.1 or newer is required -  // for OLD_PASWORD(). There does however seem to exist versions of +  // According to the documentation MySQL 4.1 or newer is required +  // for OLD_PASSWORD(). There does however seem to exist versions of    // at least 4.0 that know of OLD_PASSWORD(). -  if (db->server_info() >= "mysql/4.1") { +  if (normalized_server_version >= "004.001") {    db->query( "REPLACE INTO user (Host,User,Password) "    "VALUES (%s, %s, OLD_PASSWORD(%s)), "    " (%s, %s, OLD_PASSWORD(%s))",    host, short_name + "_rw", password,    host, short_name + "_ro", password);    } else {    db->query( "REPLACE INTO user (Host,User,Password) "    "VALUES (%s, %s, PASSWORD(%s)), "    " (%s, %s, PASSWORD(%s))",    host, short_name + "_rw", password,    host, short_name + "_ro", password);    }    }    else    { -  +  if (normalized_server_version >= "010.002") { +  db->query( "CREATE USER IF NOT EXISTS %s@%s IDENTIFIED BY ''", +  short_name + "_rw", host); +  db->query( "CREATE USER IF NOT EXISTS %s@%s IDENTIFIED BY ''", +  short_name + "_ro", host); +  } else {    db->query( "REPLACE INTO user (Host,User,Password) "    "VALUES (%s, %s, ''), (%s, %s, '')",    host, short_name + "_rw",    host, short_name + "_ro" );    }    } -  +  }    -  +  void set_perms_in_user_table(Sql.Sql db, string host, string user, int level) +  { +  multiset(string) privs = (<>); +  +  switch (level) { +  case NONE: +  db->big_query ("DELETE FROM user " +  " WHERE Host = %s AND User = %s", host, user); +  return; +  +  case READ: +  privs = (< +  "Select_priv", "Show_db_priv", "Create_tmp_table_priv", +  "Lock_tables_priv", "Execute_priv", "Show_view_priv", +  >); +  break; +  +  case WRITE: +  // Current as of MySQL 5.0.70. +  privs = (< +  "Select_priv", "Insert_priv", "Update_priv", "Delete_priv", +  "Create_priv", "Drop_priv", "Reload_priv", "Shutdown_priv", +  "Process_priv", "File_priv", "Grant_priv", "References_priv", +  "Index_priv", "Alter_priv", "Show_db_priv", "Super_priv", +  "Create_tmp_table_priv", "Lock_tables_priv", "Execute_priv", +  "Repl_slave_priv", "Repl_client_priv", "Create_view_priv", +  "Show_view_priv", "Create_routine_priv", "Alter_routine_priv", +  "Create_user_priv", +  >); +  break; +  +  case -1: +  // Special case to create a record for a user that got no access. +  break; +  +  default: +  error ("Invalid level %d.\n", level); +  } +  if (!sizeof(db->query("SELECT User FROM user " +  " WHERE Host = %s AND User = %s", +  host, user))) { +  // Ensure that the user exists. +  db->big_query("REPLACE INTO user (Host, User) VALUES (%s, %s)", +  host, user); +  } +  // Current as of MySQL 5.0.70. +  foreach(({ "Select_priv", "Insert_priv", "Update_priv", "Delete_priv", +  "Create_priv", "Drop_priv", "Reload_priv", "Shutdown_priv", +  "Process_priv", "File_priv", "Grant_priv", "References_priv", +  "Index_priv", "Alter_priv", "Show_db_priv", "Super_priv", +  "Create_tmp_table_priv", "Lock_tables_priv", "Execute_priv", +  "Repl_slave_priv", "Repl_client_priv", "Create_view_priv", +  "Show_view_priv", "Create_routine_priv", "Alter_routine_priv", +  "Create_user_priv", +  }), string field) { +  db->big_query("UPDATE user SET " + field + " = %s " +  " WHERE Host = %s AND User = %s AND " + field + " != %s", +  privs[field]?"Y":"N", +  host, user, privs[field]?"Y":"N"); +  } +  } +     void set_perms_in_db_table (Sql.Sql db, string host, array(string) dbs,    string user, int level)    {    function(string:string) q = db->quote;       switch (level) {    case NONE:    db->big_query ("DELETE FROM db WHERE"    " Host='" + q (host) + "'"    " AND Db IN ('" + (map (dbs, q) * "','") + "')"    " AND User='" + q (user) + "'");    break;       case READ:    db->big_query ("REPLACE INTO db (Host, Db, User, Select_priv, " -  "Execute_priv) " +  "Create_tmp_table_priv, Lock_tables_priv, " +  "Show_view_priv, Execute_priv) "    "VALUES " +    map (dbs, lambda (string db_name) {    return "("    "'" + q (host) + "',"    "'" + q (db_name) + "',"    "'" + q (user) + "'," -  "'Y','Y')"; +  "'Y','Y','Y','Y','Y')";    }) * ",");    break;       case WRITE:    // Current as of MySQL 5.0.70.    db->big_query ("REPLACE INTO db (Host, Db, User,"    " Select_priv, Insert_priv, Update_priv, Delete_priv,"    " Create_priv, Drop_priv, Grant_priv, References_priv,"    " Index_priv, Alter_priv, Create_tmp_table_priv,"    " Lock_tables_priv, Create_view_priv, Show_view_priv,"
Roxen.git/server/etc/modules/DBManager.pmod:192:    int c = script[i];    int cc;    switch(c) {    case ';':    res += ({ script[start..i-1] });    start = i+1;    break;       // Quote characters...    case '\"': case '\'': case '\`': case '\´': -  while (i < sizeof(script)) { +  while (i < sizeof(script) - 1) {    i++;    if ((cc = script[i]) == c) { -  if (script[i+1] == c) { +  if ((i < sizeof(script) - 1) && (script[i+1] == c)) {    i++;    continue;    }    break;    }    if (cc == '\\') i++;    }    break;       // Comments...    case '/':    i++; -  if ((cc = script[i]) == '*') { +  if ((i < sizeof(script)) && +  ((cc = script[i]) == '*')) {    // C-style comment.    int p = search(script, "*/", i+1);    if (p > i) i = p+1;    else i = sizeof(script)-1;    }    break;    case '-':    i++; -  if ((script[i] == '-') && -  ((script[i+1] == ' ') || (script[i+1] == '\t'))) { +  if ((i < sizeof(script) - 1) && +  ((script[i] == '-') && +  ((script[i+1] == ' ') || (script[i+1] == '\t')))) {    // "-- "-style comment.    int p = search(script, "\n", i+2);    int p2 = search(script, "\r", i+2);    if ((p < p2) && (p > i)) i = p;    else if (p2 > i) i = p2;    else if (p > i) i = p;    else i = sizeof(script)-1;    }    break;    case '#':
Roxen.git/server/etc/modules/DBManager.pmod:247:    }    break;    }    }    res += ({ script[start..i-1] });    return res;    }       protected class SqlFileSplitIterator    { -  inherit String.SplitIterator; +  protected Stdio.Buffer inbuf;    -  +  protected function(:string(8bit)) fill_func; +  +  protected string(8bit) current = ""; +     protected void create(Stdio.File script_file)    { -  ::create("", ';', 0, script_file->read_function(8192)); +  current = ""; +  inbuf = Stdio.Buffer(); +  fill_func = script_file->read_function(8192);    next();    }       protected int _sizeof()    {    return -1;    }    -  protected string current = ""; +  protected int(0..1) `!() +  { +  return !current; +  }    -  int next() +  //! Read a single character from the input. +  //! +  //! @returns +  //! Returns the value of the character on success. +  //! +  //! @throws +  //! Throws @expr{0@} (zero) at end of input. +  protected int(8bit) getc()    { -  +  if (!sizeof(inbuf)) { +  string(8bit) data = ""; +  if (fill_func) { +  data = fill_func(); +  } +  if (!sizeof(data)) { +  fill_func = UNDEFINED; +  throw(0); +  } +  inbuf->add(data); +  } +  return inbuf->read_int8(); +  } +  +  int(0..1) next() +  {    if (!current) return 0;    current = 0; -  if (::value()) { -  string buf = ""; +  mixed err = catch { +  Stdio.Buffer buf = Stdio.Buffer();    while (1) { -  buf += ::value() + ";"; -  if (!::next()) break; // Skip the trailer. -  -  array(string) a = split_sql_script(buf); -  if (sizeof(a) > 1) { -  current = a[0]; -  // NB: a[1] should always be "" here. +  int(8bit) cc; +  int(8bit) c = getc(); +  buf->add_int8(c); +  switch(c) { +  case ';': +  current = buf->read();    return 1; -  +  +  // Quote characters... +  case '\"': case '\'': case '\`': case '\´': +  while (1) { +  cc = getc(); +  buf->add_int8(cc); +  if (cc == c) { +  int(8bit) ccc = getc(); +  if (ccc == c) { +  buf->add_int8(ccc); +  continue;    } -  +  inbuf->unread(1); +  break;    } -  +  if (cc == '\\') { +  cc = getc(); +  buf->add_int8(cc);    } -  +  } +  break; +  +  // Comments... +  case '/': +  cc = getc(); +  buf->add_int8(cc); +  if (cc == '*') { +  // C-style comment. +  int(8bit) prev = 0; +  while (1) { +  cc = getc(); +  buf->add_int8(cc); +  if ((cc == '/') && (prev == '*')) break; +  prev = cc; +  } +  } +  break; +  case '-': +  cc = getc(); +  if (cc != '-') { +  inbuf->unread(1); +  break; +  } +  buf->add_int8(cc); +  +  cc = getc(); +  if ((cc != ' ') && (cc != '\t')) { +  inbuf->unread(1); +  break; +  } +  buf->add_int8(cc); +  +  // "-- "-style comment. +  +  // FALL_THROUGH +  case '#': +  // #-style comment. +  do { +  cc = getc(); +  buf->add_int8(cc); +  } while ((cc != '\n') && (cc != '\r')); +  break; +  } +  } +  }; +  if (err) throw(err);    return 0;    }       int index()    {    return current?-1:UNDEFINED;    }    -  string value() +  string(8bit) value()    {    return current || UNDEFINED;    }    }       protected void execute_sql_script(Sql.Sql db, string script,    int|void quiet)    {    array(string) queries = split_sql_script(script);    foreach(queries[..sizeof(queries)-2], string q) {
Roxen.git/server/etc/modules/DBManager.pmod:310:    if (err && !quiet) {    // Complain about failures only if they're not expected.    master()->handle_error(err);    }    }    }       protected void execute_sql_script_file(Sql.Sql db, Stdio.File script_file,    int|void quiet)    { +  // FIXME: What about the connection charset?    foreach(SqlFileSplitIterator(script_file);; string q) {    mixed err = catch {db->query(q);};    if (err && !quiet) {    // Complain about failures only if they're not expected.    master()->handle_error(err);    }    }    }       protected void check_upgrade_mysql()    {    Sql.Sql db = connect_to_my_mysql(0, "mysql");       mapping(string:string) mysql_location = roxenloader->parse_mysql_location();    string update_mysql; -  if ((mysql_location->basedir) && +  +  string mysql_version = db->server_info(); +  // Typically a string like "mysql/5.5.30-log", "mysql/5.5.39-MariaDB-log" or +  // "mysql/5.5.5-10.0.13-MariaDB-log". +  if (has_value(mysql_version, "/")) mysql_version = (mysql_version/"/")[1]; +  +  string db_version; +  // Catch in case mysql_upgrade_info is a directory (unlikely, but...). +  catch { +  db_version = +  Stdio.read_bytes(combine_path(roxenloader.query_mysql_data_dir(), +  "mysql_upgrade_info")); +  // Typically a string like "5.5.30", "5.5.39-MariaDB" or +  // "10.0.13-MariaDB". +  }; +  db_version = db_version && (db_version - "\n"); +  +  if (db_version && +  has_suffix(mysql_version, "-log") && +  !has_suffix(db_version, "-log")) { +  db_version += "-log"; +  } +  +  // Comparing 5.5.5-10.0.13-MariaDB-log and 10.0.13-MariaDB-log +  if (db_version && has_value(mysql_version, db_version)) { +  // Already up-to-date. +  } else { +  werror("Upgrading database from %s to %s...\n", +  db_version || "UNKNOWN", mysql_version); +  +  if (mysql_location->mysql_upgrade) { +  // Upgrade method in MySQL 5.0.19 and later (UNIX), +  // MySQL 5.0.25 and later (NT). +  int err = Process.Process(({ mysql_location->mysql_upgrade, + #ifdef __NT__ +  "--pipe", + #endif +  "-S", roxenloader.query_mysql_socket(), +  "--user=rw", +  // "--verbose", +  }))->wait(); +  if (err) { +  // NB: The first invocation of mysql_upgrade often fails with +  // (--verbose mode): +  // +  // Phase 3/7: Fixing views +  // Processing databases +  // information_schema +  // mysql +  // Phase 4/7: Running 'mysql_fix_privilege_tables' +  // [TIMESTAMP] [ERROR] Column count of mysql.db is wrong. Expected 22, found 21. The table is probably corrupted +  // [TIMESTAMP] [ERROR] mysqld: Event Scheduler: An error occurred when initializing system tables. Disabling the Event Scheduler. +  // ERROR 1408 (HY000) at line 542: Event Scheduler: An error occurred when initializing system tables. Disabling the Event Scheduler. +  // FATAL ERROR: Upgrade failed +  // +  // When run a second time (still --verbose mode) it works fine: +  // +  // Phase 3/7: Fixing views +  // Processing databases +  // information_schema +  // mysql +  // performance_schema +  // Phase 4/7: Running 'mysql_fix_privilege_tables' +  // Phase 5/7: Fixing table and database names +  // +  // Note that the performance_schema doesn't show up in +  // the first pass. +  werror("Warning: Upgrade failed with code %d; trying once more...\n", +  err); +  err = Process.Process(({ mysql_location->mysql_upgrade, + #ifdef __NT__ +  "--pipe", + #endif +  "-S", roxenloader.query_mysql_socket(), +  "--user=rw", +  // "--verbose", +  }))->wait(); +  if (err) { +  error("Upgrading to %s failed with code %d.\n", mysql_version, err); +  } +  } +  } else if ((mysql_location->basedir) &&    (update_mysql =    (Stdio.read_bytes(combine_path(mysql_location->basedir,    "share/mysql",    "mysql_fix_privilege_tables.sql")) ||    Stdio.read_bytes(combine_path(mysql_location->basedir,    "share",    "mysql_fix_privilege_tables.sql"))))) {    // Don't complain about failures, they're expected...    execute_sql_script(db, update_mysql, 1);    } else {    report_warning("Couldn't find MySQL upgrading script.\n");    }    -  +  // These table definitions come from [bug 7264], which in turn got them +  // from http://dba.stackexchange.com/questions/54608/innodb-error-table-mysql-innodb-table-stats-not-found-after-upgrade-to-mys +  foreach(({ #"CREATE TABLE IF NOT EXISTS `innodb_index_stats` ( +  `database_name` varchar(64) COLLATE utf8_bin NOT NULL, +  `table_name` varchar(64) COLLATE utf8_bin NOT NULL, +  `index_name` varchar(64) COLLATE utf8_bin NOT NULL, +  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +  `stat_name` varchar(64) COLLATE utf8_bin NOT NULL, +  `stat_value` bigint(20) unsigned NOT NULL, +  `sample_size` bigint(20) unsigned DEFAULT NULL, +  `stat_description` varchar(1024) COLLATE utf8_bin NOT NULL, +  PRIMARY KEY (`database_name`,`table_name`,`index_name`,`stat_name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0", +  #"CREATE TABLE IF NOT EXISTS `innodb_table_stats` ( +  `database_name` varchar(64) COLLATE utf8_bin NOT NULL, +  `table_name` varchar(64) COLLATE utf8_bin NOT NULL, +  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +  `n_rows` bigint(20) unsigned NOT NULL, +  `clustered_index_size` bigint(20) unsigned NOT NULL, +  `sum_of_other_index_sizes` bigint(20) unsigned NOT NULL, +  PRIMARY KEY (`database_name`,`table_name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0", +  #"CREATE TABLE IF NOT EXISTS `slave_master_info` ( +  `Number_of_lines` int(10) unsigned NOT NULL COMMENT 'Number of lines in the file.', +  `Master_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'The name of the master binary log currently being read from the master.', +  `Master_log_pos` bigint(20) unsigned NOT NULL COMMENT 'The master log position of the last read event.', +  `Host` char(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'The host name of the master.', +  `User_name` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The user name used to connect to the master.', +  `User_password` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The password used to connect to the master.', +  `Port` int(10) unsigned NOT NULL COMMENT 'The network port used to connect to the master.', +  `Connect_retry` int(10) unsigned NOT NULL COMMENT 'The period (in seconds) that the slave will wait before trying to reconnect to the master.', +  `Enabled_ssl` tinyint(1) NOT NULL COMMENT 'Indicates whether the server supports SSL connections.', +  `Ssl_ca` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The file used for the Certificate Authority (CA) certificate.', +  `Ssl_capath` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The path to the Certificate Authority (CA) certificates.', +  `Ssl_cert` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The name of the SSL certificate file.', +  `Ssl_cipher` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The name of the cipher in use for the SSL connection.', +  `Ssl_key` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The name of the SSL key file.', +  `Ssl_verify_server_cert` tinyint(1) NOT NULL COMMENT 'Whether to verify the server certificate.', +  `Heartbeat` float NOT NULL, +  `Bind` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'Displays which interface is employed when connecting to the MySQL server', +  `Ignored_server_ids` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The number of server IDs to be ignored, followed by the actual server IDs', +  `Uuid` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The master server uuid.', +  `Retry_count` bigint(20) unsigned NOT NULL COMMENT 'Number of reconnect attempts, to the master, before giving up.', +  `Ssl_crl` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The file used for the Certificate Revocation List (CRL)', +  `Ssl_crlpath` text CHARACTER SET utf8 COLLATE utf8_bin COMMENT 'The path used for Certificate Revocation List (CRL) files', +  `Enabled_auto_position` tinyint(1) NOT NULL COMMENT 'Indicates whether GTIDs will be used to retrieve events from the master.', +  PRIMARY KEY (`Host`,`Port`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 STATS_PERSISTENT=0 COMMENT='Master Information'", +  #"CREATE TABLE IF NOT EXISTS `slave_relay_log_info` ( +  `Number_of_lines` int(10) unsigned NOT NULL COMMENT 'Number of lines in the file or rows in the table. Used to version table definitions.', +  `Relay_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'The name of the current relay log file.', +  `Relay_log_pos` bigint(20) unsigned NOT NULL COMMENT 'The relay log position of the last executed event.', +  `Master_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'The name of the master binary log file from which the events in the relay log file were read.', +  `Master_log_pos` bigint(20) unsigned NOT NULL COMMENT 'The master log position of the last executed event.', +  `Sql_delay` int(11) NOT NULL COMMENT 'The number of seconds that the slave must lag behind the master.', +  `Number_of_workers` int(10) unsigned NOT NULL, +  `Id` int(10) unsigned NOT NULL COMMENT 'Internal Id that uniquely identifies this record.', +  PRIMARY KEY (`Id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 STATS_PERSISTENT=0 COMMENT='Relay Log Information'", +  #"CREATE TABLE IF NOT EXISTS `slave_worker_info` ( +  `Id` int(10) unsigned NOT NULL, +  `Relay_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, +  `Relay_log_pos` bigint(20) unsigned NOT NULL, +  `Master_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, +  `Master_log_pos` bigint(20) unsigned NOT NULL, +  `Checkpoint_relay_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, +  `Checkpoint_relay_log_pos` bigint(20) unsigned NOT NULL, +  `Checkpoint_master_log_name` text CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, +  `Checkpoint_master_log_pos` bigint(20) unsigned NOT NULL, +  `Checkpoint_seqno` int(10) unsigned NOT NULL, +  `Checkpoint_group_size` int(10) unsigned NOT NULL, +  `Checkpoint_group_bitmap` blob NOT NULL, +  PRIMARY KEY (`Id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 STATS_PERSISTENT=0 COMMENT='Worker Information'", +  }), string table_def) { +  mixed err = catch { +  db->query(table_def); +  }; +  if (err) { +  string table_name = (table_def/"`")[1]; +  werror("DBManager: Failed to add table mysql.%s: %s\n", +  table_name, describe_error(err)); +  } +  } +  } +     multiset(string) missing_privs = (<    // Current as of MySQL 5.0.70.    "Select", "Insert", "Update", "Delete", "Create", "Drop", "Grant",    "References", "Index", "Alter", "Create_tmp_table", "Lock_tables",    "Create_view", "Show_view", "Create_routine", "Alter_routine",    "Execute",    >);    foreach(db->query("DESCRIBE db"), mapping(string:string) row) {    string field = row->Field || row->field;    if (!field) {
Roxen.git/server/etc/modules/DBManager.pmod:367:    }    if (sizeof(missing_privs)) {    werror("DBManager: Updating priviliges table mysql.db with the fields\n"    " %s...", indices(missing_privs)*", ");    foreach(indices(missing_privs), string priv) {    db->query("ALTER TABLE db "    " ADD COLUMN " + priv+ "_priv "    " ENUM('N','Y') DEFAULT 'N' NOT NULL");    }    } +  +  if (!has_value(mysql_version, db_version)) { +  // Make sure no table is broken after the upgrade. +  foreach(db->list_dbs(), string dbname) { +  if (lower_case(dbname) == "information_schema") { +  // This is a virtual read-only db containing metadata +  // about the other tables, etc. Attempting to repair +  // any tables in it will cause errors to be thrown. +  continue;    } -  +  if (lower_case(dbname) == "performance_schema") { +  // This is a virtual read-only db containing metadata +  // about the other tables, etc. Attempting to repair +  // any tables in it will cause errors to be thrown. +  continue; +  } +  werror("DBManager: Repairing tables in the local db %O...\n", dbname); +  Sql.Sql sql = connect_to_my_mysql(0, dbname); +  foreach(sql->list_tables(), string table) { +  // NB: Any errors from the repair other than access +  // permission errors are in the return value. +  // +  // We ignore them for now. +  mixed err = catch { +  sql->query("REPAIR TABLE `" + table + "`"); +  }; +  if (err && has_value(describe_error(err), +  "Incompatible key or row definition between " +  "the MariaDB .frm file")) { +  // Errors 185 and 190: +  // "Incompatible key or row definition between the MariaDB .frm " +  // "file and the information in the storage engine. You have to " +  // "dump and restore the table to fix this" +  werror("DBManager: Basic repair of table %O failed:\n" +  "%s\n" +  "DBManager: Retrying with forced use of .frm file.\n", +  table, describe_error(err)); +  sql->query("REPAIR TABLE `" + table + "` USE_FRM"); +  } else if (err) { +  throw(err); +  } +  } +  } +  werror("DBManager: MySQL upgrade done.\n"); +  } +  }       void synch_mysql_perms()    {    Sql.Sql db = connect_to_my_mysql (0, "mysql");    -  +  // Force proper privs for the low-level users. +  set_perms_in_user_table(db, "localhost", "rw", WRITE); +  set_perms_in_user_table(db, "localhost", "ro", READ); +     mapping(string:int(1..1)) old_perms = ([]);       Sql.sql_result sqlres =    db->big_query ("SELECT Db, User FROM db WHERE Host='localhost'");    while (array(string) ent = sqlres->fetch_row())    if (has_suffix (ent[1], "_rw") || has_suffix (ent[1], "_ro"))    old_perms[ent[0] + "\0" + ent[1]] = 1;       mapping(string:int(1..1)) checked_users = ([]);   
Roxen.git/server/etc/modules/DBManager.pmod:714: Inside #if defined(USE_EXTSQL_ORACLE)
  #ifdef USE_EXTSQL_ORACLE    if(has_prefix(db_url, "oracle:"))    return ExtSQL.sql(db_url);   #endif    return Sql.Sql(db_url, ([ "reconnect":0 ]));   }      Sql.Sql sql_cache_get(string what, void|int reuse_in_thread,    void|string charset)   { -  Thread.MutexKey key = roxenloader.sq_cache_lock(); +     string i = replace(what,":",";")+":-";    Sql.Sql res = roxenloader.sq_cache_get(i, reuse_in_thread);    if (res) { -  destruct(key); +     return roxenloader.fix_connection_charset (res, charset);    } -  // Release the lock during the call to get_sql_handler(), -  // since it may take quite a bit of time... -  destruct(key); +     if (res = get_sql_handler(what)) { -  // Now we need the lock again... -  key = roxenloader.sq_cache_lock(); -  res = roxenloader.sq_cache_set(i, res, reuse_in_thread, charset); -  // Fool the optimizer so that key is not released prematurely -  if( res ) -  return res; +  return roxenloader.sq_cache_set(i, res, reuse_in_thread, charset);    }   }      void add_dblist_changed_callback( function(void:void) callback )   //! Add a function to be called when the database list has been   //! changed. This function will be called after all @[create_db] and   //! @[drop_db] calls.   {    changed_callbacks |= ({ callback });   }
Roxen.git/server/etc/modules/DBManager.pmod:832:    sscanf( db, "%[^:]:", db );    return db;   }      int is_mysql( string db )   //! Returns true if the specified database is a MySQL database.   {    return !(db = db_url( db )) || has_prefix( db, "mysql://" );   }    - static mapping(string:mixed) convert_obj_to_mapping(object|mapping o) + protected mapping(string:mixed) convert_obj_to_mapping(object|mapping o)   {    if (mappingp(o)) return o;    return mkmapping(indices(o), values(o));   }      array(mapping(string:mixed)) db_table_fields( string name, string table )   //! Returns a mapping of fields in the database, if it's supported by   //! the protocol handler. Otherwise returns 0.   {    Sql.Sql db = cached_get( name );
Roxen.git/server/etc/modules/DBManager.pmod:1236:    charset);   }      Sql.Sql cached_get( string name, void|Configuration c, void|int read_only,    void|string charset)   {    return get (name, c, read_only, 0, charset);   }      protected Thread.Local table_locks = Thread.Local(); +    protected class TableLockInfo (    Sql.Sql db,    int count,    multiset(string) locked_for_read,    multiset(string) locked_for_write,   ) {}      class MySQLTablesLock   //! This class is a helper to do MySQL style LOCK TABLES in a safer   //! way:
Roxen.git/server/etc/modules/DBManager.pmod:1501:    }    report_notice("Restoring backup file %s to database %s...\n",    fname, todb || dbname);    execute_sql_script_file(db, cooked);    report_notice("Backup file %s restored to database %s.\n",    fname, todb || dbname);    // FIXME: Return a proper result.    return ({});    }    +  // Old-style BACKUP format.    array q =    tables ||    query( "SELECT tbl FROM db_backups WHERE db=%s AND directory=%s",    dbname, directory )->tbl;    -  +  string db_dir = +  roxenp()->query_configuration_dir() + "/_mysql/" + dbname; +  +  int(0..1) use_restore = (normalized_server_version <= "005.005"); +  if (!use_restore) { +  report_warning("Restoring an old-style backup by hand...\n"); +  +  if (!Stdio.is_dir(db_dir + "/.")) { +  error("Failed to find database directory for db %O.\n" +  "Tried: %O\n", +  dbname, db_dir); +  } +  } +     array res = ({});    foreach( q, string table )    {    db->query( "DROP TABLE IF EXISTS "+table); -  +  if (use_restore) {    directory = combine_path( getcwd(), directory );    res += db->query( "RESTORE TABLE "+table+" FROM %s", directory ); -  +  } else { +  // Copy the files. +  foreach(({ ".frm", ".MYD", ".MYI" }), string ext) { +  if (Stdio.is_file(directory + "/" + table + ext)) { +  if (!Stdio.cp(directory + "/" + table + ext, +  db_dir + "/" + table + ext)) { +  error("Failed to copy %O to %O.\n", +  directory + "/" + table + ext, +  db_dir + "/" + table + ext);    } -  +  } else if (ext != ".MYI") { +  error("Backup file %O is missing!\n", +  directory + "/" + table + ext); +  } +  } +  res += db->query("REPAIR TABLE "+table+" USE_FRM"); +  } +  }    return res;   }      void delete_backup( string dbname, string directory )   //! Delete a backup previously done with @[backup()] or @[dump()].   {    // 1: Delete all backup files.    array(string) tables =    query( "SELECT tbl FROM db_backups WHERE db=%s AND directory=%s",    dbname, directory )->tbl;    if (!sizeof(tables)) {    // Backward compat...    directory = combine_path( getcwd(), directory );    tables =    query( "SELECT tbl FROM db_backups WHERE db=%s AND directory=%s",    dbname, directory )->tbl;    } -  +  int(0..1) partial;    foreach( tables, string table )    {    rm( directory+"/"+table+".frm" );    rm( directory+"/"+table+".MYD" ); -  +  rm( directory+"/"+table+".MYI" ); +  partial = partial || !sizeof(table);    }    rm( directory+"/dump.sql" );    rm( directory+"/dump.sql.bz2" );    rm( directory+"/dump.sql.gz" ); -  +  if (partial) { +  foreach(get_dir(directory)||({}), string file) { +  if (has_suffix(file, ".frm") || +  has_suffix(file, ".MYD") || +  has_suffix(file, ".MYI")) { +  report_notice("Deleting partial backup file %O.\n", file); +  rm(directory + "/" + file); +  } +  } +  }    rm( directory );       // 2: Delete the information about this backup.    query( "DELETE FROM db_backups WHERE db=%s AND directory=%s",    dbname, directory );   }      array(string|array(mapping)) dump(string dbname, string|void directory,    string|void tag)   //! Make a backup using @tt{mysqldump@} of all data in the specified database.
Roxen.git/server/etc/modules/DBManager.pmod:1617:    "without a mysqldump binary.\n"    "%O\n", roxenloader->parse_mysql_location());    }       if( !directory )    directory = roxen_path( "$VARDIR/backup/"+dbname+"-"+isodate(time(1)) );    directory = combine_path( getcwd(), directory );       string db_url = db_url_info->path;    -  if (db_url_info->local) { +  if ((int)db_url_info->local) {    db_url = replace(roxenloader->my_mysql_path, ({ "%user%", "%db%" }),    ({ "ro", dbname || "mysql" }));    }    if (!has_prefix(db_url, "mysql://"))    error("Currently only supports MySQL databases.\n");    string host = (db_url/"://")[1..]*"://";    string port;    string user;    string password;    string db;
Roxen.git/server/etc/modules/DBManager.pmod:1657:    } else {    error("No database specified in DB-URL for DB alias %s.\n", dbname);    }    arr = host/":";    if (sizeof(arr) > 1) {    port = arr[1..]*":";    host = arr[0];    }       // Time to build the command line... -  array(string) cmd = ({ mysqldump, "--add-drop-table", "--all", +  array(string) cmd = ({ mysqldump, "--add-drop-table", "--create-options",    "--complete-insert", "--compress",    "--extended-insert", "--hex-blob",    "--quick", "--quote-names" });    if ((host == "") || (host == "localhost")) {    // Socket.    if (port) {    cmd += ({ "--socket=" + port });    }    } else {    // Hostname.
Roxen.git/server/etc/modules/DBManager.pmod:1687:    cmd += ({ "--password=" + password });    }       mkdirhier( directory+"/" );       cmd += ({    "--result-file=" + directory + "/dump.sql",    db,    });    +  array(string) inhibited_tables = +  query("SELECT tbl FROM db_backup_inhibitions WHERE db = %s", dbname)->tbl; +  if (sizeof(inhibited_tables)) { +  // List all non-backup inhibited tables explicitly. +  foreach(db_tables(dbname), string table) { +  if (!has_value(inhibited_tables, table)) { +  cmd += ({ table }); +  } +  } +  } +  +  /* Mark this directory as an incomplete backup, +  * by having an entry for the table "". +  */ +  query( "DELETE FROM db_backups WHERE " +  "db=%s AND directory=%s AND tbl=%s", +  dbname, directory, "" ); +  query( "INSERT INTO db_backups (db,tbl,directory,whn,tag) " +  "VALUES (%s,%s,%s,%d,%s)", +  dbname, "", directory, time(), tag ); +     werror("Backing up database %s to %s/dump.sql...\n", dbname, directory);    // werror("Starting mysqldump command: %O...\n", cmd);       if (Process.Process(cmd)->wait()) { -  +  // Clean up failed backup from table "". +  query( "DELETE FROM db_backups WHERE " +  "db=%s AND directory=%s AND tbl=%s", +  dbname, directory, "" );    error("Mysql dump command failed for DB %s.\n", dbname);    }       foreach( db_tables( dbname ), string table )    { -  +  if (has_value(inhibited_tables, table)) { +  // Backup inhibited. +  continue; +  }    query( "DELETE FROM db_backups WHERE "    "db=%s AND directory=%s AND tbl=%s",    dbname, directory, table );    query( "INSERT INTO db_backups (db,tbl,directory,whn,tag) "    "VALUES (%s,%s,%s,%d,%s)",    dbname, table, directory, time(), tag );    }    -  +  /* The directory now contains a complete backup. +  * Remove the entry for the table "". +  */ +  query( "DELETE FROM db_backups WHERE " +  "db=%s AND directory=%s AND tbl=%s", +  dbname, directory, "" ); +     if (Process.Process(({ "bzip2", "-f9", directory + "/dump.sql" }))->    wait() &&    Process.Process(({ "gzip", "-f9", directory + "/dump.sql" }))->    wait()) {    werror("Failed to compress the database dump.\n");    }       // FIXME: Fix the returned table_info!    return ({ directory,    map(db_tables(dbname),
Roxen.git/server/etc/modules/DBManager.pmod:1766:   //! @endstring   //! @member string "Msg_text"   //! The message.   //! @endmapping   //! @endarray   //! @endarray   //!   //! @note   //! Currently this function only works for internal databases.   //! + //! @note + //! This method is not supported in MySQL 5.5 and later. + //!   //! @seealso   //! @[dump()]   {    Sql.Sql db = cached_get( dbname );       if( !db )    error("Illegal database\n");       if( !directory )    directory = roxen_path( "$VARDIR/backup/"+dbname+"-"+isodate(time(1)) );    directory = combine_path( getcwd(), directory );       if( is_internal( dbname ) )    { -  +  if (normalized_server_version >= "005.005") { +  error("Old-style MySQL BACKUP files are no longer supported!\n"); +  }    mkdirhier( directory+"/" ); -  +  +  /* Mark this directory as an incomplete backup, +  * by having an entry for the table "". +  */ +  query( "DELETE FROM db_backups WHERE " +  "db=%s AND directory=%s AND tbl=%s", +  dbname, directory, "" ); +  query( "INSERT INTO db_backups (db,tbl,directory,whn,tag) " +  "VALUES (%s,%s,%s,%d,%s)", +  dbname, "", directory, time(), tag ); +  +  array(string) inhibited_tables = +  query("SELECT tbl FROM db_backup_inhibitions WHERE db = %s", dbname)->tbl; +     array tables = db_tables( dbname );    array res = ({});    foreach( tables, string table )    { -  +  if (has_value(inhibited_tables, table)) { +  // Backup inhibited table. +  continue; +  }    res += db->query( "BACKUP TABLE "+table+" TO %s",directory);    query( "DELETE FROM db_backups WHERE "    "db=%s AND directory=%s AND tbl=%s",    dbname, directory, table );    query( "INSERT INTO db_backups (db,tbl,directory,whn,tag) "    "VALUES (%s,%s,%s,%d,%s)",    dbname, table, directory, time(), tag );    }    -  +  /* The directory now contains a complete backup. +  * Remove the entry for the table "". +  */ +  query( "DELETE FROM db_backups WHERE " +  "db=%s AND directory=%s AND tbl=%s", +  dbname, directory, "" ); +     return ({ directory,res });    }    else    {    error("Currently only handles internal databases\n");    // Harder. :-)    }   }      //! Call-out id's for backup schedules.
Roxen.git/server/etc/modules/DBManager.pmod:1847:    foreach(query("SELECT name "    " FROM dbs "    " WHERE schedule_id = %d",    schedule_id)->name, string db) {    mixed err = catch {    mapping lt = localtime(time(1));    string dir = roxen_path(base_dir + "/" + db + "-" + isodate(time(1)) +    sprintf("T%02d-%02d", lt->hour, lt->min));       switch(backup_info[0]->method) { +  case "backup": +  // This method is not supported in MySQL 5.5 and later. +  if (normalized_server_version < "005.005") { +  backup(db, dir, "timed_backup"); +  break; +  } +  // FALL_THROUGH    default:    report_error("Unsupported database backup method: %O for DB %O\n"    "Falling back to the default \"mysqldump\" method.\n",    backup_info[0]->method, db);    // FALL_THROUGH    case "mysqldump":    dump(db, dir, "timed_backup");    break; -  case "backup": -  backup(db, dir, "timed_backup"); -  break; +     }    int generations = (int)backup_info[0]->generations;    if (generations) {    foreach(query("SELECT directory FROM db_backups "    " WHERE db = %s "    " AND tag = %s "    " GROUP BY directory "    " ORDER BY whn DESC "    " LIMIT %d, 65536",    db, "timed_backup", generations)->directory,
Roxen.git/server/etc/modules/DBManager.pmod:2072:   }      string db_group( string db )   {    array q =query( "SELECT groupn FROM db_groups WHERE db=%s", db );    if( sizeof( q ) )    return q[0]->groupn;    return "internal";   }    + string db_schedule( string db ) + { +  array q = query("SELECT schedule FROM dbs, db_schedules " +  " WHERE schedule_id = db_schedules.id " +  " AND name = %s", db); +  if (!sizeof(q)) return UNDEFINED; +  return q[0]->schedule; + } +    string get_group_path( string db, string group )   {    mapping m = get_group( group );    if( !m )    error("The group %O does not exist.", group );    if( strlen( m->pattern ) )    {    catch    {    Sql.Sql sq = Sql.Sql( m->pattern+"mysql" );
Roxen.git/server/etc/modules/DBManager.pmod:2096:    return 0;   }      void set_db_group( string db, string group )   {    query("DELETE FROM db_groups WHERE db=%s", db);    query("INSERT INTO db_groups (db,groupn) VALUES (%s,%s)",    db, group );   }    + bool valid_db_name( string name ) { +  return sizeof( (array)name & ({ '@', ' ', '-', '&', '%', '\t', +  '\n', '\r', '\\', '/', '\'', '"', +  '(', ')', '*', '+', }) ) ? false : true; + } +    void create_db( string name, string path, int is_internal,    string|void group, string|void default_charset )   //! Create a new symbolic database alias.   //!   //! If @[is_internal] is specified, the database will be automatically   //! created if it does not exist, and the @[path] argument is ignored.   //!   //! If the database @[name] already exists, an error will be thrown   //!   //! If group is specified, the @[path] will be generated   //! automatically using the groups defined by @[create_group]   {    if( get( name ) )    error("The database "+name+" already exists\n"); -  if( sizeof((array)name & ({ '@', ' ', '-', '&', '%', '\t', -  '\n', '\r', '\\', '/', '\'', '"', -  '(', ')', '*', '+', }) ) ) +  if( !(valid_db_name(name)) )    error("Please do not use any of the characters @, -, &, /, \\ "    "or %% in database names.\nAlso avoid whitespace characters\n");    if( has_value( name, "-" ) )    name = replace( name, "-", "_" );    if( group )    {    set_db_group( name, group );    if( is_internal )    {    path = get_group_path( name, group );
Roxen.git/server/etc/modules/DBManager.pmod:2140:       if (default_charset) {    query( "INSERT INTO dbs (name, path, local, default_charset) "    "VALUES (%s, %s, %s, %s)", name,    (is_internal?name:path), (is_internal?"1":"0"), default_charset );    } else {    query( "INSERT INTO dbs (name, path, local) "    "VALUES (%s, %s, %s)",    name, (is_internal?name:path), (is_internal?"1":"0") );    } -  if (!is_internal && !has_prefix(path, "mysql://")) { +  if (!is_internal) { +  // Don't attempt to backup external databases automatically.    query("UPDATE dbs SET schedule_id = NULL WHERE name = %s", name); -  } -  if( is_internal ) +  } else {    catch(query( "CREATE DATABASE `"+name+"`")); -  +  }    changed();   }      int set_external_permission( string name, Configuration c, int level,    string password )   //! Set the permission for the configuration @[c] on the database   //! @[name] to @[level] for an external tcp connection from 127.0.0.1   //! authenticated via password @[password].   //!   //! Levels:
Roxen.git/server/etc/modules/DBManager.pmod:2236:    (level?level==2?"write":"read":"none") );       if( (int)d[0]["local"] )    set_user_permissions( c, name, level );       clear_sql_caches();       return 1;   }    + //! Check if backups are inhibited for the specified @[db] and @[table]. + //! + //! @seealso + //! @[module_table_info()] + int(0..1) backups_inhibited(string db, string table) + { +  return sizeof(query("SELECT tbl FROM db_backup_inhibitions " +  " WHERE db = %s AND tbl = %s", +  db, table)); + } +  + //! Return metadata about the specified @[db] and @[table]. + //! + //! @param db + //! Database identifier. + //! + //! @param table + //! Table name or @expr{""@} to query metadata about the @[db]. + //! + //! @returns + //! Returns a mapping containing some of the following information; + //! all fields are optional: + //! @mapping + //! @member string "comment" + //! Description of the table. + //! @member string "conf" + //! Name of @[Configuration] owning the table if known. + //! @member string "conf_varies" + //! Set to @expr{"yes"@} if @[table] is @expr{""@} and + //! multiple @[Configurations] have tables in the @[db]. + //! @member string "db" + //! Name of the database; typically the same value as @[db]. + //! @member string "inhibit_backups" + //! Set to @expr{"yes"@} if backups of this table are inhibited. + //! @member string "module" + //! Name of the @[RoxenModule] owning the table if known. + //! @member string "module_varies" + //! Set to @expr{"yes"@} if @[table] is @expr{""@} and + //! multiple modules have tables in the @[db]. + //! @member string "tbl" + //! Name of the table; typically the same value as @[table]. + //! @endmapping + //! + //! @seealso + //! @[backups_inhibited()]   mapping(string:string) module_table_info( string db, string table )   {    array(mapping(string:string)) td; -  mapping(string:string) res1; +  mapping(string:string) res = ([]); +  +  if ((table != "") && backups_inhibited(db, table)) { +  res["inhibit_backups"] = "yes"; +  } +     if( sizeof(td=query("SELECT * FROM module_tables WHERE db=%s AND tbl=%s",    db, table ) ) ) { -  res1 = td[0]; +  foreach (td, mapping(string:mixed) row) {    if (table != "" || -  (res1->conf && sizeof (res1->conf) && -  res1->module && sizeof (res1->module))) -  return res1; +  (row->conf && sizeof (row->conf) && +  row->module && sizeof (row->module))) +  return res + row;    } -  else -  res1 = ([]); +     -  +  res += td[0]; +  } +     // Many modules don't set the conf and module on the database but    // only on the individual tables, so do some more effort to find a    // common conf and module if table == "".       if (table == "" &&    sizeof (td = query ("SELECT DISTINCT conf, module, db FROM module_tables "    "WHERE db=%s AND tbl!=\"\"", db))) {    if (sizeof (td) == 1 &&    (td[0]->conf && sizeof (td[0]->conf) &&    td[0]->module && sizeof (td[0]->module))) -  return res1 + td[0]; -  res1->module_varies = "yes"; +  return res + td[0]; +  res->module_varies = "yes";       string conf;    foreach (td, mapping(string:string) ent)    if (ent->conf) {    if (!conf) conf = ent->conf;    else if (conf != ent->conf) {    conf = 0;    break;    }    } -  if (conf) res1->conf = conf; -  else res1->conf_varies = "yes"; -  -  return res1; +  if (conf) res->conf = conf; +  else res->conf_varies = "yes";    }    -  return res1; +  return res;   }      string insert_statement( string db, string table, mapping row )   //! Convenience function.   {    function q = cached_get( db )->quote;    string res = "INSERT INTO "+table+" ";    array(string) vi = ({});    array(string) vv = ({});    foreach( indices( row ), string r )
Roxen.git/server/etc/modules/DBManager.pmod:2323:   }      void is_module_db( RoxenModule module, string db, string|void comment )   //! Tell the system that the databse 'db' belongs to the module 'module'.   //! The comment is optional, and will be shown in the configuration   //! interface if present.   {    is_module_table( module, db, "", comment );   }    + //! Exclude the specified @[table] from backups of @[db]. + //! + //! This function is typically used on tables that contain + //! redundant or regeneratable data, to make backups of @[db] + //! smaller and complete faster. + //! + //! @seealso + //! @[permit_backups()] + void inhibit_backups(string db, string table) + { +  query("REPLACE INTO db_backup_inhibitions (db, tbl) VALUES(%s, %s)", +  db, table); + } +  + //! Include the specified @[table] in backups of @[db]. + //! + //! Tables default to being included in backups; this function + //! reverses the effect of @[inhibit_backups()]. + //! + //! @seealso + //! @[inhibit_backups()] + void permit_backups(string db, string table) + { +  query("DELETE FROM db_backup_inhibitions WHERE db = %s AND tbl = %s", +  db, table); + } +    protected void create()   { -  +  Sql.Sql db = connect_to_my_mysql(0, "mysql"); +  // Typically a string like "mysql/5.5.30-log" or "mysql/5.5.39-MariaDB-log". +  normalized_server_version = map(((db->server_info()/"/")[1]/"-")[0]/".", +  lambda(string d) { +  return ("000" + d)[<2..]; +  }) * "."; +     mixed err =    catch {    query("CREATE TABLE IF NOT EXISTS db_backups ("    " db varchar(80) not null, "    " tbl varchar(80) not null, "    " directory varchar(255) not null, "    " whn int unsigned not null, "    " tag varchar(20) null, "    " INDEX place (db,directory))");       if (catch { query("SELECT tag FROM db_backups LIMIT 1"); }) {    // The tag field is missing.    // Upgraded Roxen?    query("ALTER TABLE db_backups "    " ADD tag varchar(20) null");    }    -  +  // NB: Could be a field in module_tables, but having it separate +  // makes it easier. +  query("CREATE TABLE IF NOT EXISTS db_backup_inhibitions (" +  " db varchar(80) not null, " +  " tbl varchar(80) not null, " +  " UNIQUE INDEX (db, tbl))"); +     query("CREATE TABLE IF NOT EXISTS db_groups ("    " db varchar(80) not null, "    " groupn varchar(80) not null)");       query("CREATE TABLE IF NOT EXISTS groups ( "    " name varchar(80) not null primary key, "    " lname varchar(80) not null, "    " comment blob not null, "    " pattern varchar(255) not null default '')");   
Roxen.git/server/etc/modules/DBManager.pmod:2461:    {    set_permission( "local", c, WRITE );    }    }, 0 );    }       check_upgrade_mysql();       synch_mysql_perms();    -  if( file_stat( "etc/docs.frm" ) ) +  if( file_stat( "etc/docs/dump.sql" ) )    {    if( !sizeof(query( "SELECT tbl FROM db_backups WHERE "    "db=%s AND directory=%s", -  "docs", getcwd()+"/etc" ) ) ) +  "docs", getcwd()+"/etc/docs" ) ) )    query("INSERT INTO db_backups (db,tbl,directory,whn) " -  "VALUES ('docs','docs','"+getcwd()+"/etc','"+time()+"')"); +  "VALUES ('docs','docs','"+getcwd()+"/etc/docs','"+time()+"')");    }       // Start the backup timers when we have finished booting.    call_out(start_backup_timers, 0);       return;    };    -  +  if (err) {    werror( describe_backtrace( err ) ); -  +  } else { +  werror("DBManager: Internal error; something threw a %O.\n", err);    } -  + }