/*++
/* NAME
/*	dict_mysql 3
/* SUMMARY
/*	dictionary manager interface to MySQL databases
/* SYNOPSIS
/*	#include <dict_mysql.h>
/*
/*	DICT	*dict_mysql_open(name, open_flags, dict_flags)
/*	const char *name;
/*	int	open_flags;
/*	int	dict_flags;
/* DESCRIPTION
/*	dict_mysql_open() creates a dictionary of type 'mysql'.  This
/*	dictionary is an interface for the postfix key->value mappings
/*	to mysql.  The result is a pointer to the installed dictionary,
/*	or a null pointer in case of problems.
/*
/*	The mysql dictionary can manage multiple connections to different
/*	sql servers on different hosts.  It assumes that the underlying data
/*	on each host is identical (mirrored) and maintains one connection
/*	at any given time.  If any connection fails,  any other available
/*	ones will be opened and used.  The intent of this feature is to eliminate
/*	a single point of failure for mail systems that would otherwise rely
/*	on a single mysql server.
/* .PP
/*	Arguments:
/* .IP name
/*	Either the path to the MySQL configuration file (if it starts
/*	with '/' or '.'), or the prefix which will be used to obtain
/*	main.cf configuration parameters for this search.
/*
/*	In the first case, the configuration parameters below are
/*	specified in the file as \fIname\fR=\fIvalue\fR pairs.
/*
/*	In the second case, the configuration parameters are
/*	prefixed with the value of \fIname\fR and an underscore,
/*	and they are specified in main.cf.  For example, if this
/*	value is \fImysqlsource\fR, the parameters would look like
/*	\fImysqlsource_user\fR, \fImysqlsource_table\fR, and so on.
/*
/* .IP other_name
/*	reference for outside use.
/* .IP open_flags
/*	Must be O_RDONLY.
/* .IP dict_flags
/*	See dict_open(3).
/* SEE ALSO
/*	dict(3) generic dictionary manager
/*	mysql_table(5) MySQL client configuration
/* AUTHOR(S)
/*	Scott Cotton, Joshua Marcus
/*	IC Group, Inc.
/*	scott@icgroup.com
/*
/*	Liviu Daia
/*	Institute of Mathematics of the Romanian Academy
/*	P.O. BOX 1-764
/*	RO-014700 Bucharest, ROMANIA
/*
/*	John Fawcett
/*
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*
/*	Prepared statement support by
/*	Ömer Güven
/*--*/

/* System library. */
#include "sys_defs.h"

#ifdef HAS_MYSQL
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>
#include <time.h>
#include <mysql.h>
#include <limits.h>
#include <errno.h>

#if !defined(MYSQL_VERSION_ID) || MYSQL_VERSION_ID < 40000
#error "MySQL versions <4 are no longer supported"
#endif
#ifndef MYSQL_DATA_TRUNCATED
#define MYSQL_DATA_TRUNCATED 101
#endif

#ifdef STRCASECMP_IN_STRINGS_H
#include <strings.h>
#endif

/* Utility library. */

#include "dict.h"
#include "msg.h"
#include "mymalloc.h"
#include "argv.h"
#include "vstring.h"
#include "split_at.h"
#include "find_inet.h"
#include "myrand.h"
#include "events.h"
#include "stringops.h"

/* Global library. */

#include "cfg_parser.h"
#include "db_common.h"

/* Application-specific. */

#include "dict_mysql.h"

/* MySQL 8.x API change */

#if defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 50023
#define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_VERIFY_SERVER_CERT
#elif MYSQL_VERSION_ID >= 80000
#define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_MODE
#endif

 /*
  * MariaDB Connector/C 3.0.0 lists mysql_options() as deprecated and
  * recommends using mysql_optionsv() instead. Option names and semantics
  * have not changed.
  */
#if defined(MARIADB_PACKAGE_VERSION_ID) && MARIADB_PACKAGE_VERSION_ID >= 30000
#define mysql_options	mysql_optionsv
#endif

/* need some structs to help organize things */
typedef struct {
    MYSQL  *db;
    MYSQL_STMT *stmt;
    char   *hostname;
    char   *name;
    unsigned port;
    unsigned type;			/* TYPEUNIX | TYPEINET */
    unsigned stat;			/* STATUNTRIED | STATFAIL | STATCUR */
    time_t  ts;				/* used for attempting reconnection
					 * every so often if a host is down */
} HOST;

typedef struct {
    int     len_hosts;			/* number of hosts */
    HOST  **db_hosts;			/* the hosts on which the databases
					 * reside */
} PLMYSQL;

#define MYSQL_STATEMENT_LEGACY		0
#define MYSQL_STATEMENT_PREPARED	1

typedef struct {
    char   *sql;
    ARGV   *param_formats;
    VSTRING **param_bufs;
    MYSQL_BIND *param_binds;
    unsigned long *param_lengths;
} MYSQL_STATEMENT;

typedef struct {
    DICT    dict;
    CFG_PARSER *parser;
    char   *query;
    char   *result_format;
    char   *option_file;
    char   *option_group;
    void   *ctx;
    int     expansion_limit;
    char   *username;
    char   *password;
    char   *dbname;
    char   *charset;
    int     retry_interval;
    int     idle_interval;
    ARGV   *hosts;
    PLMYSQL *pldb;
    HOST   *active_host;
    int     statement_mode;
    MYSQL_STATEMENT *stmt;
    char   *tls_cert_file;
    char   *tls_key_file;
    char   *tls_CAfile;
    char   *tls_CApath;
    char   *tls_ciphers;
#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
    int     tls_verify_cert;
#endif
    int     require_result_set;
} DICT_MYSQL;

#define STATACTIVE			(1<<0)
#define STATFAIL			(1<<1)
#define STATUNTRIED			(1<<2)

#define TYPEUNIX			(1<<0)
#define TYPEINET			(1<<1)

#define RETRY_CONN_MAX			100
#define DEF_RETRY_INTV			60	/* 1 minute */
#define DEF_IDLE_INTV			60	/* 1 minute */

/* internal function declarations */
static PLMYSQL *plmysql_init(ARGV *);
static int plmysql_query(DICT_MYSQL *, const char *, VSTRING *, MYSQL_RES **);
static int plmysql_query_prepared(DICT_MYSQL *, const char *, VSTRING *, int *);
static int plmysql_prepare_stmt(DICT_MYSQL *, HOST *);
static void plmysql_dealloc(PLMYSQL *);
static void plmysql_close_host(HOST *);
static void plmysql_down_host(HOST *, int);
static void plmysql_connect_single(DICT_MYSQL *, HOST *);
static const char *dict_mysql_lookup(DICT *, const char *);
DICT   *dict_mysql_open(const char *, int, int);
static void dict_mysql_close(DICT *);
static void mysql_parse_config(DICT_MYSQL *, const char *);
static HOST *host_init(const char *);
static int mysql_stmt_param(const char);
static int mysql_stmt_param_slot(ARGV *, const char *);
static void mysql_stmt_append_expr_param(VSTRING *, ARGV *, const char, int *);
static void mysql_stmt_append_expr_literal(VSTRING *, const char *, ssize_t, int *);
static void mysql_stmt_append_quoted(VSTRING *, ARGV *, const char *, ssize_t);
static MYSQL_STATEMENT *mysql_statement_alloc(const char *, const char *);
static void mysql_statement_free(MYSQL_STATEMENT *);
static int mysql_stmt_bind_value(DICT_MYSQL *, const char *,
				         const char *, VSTRING *);
static int mysql_stmt_bind(DICT_MYSQL *, MYSQL_STMT *, const char *);

static int mysql_stmt_param(const char ch)
{
    return (strchr("sudSUD123456789", ch) != 0);
}

static int mysql_stmt_param_slot(ARGV *param_formats, const char *format)
{
    /* MySQL '?' markers are anonymous; repeated %u needs repeated slots. */
    argv_add(param_formats, format, ARGV_END);
    return (param_formats->argc - 1);
}

static void mysql_stmt_append_expr_param(VSTRING *buf, ARGV *param_formats,
					         const char ch, int *pieces)
{
    char    format[] = {'%', ch, 0};

    if ((*pieces)++ > 0)
	VSTRING_ADDCH(buf, ',');
    (void) mysql_stmt_param_slot(param_formats, format);
    VSTRING_ADDCH(buf, '?');
}

static void mysql_stmt_append_expr_literal(VSTRING *buf, const char *text,
					           ssize_t len, int *pieces)
{
    if ((*pieces)++ > 0)
	VSTRING_ADDCH(buf, ',');
    VSTRING_ADDCH(buf, '\'');
    vstring_strncat(buf, text, len);
    VSTRING_ADDCH(buf, '\'');
}

static void mysql_stmt_append_quoted(VSTRING *sql, ARGV *param_formats,
					       const char *text, ssize_t len)
{
    VSTRING *expr;
    VSTRING *lit;
    const char *end = text + len;
    const char *cp;
    int     pieces = 0;
    int     has_param = 0;

    /* Fast path for common whole-literal placeholders: '%u' -> ?. */
    if (len == 2 && text[0] == '%' && mysql_stmt_param(text[1])) {
	char    format[] = {'%', text[1], 0};

	(void) mysql_stmt_param_slot(param_formats, format);
	VSTRING_ADDCH(sql, '?');
	return;
    }

    expr = vstring_alloc(10);
    lit = vstring_alloc(10);

    /*
     * MySQL does not use || for string concatenation by default, so mixed
     * quoted literals become CONCAT('prefix-', ?, ...).
     */
    for (cp = text; cp < end; cp++) {
	if (*cp == '\'' && cp + 1 < end && cp[1] == '\'') {
	    VSTRING_ADDCH(lit, *cp);
	    VSTRING_ADDCH(lit, *++cp);
	} else if (*cp == '%' && cp + 1 < end) {
	    if (cp[1] == '%') {
		VSTRING_ADDCH(lit, '%');
		cp += 1;
	    } else if (mysql_stmt_param(cp[1])) {
		if (VSTRING_LEN(lit) > 0) {
		    VSTRING_TERMINATE(lit);
		    mysql_stmt_append_expr_literal(expr, vstring_str(lit),
						   VSTRING_LEN(lit), &pieces);
		    VSTRING_RESET(lit);
		    VSTRING_TERMINATE(lit);
		}
		mysql_stmt_append_expr_param(expr, param_formats, cp[1], &pieces);
		has_param = 1;
		cp += 1;
	    } else {
		msg_fatal("unexpected '%.2s' in mysql query template: %.*s",
			  cp, (int) len, text);
	    }
	} else {
	    VSTRING_ADDCH(lit, *cp);
	}
    }
    if (VSTRING_LEN(lit) > 0 || pieces == 0) {
	VSTRING_TERMINATE(lit);
	mysql_stmt_append_expr_literal(expr, vstring_str(lit),
				       VSTRING_LEN(lit), &pieces);
    }

    VSTRING_TERMINATE(expr);
    if (has_param)
	vstring_sprintf_append(sql, "CONCAT(%s)", vstring_str(expr));
    else
	vstring_strcat(sql, vstring_str(expr));
    vstring_free(expr);
    vstring_free(lit);
}

static MYSQL_STATEMENT *mysql_statement_alloc(const char *mapname,
						      const char *query)
{
    MYSQL_STATEMENT *stmt;
    VSTRING *sql;
    ARGV   *param_formats;
    const char *cp;
    int     i;

    stmt = (MYSQL_STATEMENT *) mymalloc(sizeof(*stmt));
    sql = vstring_alloc(100);
    param_formats = argv_alloc(1);

    /*
     * Parse once at open time. This keeps the legacy query template intact
     * for users, while producing a MYSQL_STMT string with '?' markers.
     */
    for (cp = query; *cp; cp++) {
	if (*cp == '\'') {
	    const char *start;

	    for (start = ++cp; *cp; cp++) {
		if (*cp == '\'') {
		    if (cp[1] == '\'')
			cp += 1;
		    else
			break;
		}
	    }
	    if (*cp == 0)
		msg_fatal("%s: unterminated quote in query template: %s",
			  mapname, query);
	    mysql_stmt_append_quoted(sql, param_formats, start, cp - start);
	} else if (*cp == '%') {
	    if (cp[1] == 0) {
		msg_fatal("%s: '%%' at end of query template: %s",
			  mapname, query);
	    } else if (cp[1] == '%') {
		VSTRING_ADDCH(sql, '%');
		cp += 1;
	    } else if (mysql_stmt_param(cp[1])) {
		char    format[] = {'%', cp[1], 0};

		(void) mysql_stmt_param_slot(param_formats, format);
		VSTRING_ADDCH(sql, '?');
		cp += 1;
	    } else {
		msg_fatal("%s: unexpected '%.2s' in query template: %s",
			  mapname, cp, query);
	    }
	} else {
	    VSTRING_ADDCH(sql, *cp);
	}
    }
    VSTRING_TERMINATE(sql);

    stmt->sql = vstring_export(sql);
    stmt->param_formats = param_formats;
    /* No-placeholder templates avoid all per-parameter allocations. */
    if (param_formats->argc > 0) {
	stmt->param_bufs = (VSTRING **) mymalloc(param_formats->argc
						 * sizeof(*stmt->param_bufs));
	stmt->param_binds = (MYSQL_BIND *) mymalloc(param_formats->argc
						    * sizeof(*stmt->param_binds));
	stmt->param_lengths = (unsigned long *) mymalloc(param_formats->argc
						* sizeof(*stmt->param_lengths));
	for (i = 0; i < param_formats->argc; i++)
	    stmt->param_bufs[i] = vstring_alloc(10);
    } else {
	stmt->param_bufs = 0;
	stmt->param_binds = 0;
	stmt->param_lengths = 0;
    }
    return (stmt);
}

static void mysql_statement_free(MYSQL_STATEMENT *stmt)
{
    int     i;

    if (stmt->sql)
	myfree(stmt->sql);
    if (stmt->param_bufs) {
	for (i = 0; i < stmt->param_formats->argc; i++)
	    vstring_free(stmt->param_bufs[i]);
	myfree((void *) stmt->param_bufs);
    }
    if (stmt->param_binds)
	myfree((void *) stmt->param_binds);
    if (stmt->param_lengths)
	myfree((void *) stmt->param_lengths);
    if (stmt->param_formats)
	argv_free(stmt->param_formats);
    myfree((void *) stmt);
}

static int mysql_stmt_bind_value(DICT_MYSQL *dict_mysql, const char *format,
				         const char *name, VSTRING *buf)
{
    /* Re-expand this placeholder for the current lookup key into its slot. */
    VSTRING_RESET(buf);
    VSTRING_TERMINATE(buf);
    return (db_common_expand(dict_mysql->ctx, format, name, 0, buf,
			     (db_quote_callback_t) 0));
}

static int mysql_stmt_bind(DICT_MYSQL *dict_mysql, MYSQL_STMT *stmt,
			           const char *name)
{
    MYSQL_STATEMENT *sql_stmt = dict_mysql->stmt;
    VSTRING *param_buf;
    int     i;
    int     prev;

    if (sql_stmt->param_formats->argc == 0)
	return (1);
    /* Fill every MYSQL_BIND in the same order as the generated '?' markers. */
    memset(sql_stmt->param_binds, 0,
	   sql_stmt->param_formats->argc * sizeof(*sql_stmt->param_binds));
    for (i = 0; i < sql_stmt->param_formats->argc; i++) {
	/*
	 * Repeated %u needs repeated MySQL bind slots, but it can reuse the
	 * first expansion buffer for this lookup.
	 */
	for (prev = 0; prev < i; prev++)
	    if (strcmp(sql_stmt->param_formats->argv[prev],
		       sql_stmt->param_formats->argv[i]) == 0)
		break;
	if (prev == i) {
	    if (!mysql_stmt_bind_value(dict_mysql,
				       sql_stmt->param_formats->argv[i],
				       name, sql_stmt->param_bufs[i]))
		return (0);
	    param_buf = sql_stmt->param_bufs[i];
	} else {
	    param_buf = sql_stmt->param_bufs[prev];
	}
	sql_stmt->param_lengths[i] = VSTRING_LEN(param_buf);
	sql_stmt->param_binds[i].buffer_type = MYSQL_TYPE_STRING;
	sql_stmt->param_binds[i].buffer = (char *) vstring_str(param_buf);
	sql_stmt->param_binds[i].buffer_length = sql_stmt->param_lengths[i];
	sql_stmt->param_binds[i].length = &sql_stmt->param_lengths[i];
    }
    if (mysql_stmt_bind_param(stmt, sql_stmt->param_binds) != 0) {
	msg_warn("%s:%s: prepared query bind failed: %s",
		 dict_mysql->dict.type, dict_mysql->dict.name,
		 mysql_stmt_error(stmt));
	return (-1);
    }
    return (1);
}

/* dict_mysql_quote - escape SQL metacharacters in input string */

static void dict_mysql_quote(DICT *dict, const char *name, VSTRING *result)
{
    DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
    int     len = strlen(name);
    int     buflen;

    /*
     * We won't get integer overflows in 2*len + 1, because Postfix input
     * keys have reasonable size limits, better safe than sorry.
     */
    if (len > (INT_MAX - VSTRING_LEN(result) - 1) / 2)
	msg_panic("dict_mysql_quote: integer overflow in %lu+2*%d+1",
		  (unsigned long) VSTRING_LEN(result), len);
    buflen = 2 * len + 1;
    VSTRING_SPACE(result, buflen);

    if (dict_mysql->active_host == 0)
	msg_panic("dict_mysql_quote: no active host");
#if MYSQL_VERSION_ID >= 50706 && !defined(MARIADB_VERSION_ID)
    mysql_real_escape_string_quote(dict_mysql->active_host->db,
				   vstring_end(result), name, len, '\'');
#else
    if (mysql_real_escape_string(dict_mysql->active_host->db,
				 vstring_end(result), name, len) ==
	(unsigned long) -1) {
	msg_warn("dict_mysql: host (%s) cannot escape input string: >%s<",
		 dict_mysql->active_host->hostname,
		 mysql_error(dict_mysql->active_host->db));
	dict_mysql->active_host->stat = STATFAIL;
    }
#endif

    VSTRING_SKIP(result);
}

/* dict_mysql_lookup - find database entry */

static const char *dict_mysql_lookup(DICT *dict, const char *name)
{
    const char *myname = "dict_mysql_lookup";
    DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
    MYSQL_RES *query_res;
    MYSQL_ROW row;
    static VSTRING *result;
    static VSTRING *query;
    int     i;
    int     j;
    int     numrows;
    int     expansion;
    const char *r;
    int     domain_rc;

    dict->error = 0;

    /*
     * Don't frustrate future attempts to make Postfix UTF-8 transparent.
     */
#ifdef SNAPSHOT
    if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0
	&& !valid_utf8_stringz(name)) {
	if (msg_verbose)
	    msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'",
		     myname, dict_mysql->parser->name, name);
	return (0);
    }
#endif

    /*
     * Optionally fold the key.
     */
    if (dict->flags & DICT_FLAG_FOLD_FIX) {
	if (dict->fold_buf == 0)
	    dict->fold_buf = vstring_alloc(10);
	vstring_strcpy(dict->fold_buf, name);
	name = lowercase(vstring_str(dict->fold_buf));
    }

    /*
     * If there is a domain list for this map, then only search for addresses
     * in domains on the list. This can significantly reduce the load on the
     * server.
     */
    if ((domain_rc = db_common_check_domain(dict_mysql->ctx, name)) == 0) {
	if (msg_verbose)
	    msg_info("%s: Skipping lookup of '%s'", myname, name);
	return (0);
    }
    if (domain_rc < 0) {
	msg_warn("%s:%s 'domain' pattern match failed for '%s'",
		 dict->type, dict->name, name);
	DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0);
    }
#define INIT_VSTR(buf, len) do { \
	if (buf == 0) \
	    buf = vstring_alloc(len); \
	VSTRING_RESET(buf); \
	VSTRING_TERMINATE(buf); \
    } while (0)

    if (dict_mysql->statement_mode == MYSQL_STATEMENT_PREPARED) {
	int     found;

	INIT_VSTR(result, 10);
	if (plmysql_query_prepared(dict_mysql, name, result, &found) == 0) {
	    dict->error = DICT_ERR_RETRY;
	    return (0);
	}
	r = vstring_str(result);
	return ((dict->error == 0 && found && *r) ? r : 0);
    }

    INIT_VSTR(query, 10);

    /*
     * Suppress the lookup if the query expansion is empty
     * 
     * This initial expansion is outside the context of any specific host
     * connection, we just want to check the key pre-requisites, so when
     * quoting happens separately for each connection, we don't bother with
     * quoting...
     */
    if (!db_common_expand(dict_mysql->ctx, dict_mysql->query,
			  name, 0, query, (db_quote_callback_t) 0))
	return (0);

    /* do the query - set dict->error & cleanup if there's an error */
    if (plmysql_query(dict_mysql, name, query, &query_res) == 0) {
	dict->error = DICT_ERR_RETRY;
	return (0);
    }
    if (query_res == 0)
	return (0);
    numrows = mysql_num_rows(query_res);
    if (msg_verbose)
	msg_info("%s: retrieved %d rows", myname, numrows);
    if (numrows == 0) {
	mysql_free_result(query_res);
	return 0;
    }
    INIT_VSTR(result, 10);

    for (expansion = i = 0; i < numrows && dict->error == 0; i++) {
	row = mysql_fetch_row(query_res);
	for (j = 0; j < mysql_num_fields(query_res); j++) {
	    if (db_common_expand(dict_mysql->ctx, dict_mysql->result_format,
				 row[j], name, result, 0)
		&& dict_mysql->expansion_limit > 0
		&& ++expansion > dict_mysql->expansion_limit) {
		msg_warn("%s: %s: Expansion limit exceeded for key: '%s'",
			 myname, dict_mysql->parser->name, name);
		dict->error = DICT_ERR_RETRY;
		break;
	    }
	}
    }
    mysql_free_result(query_res);
    r = vstring_str(result);
    return ((dict->error == 0 && *r) ? r : 0);
}

/* dict_mysql_check_stat - check the status of a host */

static int dict_mysql_check_stat(HOST *host, unsigned stat, unsigned type,
				         time_t t)
{
    if ((host->stat & stat) && (!type || host->type & type)) {
	/* try not to hammer the dead hosts too often */
	if (host->stat == STATFAIL && host->ts > 0 && host->ts >= t)
	    return 0;
	return 1;
    }
    return 0;
}

/* dict_mysql_find_host - find a host with the given status */

static HOST *dict_mysql_find_host(PLMYSQL *PLDB, unsigned stat, unsigned type)
{
    time_t  t;
    int     count = 0;
    int     idx;
    int     i;

    t = time((time_t *) 0);
    for (i = 0; i < PLDB->len_hosts; i++) {
	if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t))
	    count++;
    }

    if (count) {
	idx = (count > 1) ?
	    1 + count * (double) myrand() / (1.0 + RAND_MAX) : 1;

	for (i = 0; i < PLDB->len_hosts; i++) {
	    if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t) &&
		--idx == 0)
		return PLDB->db_hosts[i];
	}
    }
    return 0;
}

/* dict_mysql_get_active - get an active connection */

static HOST *dict_mysql_get_active(DICT_MYSQL *dict_mysql)
{
    const char *myname = "dict_mysql_get_active";
    PLMYSQL *PLDB = dict_mysql->pldb;
    HOST   *host;
    int     count = RETRY_CONN_MAX;

    /* Try the active connections first; prefer the ones to UNIX sockets. */
    if ((host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL ||
	(host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL) {
	if (msg_verbose)
	    msg_info("%s: found active connection to host %s", myname,
		     host->hostname);
	return host;
    }

    /*
     * Try the remaining hosts. "count" is a safety net, in case the loop
     * takes more than RETRY_CONN_INTV and the dead hosts are no longer
     * skipped.
     */
    while (--count > 0 &&
	   ((host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
					 TYPEUNIX)) != NULL ||
	    (host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
					 TYPEINET)) != NULL)) {
	if (msg_verbose)
	    msg_info("%s: attempting to connect to host %s", myname,
		     host->hostname);
	plmysql_connect_single(dict_mysql, host);
	if (host->stat == STATACTIVE)
	    return host;
    }

    /* bad news... */
    return 0;
}

/* dict_mysql_event - callback: close idle connections */

static void dict_mysql_event(int unused_event, void *context)
{
    HOST   *host = (HOST *) context;

    if (host->db)
	plmysql_close_host(host);
}

/*
 * plmysql_query - process a MySQL query.  Return 'true' on success.
 *			On failure, log failure and try other db instances.
 *			on failure of all db instances, return 'false';
 *			close unnecessary active connections
 */

static int plmysql_query(DICT_MYSQL *dict_mysql,
				         const char *name,
				         VSTRING *query,
				         MYSQL_RES **result)
{
    HOST   *host;
    MYSQL_RES *first_result = 0;

    /* In case all hosts are down. */
    int     query_error = 1;

    errno = ENOTSUP;

    /*
     * Helper to avoid spamming the log with warnings.
     */
#define SET_ERROR_AND_WARN_ONCE(err, ...) \
    do { \
	if (err == 0) { \
	    err = 1; \
	    msg_warn(__VA_ARGS__); \
	} \
    } while (0)

    while ((host = dict_mysql_get_active(dict_mysql)) != NULL) {

	/*
	 * The active host is used to escape strings in the context of the
	 * active connection's character encoding.
	 */
	dict_mysql->active_host = host;
	VSTRING_RESET(query);
	VSTRING_TERMINATE(query);
	db_common_expand(dict_mysql->ctx, dict_mysql->query,
			 name, 0, query, dict_mysql_quote);
	/* Check for potential dict_mysql_quote() failure. */
	if (host->stat == STATFAIL) {
	    plmysql_down_host(host, dict_mysql->retry_interval);
	    continue;
	}
	if (msg_verbose)
	    msg_info("expanded and quoted query: >%s<", vstring_str(query));
	dict_mysql->active_host = 0;

	query_error = 0;
	errno = 0;

	/*
	 * The query must complete.
	 */
	if (mysql_query(host->db, vstring_str(query)) != 0) {
	    query_error = 1;
	    msg_warn("%s:%s: query failed: %s",
		     dict_mysql->dict.type, dict_mysql->dict.name,
		     mysql_error(host->db));
	}

	/*
	 * Collect all result sets to avoid synchronization errors.
	 */
	else {
	    int     next_res_status;

	    do {
		MYSQL_RES *temp_result;

		/*
		 * Keep the first result set. Reject multiple result sets.
		 */
		if ((temp_result = mysql_store_result(host->db)) != 0) {
		    if (first_result == 0) {
			first_result = temp_result;
		    } else {
			SET_ERROR_AND_WARN_ONCE(query_error,
				"%s:%s: query failed: multiple result sets "
					 "returning data are not supported",
						dict_mysql->dict.type,
						dict_mysql->dict.name);
			mysql_free_result(temp_result);
		    }
		}

		/*
		 * No result: the mysql_field_count() function must return 0
		 * to indicate that mysql_store_result() completed normally.
		 */
		else if (mysql_field_count(host->db) != 0) {
		    SET_ERROR_AND_WARN_ONCE(query_error,
			     "%s:%s: query failed (mysql_store_result): %s",
					    dict_mysql->dict.type,
					    dict_mysql->dict.name,
					    mysql_error(host->db));
		}

		/*
		 * Are there more results? -1 = no, 0 = yes, > 0 = error.
		 */
		if ((next_res_status = mysql_next_result(host->db)) > 0) {
		    SET_ERROR_AND_WARN_ONCE(query_error,
			      "%s:%s: query failed (mysql_next_result): %s",
					    dict_mysql->dict.type,
					    dict_mysql->dict.name,
					    mysql_error(host->db));
		}
	    } while (next_res_status == 0);

	    /*
	     * Enforce the require_result_set setting.
	     */
	    if (first_result == 0 && dict_mysql->require_result_set) {
		SET_ERROR_AND_WARN_ONCE(query_error,
			 "%s:%s: query failed: query returned no result set"
					"(require_result_set = yes)",
					dict_mysql->dict.type,
					dict_mysql->dict.name);
	    }
	}

	/*
	 * See what we got.
	 */
	if (query_error) {
	    plmysql_down_host(host, dict_mysql->retry_interval);
	    if (errno == 0)
		errno = ENOTSUP;
	    if (first_result) {
		mysql_free_result(first_result);
		first_result = 0;
	    }
	} else {
	    if (msg_verbose)
		msg_info("%s:%s: successful query result from host %s",
			 dict_mysql->dict.type, dict_mysql->dict.name,
			 host->hostname);
	    event_request_timer(dict_mysql_event, (void *) host,
				dict_mysql->idle_interval);
	    break;
	}
    }

    *result = first_result;
    return (query_error == 0);
}

static int plmysql_prepare_stmt(DICT_MYSQL *dict_mysql, HOST *host)
{
    if (host->stmt)
	return (1);
    if ((host->stmt = mysql_stmt_init(host->db)) == 0) {
	msg_warn("%s:%s: prepared query init failed: %s",
		 dict_mysql->dict.type, dict_mysql->dict.name,
		 mysql_error(host->db));
	return (0);
    }
    if (mysql_stmt_prepare(host->stmt, dict_mysql->stmt->sql,
			   strlen(dict_mysql->stmt->sql)) != 0) {
	msg_warn("%s:%s: prepared query failed: %s",
		 dict_mysql->dict.type, dict_mysql->dict.name,
		 mysql_stmt_error(host->stmt));
	mysql_stmt_close(host->stmt);
	host->stmt = 0;
	return (0);
    }
    return (1);
}

static int plmysql_query_prepared(DICT_MYSQL *dict_mysql, const char *name,
				          VSTRING *result, int *found)
{
    HOST   *host;
    MYSQL_RES *metadata;
    MYSQL_BIND *result_binds;
    MYSQL_BIND column_bind;
    unsigned long *lengths;
    unsigned long column_length;
    char   *dummy;
    char   *column;
    int     num_fields;
    int     i;
    int     status;
    int     query_error = 1;
    int     expansion = 0;

    *found = 0;
    errno = ENOTSUP;

    while ((host = dict_mysql_get_active(dict_mysql)) != NULL) {
	if (!plmysql_prepare_stmt(dict_mysql, host)) {
	    plmysql_down_host(host, dict_mysql->retry_interval);
	    continue;
	}
	status = mysql_stmt_bind(dict_mysql, host->stmt, name);
	if (status == 0)
	    return (1);
	if (status < 0) {
	    plmysql_down_host(host, dict_mysql->retry_interval);
	    continue;
	}

	query_error = 0;
	errno = 0;
	if (mysql_stmt_execute(host->stmt) != 0) {
	    query_error = 1;
	    msg_warn("%s:%s: prepared query execute failed: %s",
		     dict_mysql->dict.type, dict_mysql->dict.name,
		     mysql_stmt_error(host->stmt));
	}

	metadata = 0;
	result_binds = 0;
	lengths = 0;
	dummy = 0;

	if (query_error == 0) {
	    metadata = mysql_stmt_result_metadata(host->stmt);
	    if (metadata == 0) {
		if (mysql_stmt_field_count(host->stmt) != 0) {
		    query_error = 1;
		    msg_warn("%s:%s: prepared query metadata failed: %s",
			     dict_mysql->dict.type, dict_mysql->dict.name,
			     mysql_stmt_error(host->stmt));
		} else if (dict_mysql->require_result_set) {
		    query_error = 1;
		    msg_warn("%s:%s: prepared query returned no result set"
			     "(require_result_set = yes)",
			     dict_mysql->dict.type, dict_mysql->dict.name);
		}
	    }
	}

	if (query_error == 0 && metadata != 0) {
	    if (mysql_stmt_store_result(host->stmt) != 0) {
		query_error = 1;
		msg_warn("%s:%s: prepared query store result failed: %s",
			 dict_mysql->dict.type, dict_mysql->dict.name,
			 mysql_stmt_error(host->stmt));
	    } else {
		num_fields = mysql_num_fields(metadata);
		result_binds = (MYSQL_BIND *) mymalloc(num_fields
						       * sizeof(*result_binds));
		lengths = (unsigned long *) mymalloc(num_fields
						     * sizeof(*lengths));
		dummy = (char *) mymalloc(num_fields > 0 ? num_fields : 1);
		memset(result_binds, 0, num_fields * sizeof(*result_binds));
		for (i = 0; i < num_fields; i++) {
		    result_binds[i].buffer_type = MYSQL_TYPE_STRING;
		    result_binds[i].buffer = dummy + i;
		    result_binds[i].buffer_length = 1;
		    result_binds[i].length = lengths + i;
		}
		if (mysql_stmt_bind_result(host->stmt, result_binds) != 0) {
		    query_error = 1;
		    msg_warn("%s:%s: prepared query bind result failed: %s",
			     dict_mysql->dict.type, dict_mysql->dict.name,
			     mysql_stmt_error(host->stmt));
		}
	    }
	}

	while (query_error == 0 && metadata != 0
	       && (status = mysql_stmt_fetch(host->stmt)) != MYSQL_NO_DATA) {
	    if (status != 0 && status != MYSQL_DATA_TRUNCATED) {
		query_error = 1;
		msg_warn("%s:%s: prepared query fetch failed: %s",
			 dict_mysql->dict.type, dict_mysql->dict.name,
			 mysql_stmt_error(host->stmt));
		break;
	    }
	    *found = 1;
	    for (i = 0; i < num_fields; i++) {
		column = (char *) mymalloc(lengths[i] + 1);
		memset(&column_bind, 0, sizeof(column_bind));
		column_bind.buffer_type = MYSQL_TYPE_STRING;
		column_bind.buffer = column;
		column_bind.buffer_length = lengths[i] + 1;
		column_bind.length = &column_length;
		column_length = 0;
		if (mysql_stmt_fetch_column(host->stmt, &column_bind, i, 0) != 0) {
		    query_error = 1;
		    msg_warn("%s:%s: prepared query fetch column failed: %s",
			     dict_mysql->dict.type, dict_mysql->dict.name,
			     mysql_stmt_error(host->stmt));
		    myfree(column);
		    break;
		}
		column[column_length] = 0;
		if (db_common_expand(dict_mysql->ctx, dict_mysql->result_format,
				     column, name, result, 0)
		    && dict_mysql->expansion_limit > 0
		    && ++expansion > dict_mysql->expansion_limit) {
		    msg_warn("%s: %s: Expansion limit exceeded for key: '%s'",
			     __func__, dict_mysql->parser->name, name);
		    dict_mysql->dict.error = DICT_ERR_RETRY;
		    query_error = 1;
		    myfree(column);
		    break;
		}
		myfree(column);
	    }
	}

	if (result_binds)
	    myfree((void *) result_binds);
	if (lengths)
	    myfree((void *) lengths);
	if (dummy)
	    myfree(dummy);
	if (metadata)
	    mysql_free_result(metadata);
	mysql_stmt_free_result(host->stmt);

	if (dict_mysql->dict.error) {
	    event_request_timer(dict_mysql_event, (void *) host,
				dict_mysql->idle_interval);
	    return (1);
	}
	if (query_error) {
	    plmysql_down_host(host, dict_mysql->retry_interval);
	    if (errno == 0)
		errno = ENOTSUP;
	} else {
	    if (msg_verbose)
		msg_info("%s:%s: successful prepared query result from host %s",
			 dict_mysql->dict.type, dict_mysql->dict.name,
			 host->hostname);
	    event_request_timer(dict_mysql_event, (void *) host,
				dict_mysql->idle_interval);
	    break;
	}
    }
    return (query_error == 0);
}

/*
 * plmysql_connect_single -
 * used to reconnect to a single database when one is down or none is
 * connected yet. Log all errors and set the stat field of host accordingly
 */
static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host)
{
    if ((host->db = mysql_init(NULL)) == NULL)
	msg_fatal("dict_mysql: insufficient memory");
    if (dict_mysql->option_file)
	mysql_options(host->db, MYSQL_READ_DEFAULT_FILE, dict_mysql->option_file);
    if (dict_mysql->option_group && dict_mysql->option_group[0])
	mysql_options(host->db, MYSQL_READ_DEFAULT_GROUP, dict_mysql->option_group);
#if MYSQL_VERSION_ID >= 80035
    /* Preferred API. */
    if (dict_mysql->tls_key_file)
	mysql_options(host->db, MYSQL_OPT_SSL_KEY, dict_mysql->tls_key_file);
    if (dict_mysql->tls_cert_file)
	mysql_options(host->db, MYSQL_OPT_SSL_CERT, dict_mysql->tls_cert_file);
    if (dict_mysql->tls_CAfile)
	mysql_options(host->db, MYSQL_OPT_SSL_CA, dict_mysql->tls_CAfile);
    if (dict_mysql->tls_CApath)
	mysql_options(host->db, MYSQL_OPT_SSL_CAPATH, dict_mysql->tls_CApath);
    if (dict_mysql->tls_ciphers)
	mysql_options(host->db, MYSQL_OPT_SSL_CIPHER, dict_mysql->tls_ciphers);
#else
    /* Deprecated API. */
    if (dict_mysql->tls_key_file || dict_mysql->tls_cert_file ||
	dict_mysql->tls_CAfile || dict_mysql->tls_CApath || dict_mysql->tls_ciphers)
	mysql_ssl_set(host->db,
		      dict_mysql->tls_key_file, dict_mysql->tls_cert_file,
		      dict_mysql->tls_CAfile, dict_mysql->tls_CApath,
		      dict_mysql->tls_ciphers);
#endif
#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
    if (dict_mysql->tls_verify_cert != -1)
	mysql_options(host->db, DICT_MYSQL_SSL_VERIFY_SERVER_CERT,
		      &dict_mysql->tls_verify_cert);
#endif
    if (mysql_real_connect(host->db,
			   (host->type == TYPEINET ? host->name : 0),
			   dict_mysql->username,
			   dict_mysql->password,
			   dict_mysql->dbname,
			   host->port,
			   (host->type == TYPEUNIX ? host->name : 0),
			   CLIENT_MULTI_RESULTS)) {
	if (mysql_set_character_set(host->db, dict_mysql->charset) != 0) {
	    msg_warn("dict_mysql: mysql_set_character_set '%s' failed: %s",
		     dict_mysql->charset, mysql_error(host->db));
	    plmysql_down_host(host, dict_mysql->retry_interval);
	    return;
	}
	if (msg_verbose)
	    msg_info("dict_mysql: successful connection to host %s",
		     host->hostname);
	host->stat = STATACTIVE;
    } else {
	msg_warn("connect to mysql server %s: %s",
		 host->hostname, mysql_error(host->db));
	plmysql_down_host(host, dict_mysql->retry_interval);
    }
}

/* plmysql_close_host - close an established MySQL connection */
static void plmysql_close_host(HOST *host)
{
    if (host->stmt) {
	mysql_stmt_close(host->stmt);
	host->stmt = 0;
    }
    mysql_close(host->db);
    host->db = 0;
    host->stat = STATUNTRIED;
}

/*
 * plmysql_down_host - close a failed connection AND set a "stay away from
 * this host" timer
 */
static void plmysql_down_host(HOST *host, int retry_interval)
{
    if (host->stmt) {
	mysql_stmt_close(host->stmt);
	host->stmt = 0;
    }
    mysql_close(host->db);
    host->db = 0;
    host->ts = time((time_t *) 0) + retry_interval;
    host->stat = STATFAIL;
    event_cancel_timer(dict_mysql_event, (void *) host);
}

/* mysql_parse_config - parse mysql configuration file */

static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf)
{
    const char *myname = "mysql_parse_config";
    CFG_PARSER *p = dict_mysql->parser;
    VSTRING *buf;
    char   *hosts;
    char   *statement_mode;

    dict_mysql->username = cfg_get_str(p, "user", "", 0, 0);
    dict_mysql->password = cfg_get_str(p, "password", "", 0, 0);
    dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0);
    dict_mysql->charset = cfg_get_str(p, "charset", "utf8mb4", 1, 0);
    dict_mysql->retry_interval = cfg_get_int(p, "retry_interval",
					     DEF_RETRY_INTV, 1, 0);
    dict_mysql->idle_interval = cfg_get_int(p, "idle_interval",
					    DEF_IDLE_INTV, 1, 0);
    dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0);
    statement_mode = cfg_get_str(p, "statement_mode", "legacy", 1, 0);
    if (strcasecmp(statement_mode, "legacy") == 0) {
	dict_mysql->statement_mode = MYSQL_STATEMENT_LEGACY;
    } else if (strcasecmp(statement_mode, "prepared") == 0) {
	dict_mysql->statement_mode = MYSQL_STATEMENT_PREPARED;
    } else {
	msg_fatal("%s: %s: bad statement_mode value '%s'; specify "
		  "'legacy' or 'prepared'", myname, mysqlcf, statement_mode);
    }
    myfree(statement_mode);
    dict_mysql->option_file = cfg_get_str(p, "option_file", NULL, 0, 0);
    dict_mysql->option_group = cfg_get_str(p, "option_group", "client", 0, 0);
    dict_mysql->tls_key_file = cfg_get_str(p, "tls_key_file", NULL, 0, 0);
    dict_mysql->tls_cert_file = cfg_get_str(p, "tls_cert_file", NULL, 0, 0);
    dict_mysql->tls_CAfile = cfg_get_str(p, "tls_CAfile", NULL, 0, 0);
    dict_mysql->tls_CApath = cfg_get_str(p, "tls_CApath", NULL, 0, 0);
    dict_mysql->tls_ciphers = cfg_get_str(p, "tls_ciphers", NULL, 0, 0);
#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
    dict_mysql->tls_verify_cert = cfg_get_bool(p, "tls_verify_cert", -1);
#endif
    dict_mysql->require_result_set = cfg_get_bool(p, "require_result_set", 1);

    /*
     * XXX: The default should be non-zero for safety, but that is not
     * backwards compatible.
     */
    dict_mysql->expansion_limit = cfg_get_int(dict_mysql->parser,
					      "expansion_limit", 0, 0, 0);

    if ((dict_mysql->query = cfg_get_str(p, "query", NULL, 0, 0)) == 0) {

	/*
	 * No query specified -- fallback to building it from components (old
	 * style "select %s from %s where %s")
	 */
	buf = vstring_alloc(64);
	db_common_sql_build_query(buf, p);
	dict_mysql->query = vstring_export(buf);
    }

    /*
     * Must parse all templates before we can use db_common_expand()
     */
    dict_mysql->ctx = 0;
    (void) db_common_parse(&dict_mysql->dict, &dict_mysql->ctx,
			   dict_mysql->query, 1);
    (void) db_common_parse(0, &dict_mysql->ctx, dict_mysql->result_format, 0);
    db_common_parse_domain(p, dict_mysql->ctx);
    if (dict_mysql->statement_mode != MYSQL_STATEMENT_LEGACY)
	dict_mysql->stmt = mysql_statement_alloc(dict_mysql->parser->name,
						 dict_mysql->query);

    /*
     * Maps that use substring keys should only be used with the full input
     * key.
     */
    if (db_common_dict_partial(dict_mysql->ctx))
	dict_mysql->dict.flags |= DICT_FLAG_PATTERN;
    else
	dict_mysql->dict.flags |= DICT_FLAG_FIXED;
    if (dict_mysql->dict.flags & DICT_FLAG_FOLD_FIX)
	dict_mysql->dict.fold_buf = vstring_alloc(10);

    hosts = cfg_get_str(p, "hosts", "", 0, 0);

    dict_mysql->hosts = argv_split(hosts, CHARS_COMMA_SP);
    if (dict_mysql->hosts->argc == 0) {
	argv_add(dict_mysql->hosts, "localhost", ARGV_END);
	argv_terminate(dict_mysql->hosts);
	if (msg_verbose)
	    msg_info("%s: %s: no hostnames specified, defaulting to '%s'",
		     myname, mysqlcf, dict_mysql->hosts->argv[0]);
    }
    /* Don't blacklist the load balancer! */
    if (dict_mysql->hosts->argc == 1)
	argv_add(dict_mysql->hosts, dict_mysql->hosts->argv[0], (char *) 0);
    myfree(hosts);
}

/* dict_mysql_open - open MYSQL data base */

DICT   *dict_mysql_open(const char *name, int open_flags, int dict_flags)
{
    DICT_MYSQL *dict_mysql;
    CFG_PARSER *parser;

    /*
     * Sanity checks.
     */
    if (open_flags != O_RDONLY)
	return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags,
			       "%s:%s map requires O_RDONLY access mode",
			       DICT_TYPE_MYSQL, name));

    /*
     * Open the configuration file.
     */
    if ((parser = cfg_parser_alloc(name)) == 0)
	return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags,
			       "open %s: %m", name));

    dict_mysql = (DICT_MYSQL *) dict_alloc(DICT_TYPE_MYSQL, name,
					   sizeof(DICT_MYSQL));
    dict_mysql->dict.lookup = dict_mysql_lookup;
    dict_mysql->dict.close = dict_mysql_close;
    dict_mysql->dict.flags = dict_flags;
    dict_mysql->parser = parser;
    dict_mysql->stmt = 0;
    mysql_parse_config(dict_mysql, name);
    dict_mysql->active_host = 0;
    dict_mysql->pldb = plmysql_init(dict_mysql->hosts);
    if (dict_mysql->pldb == NULL)
	msg_fatal("couldn't initialize pldb!\n");
    dict_mysql->dict.owner = cfg_get_owner(dict_mysql->parser);
    return (&dict_mysql->dict);
}

/*
 * plmysql_init - initialize a MYSQL database.
 *		    Return NULL on failure, or a PLMYSQL * on success.
 */
static PLMYSQL *plmysql_init(ARGV *hosts)
{
    PLMYSQL *PLDB;
    int     i;

    if ((PLDB = (PLMYSQL *) mymalloc(sizeof(PLMYSQL))) == 0)
	msg_fatal("mymalloc of pldb failed");

    PLDB->len_hosts = hosts->argc;
    if ((PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc)) == 0)
	return (0);
    for (i = 0; i < hosts->argc; i++)
	PLDB->db_hosts[i] = host_init(hosts->argv[i]);

    return PLDB;
}


/* host_init - initialize HOST structure */
static HOST *host_init(const char *hostname)
{
    const char *myname = "mysql host_init";
    HOST   *host = (HOST *) mymalloc(sizeof(HOST));
    const char *d = hostname;
    char   *s;

    host->db = 0;
    host->stmt = 0;
    host->hostname = mystrdup(hostname);
    host->port = 0;
    host->stat = STATUNTRIED;
    host->ts = 0;

    /*
     * Ad-hoc parsing code. Expect "unix:pathname" or "inet:host:port", where
     * both "inet:" and ":port" are optional.
     */
    if (strncmp(d, "unix:", 5) == 0) {
	d += 5;
	host->type = TYPEUNIX;
    } else {
	if (strncmp(d, "inet:", 5) == 0)
	    d += 5;
	host->type = TYPEINET;
    }
    host->name = mystrdup(d);
    if ((s = split_at_right(host->name, ':')) != 0)
	host->port = ntohs(find_inet_port(s, "tcp"));
    if (strcasecmp(host->name, "localhost") == 0) {
	/* The MySQL way: this will actually connect over the UNIX socket */
	myfree(host->name);
	host->name = 0;
	host->type = TYPEUNIX;
    }
    if (msg_verbose > 1)
	msg_info("%s: host=%s, port=%d, type=%s", myname,
		 host->name ? host->name : "localhost",
		 host->port, host->type == TYPEUNIX ? "unix" : "inet");
    return host;
}

/* dict_mysql_close - close MYSQL database */

static void dict_mysql_close(DICT *dict)
{
    DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;

    plmysql_dealloc(dict_mysql->pldb);
    cfg_parser_free(dict_mysql->parser);
    myfree(dict_mysql->username);
    myfree(dict_mysql->password);
    myfree(dict_mysql->dbname);
    myfree(dict_mysql->charset);
    myfree(dict_mysql->query);
    myfree(dict_mysql->result_format);
    if (dict_mysql->option_file)
	myfree(dict_mysql->option_file);
    if (dict_mysql->option_group)
	myfree(dict_mysql->option_group);
    if (dict_mysql->tls_key_file)
	myfree(dict_mysql->tls_key_file);
    if (dict_mysql->tls_cert_file)
	myfree(dict_mysql->tls_cert_file);
    if (dict_mysql->tls_CAfile)
	myfree(dict_mysql->tls_CAfile);
    if (dict_mysql->tls_CApath)
	myfree(dict_mysql->tls_CApath);
    if (dict_mysql->tls_ciphers)
	myfree(dict_mysql->tls_ciphers);
    if (dict_mysql->hosts)
	argv_free(dict_mysql->hosts);
    if (dict_mysql->ctx)
	db_common_free_ctx(dict_mysql->ctx);
    if (dict_mysql->stmt)
	mysql_statement_free(dict_mysql->stmt);
    if (dict->fold_buf)
	vstring_free(dict->fold_buf);
    dict_free(dict);
}

/* plmysql_dealloc - free memory associated with PLMYSQL close databases */
static void plmysql_dealloc(PLMYSQL *PLDB)
{
    int     i;

    for (i = 0; i < PLDB->len_hosts; i++) {
	event_cancel_timer(dict_mysql_event, (void *) (PLDB->db_hosts[i]));
	if (PLDB->db_hosts[i]->stmt)
	    mysql_stmt_close(PLDB->db_hosts[i]->stmt);
	if (PLDB->db_hosts[i]->db)
	    mysql_close(PLDB->db_hosts[i]->db);
	myfree(PLDB->db_hosts[i]->hostname);
	if (PLDB->db_hosts[i]->name)
	    myfree(PLDB->db_hosts[i]->name);
	myfree((void *) PLDB->db_hosts[i]);
    }
    myfree((void *) PLDB->db_hosts);
    myfree((void *) (PLDB));
}

#endif
