/* -*- mode: c++; indent-tabs-mode: nil -*- */
/*
    QC_DatasourcePool.cpp

    Qore Programming Language

    Copyright (C) 2003 - 2023 Qore Technologies, s.r.o.

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.

    Note that the Qore library is released under a choice of three open-source
    licenses: MIT (as above), LGPL 2+, or GPL 2+; see README-LICENSE for more
    information.
*/

#include <qore/Qore.h>
#include <qore/DBI.h>

#include "qore/intern/QC_DatasourcePool.h"
#include "qore/intern/QC_Serializable.h"

DLLLOCAL extern QoreClass* QC_ABSTRACTDATASOURCE;
DLLLOCAL extern QoreClass* QC_ABSTRACTSQLSTATEMENT;
DLLLOCAL extern QoreClass* QC_SQLSTATEMENT;

#define DP_MIN 3
#define DP_MAX 10

static const char *DSPC_ERR = "DATASOURCEPOOL-CONSTRUCTOR-ERROR";

static void dsp_constructor_hash(const QoreHashNode* opts, QoreObject* self, Queue* queue, const QoreValue& arg, ExceptionSink* xsink) {
   ReferenceHolder<Queue> q(queue, xsink);

   const char *str = check_hash_key(opts, "type", DSPC_ERR, xsink);
   if (*xsink)
      return;

   if (!str) {
      xsink->raiseException(DSPC_ERR, "expecting a string value with the 'type' key giving the driver name");
      return;
   }

   DBIDriver* db_driver = DBI.find(str);
   if (!db_driver) {
      if (!*xsink)
         xsink->raiseException("DATASOURCEPOOL-UNSUPPORTED-DATABASE", "no DBI driver can be found for database type '%s'", str);
      return;
   }

   const char* user = check_hash_key(opts, "user", DSPC_ERR, xsink);
   if (*xsink) return;

   const char* pass = check_hash_key(opts, "pass", DSPC_ERR, xsink);
   if (*xsink) return;

   const char* db = check_hash_key(opts, "db", DSPC_ERR, xsink);
   if (*xsink) return;

   const char* charset = check_hash_key(opts, "charset", DSPC_ERR, xsink);
   if (*xsink) return;

   const char* host = check_hash_key(opts, "host", DSPC_ERR, xsink);
   if (*xsink) return;

   bool found;
   int port = (int)opts->getKeyValue("port").getAsBigInt();
   if (port < 0) {
      xsink->raiseException(DSPC_ERR, "port value must be zero (meaning use the default port) or positive (value given: %d)", port);
      return;
   }

   // get options
   int min = 0, max = 0;

   QoreValue p = opts->getKeyValue("options");
   const QoreHashNode* opt_hash;
   if (!p.isNothing()) {
      if (p.getType() != NT_HASH) {
         xsink->raiseException(DSPC_ERR, "'options' key is not type 'hash', instead got type '%s'", p.getTypeName());
         return;
      }

      opt_hash = p.get<const QoreHashNode>();

      min = (int)opt_hash->getKeyValueExistence("min", found, xsink).getAsBigInt();
      if (*xsink)
         return;
      if (found) {
         if (min < 0) {
            xsink->raiseException(DSPC_ERR, "minimum connections must be > 0 (value given: %d)", min);
            return;
         }
      }

      max = (int)opt_hash->getKeyValueExistence("max", found, xsink).getAsBigInt();
      if (*xsink)
         return;
      if (found) {
         if (max < min) {
            xsink->raiseException(DSPC_ERR, "maximum connections must be >= min(%d) (value given: %d)", min, max);
            return;
         }
         if (!max) {
            xsink->raiseException(DSPC_ERR, "maximum connections must be > 0 (value given: %d)", min, max);
            return;
         }
      }
   }
   else
      opt_hash = nullptr;

   if (!min)
      min = DP_MIN;

   if (!max)
      max = DP_MAX;

   SimpleRefHolder<DatasourcePool> ds(new DatasourcePool(xsink, db_driver, user, pass, db, charset, host, min, max, port, opt_hash, q.release(), arg.refSelf()));
   if (!*xsink)
      self->setPrivate(CID_DATASOURCEPOOL, ds.release());
   else
      ds->destructor(xsink);
}

static QoreHashNode* DatasourcePool_serializer(const QoreObject& self, const DatasourcePool& dsp, QoreSerializationContext& context, ExceptionSink* xsink) {
    SimpleRefHolder<QoreStringNode> str(const_cast<DatasourcePool&>(dsp).getConfigString(xsink));
    if (*xsink) {
        return nullptr;
    }

    ReferenceHolder<QoreHashNode> rv(new QoreHashNode(autoTypeInfo), xsink);
    rv->setKeyValue("conf", str.release(), xsink);
    return rv.release();
}

static void DatasourcePool_deserializer(QoreObject& self, const QoreHashNode* sdata, QoreDeserializationContext& context, ExceptionSink* xsink) {
    if (sdata) {
        QoreValue val = sdata->getKeyValue("conf");
        if (val) {
            if (val.getType() != NT_STRING) {
                xsink->raiseException("DESERIALIZATION-ERROR", "DatasourcePool serialization key 'conf' has type '%s'; expecting 'string'", val.getTypeName());
                return;
            }

            ReferenceHolder<QoreHashNode> h(parseDatasource(val.get<QoreStringNode>()->c_str(), xsink), xsink);
            if (!h) {
                assert(*xsink);
                return;
            }

            dsp_constructor_hash(*h, &self, nullptr, QoreValue(), xsink);
            return;
        }
    }

    xsink->raiseException("DESERIALIZATION-ERROR", "missing serialization data for the DatasourcePool class");
}

//! Provides transparent per-thread, per-transaction datasource connection pooling
/** @par Overview
    In most cases, the %DatasourcePool class can be used as a drop-in replacement for the Datasource class with autocommit disabled; when a transaction begins, a datasource will be automatically assigned to the calling thread, and it will only be released when a commit or rollback is called on the object. If no datasource is available, the calling thread will block until a datasource comes available.\n\n
    Note that the same principles apply to %SQL and database driver usage as with the Datasource class, see the @ref Qore::SQL::Datasource "Datasource" class documentation for more information.\n\n
    The %DatasourcePool class uses %Qore's thread resource tracking infrastructure to raise an exception if a thread terminates while a connection is allocated to it. If %Qore user code enters a transaction with a %DatasourcePool object and the thread terminates without closing the transaction (via DatasourcePool::commit() or DatasourcePool::rollback()), an exception will automatically be raised, the transaction will be rolled back, and the Datasource connection will be freed to the pool.

    @anchor datasourcepool_connection_allocations
    @par DatasourcePool Connection Allocations
    The following methods allocate a persistent connection to the calling thread:
    - DatasourcePool::exec()
    - DatasourcePool::vexec()
    - DatasourcePool::execRaw()
    - DatasourcePool::beginTransaction()
    .
    The connection is released to the pool when DatasourcePool::commit() or DatasourcePool::rollback() are called (or in the case the thread terminates, in which case an exception is raised as well).\n\n
    To begin a transaction with one of the select methods (for example, with \c "select for update"), call DatasourcePool::beginTransaction() first to manually dedicate a Datasource to the thread before calling the select method. Otherwise statements that should be in the same transaction may be executed in different connections.\n\n
    Executing a %DatasourcePool method while not in a transaction is realized by allocating a temporary connection to the calling thread which is re-released when the method returns. No explicit commits are executed by the class, therefore it is an error to execute transaction-relevant commands without first calling DatasourcePool::exec(), DatasourcePool::vexec(), DatasourcePool::execRaw(), or DatasourcePool::beginTransaction().\n\n
    Note that the @ref Qore::SQL::SQLStatement "SQLStatement" class also grabs allocates a persistent connection to the calling thread when executing if it is created using a %DatasourcePool object in the constructor; when connections are returned to the pool, all SQLStatement objects using the allocated datasource are closed automatically; for more information see the @ref Qore::SQL::SQLStatement "SQLStatement" class documentation.

    @par Thread Resource Handling
    The DatasourcePool class manages connection allocations as a @ref thread_resources "thread resource"; if the
    connection is not released with a call to @ref Qore::SQL::DatasourcePool::commit() "DatasourcePool::commit()"
    or @ref Qore::SQL::DatasourcePool::rollback() "DatasourcePool::rollback()" when the
    thread exits (or when @ref Qore::throw_thread_resource_exceptions() or
    @ref Qore::throw_thread_resource_exceptions_to_mark() is called), the transaction is rolled back automatically
    and a \c DATASOURCE-TRANSACTION-EXCEPTION exception is raised describing the situation.\n\n
    Being an builtin class, the DatasourcePool class does not
    inherit @ref Qore::Thread::AbstractThreadResource "AbstractThreadResource" explicitly as a part of the exported API,
    and the internal @ref Qore::Thread::AbstractThreadResource::cleanup() "AbstractThreadResource::cleanup()" method
    cannot be overridden or suppressed.

    @par Data Serialization
    The DatasourcePool class supports @ref Serializable "data serialization"; deserialization can fail if the database is not
    supported on or reachable from the target machine.

    When deserializing, any datasource event queue is lost; only the connection information is propagated in the
    deserialized copy.

    @note This class is not available with the @ref PO_NO_DATABASE parse option

    @see <a href="../../modules/SqlUtil/html/index.html">SqlUtil</a> for a high level database-independent API
 */
qclass DatasourcePool [dom=DATABASE; arg=DatasourcePool* ds; ns=Qore::SQL; vparent=AbstractDatasource, Serializable; serializer=reinterpret_cast<q_serializer_t>(DatasourcePool_serializer); deserializer=DatasourcePool_deserializer];

//! Creates the DatasourcePool object; attempts to load a DBI driver if the driver is not already present in %Qore
/** @param driver The name of the DBI driver for the Datasource. See @ref sql_constants for builtin constants for DBI drivers shipped with %Qore, or see the DBI driver documentation to use an add-on driver (this string should be the name of the driver to be loaded)
    @param user The user name for the new connection. Also see Datasource::setUserName() for a method that allows this parameter to be set after the constructor.
    @param pass The password for the new connection. Also see Datasource::setPassword() for a method that allows this parameter to be set after the constructor.
    @param db The database name for the new connection. Also see Datasource::setDBName() for a method that allows this parameter to be set after the constructor.
    @param encoding The database-specific name of the @ref character_encoding "character encoding" to use for the new connection. Also see Datasource::setDBCharset() for a method that allows this parameter to be set after the constructor. If no value is passed for this parameter, then the database character encoding corresponding to the @ref default_encoding "default character encoding" will be used instead.
    @param host The host name for the new connection. Also see Datasource::setHostName() for a method that allows this parameter to be set after the constructor.
    @param min The minimum number of connections in the pool (this number of connections is opened in the constructor)
    @param max The maximum number of connections in the pool (not more than this number of connections will be opened)
    @param port The port number for the new connection. Also see Datasource::setPort() for a method that allows this parameter to be set after the constructor.
    @param queue An optional @ref Qore::Thread::Queue "Queue" object to receive datasource events; note that the @ref Qore::Thread::Queue "Queue" passed cannot have any maximum size set or a \c QUEUE-ERROR will be thrown; passing @ref nothing will clear any event queue
    @param arg an optional argument to be included in the \c arg key of datasource events

    @par Example:
    @code{.py}
DatasourcePool db(DSPGSQL, "user", "pass", "database", "utf8", "localhost", 3, 10, 5432);
    @endcode

    @throw DATASOURCEPOOL-UNSUPPORTED-DATABASE DBI driver cannot be found
    @throw DATASOURCEPOOL-CONSTRUCTOR-ERROR invalid min, max, or port argument
    @throw DBI-OPTION-ERROR unknown or unsupported option passed to driver
 */
DatasourcePool::constructor(string driver, *string user, *string pass, *string db, *string encoding, *string host, softint min = 3, softint max = 10, softint port = 0, *Qore::Thread::Queue[Queue] queue, auto arg) {
   if (!port && min == 3 && max == 10 && driver->find(':') != -1) {
      ReferenceHolder<QoreHashNode> h(parseDatasource(driver->getBuffer(), xsink), xsink);
      if (!h) {
         assert(*xsink);
         return;
      }
      dsp_constructor_hash(*h, self, queue, arg, xsink);
      return;
   }

   ReferenceHolder<Queue> q(queue, xsink);

   DBIDriver *db_driver = DBI.find(driver->getBuffer());
   if (!db_driver) {
      xsink->raiseException("DATASOURCEPOOL-UNSUPPORTED-DATABASE", "no DBI driver can be found for database type '%s'", driver->getBuffer());
      return;
   }

   if (min <= 0) {
      xsink->raiseException(DSPC_ERR, "minimum connections must be > 0 (value given: " QLLD ")", min);
      return;
   }

   if (max < min) {
      xsink->raiseException(DSPC_ERR, "maximum connections must be >= min(%d) (value given: " QLLD ")", min, max);
      return;
   }

   if (port < 0) {
      xsink->raiseException(DSPC_ERR, "port value must be zero (meaning use the default port) or positive (value given: " QLLD ")", port);
      return;
   }

   SimpleRefHolder<DatasourcePool> ds(new DatasourcePool(xsink, db_driver, user ? user->getBuffer() : 0, pass ? pass->getBuffer() : 0,
							 db ? db->getBuffer() : 0, encoding ? encoding->getBuffer() : 0, host ? host->getBuffer() : 0,
							 min, max, port, 0, q.release(), arg.refSelf()));
   if (!*xsink)
      self->setPrivate(CID_DATASOURCEPOOL, ds.release());
   else
      ds->destructor(xsink);
}

//! Creates a DatasourcePool object from a single string giving all parameters that can be parsed by parse_datasource()
/** @param desc a datasource description string in the format that can be parsed by parse_datasource()
    @param queue An optional @ref Qore::Thread::Queue "Queue" object to receive datasource events; note that the @ref Qore::Thread::Queue "Queue" passed cannot have any maximum size set or a \c QUEUE-ERROR will be thrown; passing @ref nothing will clear any event queue
    @param arg an optional argument to be included in the \c arg key of datasource events

    @par Example:
    @code{.py}
DatasourcePool ds("pgsql:user/pass@db01(utf8)%localhost:5432");
    @endcode

    @throw DATASOURCE-UNSUPPORTED-DATABASE DBI driver cannot be loaded
    @throw DATASOURCE-CONSTRUCTOR-ERRO missing required parameter for connection; port value is \<= 0
    @throw DBI-OPTION-ERROR unknown or unsupported option passed to driver

    @since %Qore 0.8.6
 */
DatasourcePool::constructor(string desc, *Qore::Thread::Queue[Queue] queue, auto arg) {
    ReferenceHolder<Queue> q(queue, xsink);
    ReferenceHolder<QoreHashNode> h(parseDatasource(desc->getBuffer(), xsink), xsink);
    if (!h) {
        assert(*xsink);
        return;
    }

    dsp_constructor_hash(*h, self, q.release(), arg, xsink);
}

//! Creates a DatasourcePool object from a hash argument giving parameters for the constructor
/** @param opts a hash giving parameters for the new datasource with the following possible keys (the \c "type" key is mandatory, also usable with the output of the parse_datasource() function):
    - \c type: (@ref string_or_nothing_type "*string") The name of the database driver to use; this key is mandatory; if not present, an exception will be raised. See @ref sql_constants for builtin constants for DBI drivers shipped with Qore, or see the DBI driver documentation to use an add-on driver (this string should be the name of the driver to be loaded)
    - \c user: (@ref string_or_nothing_type "*string") The user name for the new connection
    - \c pass: (@ref string_or_nothing_type "*string") The password for the new connection
    - \c db: (@ref string_or_nothing_type "*string") The database name for the new connection
    - \c charset: (@ref string_or_nothing_type "*string") The database-specific name of the character encoding to use for the new connection. Also see DatasourcePool::setDBCharset() for a method that allows this parameter to be set after the constructor. If no value is passed for this parameter, then the database character encoding corresponding to the default character encoding for the %Qore process will be used instead.
    - \c host: (@ref string_or_nothing_type "*string") The host name for the new connection
    - \c port: (@ref softint_type "softint") The port number for the new connection
    - \c options: (@ref hash_type "hash") An optional hash having \c "min" and \c "max" keys giving the minimum and maximum number of connections in the pool, respectively; all other options will be passed to the database driver
    @param queue An optional @ref Qore::Thread::Queue "Queue" object to receive datasource events; note that the @ref Qore::Thread::Queue "Queue" passed cannot have any maximum size set or a \c QUEUE-ERROR will be thrown; passing @ref nothing will clear any event queue
    @param arg an optional argument to be included in the \c arg key of datasource events

    @par Example:
    @code{.py}
Datasource db(("type": DSPGSQL, "user": "username", "pass": "password", "db": "database", "charset": "utf8", "host": "localhost", "port": 5432, "options": ("min": 3, "max": 10)));
    @endcode

    @throw DATASOURCEPOOL-UNSUPPORTED-DATABASE DBI driver cannot be loaded
    @throw DATASOURCEPOOL-CONSTRUCTOR-ERROR missing or invalid \c "driver" key, other key name not assigned to a string; \c "port" value is \<= 0; invalid \c "min" or \c "max" keys
    @throw DBI-OPTION-ERROR unknown or unsupported option passed to driver
 */
DatasourcePool::constructor(hash<auto> opts, *Qore::Thread::Queue[Queue] queue, auto arg) {
   dsp_constructor_hash(opts, self, queue, arg, xsink);
}

//! Throws an exception if any transactions are in progress and returns immediately; the object is destroyed after any in-progress requests are completed
/**
    @par Example:
    @code{.py}
delete db;
    @endcode

    @throw DATASOURCEPOOL-ERROR The destructor was called while a transaction was still in progress
 */
DatasourcePool::destructor() {
   ds->destructor(xsink);
   ds->deref();
}

//! Creates a new Datasource object with the same driver as the original and copies of all the connection parameters
/**
    @par Example:
    @code{.py}
DatasourcePool new_dsp = dsp.copy();
    @endcode
 */
DatasourcePool::copy() {
   self->setPrivate(CID_DATASOURCEPOOL, new DatasourcePool(*ds, xsink));
}

//! Commits the current transaction and releases the connection to the pool
/**
    @par Example:
    @code{.py}
db.commit();
    @endcode

 */
nothing DatasourcePool::commit() {
   ds->commit(xsink);
}

//! Rolls back the current transaction and releases the connection to the pool
/**
    @par Example:
    @code{.py}
db.rollback();
    @endcode

 */
nothing DatasourcePool::rollback() {
   ds->rollback(xsink);
}

//! Allocates a persistent connection to the current thread from the pool (if one has not already been allocated) and executes an %SQL command on the server and returns either the integer row count (for example, for updates, inserts, and deletes) or the data retrieved (for example, if a stored procedure is executed that returns values)
/** This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    @param sql The %SQL command to execute on the server
    @param ... Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return The return value depends on the DBI driver; normally, for commands with placeholders, a hash is returned holding the values acquired from executing the %SQL statement. For all other commands, normally an integer row count is returned. However, some DBI drivers also allow select statements to be executed through this interface, which would also return a hash (column names) of lists (values for each column).

    @par Example:
    @code{.py}
int rows = db.exec("insert into table (varchar_col, timestamp_col, blob_col, numeric_col) values (%v, %v, %v, %d)", string, now(), binary, 100);
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions
 */
auto DatasourcePool::exec(string sql, ...) {
   ReferenceHolder<QoreListNode> vargs(args->size() > 1 ? args->copyListFrom(1) : 0, xsink);
   return ds->exec(sql, *vargs, xsink);
}

//! Allocates a persistent connection to the current thread from the pool (if one has not already been allocated) and executes %SQL code on the DB connection, taking a list for all bind arguments
/** Same as DatasourcePool::exec() except takes an explicit list for bind arguments

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    @param sql The %SQL command to execute on the server
    @param vargs Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return The return value depends on the DBI driver; normally, for commands with placeholders, a hash is returned holding the values acquired from executing the %SQL statement. For all other commands, normally an integer row count is returned. However, some DBI drivers also allow select statements to be executed through this interface, which would also return a hash (column names) of lists (values for each column).

    @par Example:
    @code{.py}
int rows = db.vexec("insert into example_table value (%v, %v, %v)", arg_list);
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions
 */
auto DatasourcePool::vexec(string sql, *softlist<auto> vargs) {
   return ds->exec(sql, vargs, xsink);
}

//! Allocates a persistent connection to the current thread from the pool (if one has not already been allocated) and executes an %SQL command on the server and returns either the row count (for example, for updates and inserts) or the data retrieved (for example, if a stored procedure is executed that returns values)
/** This method does not do any variable binding, so it's useful for example for DDL statements etc

    @par Warning:
    Using this method to execute pure dynamic %SQL many times with different %SQL strings (as opposed to using the same string and binding by value instead of dynamic SQL) can affect application performance by prohibiting the efficient usage of the DB server's statement cache. See DB server documentation for variable binding and the %SQL statement cache for more information.

    @param sql The %SQL command to execute on the server; this string will not be subjected to any transformations for variable binding

    @return The return value depends on the DBI driver; normally, for commands with placeholders, a hash is returned holding the values acquired from executing the %SQL statement. For all other commands, normally an integer row count is returned. However, some DBI drivers also allow select statements to be executed through this interface, which would also return a hash (column names) of lists (values for each column).

    @par Example:
    @code{.py}
db.execRaw("create table my_tab (id number, some_text varchar2(30))");
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions
 */
auto DatasourcePool::execRaw(string sql) {
   return ds->execRaw(sql, xsink);
}

//! Executes an %SQL select statement on the server and returns the result as a hash (column names) of lists (column values per row)
/** The return format of this method is suitable for use with @ref context "context statements", for easy iteration and processing of query results.
    Alternatively, the HashListIterator class can be used to iterate the return value of this method.

    Additionally, this format is a more efficient format than that returned by the Datasource::selectRows() method, because the column names are not repeated for each row returned. Therefore, for retrieving anything greater than small amounts of data, it is recommended to use this method instead of Datasource::selectRows().

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    This method does not retain the datasource connection for the current thread if one was not already allocated before this method is called, so to execute select statements that begin a transaction (such as \c "select for update"), execute DatasourcePool::beginTransaction() first to ensure that the connection is dedicated to the calling thread.

    @param sql The %SQL command to execute on the server
    @param ... Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return This method returns a hash (the keys are the column names) of lists (the column data per row) when executed with an %SQL select statement, however some DBI drivers allow any %SQL to be executed through this method, in which case other data types can be returned (such as an integer for a row count or a hash for output parameters when executing a stored procedure).  If no rows are found, a hash of column names assigned to empty lists is returned.

    @par Example:
    @code{.py}
# bind a string and a date/time value by value in a query
hash query = db.select("select * from table where varchar_column = %v and timestamp_column > %v", string, 2007-10-11T15:31:26.289);
if (query.firstValue())
    printf("got results\n");
    @endcode

    @note
    - See the documentation for the DBI driver being used for additional possible exceptions
    - This method returns all the data available immediately; to process query data piecewise, use the SQLStatement class

    @see
    - DatasourcePool::vselect()
    - <hash>::contextIterator()
    - @ref context
 */
auto DatasourcePool::select(string sql, ...) {
   ReferenceHolder<QoreListNode> vargs(args->size() > 1 ? args->copyListFrom(1) : 0, xsink);
   return ds->select(sql, *vargs, xsink);
}

//! Executes an %SQL select statement on the server and returns the first row as a hash (the column values)
/** If more than one row is returned, then it is treated as an error and a \c DBI-SELECT-ROW-ERROR is returned (however the DBI driver should raise its own exception here to avoid retrieving more than one row from the server). For a similar method taking a list for all bind arguments, see DatasourcePool::vselectRow().

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    This method does not retain the datasource connection for the current thread if one was not already allocated before this method is called, so to execute select statements that begin a transaction (such as \c "select for update"), execute DatasourcePool::beginTransaction() first to ensure that the connection is dedicated to the calling thread.

    @param sql The %SQL command to execute on the server
    @param ... Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return This method normally returns a hash (the keys are the column names) of row data or @ref nothing if no row is found for the query when executed with an %SQL select statement, however some DBI drivers allow any %SQL statement to be executed through this method (not only select statements), in this case other data types can be returned

    @par Example:
    @code{.py}
*hash<auto> h = db.selectRow("select * from example_table where id = 1");
    @endcode

    @throw DBI-SELECT-ROW-ERROR more than 1 row retrieved from the server

    @note see the documentation for the DBI driver being used for additional possible exceptions
 */
auto DatasourcePool::selectRow(string sql, ...) {
   ReferenceHolder<QoreListNode> vargs(args->size() > 1 ? args->copyListFrom(1) : 0, xsink);
   return ds->selectRow(sql, *vargs, xsink);
}

//! Executes an %SQL select statement on the server and returns the result as a list (rows) of hashes (the column values)
/** The return format of this method is not as memory efficient as that returned by the DatasourcePool::select() method, therefore for larger amounts of data, it is recommended to use DatasourcePool::select().

    The usual return value of this method can be iterated with the ListHashIterator class.

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    This method does not retain the datasource connection for the current thread if one was not already allocated before this method is called, so to execute select statements that begin a transaction (such as \c "select for update"), execute DatasourcePool::beginTransaction() first to ensure that the connection is dedicated to the calling thread.

    @param sql The %SQL command to execute on the server
    @param ... Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return Normally returns a list (rows) of hash (where the keys are the column names of each row) or @ref nothing if no rows are found for the query, however some DBI drivers allow any %SQL statement to be executed through this method (not only select statements), in this case other data types can be returned

    @par Example:
    @code{.py}
*list<auto> list = db.selectRows("select * from example_table");
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions

    @see DatasourcePool::select()
 */
auto DatasourcePool::selectRows(string sql, ...) {
   ReferenceHolder<QoreListNode> vargs(args->size() > 1 ? args->copyListFrom(1) : 0, xsink);
   return ds->selectRows(sql, *vargs, xsink);
}

//! Executes a select statement on the server and returns the results in a hash (column names) of lists (column values per row), taking a list for all bind arguments
/** The return format of this method is suitable for use with @ref context "context statements", for easy iteration and processing of query results.
    Alternatively, the HashListIterator class can be used to iterate the return value of this method.

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    This method does not retain the datasource connection for the current thread if one was not already allocated before this method is called, so to execute select statements that begin a transaction (such as \c "select for update"), execute DatasourcePool::beginTransaction() first to ensure that the connection is dedicated to the calling thread.

    @param sql The %SQL command to execute on the server
    @param vargs Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return Normally returns a hash (the keys are the column names) of list (each hash key's value is a list giving the row data), however some DBI drivers allow any %SQL statement to be executed through this method (not only select statements), in this case other data types can be returned.  If no rows are found, a hash of column names assigned to empty lists is returned.

    @par Example:
    @code{.py}
hash<auto> query = db.vselect("select * from example_table where id = %v and name = %v", arg_list);
if (query.firstValue())
    printf("got results\n");
    @endcode

    @note
    - See the documentation for the DBI driver being used for additional possible exceptions
    - This method returns all the data available immediately; to process query data piecewise, use the SQLStatement class

    @see
    - DatasourcePool::select()
    - <hash>::contextIterator()
    - @ref context
 */
auto DatasourcePool::vselect(string sql, *softlist<auto> vargs) {
   return ds->select(sql, vargs, xsink);
}

//! Executes a select statement on the server and returns the first row as a hash (column names and values), taking a list for all bind arguments
/** This method is the same as the DatasourcePool::selectRow() method, except this method takes a single argument after the %SQL command giving the list of bind value parameters

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    This method does not retain the datasource connection for the current thread if one was not already allocated before this method is called, so to execute select statements that begin a transaction (such as \c "select for update"), execute DatasourcePool::beginTransaction() first to ensure that the connection is dedicated to the calling thread.

    @param sql The %SQL command to execute on the server
    @param vargs Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return This method normally returns a hash (the keys are the column names) of row data or @ref nothing if no row is found for the query when executed with an %SQL select statement, however some DBI drivers allow any %SQL statement to be executed through this method (not only select statements), in this case other data types can be returned

    @par Example:
    @code{.py}
*hash<auto> h = db.vselectRow("select * from example_table where id = %v and type = %v", arg_list);
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions

    @see DatasourcePool::selectRow()
 */
auto DatasourcePool::vselectRow(string sql, *softlist<auto> vargs) {
   return ds->selectRow(sql, vargs, xsink);
}

//! Executes a select statement on the server and returns the results in a list (rows) of hashes (column names and values), taking a list for all bind arguments
/** Same as the Datasource::selectRows() method, except this method takes a single argument after the %SQL command giving the list of bind value parameters.

    The usual return value of this method can be iterated with the ListHashIterator class.

    The return format of this method is not as memory efficient as that returned by the DatasourcePool::select() method, therefore for larger amounts of data, it is recommended to use DatasourcePool::select().

    This method also accepts all bind parameters (<tt>%%d</tt>, <tt>%%v</tt>, <tt>%%s</tt>, etc) as documented in @ref sql_binding "Binding by Value and Placeholder"

    This method does not retain the datasource connection for the current thread if one was not already allocated before this method is called, so to execute select statements that begin a transaction (such as \c "select for update"), execute DatasourcePool::beginTransaction() first to ensure that the connection is dedicated to the calling thread.

    @param sql The %SQL command to execute on the server
    @param vargs Include any values to be bound (using <tt>%%v</tt> in the command string) or placeholder specifications (using <tt>:</tt><em>key_name</em> in the command string) in order after the command string

    @return Normally returns a list (rows) of hash (where the keys are the column names of each row) or @ref nothing if no rows are found for the query, however some DBI drivers allow any %SQL statement to be executed through this method (not only select statements), in this case other data types can be returned

    @par Example:
    @code{.py}
*list<auto> list = db.vselectRows("select * from example_table where id = %v and type = %v", arg_list);
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions

    @see DatasourcePool::selectRows()
 */
auto DatasourcePool::vselectRows(string sql, *softlist<auto> vargs) {
   return ds->selectRows(sql, vargs, xsink);
}

//! Manually allocates a persistent connection from the pool to the calling thread
/** This method should be called when a transaction will be started with a DatasourcePool::select() method (or DatasourcePool::vselect(), etc).

    This method does not make any communication with the server to start a transaction; it only allocates a persistent connection to the current thread in %Qore.

    To release the connection, call DatasourcePool::commit() or DatasourcePool::rollback()

    @par Example:
    @code{.py}
db.beginTransaction();
    @endcode
 */
nothing DatasourcePool::beginTransaction() {
   ds->beginTransaction(xsink);
}

//! Returns the username parameter as a string or @ref nothing if none is set
/** @return the username parameter as a string or @ref nothing if none is set

    @par Example:
    @code{.py}
*string user = db.getUserName();
    @endcode
 */
*string DatasourcePool::getUserName() [flags=CONSTANT] {
   return ds->getPendingUsername();
}

//! Returns the password parameter as a string or @ref nothing if none is set
/** @return the password parameter as a string or @ref nothing if none is set

    @par Example:
    @code{.py}
*string pass = db.getPassword();
    @endcode
 */
*string DatasourcePool::getPassword() [flags=CONSTANT] {
   return ds->getPendingPassword();
}

//! Returns the database name parameter as a string or @ref nothing if none is set
/** @return the database name parameter as a string or @ref nothing if none is set

    @par Example:
    @code{.py}
*string db = db.getDBName();
    @endcode
 */
*string DatasourcePool::getDBName() [flags=CONSTANT] {
   return ds->getPendingDBName();
}

//! Retrieves the database-specific charset set encoding for the object
/** A method synonym for DatasourcePool::getDBEncoding() kept for backwards-compatibility

    @return the database-specific charset set encoding for the object

    @par Example:
    @code{.py}
*string enc = db.getDBCharset();
    @endcode
 */
*string DatasourcePool::getDBCharset() [flags=CONSTANT] {
   return ds->getPendingDBEncoding();
}

//! Retrieves the database-specific charset set encoding for the object
/** @return the database-specific charset set encoding for the object

    @par Example:
    @code{.py}
*string enc = db.getDBEncoding();
    @endcode

    @see DatasourcePool::getOSEncoding()
 */
*string DatasourcePool::getDBEncoding() [flags=CONSTANT] {
   return ds->getPendingDBEncoding();
}

//! Returns the %Qore character encoding name for the object as a string or \c "(unknown)" if none is set
/** @return the %Qore character encoding name for the object as a string or \c "(unknown)" if none is set

    @par Example:
    @code{.py}
string enc = db.getOSCharset();
    @endcode

    @see DatasourcePool::getOSEncoding()
 */
string DatasourcePool::getOSCharset() [flags=CONSTANT] {
   const QoreEncoding *enc = ds->getQoreEncoding();
   return new QoreStringNode(enc ? enc->getCode() : "(unknown)");
}

//! Returns the %Qore character encoding name for the object as a string or @ref nothing if none is set
/** @return the %Qore character encoding name for the object as a string or @ref nothing if none is set

    @par Example:
    @code{.py}
*string enc = db.getOSEncoding();
    @endcode
 */
*string DatasourcePool::getOSEncoding() [flags=CONSTANT] {
   const QoreEncoding *enc = ds->getQoreEncoding();
   return enc ? new QoreStringNode(enc->getCode()) : 0;
}

//! Returns the hostname parameter as a string or @ref nothing if none is set
/** @return the hostname parameter as a string or @ref nothing if none is set

    @par Example:
    @code{.py}
*string host = db.getHostName();
    @endcode
 */
*string DatasourcePool::getHostName() [flags=CONSTANT] {
   return ds->getPendingHostName();
}

//! Gets the port number that will be used for the next connection to the server
/** Invalid port numbers will cause an exception to be thrown when the connection is opened

    @par Example:
    @code{.py}
*int port = db.getPort();
    @endcode
 */
*int DatasourcePool::getPort() [flags=CONSTANT] {
    int port = ds->getPendingPort();
    return port ? QoreValue(port) : QoreValue();
}

//! Returns the name of the driver used for the object
/** @return the name of the driver used for the object

    @par Example:
    @code{.py}
string driver = db.getDriverName();
    @endcode

    @see getDriverRealName()
*/
string DatasourcePool::getDriverName() [flags=CONSTANT] {
   return new QoreStringNode(ds->getDriverName());
}

//! Returns the real DB driver name if supported by the driver, otherwise the %Qore driver name
/** @return the real DB driver name if supported by the driver, otherwise the %Qore driver name

    @par Example:
    @code{.py}
string driver = db.getDriverRealName();
    @endcode

    The return value will differ from getDriverName() in the case of "wrapper" drivers such as \c odbc or \c jdbc,
    where the generic %Qore driver name is insufficient to identify the database server type.

    In other cases, this method will return the same value as getDriverName()

    @see getDriverRName()

    @since %Qore 1.14
*/
string DatasourcePool::getDriverRealName() [flags=RET_VALUE_ONLY] {
    return ds->getDriverRealName(xsink);
}

//! Returns the minimum number of connections in this object
/** @return the minimum number of connections in this object

    @par Example:
int min = db.getMinimum();
    @code{.py}

    @endcode

 */
int DatasourcePool::getMinimum() [flags=CONSTANT] {
   return ds->getMin();
}

//! Returns the maximum number of connections in this object
/** @return the maximum number of connections in this object

    @par Example:
    @code{.py}
int max = db.getMaximum();
    @endcode

 */
int DatasourcePool::getMaximum() [flags=CONSTANT] {
   return ds->getMax();
}

//! Returns a string with technical information about the object
/** @return a string with technical information about the object

    @par Example:
    @code{.py}
string str = db.toString();
    @endcode

 */
string DatasourcePool::toString() [flags=CONSTANT] {
   return ds->toString();
}

//! Returns the driver-specific server version data for the current connection
/** @return the driver-specific server version data for the current connection; see the DBI driver documentation for the return data type and format

    @par Example:
    @code{.py}
auto ver = db.getServerVersion();
    @endcode

    @note see the documentation for the DBI driver being used for additional possible exceptions
 */
auto DatasourcePool::getServerVersion() {
   return ds->getServerVersion(xsink);
}

//! Retrieves the driver-specific client library version information; this method may not be implemented for all drivers
/** @return the driver-specific client library version information; this method may not be implemented for all drivers; see the DBI driver documentation for the return data type and format

    @par Example:
    @code{.py}
auto ver = db.getClientVersion();
    @endcode

    @note see the documentation for the DBI driver being used for possible exceptions
 */
auto DatasourcePool::getClientVersion() {
   return ds->getClientVersion(xsink);
}

//! Returns @ref True if a transaction is currently in progress (meaning in this case that a datasource form the pool is dedicated to the calling thread), @ref False if not
/** @return @ref True if a transaction is currently in progress (meaning in this case that a datasource form the pool is dedicated to the calling thread), @ref False if not

    @par Example:
    @code{.py}
bool b = db.inTransaction();
    @endcode
 */
bool DatasourcePool::inTransaction() [flags=CONSTANT] {
   return ds->inTransaction();
}

//! Returns @ref True if the current thread is in a transaction (i.e. has a dedicated datasource allocation), @ref False if not
/** @return @ref True if the current thread is in a transaction (i.e. has a dedicated datasource allocation), @ref False if not

    @par Example:
    @code{.py}
bool b = db.currentThreadInTransaction();
    @endcode

    @since %Qore 0.8.7
 */
bool DatasourcePool::currentThreadInTransaction() [flags=CONSTANT] {
   return ds->currentThreadInTransaction();
}

//! returns the valid options for the driver associated with the Datasource with descriptions and current values for the current Datasource object
/** @return a hash where the keys are valid option names, and the values are hashes with the following keys:
    - \c "desc": a string description of the option
    - \c "type": a string giving the data type restriction for the option
    - \c "value": the current value of the option

    @since %Qore 0.8.6
 */
hash<auto> DatasourcePool::getOptionHash() [flags=CONSTANT] {
   return ds->getOptionHash();
}

//! Returns the current value for the given option
/** @param opt the option to get

    @throw DBI-OPTION-ERROR unknown or unsupported option passed to driver

    @since %Qore 0.8.6
 */
auto DatasourcePool::getOption(string opt) [flags=RET_VALUE_ONLY] {
   return ds->getOption(opt->getBuffer(), xsink);
}

//! Returns a @ref datasource_hash "datasource hash" describing the configuration of the current object
/** @par Example:
    @code{.py}
hash<auto> h = ds.getConfigHash();
    @endcode

    @return a @ref datasource_hash "datasource hash" describing the configuration of the current object

    @since %Qore 0.8.8
 */
hash<auto> DatasourcePool::getConfigHash() [flags=CONSTANT] {
   return ds->getConfigHash(xsink);
}

//! Returns a string giving the configuration of the current object in a format that can be parsed by parse_datasource()
/** @par Example:
    @code{.py}
string str = ds.getConfigString();
    @endcode

    @return a string giving the configuration of the current object in a format that can be parsed by parse_datasource()

    @since %Qore 0.8.8
 */
string DatasourcePool::getConfigString() [flags=CONSTANT] {
   return ds->getConfigString(xsink);
}

//! clears any connection delay warning callback from the object
/** @par Example:
    @code{.py}
ds.clearWarningCallback();
    @endcode

    @since %Qore 0.8.9
*/
DatasourcePool::clearWarningCallback() {
   ds->clearWarningCallback(xsink);
}

//! sets a connection delay warning callback to be called any time the delay assigning a connection from the pool exceeds the given timeout in milliseconds
/** @par Example:
    @code{.py}
code cb = sub (string dsstr, int us, int to) {
    printf("WARNING: datasource pool %y took %f ms (threshold %d ms) to assign a new connection\n", dsstr, us.toNumber() / 1000n, to);
};
ds.setWarningCallback(5s, cb);
    @endcode

    @param ms the period of time with a resolution of milliseconds after which the callback will be called if a connection cannot be allocated in the given time period
    @param callback a @ref closure "closure" or @ref call_reference "call reference" taking three or four arguments: (@ref string_type "string" \a desc, @ref int_type "int" \a time, @ref int_type "int" \a warning_timeout[, @ref any_type "any" \a arg]) which will be passed a datasource description string for the pool, an integer giving the amount of time it took to acquire the connection in microseconds (divide by 1000 to get milliseconds), an integer giving the warning timeout threshold in milliseconds, and optionally the \a arg value passed to this method
    @param arg an optional argument that will be passed to the warning callback

    @note that the warning timeout has a resolution of milliseconds, but the actual wait time is reported in microseconds (1 ms = 1000 us)

    @since %Qore 0.8.9
*/
DatasourcePool::setWarningCallback(timeout ms, code callback, auto arg) {
   ds->setWarningCallback(ms, callback->refRefSelf(), arg.refSelf(), xsink);
}

//! Returns a hash with usage information about the DatasourcePool object
/** @par Example:
    @code{.py}
hash<auto> h = ds.getUsageInfo();
    @endcode

    @return a hash with the following keys (note that the \c callback, \c timeout, and optionally \c arg keys are only set if a warning callback is set):
    - \c callback: the @ref closure "closure" or @ref call_reference "call reference" set as the warning callback
    - \c timeout: an integer giving the timeout threshold in milliseconds before the warning callback is executed
    - \c arg: an optional argument passed to the warning callback
    - \c wait_max: the maximum number of microseconds that threads have had to wait for a free connection
    - \c stats_reqs: the total number of requests for connections / transactions on this DatasourcePool
    - \c stats_hits: the total number of requests for connections / transactions on this DatasourcePool that did not have to wait for a connection

    @note \c wait_max is reported in microseconds (1 ms = 1000 us) while the warning timeout has a resolution of milliseconds

    @since %Qore 0.8.9
*/
*hash<auto> DatasourcePool::getUsageInfo() [flags=CONSTANT] {
   return ds->getUsageInfo();
}

//! Sets the timeout period for waiting for a connection to come free; note that timeout values less than or equal to zero mean that requests for a connection will wait indefinitely
/** @par Example:
    @code{.py}
ds.setErrorTimeout(30s);
    @endcode

    @since %Qore 0.8.9
*/
DatasourcePool::setErrorTimeout(timeout ts) {
   if (ts < 0)
      ts = 0;
   ds->setErrorTimeout(ts);
}

//! Returns the error timeout period for waiting for a connection to come free as an integer giving milliseconds; note that timeout values less than or equal to zero mean that requests for a connection will wait indefinitely
/** @par Example:
    @code{.py}
int ms = ds.getErrorTimeout();
    @endcode

    @return the error timeout period for waiting for a connection to come free as an integer giving milliseconds; note that timeout values less than or equal to zero mean that requests for a connection will wait indefinitely

    @since %Qore 0.8.9
*/
int DatasourcePool::getErrorTimeout() [flags=CONSTANT] {
   return ds->getErrorTimeout();
}

//! Sets a queue object for DBI events on the pool
/**
    @param queue the @ref Qore::Thread::Queue "Queue" object to receive datasource events; note that the @ref Qore::Thread::Queue "Queue" passed cannot have any maximum size set or a \c QUEUE-ERROR will be thrown; passing @ref nothing will clear any event queue
    @param arg an argument to be included in the \c arg key of datasource events

    @throw QUEUE-ERROR the Queue passed has a maximum size set

    @since %Qore 0.8.9
 */
nothing DatasourcePool::setEventQueue(Qore::Thread::Queue[Queue] queue, auto arg) {
   ReferenceHolder<Queue> q(queue, xsink);

   if (queue->getMax() != -1)
      return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which could cause internal I/O to freeze in the Datasource object if used; use a Queue object with no maximum size for datasource event monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");

   ds->setEventQueue(q.release(), arg.refSelf(), xsink);
}

//! Clears the queue object for DBI events on the pool
/**
    @since %Qore 0.8.9
 */
nothing DatasourcePool::clearEventQueue() {
   ds->setEventQueue(0, 0, xsink);
}

//! Returns an integer bitfield of DBI driver capabilities
/** @return an integer bitfield of DBI driver capabilities; see @ref dbi_capabilities for the meaning of each bit

    @par Example:
    @code{.py}
int caps = pool.getCapabilities();
if (!(caps & DBI_CAP_TRANSACTION_MANAGEMENT))
    throw "DATASOURCE-ERROR", sprintf("DBI driver %y does not support transaction management", db.getDriverName());
    @endcode

    @since %Qore 0.8.12
*/
int DatasourcePool::getCapabilities() [flags=CONSTANT] {
   return ds->getCapabilities();
}

//! Returns a list of strings giving the capabilities of the current DBI driver
/** @return a list of strings giving the capabilities of the current DBI driver

    @par Example:
    @code{.py}
printf("driver %y has the following capabilities:\n", db.getDriverName());
foreach string cap in (db.getCapabilityList())
    printf("- %s\n", cap);
    @endcode

    @since %Qore 0.8.12
*/
list<auto> DatasourcePool::getCapabilityList() [flags=CONSTANT] {
   return ds->getCapabilityList();
}

//! Returns an @ref Qore::SQL::AbstractSQLStatement "AbstractSQLStatement" object based on the current database connection object
/** @par Example:
    @code{.py}
AbstractSQLStatement stmt = ds.getSQLStatement();
    @endcode

    @throw SQLSTATEMENT-ERROR the DBI driver for the given object does not support the prepared statement API

    @since Qore 0.9.0
*/
AbstractSQLStatement DatasourcePool::getSQLStatement() {
    if (!ds->getDriver()->hasStatementAPI()) {
        xsink->raiseException("SQLSTATEMENT-ERROR", "DBI driver '%s' does not support the prepared statement API", ds->getDriver()->getName());
        return QoreValue();
     }

    return new QoreObject(QC_SQLSTATEMENT, getProgram(), new QoreSQLStatement(ds));
}
