/* omlibdbi.c
 * This is the implementation of the dbi output module.
 *
 * NOTE: read comments in module-template.h to understand how this file
 *       works!
 *
 * This depends on libdbi being present with the proper settings. Older
 * versions do not necessarily have them. Please visit this bug tracker
 * for details: http://bugzilla.adiscon.com/show_bug.cgi?id=31
 *
 * File begun on 2008-02-14 by RGerhards (extracted from syslogd.c)
 *
 * Copyright 2008-2016 Adiscon GmbH.
 *
 * This file is part of rsyslog.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *       -or-
 *       see COPYING.ASL20 in the source distribution
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "config.h"
#include "rsyslog.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
#include <libgen.h>
#include <dbi/dbi.h>
#include "dirty.h"
#include "syslogd-types.h"
#include "cfsysline.h"
#include "conf.h"
#include "srUtils.h"
#include "template.h"
#include "module-template.h"
#include "debug.h"
#include "errmsg.h"
#include "conf.h"

#undef HAVE_DBI_TXSUPP
/* transaction support disabled in v8 -- TODO: reenable */

MODULE_TYPE_OUTPUT;
MODULE_TYPE_NOKEEP;
MODULE_CNFNAME("omlibdbi")

/* internal structures
 */
DEF_OMOD_STATIC_DATA;
static int bDbiInitialized = 0; /* dbi_initialize() can only be called one - this keeps track of it */

typedef struct _instanceData {
    uchar *dbiDrvrDir; /* where do the dbi drivers reside? */
    dbi_conn conn; /* handle to database */
    uchar *drvrName; /* driver to use */
    uchar *host; /* host to connect to */
    uchar *usrName; /* user name for connect */
    uchar *pwd; /* password for connect */
    uchar *dbName; /* database to use */
    unsigned uLastDBErrno; /* last errno returned by libdbi or 0 if all is well */
    uchar *tplName; /* format template to use */
    int txSupport; /* transaction support */
} instanceData;

typedef struct wrkrInstanceData {
    instanceData *pData;
} wrkrInstanceData_t;

typedef struct configSettings_s {
    uchar *dbiDrvrDir; /* global: where do the dbi drivers reside? */
    uchar *drvrName; /* driver to use */
    uchar *host; /* host to connect to */
    uchar *usrName; /* user name for connect */
    uchar *pwd; /* password for connect */
    uchar *dbName; /* database to use */
} configSettings_t;
static configSettings_t cs;
static uchar *pszFileDfltTplName; /* name of the default template to use */

struct modConfData_s {
    rsconf_t *pConf; /* our overall config object */
    uchar *dbiDrvrDir; /* where do the dbi drivers reside? */
    uchar *tplName; /* default template */
};

static modConfData_t *loadModConf = NULL; /* modConf ptr to use for the current load process */
static modConfData_t *runModConf = NULL; /* modConf ptr to use for the current exec process */
static int bLegacyCnfModGlobalsPermitted; /* are legacy module-global config parameters permitted? */

static pthread_mutex_t mutDoAct = PTHREAD_MUTEX_INITIALIZER;


/* tables for interfacing with the v6 config system */
/* module-global parameters */
static struct cnfparamdescr modpdescr[] = {{"template", eCmdHdlrGetWord, 0}, {"driverdirectory", eCmdHdlrGetWord, 0}};
static struct cnfparamblk modpblk = {CNFPARAMBLK_VERSION, sizeof(modpdescr) / sizeof(struct cnfparamdescr), modpdescr};
/* action (instance) parameters */
static struct cnfparamdescr actpdescr[] = {{"server", eCmdHdlrGetWord, 1}, {"db", eCmdHdlrGetWord, 1},
                                           {"uid", eCmdHdlrGetWord, 1},    {"pwd", eCmdHdlrGetWord, 1},
                                           {"driver", eCmdHdlrGetWord, 1}, {"template", eCmdHdlrGetWord, 0}};
static struct cnfparamblk actpblk = {CNFPARAMBLK_VERSION, sizeof(actpdescr) / sizeof(struct cnfparamdescr), actpdescr};

/* this function gets the default template. It coordinates action between
 * old-style and new-style configuration parts.
 */
static uchar *getDfltTpl(void) {
    if (loadModConf != NULL && loadModConf->tplName != NULL)
        return loadModConf->tplName;
    else if (pszFileDfltTplName == NULL)
        return (uchar *)" StdDBFmt";
    else
        return pszFileDfltTplName;
}


BEGINinitConfVars /* (re)set config variables to default values */
    CODESTARTinitConfVars;
    cs.dbiDrvrDir = NULL;
    cs.drvrName = NULL;
    cs.host = NULL;
    cs.usrName = NULL;
    cs.pwd = NULL;
    cs.dbName = NULL;
ENDinitConfVars


/* config settings */
#ifdef HAVE_DBI_R
static dbi_inst dbiInst;
#endif


BEGINcreateInstance
    CODESTARTcreateInstance;
ENDcreateInstance

BEGINcreateWrkrInstance
    CODESTARTcreateWrkrInstance;
ENDcreateWrkrInstance


BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
    /* we do not like repeated message reduction inside the database */
ENDisCompatibleWithFeature


/* The following function is responsible for closing a
 * database connection.
 */
static void closeConn(instanceData *pData) {
    assert(pData != NULL);
    if (pData->conn != NULL) { /* just to be on the safe side... */
        dbi_conn_close(pData->conn);
        pData->conn = NULL;
    }
}

BEGINfreeInstance
    CODESTARTfreeInstance;
    free(pData->drvrName);
    free(pData->host);
    free(pData->usrName);
    free(pData->pwd);
    free(pData->dbName);
ENDfreeInstance

BEGINfreeWrkrInstance
    CODESTARTfreeWrkrInstance;
    closeConn(pWrkrData->pData);
ENDfreeWrkrInstance

BEGINdbgPrintInstInfo
    CODESTARTdbgPrintInstInfo;
    /* nothing special here */
ENDdbgPrintInstInfo


/* log a database error with descriptive message.
 * We check if we have a valid database handle. If not, we simply
 * report an error, but can not be specific. RGerhards, 2007-01-30
 */
static void reportDBError(instanceData *pData, int bSilent) {
    unsigned uDBErrno;
    char errMsg[1024];
    const char *pszDbiErr;

    assert(pData != NULL);

    /* output log message */
    errno = 0;
    if (pData->conn == NULL) {
        LogError(0, NO_ERRCODE, "unknown DB error occurred - could not obtain connection handle");
    } else { /* we can ask dbi for the error description... */
        uDBErrno = dbi_conn_error(pData->conn, &pszDbiErr);
        snprintf(errMsg, sizeof(errMsg), "db error (%d): %s\n", uDBErrno, pszDbiErr);
        if (bSilent || uDBErrno == pData->uLastDBErrno)
            dbgprintf("libdbi, DBError(silent): %s\n", errMsg);
        else {
            pData->uLastDBErrno = uDBErrno;
            LogError(0, NO_ERRCODE, "%s", errMsg);
        }
    }
}


/* The following function is responsible for initializing a connection
 */
static rsRetVal initConn(instanceData *pData, int bSilent) {
    DEFiRet;
    int iDrvrsLoaded;

    assert(pData != NULL);
    assert(pData->conn == NULL);

    if (bDbiInitialized == 0) {
        /* we need to init libdbi first */
#ifdef HAVE_DBI_R
        iDrvrsLoaded = dbi_initialize_r((char *)pData->dbiDrvrDir, &dbiInst);
#else
        iDrvrsLoaded = dbi_initialize((char *)pData->dbiDrvrDir);
#endif
        if (iDrvrsLoaded == 0) {
            LogError(0, RS_RET_SUSPENDED,
                     "libdbi error: libdbi or libdbi drivers not "
                     "present on this system - suspending.");
            ABORT_FINALIZE(RS_RET_SUSPENDED);
        } else if (iDrvrsLoaded < 0) {
            LogError(0, RS_RET_SUSPENDED,
                     "libdbi error: libdbi could not be "
                     "initialized (do you have any dbi drivers installed?) - suspending.");
            ABORT_FINALIZE(RS_RET_SUSPENDED);
        }
        bDbiInitialized = 1; /* we are done for the rest of our existence... */
    }

#ifdef HAVE_DBI_R
    pData->conn = dbi_conn_new_r((char *)pData->drvrName, dbiInst);
#else
    pData->conn = dbi_conn_new((char *)pData->drvrName);
#endif
    if (pData->conn == NULL) {
        LogError(0, RS_RET_SUSPENDED, "can not initialize libdbi connection");
        ABORT_FINALIZE(RS_RET_SUSPENDED);
    } else { /* we could get the handle, now on with work... */
        /* Connect to database */
        dbi_conn_set_option(pData->conn, "host", (char *)pData->host);
        dbi_conn_set_option(pData->conn, "username", (char *)pData->usrName);

        /* libdbi-driver-sqlite(2/3) requires to provide sqlite3_db dir which is absolute
           path, where database file lives,
         * and dbname, which is database file name itself. So in order to keep the config API unchanged,
         * we split the dbname to path and filename.
         */
        int is_sqlite2 = !strcmp((const char *)pData->drvrName, "sqlite");
        int is_sqlite3 = !strcmp((const char *)pData->drvrName, "sqlite3");
        if (is_sqlite2 || is_sqlite3) {
            char *const dn_org = strdup((char *)pData->dbName);
            char *const dn = dirname(dn_org);
            dbi_conn_set_option(pData->conn, is_sqlite3 ? "sqlite3_dbdir" : "sqlite_dbdir", dn);
            free(dn_org); /* Free original buffer - dirname may return different pointer */

            char *tmp = strdup((char *)pData->dbName);
            char *bn = basename(tmp);
            dbi_conn_set_option(pData->conn, "dbname", bn);
            free(tmp);
        } else {
            dbi_conn_set_option(pData->conn, "dbname", (char *)pData->dbName);
        }
        if (pData->pwd != NULL) dbi_conn_set_option(pData->conn, "password", (char *)pData->pwd);
        if (dbi_conn_connect(pData->conn) < 0) {
            reportDBError(pData, bSilent);
            closeConn(pData); /* ignore any error we may get */
            ABORT_FINALIZE(RS_RET_SUSPENDED);
        }
        pData->txSupport = dbi_conn_cap_get(pData->conn, "transaction_support");
    }

finalize_it:
    RETiRet;
}


/* The following function writes the current log entry
 * to an established database connection.
 */
static rsRetVal writeDB(const uchar *psz, instanceData *const __restrict__ pData) {
    DEFiRet;
    dbi_result dbiRes = NULL;

    assert(psz != NULL);
    assert(pData != NULL);

    /* see if we are ready to proceed */
    if (pData->conn == NULL) {
        CHKiRet(initConn(pData, 0));
    }

    /* try insert */
    if ((dbiRes = dbi_conn_query(pData->conn, (const char *)psz)) == NULL) {
        /* error occurred, try to re-init connection and retry */
        closeConn(pData); /* close the current handle */
        CHKiRet(initConn(pData, 0)); /* try to re-open */
        if ((dbiRes = dbi_conn_query(pData->conn, (const char *)psz)) == NULL) { /* re-try insert */
            /* we failed, giving up for now */
            reportDBError(pData, 0);
            closeConn(pData); /* free ressources */
            ABORT_FINALIZE(RS_RET_SUSPENDED);
        }
    }

finalize_it:
    if (iRet == RS_RET_OK) {
        pData->uLastDBErrno = 0; /* reset error for error supression */
    }

    if (dbiRes != NULL) dbi_result_free(dbiRes);

    RETiRet;
}


BEGINtryResume
    CODESTARTtryResume;
    if (pWrkrData->pData->conn == NULL) {
        iRet = initConn(pWrkrData->pData, 1);
    }
ENDtryResume

/* transaction support 2013-03 */
BEGINbeginTransaction
    CODESTARTbeginTransaction;
    if (pWrkrData->pData->conn == NULL) {
        CHKiRet(initConn(pWrkrData->pData, 0));
    }
#ifdef HAVE_DBI_TXSUPP
    if (pData->txSupport == 1) {
        if (dbi_conn_transaction_begin(pData->conn) != 0) {
            const char *emsg;
            dbi_conn_error(pData->conn, &emsg);
            dbgprintf(
                "libdbi server error: begin transaction "
                "not successful: %s\n",
                emsg);
            closeConn(pData);
            ABORT_FINALIZE(RS_RET_SUSPENDED);
        }
    }
#endif
finalize_it:
ENDbeginTransaction
/* end transaction */

BEGINdoAction
    CODESTARTdoAction;
    pthread_mutex_lock(&mutDoAct);
    CHKiRet(writeDB(ppString[0], pWrkrData->pData));
#ifdef HAVE_DBI_TXSUPP
    if (pData->txSupport == 1) {
        iRet = RS_RET_DEFER_COMMIT;
    }
#endif
finalize_it:
    pthread_mutex_unlock(&mutDoAct);
ENDdoAction

/* transaction support 2013-03 */
BEGINendTransaction
    CODESTARTendTransaction;
#ifdef HAVE_DBI_TXSUPP
    if (dbi_conn_transaction_commit(pData->conn) != 0) {
        const char *emsg;
        dbi_conn_error(pData->conn, &emsg);
        dbgprintf("libdbi server error: transaction not committed: %s\n", emsg);
        closeConn(pData);
        iRet = RS_RET_SUSPENDED;
    }
#endif
ENDendTransaction
/* end transaction */

BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    loadModConf = pModConf;
    pModConf->pConf = pConf;
    pModConf->tplName = NULL;
    bLegacyCnfModGlobalsPermitted = 1;
ENDbeginCnfLoad

BEGINsetModCnf
    struct cnfparamvals *pvals = NULL;
    int i;
    CODESTARTsetModCnf;
    pvals = nvlstGetParams(lst, &modpblk, NULL);
    if (pvals == NULL) {
        LogError(0, RS_RET_MISSING_CNFPARAMS,
                 "omlibdbi: error processing "
                 "module config parameters [module(...)]");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    if (Debug) {
        dbgprintf("module (global) param blk for omlibdbi:\n");
        cnfparamsPrint(&modpblk, pvals);
    }

    for (i = 0; i < modpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(modpblk.descr[i].name, "template")) {
            CHKmalloc(loadModConf->tplName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            if (pszFileDfltTplName != NULL) {
                LogError(0, RS_RET_DUP_PARAM,
                         "omlibdbi: warning: default template "
                         "was already set via legacy directive - may lead to inconsistent "
                         "results.");
            }
        } else if (!strcmp(modpblk.descr[i].name, "driverdirectory")) {
            CHKmalloc(loadModConf->dbiDrvrDir = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else {
            dbgprintf(
                "omlibdbi: program error, non-handled "
                "param '%s' in beginCnfLoad\n",
                modpblk.descr[i].name);
        }
    }
    bLegacyCnfModGlobalsPermitted = 0;
finalize_it:
    if (pvals != NULL) cnfparamvalsDestruct(pvals, &modpblk);
ENDsetModCnf

BEGINendCnfLoad
    CODESTARTendCnfLoad;
    loadModConf = NULL; /* done loading */
    /* free legacy config vars */
    free(cs.dbiDrvrDir);
    free(cs.drvrName);
    free(cs.host);
    free(cs.usrName);
    free(cs.pwd);
    free(cs.dbName);
    cs.dbiDrvrDir = NULL;
    cs.drvrName = NULL;
    cs.host = NULL;
    cs.usrName = NULL;
    cs.pwd = NULL;
    cs.dbName = NULL;
    free(pszFileDfltTplName);
    pszFileDfltTplName = NULL;
ENDendCnfLoad

BEGINcheckCnf
    CODESTARTcheckCnf;
ENDcheckCnf

BEGINactivateCnf
    CODESTARTactivateCnf;
    runModConf = pModConf;
ENDactivateCnf

BEGINfreeCnf
    CODESTARTfreeCnf;
    free(pModConf->tplName);
    free(pModConf->dbiDrvrDir);
ENDfreeCnf


static inline void setInstParamDefaults(instanceData *pData) {
    pData->tplName = NULL;
}


BEGINnewActInst
    struct cnfparamvals *pvals;
    uchar *tplToUse;
    int i;
    CODESTARTnewActInst;
    if ((pvals = nvlstGetParams(lst, &actpblk, NULL)) == NULL) {
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    CHKiRet(createInstance(&pData));
    setInstParamDefaults(pData);
    CODE_STD_STRING_REQUESTnewActInst(1);
    for (i = 0; i < actpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(actpblk.descr[i].name, "server")) {
            CHKmalloc(pData->host = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "db")) {
            CHKmalloc(pData->dbName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "uid")) {
            CHKmalloc(pData->usrName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "pwd")) {
            CHKmalloc(pData->pwd = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "driver")) {
            CHKmalloc(pData->drvrName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "template")) {
            CHKmalloc(pData->tplName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else {
            dbgprintf(
                "omlibdbi: program error, non-handled "
                "param '%s'\n",
                actpblk.descr[i].name);
        }
    }

    tplToUse = (pData->tplName == NULL) ? (uchar *)strdup((char *)getDfltTpl()) : pData->tplName;
    CHKiRet(OMSRsetEntry(*ppOMSR, 0, tplToUse, OMSR_RQD_TPL_OPT_SQL));
    CODE_STD_FINALIZERnewActInst;
    cnfparamvalsDestruct(pvals, &actpblk);
ENDnewActInst


BEGINparseSelectorAct
    CODESTARTparseSelectorAct;
    CODE_STD_STRING_REQUESTparseSelectorAct(1) if (!strncmp((char *)p, ":omlibdbi:", sizeof(":omlibdbi:") - 1)) {
        p += sizeof(":omlibdbi:") - 1; /* eat indicator sequence (-1 because of '\0'!) */
    }
    else {
        ABORT_FINALIZE(RS_RET_CONFLINE_UNPROCESSED);
    }

    /* ok, if we reach this point, we have something for us */
    CHKiRet(createInstance(&pData));
    /* no create the instance based on what we currently have */
    if (cs.drvrName == NULL) {
        LogError(0, RS_RET_NO_DRIVERNAME,
                 "omlibdbi: no db driver name given - action can not "
                 "be created");
        ABORT_FINALIZE(RS_RET_NO_DRIVERNAME);
    }

    if ((pData->drvrName = (uchar *)strdup((char *)cs.drvrName)) == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
    /* NULL values are supported because drivers have different needs.
     * They will err out on connect. -- rgerhards, 2008-02-15
     */
    if (cs.host != NULL) CHKmalloc(pData->host = (uchar *)strdup((char *)cs.host));
    if (cs.usrName != NULL) CHKmalloc(pData->usrName = (uchar *)strdup((char *)cs.usrName));
    if (cs.dbName != NULL) CHKmalloc(pData->dbName = (uchar *)strdup((char *)cs.dbName));
    if (cs.pwd != NULL) CHKmalloc(pData->pwd = (uchar *)strdup((char *)cs.pwd));
    if (cs.dbiDrvrDir != NULL) CHKmalloc(loadModConf->dbiDrvrDir = (uchar *)strdup((char *)cs.dbiDrvrDir));
    iRet = cflineParseTemplateName(&p, *ppOMSR, 0, OMSR_RQD_TPL_OPT_SQL, getDfltTpl());
    CODE_STD_FINALIZERparseSelectorAct
ENDparseSelectorAct


BEGINmodExit
    CODESTARTmodExit;
    /* if we initialized libdbi, we now need to cleanup */
    if (bDbiInitialized) {
#ifdef HAVE_DBI_R
        dbi_shutdown_r(dbiInst);
#else
        dbi_shutdown();
#endif
    }
ENDmodExit


BEGINqueryEtryPt
    CODESTARTqueryEtryPt;
    CODEqueryEtryPt_STD_OMOD_QUERIES;
    CODEqueryEtryPt_STD_OMOD8_QUERIES;
    CODEqueryEtryPt_STD_CONF2_QUERIES;
    CODEqueryEtryPt_STD_CONF2_setModCnf_QUERIES;
    CODEqueryEtryPt_STD_CONF2_OMOD_QUERIES;
    CODEqueryEtryPt_TXIF_OMOD_QUERIES /* we support the transactional interface! */
ENDqueryEtryPt


/* Reset config variables for this module to default values.
 */
static rsRetVal resetConfigVariables(uchar __attribute__((unused)) * pp, void __attribute__((unused)) * pVal) {
    DEFiRet;
    free(cs.dbiDrvrDir);
    cs.dbiDrvrDir = NULL;
    free(cs.drvrName);
    cs.drvrName = NULL;
    free(cs.host);
    cs.host = NULL;
    free(cs.usrName);
    cs.usrName = NULL;
    free(cs.pwd);
    cs.pwd = NULL;
    free(cs.dbName);
    cs.dbName = NULL;
    RETiRet;
}


BEGINmodInit()
    CODESTARTmodInit;
    INITLegCnfVars;
    *ipIFVersProvided = CURR_MOD_IF_VERSION; /* we only support the current interface specification */
    CODEmodInit_QueryRegCFSLineHdlr
#ifndef HAVE_DBI_TXSUPP
        DBGPRINTF("omlibdbi: no transaction support in libdbi\n");
#endif
    CHKiRet(regCfSysLineHdlr2((uchar *)"actionlibdbidriverdirectory", 0, eCmdHdlrGetWord, NULL, &cs.dbiDrvrDir,
                              STD_LOADABLE_MODULE_ID, &bLegacyCnfModGlobalsPermitted));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"actionlibdbidriver", 0, eCmdHdlrGetWord, NULL, &cs.drvrName,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(
        omsdRegCFSLineHdlr((uchar *)"actionlibdbihost", 0, eCmdHdlrGetWord, NULL, &cs.host, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"actionlibdbiusername", 0, eCmdHdlrGetWord, NULL, &cs.usrName,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(
        omsdRegCFSLineHdlr((uchar *)"actionlibdbipassword", 0, eCmdHdlrGetWord, NULL, &cs.pwd, STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"actionlibdbidbname", 0, eCmdHdlrGetWord, NULL, &cs.dbName,
                               STD_LOADABLE_MODULE_ID));
    CHKiRet(omsdRegCFSLineHdlr((uchar *)"resetconfigvariables", 1, eCmdHdlrCustomHandler, resetConfigVariables, NULL,
                               STD_LOADABLE_MODULE_ID));
    DBGPRINTF("omlibdbi compiled with version %s loaded, libdbi version %s\n", VERSION, dbi_version());
ENDmodInit

/* vim:set ai:
 */
