/* Copyright (C) 2003 MySQL AB

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */

#include <common/OdbcData.hpp>
#include <codegen/CodeGen.hpp>
#include "HandleRoot.hpp"
#include "HandleDbc.hpp"
#include "HandleStmt.hpp"
#include "HandleDesc.hpp"

HandleStmt::HandleStmt(HandleDbc* pDbc) :
    StmtArea(*pDbc),
    m_dbc(pDbc),
    m_attrArea(m_attrSpec)
{
    m_attrArea.setHandle(this);
    for (unsigned i = 0; i <= 1; i++) {
	for (unsigned u = 0; u <= 4; u++) {
	    m_handleDesc[i][u] = 0;
	}
    }
}

HandleStmt::~HandleStmt()
{
}

void
HandleStmt::ctor(Ctx& ctx)
{
    for (unsigned u = 1; u <= 4; u++) {
	HandleDesc** ppDesc = &m_handleDesc[0][u];
	m_dbc->sqlAllocDesc(ctx, ppDesc);
	if (! ctx.ok())
	    return;
	m_descArea[u] = &(*ppDesc)->descArea();
	m_descArea[u]->setAlloc(Desc_alloc_auto);
	m_descArea[u]->setUsage((DescUsage)u);
    }
}

void
HandleStmt::dtor(Ctx& ctx)
{
    free(ctx);
    for (unsigned u = 1; u <= 4; u++) {
	HandleDesc** ppDesc = &m_handleDesc[0][u];
	if (*ppDesc != 0)
	    m_dbc->sqlFreeDesc(ctx, *ppDesc);
	*ppDesc = 0;
    }
}

// descriptor handles

HandleDesc*
HandleStmt::getHandleDesc(Ctx& ctx, DescUsage u) const
{
    ctx_assert(1 <= u && u <= 4);
    if (m_handleDesc[1][u] != 0)
	return m_handleDesc[1][u];
    return m_handleDesc[0][u];
}

void
HandleStmt::setHandleDesc(Ctx& ctx, DescUsage u, SQLPOINTER handle)
{
    ctx_assert(1 <= u && u <= 4);
    if (handle == SQL_NULL_HDESC) {
	m_handleDesc[1][u] = 0;		// revert to implicit
	m_descArea[u] = &m_handleDesc[0][u]->descArea();
	return;
    }
    HandleDesc* pDesc = getRoot()->findDesc(handle);
    if (pDesc == 0) {
	ctx.pushStatus(Sqlstate::_HY024, Error::Gen, "cannot set %s handle to invalid descriptor handle %x", DescArea::nameUsage(u), (unsigned)handle);
	return;
    }
    if (pDesc == m_handleDesc[0][u]) {
	m_handleDesc[1][u] = 0;		// revert to implicit
	m_descArea[u] = &m_handleDesc[0][u]->descArea();
	return;
    }
    // XXX should check implicit handles on all statements
    for (unsigned v = 1; v <= 4; v++) {
	if (pDesc == m_handleDesc[0][v]) {
	    ctx.pushStatus(Sqlstate::_HY024, Error::Gen, "cannot set %s handle to implicitly allocated %s handle", DescArea::nameUsage(u), DescArea::nameUsage((DescUsage)v));
	    return;
	}
    }
    m_handleDesc[1][u] = pDesc;
    m_descArea[u] = &m_handleDesc[1][u]->descArea();
}

// allocate and free handles (no valid case)

void
HandleStmt::sqlAllocHandle(Ctx& ctx, SQLSMALLINT childType, HandleBase** ppChild)
{
    ctx.pushStatus(Sqlstate::_HY092, Error::Gen, "inappropriate handle type");
}

void
HandleStmt::sqlFreeHandle(Ctx& ctx, SQLSMALLINT childType, HandleBase* ppChild)
{
    ctx.pushStatus(Sqlstate::_HY092, Error::Gen, "inappropriate handle type");
}

// attributes and info

static bool
ignore_attr(Ctx& ctx, SQLINTEGER attribute)
{
    switch (attribute) {
    case 1211:
    case 1227:
    case 1228:
	ctx_log2(("ignore unknown ADO.NET stmt attribute %d", (int)attribute));
	return true;
    }
    return false;
}

void
HandleStmt::sqlSetStmtAttr(Ctx& ctx, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER stringLength)
{
    if (ignore_attr(ctx, attribute))
	return;
    baseSetHandleAttr(ctx, m_attrArea, attribute, value, stringLength);
}

void
HandleStmt::sqlGetStmtAttr(Ctx& ctx, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER bufferLength, SQLINTEGER* stringLength)
{
    if (ignore_attr(ctx, attribute))
	return;
    baseGetHandleAttr(ctx, m_attrArea, attribute, value, bufferLength, stringLength);
}

void
HandleStmt::sqlSetStmtOption(Ctx& ctx, SQLUSMALLINT option, SQLUINTEGER value)
{
    if (ignore_attr(ctx, option))
	return;
    baseSetHandleOption(ctx, m_attrArea, option, value);
}

void
HandleStmt::sqlGetStmtOption(Ctx& ctx, SQLUSMALLINT option, SQLPOINTER value)
{
    if (ignore_attr(ctx, option))
	return;
    baseGetHandleOption(ctx, m_attrArea, option, value);
}

void
HandleStmt::sqlGetTypeInfo(Ctx& ctx, SQLSMALLINT dataType)
{
    BaseString text;
    // odbc$typeinfo is a (possible unordered) view matching SQLGetTypeInfo result set
    text.append("SELECT * FROM odbc$typeinfo");
    if (dataType != SQL_ALL_TYPES) {
	switch (dataType) {
	case SQL_CHAR:
	case SQL_VARCHAR:
	case SQL_BINARY:
	case SQL_VARBINARY:
	case SQL_SMALLINT:
	case SQL_INTEGER:
	case SQL_BIGINT:
	case SQL_REAL:
	case SQL_DOUBLE:
	    break;
	default:
	    // XXX unsupported vs illegal
	    ctx_log1(("sqlGetTypeInfo: unknown data type %d", (int)dataType));
	    break;
	}
	text.appfmt(" WHERE data_type = %d", (int)dataType);
    }
    // sort signed before unsigned
    text.append(" ORDER BY data_type, unsigned_attribute, type_name");
    sqlExecDirect(ctx, (SQLCHAR*)text.c_str(), text.length());
}

void
HandleStmt::sqlTables(Ctx& ctx, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLCHAR* tableType, SQLSMALLINT nameLength4)
{
    BaseString text;
    // odbc$tables is a (possibly unordered) view matching SQLTables result set
    text.append("SELECT * FROM odbc$tables");
    SQLUINTEGER metadata_id = SQL_FALSE;
    sqlGetStmtAttr(ctx, SQL_ATTR_METADATA_ID, (SQLPOINTER)&metadata_id, SQL_IS_POINTER, 0);
    if (! ctx.ok())
	return;
    unsigned count = 0;
    if (catalogName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)catalogName, nameLength1);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (getOdbcVersion(ctx) == 2)
	    text.appfmt(" table_cat = '%s'", data.sqlchar());
	else if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_cat = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_cat LIKE '%s'", data.sqlchar());
    }
    if (schemaName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)schemaName, nameLength2);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_schem = '%s'", data.sqlchar());	// XXX UPPER
	else
	    text.appfmt(" table_schem LIKE '%s'", data.sqlchar());
    }
    if (tableName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)tableName, nameLength3);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_name = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_name LIKE '%s'", data.sqlchar());
    }
    if (tableType != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)tableType, nameLength4);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	text.appfmt(" table_type IN (%s)", data.sqlchar());		// XXX UPPER, quotes
    }
    text.append(" ORDER BY table_type, table_cat, table_schem, table_name");
    sqlExecDirect(ctx, (SQLCHAR*)text.c_str(), text.length());
}

void
HandleStmt::sqlColumns(Ctx& ctx, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLCHAR* columnName, SQLSMALLINT nameLength4)
{
    BaseString text;
    // odbc$columns is a (possibly unordered) view matching SQLColumns result set
    text.append("SELECT * FROM odbc$columns");
    SQLUINTEGER metadata_id = SQL_FALSE;
    sqlGetStmtAttr(ctx, SQL_ATTR_METADATA_ID, (SQLPOINTER)&metadata_id, SQL_IS_POINTER, 0);
    if (! ctx.ok())
	return;
    unsigned count = 0;
    if (catalogName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)catalogName, nameLength1);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (getOdbcVersion(ctx) == 2)
	    text.appfmt(" table_cat = '%s'", data.sqlchar());
	else if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_cat = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_cat LIKE '%s'", data.sqlchar());
    }
    if (schemaName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)schemaName, nameLength2);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_schem = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_schem LIKE '%s'", data.sqlchar());
    }
    if (tableName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)tableName, nameLength3);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_name = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_name LIKE '%s'", data.sqlchar());
    }
    if (columnName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)columnName, nameLength4);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" column_name = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" column_name LIKE '%s'", data.sqlchar());
    }
    text.append(" ORDER BY table_cat, table_schem, table_name, ordinal_position");
    sqlExecDirect(ctx, (SQLCHAR*)text.c_str(), text.length());
}

void
HandleStmt::sqlPrimaryKeys(Ctx& ctx, SQLCHAR* szCatalogName, SQLSMALLINT cbCatalogName, SQLCHAR* szSchemaName, SQLSMALLINT cbSchemaName, SQLCHAR* szTableName, SQLSMALLINT cbTableName)
{
    BaseString text;
    // odbc$primarykeys is a (possible unordered) view matching SQLPrimaryKeys result set
    text.append("SELECT * FROM odbc$primarykeys");
    SQLUINTEGER metadata_id = SQL_FALSE;
    sqlGetStmtAttr(ctx, SQL_ATTR_METADATA_ID, (SQLPOINTER)&metadata_id, SQL_IS_POINTER, 0);
    if (! ctx.ok())
	return;
    unsigned count = 0;
    if (szCatalogName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)szCatalogName, cbCatalogName);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (getOdbcVersion(ctx) == 2)
	    text.appfmt(" table_cat = '%s'", data.sqlchar());
	else if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_cat = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_cat = '%s'", data.sqlchar());		// no pattern
    }
    if (szSchemaName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)szSchemaName, cbSchemaName);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_schem = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_schem = '%s'", data.sqlchar());		// no pattern
    }
    if (szTableName != 0) {
	OdbcData data;
	data.copyin(ctx, OdbcData::Sqlchar, (SQLPOINTER)szTableName, cbTableName);
	if (! ctx.ok())
	    return;
	text.append(++count == 1 ? " WHERE" : " AND");
	if (metadata_id == SQL_TRUE)
	    text.appfmt(" table_name = '%s'", data.sqlchar());		// XXX UPPER
	else
	    text.appfmt(" table_name = '%s'", data.sqlchar());		// no pattern
    } else {								// no null
	ctx.pushStatus(Sqlstate::_HY009, Error::Gen, "null table name");
	return;
    }
    text.append(" ORDER BY table_cat, table_schem, table_name, key_seq");
    sqlExecDirect(ctx, (SQLCHAR*)text.c_str(), text.length());
}

int
HandleStmt::getOdbcVersion(Ctx& ctx)
{
    return m_dbc->getOdbcVersion(ctx);
}

// prepare and execute

void
HandleStmt::sqlPrepare(Ctx& ctx, SQLCHAR* statementText, SQLINTEGER textLength)
{
    if (m_state == Open) {
	ctx.pushStatus(Sqlstate::_24000, Error::Gen, "cursor is open");
	return;
    }
    free(ctx);
    const char* text = reinterpret_cast<char*>(statementText);
    if (textLength == SQL_NTS) {
	m_sqlText.assign(text);
    } else if (textLength >= 0) {
	m_sqlText.assign(text, textLength);
    } else {
	ctx.pushStatus(Sqlstate::_HY090, Error::Gen, "missing SQL text");
	return;
    }
    if (! useSchemaCon(ctx, true))
	return;
    CodeGen codegen(*this);
    codegen.prepare(ctx);
    useSchemaCon(ctx, false);
    if (! ctx.ok()) {
	free(ctx);
	return;
    }
    ctx_log2(("prepared %s statement", m_stmtInfo.getDesc()));
    m_state = Prepared;
}

void
HandleStmt::sqlExecute(Ctx& ctx)
{
    if (m_state == Open) {
	ctx.pushStatus(Sqlstate::_24000, Error::Gen, "cursor is open");
	return;
    }
    StmtType stmtType = m_stmtInfo.getType();
    switch (stmtType) {
    case Stmt_type_DDL:
	if (! useSchemaCon(ctx, true))
	    return;
	break;
    case Stmt_type_query:
    case Stmt_type_DML:
	if (! useConnection(ctx, true))
	    return;
	break;
    default:
	ctx_assert(false);
	break;
    }
    CodeGen codegen(*this);
    codegen.execute(ctx);
    // valid only after execute says M$  XXX create this diag only on demand
    setFunction(ctx);
    if (ctx.getCode() == SQL_NEED_DATA) {
	m_state = NeedData;
	return;
    }
    ctx_log2(("execute: fetched %u tuples from NDB", (unsigned)m_tuplesFetched));
    switch (stmtType) {
    case Stmt_type_DDL:
	codegen.close(ctx);
	useSchemaCon(ctx, false);
	m_state = Prepared;
	break;
    case Stmt_type_query:
	if (! ctx.ok()) {
	    codegen.close(ctx);
	    useConnection(ctx, false);
	    m_state = Prepared;
	} else {
	    m_state = Open;
	}
	break;
    case Stmt_type_DML:
	codegen.close(ctx);
	if (m_dbc->autocommit()) {
	    // even if error
	    m_dbc->sqlEndTran(ctx, SQL_COMMIT);
	} else {
	    m_dbc->uncommitted(true);	// uncommitted changes possible
	}
	useConnection(ctx, false);
	m_state = Prepared;
	break;
    default:
	ctx_assert(false);
	break;
    }
}

void
HandleStmt::sqlExecDirect(Ctx& ctx, SQLCHAR* statementText, SQLINTEGER textLength)
{
    sqlPrepare(ctx, statementText, textLength);
    if (! ctx.ok())
	return;
    sqlExecute(ctx);
}

void
HandleStmt::sqlFetch(Ctx& ctx)
{
    if (m_state != Open) {
	ctx.pushStatus(Sqlstate::_24000, Error::Gen, "cursor is not open");
	return;
    }
    StmtType stmtType = m_stmtInfo.getType();
    switch (stmtType) {
    case Stmt_type_query: {
	CodeGen codegen(*this);
	codegen.fetch(ctx);
	if (! ctx.ok()) {
	    codegen.close(ctx);
	    useConnection(ctx, false);
	}
	break;
	}
    default:
	ctx_assert(false);
	break;
    }
}

void
HandleStmt::sqlRowCount(Ctx& ctx, SQLINTEGER* rowCount)
{
    *rowCount = m_rowCount;
}

void
HandleStmt::sqlMoreResults(Ctx& ctx)
{
    if (m_state == Open) {
	sqlCloseCursor(ctx);
	if (! ctx.ok())
	    return;
    }
    ctx.setCode(SQL_NO_DATA);
}

void
HandleStmt::sqlCancel(Ctx& ctx)
{
    if (m_state == NeedData) {
	CodeGen codegen(*this);
	codegen.close(ctx);
	m_state = Prepared;
    }
}

void
HandleStmt::sqlCloseCursor(Ctx& ctx)
{
    if (m_state != Open) {
	ctx.pushStatus(Sqlstate::_24000, Error::Gen, "cursor is not open");
	return;
    }
    ctx_log2(("execute: fetched %u tuples from NDB", (unsigned)m_tuplesFetched));
    StmtType stmtType = m_stmtInfo.getType();
    switch (stmtType) {
    case Stmt_type_query: {
	CodeGen codegen(*this);
	codegen.close(ctx);
	useConnection(ctx, false);
	m_state = Prepared;
	m_rowCount = 0;
	m_tuplesFetched = 0;
	break;
	}
    default:
	ctx_assert(false);
	break;
    }
}

void
HandleStmt::sqlGetCursorName(Ctx& ctx, SQLCHAR* cursorName, SQLSMALLINT bufferLength, SQLSMALLINT* nameLength)
{
    OdbcData name("SQL_CUR_DUMMY");
    name.copyout(ctx, cursorName, bufferLength, 0, nameLength);
}

void
HandleStmt::sqlSetCursorName(Ctx& ctx, SQLCHAR* cursorName, SQLSMALLINT nameLength)
{
}

// special data access

void
HandleStmt::sqlGetData(Ctx& ctx, SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLINTEGER bufferLength, SQLINTEGER* strlen_or_Ind)
{
    if (m_state != Open) {
	ctx.pushStatus(Sqlstate::_24000, Error::Gen, "cursor is not open");
	return;
    }
    if (bufferLength < 0) {
	ctx.pushStatus(Sqlstate::_HY090, Error::Gen, "invalid buffer length %d", (int)bufferLength);
	return;
    }
    CodeGen codegen(*this);
    codegen.sqlGetData(ctx, columnNumber, targetType, targetValue, bufferLength, strlen_or_Ind);
}

void
HandleStmt::sqlParamData(Ctx& ctx, SQLPOINTER* value)
{
    if (m_state != NeedData) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "not expecting data-at-exec");
	return;
    }
    CodeGen codegen(*this);
    codegen.sqlParamData(ctx, value);
    if (! ctx.ok())
	return;
    sqlExecute(ctx);
}

void
HandleStmt::sqlPutData(Ctx& ctx, SQLPOINTER data, SQLINTEGER strlen_or_Ind)
{
    if (m_state != NeedData) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "not expecting data-at-exec");
	return;
    }
    CodeGen codegen(*this);
    codegen.sqlPutData(ctx, data, strlen_or_Ind);
}

// describe statement

void
HandleStmt::sqlNumParams(Ctx& ctx, SQLSMALLINT* parameterCountPtr)
{
    if (m_state == Free) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "statement is not prepared");
	return;
    }
    HandleDesc* ipd = getHandleDesc(ctx, Desc_usage_IPD);
    ipd->sqlGetDescField(ctx, 0, SQL_DESC_COUNT, static_cast<SQLPOINTER>(parameterCountPtr), -1, 0);
}

void
HandleStmt::sqlDescribeParam(Ctx& ctx, SQLUSMALLINT ipar, SQLSMALLINT* pfSqlType, SQLUINTEGER* pcbParamDef, SQLSMALLINT* pibScale, SQLSMALLINT* pfNullable)
{
    if (m_state == Free) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "statement is not prepared");
	return;
    }
    HandleDesc* ipd = getHandleDesc(ctx, Desc_usage_IPD);
    ipd->sqlGetDescField(ctx, ipar, SQL_DESC_CONCISE_TYPE, static_cast<SQLPOINTER>(pfSqlType), -1, 0);
    {	// XXX fix it
	OdbcData data((SQLUINTEGER)0);
	data.copyout(ctx, (SQLPOINTER)pcbParamDef, -1, 0);
    }
    {	// XXX fix it
	OdbcData data((SQLSMALLINT)0);
	data.copyout(ctx, (SQLPOINTER)pibScale, -1, 0);
    }
    ipd->sqlGetDescField(ctx, ipar, SQL_DESC_NULLABLE, static_cast<SQLPOINTER>(pfNullable), -1, 0);
}

void
HandleStmt::sqlNumResultCols(Ctx& ctx, SQLSMALLINT* columnCount)
{
    if (m_state == Free) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "statement is not prepared");
	return;
    }
    HandleDesc* const ird = getHandleDesc(ctx, Desc_usage_IRD);
    ird->sqlGetDescField(ctx, 0, SQL_DESC_COUNT, static_cast<SQLPOINTER>(columnCount), -1, 0);
    setFunction(ctx);	// unixODBC workaround
}

void
HandleStmt::sqlDescribeCol(Ctx& ctx, SQLUSMALLINT columnNumber, SQLCHAR* columnName, SQLSMALLINT bufferLength, SQLSMALLINT* nameLength, SQLSMALLINT* dataType, SQLUINTEGER* columnSize, SQLSMALLINT* decimalDigits, SQLSMALLINT* nullable)
{
    if (m_state == Free) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "statement is not prepared");
	return;
    }
    HandleDesc* const ird = getHandleDesc(ctx, Desc_usage_IRD);
    ird->sqlGetDescField(ctx, columnNumber, SQL_DESC_NAME, static_cast<SQLPOINTER>(columnName), bufferLength, 0, nameLength);
    ird->sqlGetDescField(ctx, columnNumber, SQL_DESC_CONCISE_TYPE, static_cast<SQLPOINTER>(dataType), -1, 0);
    if (! ctx.ok())
	return;
    if (columnSize != 0) {
	switch (*dataType) {
	case SQL_CHAR:
	case SQL_VARCHAR:
	case SQL_BINARY:
	case SQL_VARBINARY:
	    ird->sqlGetDescField(ctx, columnNumber, SQL_DESC_LENGTH, static_cast<SQLPOINTER>(columnSize), -1, 0);
	    break;
	case SQL_SMALLINT:
	    *columnSize = 5;
	    break;
	case SQL_INTEGER:
	    *columnSize = 10;
	    break;
	case SQL_BIGINT:
	    *columnSize = 20;	// XXX 19 for signed
	    break;
	case SQL_REAL:
	    *columnSize = 7;
	    break;
	case SQL_DOUBLE:
	    *columnSize = 15;
	    break;
	case SQL_TYPE_TIMESTAMP:
	    *columnSize = 30;
	    break;
	default:
	    *columnSize = 0;
	    break;
	}
    }
    if (decimalDigits != 0) {
	switch (*dataType) {
	case SQL_SMALLINT:
	case SQL_INTEGER:
	case SQL_BIGINT:
	    *decimalDigits = 0;
	    break;
	case SQL_TYPE_TIMESTAMP:
	    *decimalDigits = 10;
	    break;
	default:
	    *decimalDigits = 0;
	    break;
	}
    }
    ird->sqlGetDescField(ctx, columnNumber, SQL_DESC_NULLABLE, static_cast<SQLPOINTER>(nullable), -1, 0);
}

void
HandleStmt::sqlColAttribute(Ctx& ctx, SQLUSMALLINT columnNumber, SQLUSMALLINT fieldIdentifier, SQLPOINTER characterAttribute, SQLSMALLINT bufferLength, SQLSMALLINT* stringLength, SQLPOINTER numericAttribute)
{
    if (m_state == Free) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "statement is not prepared");
	return;
    }
    HandleDesc* const ird = getHandleDesc(ctx, Desc_usage_IRD);
    ird->sqlColAttribute(ctx, columnNumber, fieldIdentifier, characterAttribute, bufferLength, stringLength, numericAttribute);
}

void
HandleStmt::sqlColAttributes(Ctx& ctx, SQLUSMALLINT icol, SQLUSMALLINT fdescType, SQLPOINTER rgbDesc, SQLSMALLINT cbDescMax, SQLSMALLINT* pcbDesc, SQLINTEGER* pfDesc)
{
    if (m_state == Free) {
	ctx.pushStatus(Sqlstate::_HY010, Error::Gen, "statement is nor prepared");
	return;
    }
    HandleDesc* const ird = getHandleDesc(ctx, Desc_usage_IRD);
    ird->sqlColAttributes(ctx, icol, fdescType, rgbDesc, cbDescMax, pcbDesc, pfDesc);
}

// descriptor manipulation

void
HandleStmt::sqlBindCol(Ctx& ctx, SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLINTEGER bufferLength, SQLINTEGER* strlen_or_Ind)
{
    HandleDesc* const ard = getHandleDesc(ctx, Desc_usage_ARD);
    DescArea& desc = ard->descArea();
    if (desc.getCount() < columnNumber) {
	desc.setCount(ctx, columnNumber);
    }
    DescRec& rec = desc.getRecord(columnNumber);
    rec.setField(ctx, SQL_DESC_TYPE, targetType);
    rec.setField(ctx, SQL_DESC_CONCISE_TYPE, targetType);
    rec.setField(ctx, SQL_DESC_DATA_PTR, targetValue);
    rec.setField(ctx, SQL_DESC_OCTET_LENGTH, bufferLength);
    rec.setField(ctx, SQL_DESC_OCTET_LENGTH_PTR, strlen_or_Ind);
    rec.setField(ctx, SQL_DESC_INDICATOR_PTR, strlen_or_Ind);
}

void
HandleStmt::sqlBindParameter(Ctx& ctx, SQLUSMALLINT ipar, SQLSMALLINT fParamType, SQLSMALLINT fCType, SQLSMALLINT fSqlType, SQLUINTEGER cbColDef, SQLSMALLINT ibScale, SQLPOINTER rgbValue, SQLINTEGER cbValueMax, SQLINTEGER* pcbValue)
{
    HandleDesc* const ipd = getHandleDesc(ctx, Desc_usage_IPD);
    HandleDesc* const apd = getHandleDesc(ctx, Desc_usage_APD);
    DescArea& descIPD = ipd->descArea();
    DescArea& descAPD = apd->descArea();
    if (ipar < 1) {
	ctx.pushStatus(Sqlstate::_07009, Error::Gen, "invalid parameter index %u", (unsigned)ipar);
	return;
    }
    if (descIPD.getCount() < ipar) {
	descIPD.setCount(ctx, ipar);
    }
    if (descAPD.getCount() < ipar) {
	descAPD.setCount(ctx, ipar);
    }
    DescRec& recIPD = descIPD.getRecord(ipar);
    DescRec& recAPD = descAPD.getRecord(ipar);
    recIPD.setField(ctx, SQL_DESC_PARAMETER_TYPE, fParamType);
    recAPD.setField(ctx, SQL_DESC_TYPE, fCType);
    recAPD.setField(ctx, SQL_DESC_CONCISE_TYPE, fCType);
    recIPD.setField(ctx, SQL_DESC_TYPE, fSqlType);
    recIPD.setField(ctx, SQL_DESC_CONCISE_TYPE, fSqlType);
    switch (fSqlType) {
    case SQL_CHAR:		// XXX not complete
    case SQL_VARCHAR:
    case SQL_BINARY:
    case SQL_VARBINARY:
	recIPD.setField(ctx, SQL_DESC_LENGTH, cbColDef);
	break;
    case SQL_DECIMAL:
    case SQL_NUMERIC:
    case SQL_FLOAT:
    case SQL_REAL:
    case SQL_DOUBLE:
	recIPD.setField(ctx, SQL_DESC_PRECISION, cbColDef);
	break;
    }
    switch (fSqlType) {
    case SQL_TYPE_TIME:		// XXX not complete
    case SQL_TYPE_TIMESTAMP:
	recIPD.setField(ctx, SQL_DESC_PRECISION, ibScale);
	break;
    case SQL_NUMERIC:
    case SQL_DECIMAL:
	recIPD.setField(ctx, SQL_DESC_SCALE, ibScale);
	break;
    }
    recAPD.setField(ctx, SQL_DESC_DATA_PTR, rgbValue);
    recAPD.setField(ctx, SQL_DESC_OCTET_LENGTH, cbValueMax);
    recAPD.setField(ctx, SQL_DESC_OCTET_LENGTH_PTR, pcbValue);
    recAPD.setField(ctx, SQL_DESC_INDICATOR_PTR, pcbValue);
}

void
HandleStmt::sqlBindParam(Ctx& ctx, SQLUSMALLINT parameterNumber, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLUINTEGER lengthPrecision, SQLSMALLINT parameterScale, SQLPOINTER parameterValue, SQLINTEGER* strLen_or_Ind)
{
    sqlBindParameter(ctx, parameterNumber, SQL_PARAM_INPUT_OUTPUT, valueType, parameterType, lengthPrecision, parameterScale, parameterValue, SQL_SETPARAM_VALUE_MAX, strLen_or_Ind);
}

void
HandleStmt::sqlSetParam(Ctx& ctx, SQLUSMALLINT parameterNumber, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLUINTEGER lengthPrecision, SQLSMALLINT parameterScale, SQLPOINTER parameterValue, SQLINTEGER* strLen_or_Ind)
{
    sqlBindParameter(ctx, parameterNumber, SQL_PARAM_INPUT_OUTPUT, valueType, parameterType, lengthPrecision, parameterScale, parameterValue, SQL_SETPARAM_VALUE_MAX, strLen_or_Ind);
}