/**
 * @file   FSEGODB.c
 * @brief  FSEG SPI sample: store FLAM archive segments in a relational
 *         database via ODBC.
 * @author limes datentechnik gmbh
 *
 * DISCLAIMER: This file is a sample implementation provided by limes
 * datentechnik gmbh for educational and integration purposes.  It is
 * delivered as source code free of charge, including with the FL5 DEMO
 * license, and may be used, adapted, and redistributed without restriction.
 * This sample has NOT passed through the full limes datentechnik gmbh QA
 * process.  Users are responsible for verifying correctness, security, and
 * fitness for their specific environment before deploying it in production.
 * limes datentechnik gmbh provides NO WARRANTY of any kind for this sample
 * and is not liable for any damages arising from its use, including but not
 * limited to data loss, security vulnerabilities, or service interruptions.
 *
 * It requires an ODBC-capable relational database and the unixODBC
 * (Linux/AIX/Solaris), iODBC (macOS), or ODBC32 (Windows) driver manager.
 * Compile with -D__HAS_ODBC__ to enable.
 * Tested against: PostgreSQL, MySQL, SQL Server, DB2, SQLite (via ODBC).
 *
 * DESIGN: PARAMETER vs NAME separation
 *
 *   FSEG SPI receives two distinct identifiers per open call:
 *
 *   pcParStr (PARAMETER=) -- static, deployment-specific configuration.
 *     Contains everything that describes WHERE and HOW to access the database:
 *     ODBC data source name (dsn) or connection string (connstr), credentials
 *     (uid/pwd), the SQL schema prefix that qualifies the table names
 *     (schema), and tuning parameters (seqtype, seqname, odbclib).  These
 *     values are the same for every archive stored in one database deployment
 *     and typically do not change between invocations.  Because they include
 *     credentials they must be treated as security-sensitive (see CREDENTIAL
 *     PROTECTION below).
 *
 *   pcNamStr (NAME=) -- dynamic, per-archive identifier.
 *     The logical name of the specific archive being opened, e.g.
 *     "backups/hr/payroll/2026".  This string is stored verbatim in the
 *     @c namespace column of both @c FLAM_SEGMENT and @c FLAM_ARCHIVE tables
 *     and uniquely identifies the archive within the database.  It is not
 *     security-sensitive and changes with every new archive.
 *
 *   Separating these two concerns keeps credential management independent of
 *   the archive name: many different archives (pcNamStr values) can coexist
 *   in the same database schema using a single shared PARAMETER file.
 *
 * CREDENTIAL PROTECTION:
 *
 *   The PARAMETER field is a CLEP DYNSTR, which means flcl accepts the
 *   special syntax PARAMETER=f'<path>' to read the parameter string at
 *   runtime from a file instead of passing it on the command line.  This
 *   is the recommended approach for production use:
 *
 *   1. Create a protected configuration directory:
 *        mkdir -m 0700 ~/.cloud
 *
 *   2. Create a per-deployment credential file, e.g. ~/.cloud/db-prod.txt:
 *        dsn=FLAMDB;uid=flam;pwd=secret;schema=FLAMDATA.
 *      Protect it:
 *        chmod 0600 ~/.cloud/db-prod.txt
 *
 *   3. Reference it in the CLEP command:
 *        store.seguser(
 *            LIBRARY='libfsegodbc',
 *            FUNC='FSEGUSR',
 *            PARAMETER=f'~/.cloud/db-prod.txt',
 *            NAME='backups/hr/payroll/2026')
 *
 *   This pattern keeps credentials out of command lines, shell history,
 *   and audit logs.  It mirrors the ~/.ssh/ convention used for SSH keys.
 *   On z/OS use a SAF-protected dataset or HFS file in the home directory.
 *
 * @section USAGE Usage
 *
 * Register the compiled shared library (@c libfsegodbc) in the FLAM5
 * archive open qualifier:
 *
 * @code
 * store.seguser(
 *     LIBRARY='libfsegodbc',
 *     FUNC='FSEGUSR',
 *     PARAMETER=f'~/.cloud/db-prod.txt',
 *     NAME='backups/hr/payroll/2026')
 * @endcode
 *
 * The entry point exported by this file is @c FSEGUSR.
 *
 * @section SCHEMA Required database schema
 *
 * The following tables must be created by the DBA before use:
 *
 * @code{.sql}
 * -- Segment data (binary payload)
 * CREATE TABLE FLAM_SEGMENT (
 *     namespace  VARCHAR(512) NOT NULL,
 *     version    BIGINT       NOT NULL,
 *     segidx     BIGINT       NOT NULL,
 *     segtyp     INTEGER      NOT NULL,
 *     segdat     <binary>,            -- BYTEA / LONGBLOB / VARBINARY(MAX) / BLOB
 *     PRIMARY KEY (namespace, version, segidx)
 * );
 * -- Archive version registry
 * CREATE TABLE FLAM_ARCHIVE (
 *     namespace  VARCHAR(512) NOT NULL,
 *     version    BIGINT       NOT NULL,
 *     mainidx    BIGINT       NOT NULL DEFAULT -1,
 *     crtdat     TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
 *     segcnt     BIGINT       NOT NULL DEFAULT 0,
 *     datsiz     BIGINT       NOT NULL DEFAULT 0,
 *     PRIMARY KEY (namespace, version)
 * );
 * @endcode
 *
 * @section PARAMS Parameter string keys (static, per-deployment)
 *
 * The PARAMETER string passed via the CLEP qualifier is a semicolon-separated
 * list of @c key=value pairs.  It describes WHERE and HOW to connect -- store
 * it in a protected file and reference it via @c PARAMETER=f'<path>' (see
 * CREDENTIAL PROTECTION above).
 *
 * | Key       | Required       | Default                  | Description                    |
 * |-----------|----------------|--------------------------|--------------------------------|
 * | dsn       | yes (or cs)    | -                        | ODBC DSN name                  |
 * | connstr   | yes (or dsn)   | -                        | Full ODBC connection string    |
 * | uid       | no             | DSN default              | Database user                  |
 * | pwd       | no             | DSN default              | Database password              |
 * | schema    | no             | "" (no prefix)           | SQL schema prefix (e.g. FLAM.) |
 * | seqtype   | no             | max+1                    | Version ID strategy            |
 * | seqname   | no             | flam_segidx_seq          | Sequence object name           |
 * | odbclib   | no             | platform default         | ODBC library path for dlopen   |
 *
 * @section NAME Name string (dynamic, per-archive)
 *
 * The NAME string (@c pcNamStr) is the logical identifier of the specific
 * archive being opened, e.g. @c "backups/hr/payroll/2026".  It is stored
 * verbatim in the @c namespace column of @c FLAM_SEGMENT and @c FLAM_ARCHIVE
 * and uniquely identifies the archive within the database.  Multiple archives
 * can coexist in the same database schema simply by using distinct names.
 * The NAME string is not security-sensitive and is passed directly in the
 * CLEP command (not in the credential file).
 *
 * @section COMPILE Compilation
 *
 * @code
 * gcc -D__HAS_ODBC__ -fPIC -shared -o libfsegodbc.so FSEGODB.c -ldl
 * @endcode
 *
 * On Windows:
 * @code
 * cl /D__HAS_ODBC__ /LD FSEGODB.c odbc32.lib
 * @endcode
 *
 * @version 1.2.0
 * @par Changelog
 * - 1.0.0: initial implementation
 * - 1.1.0: adopt FSEG SPI NEW/FRE lifecycle; CLS no longer frees handle;
 *          OPN supports pre-allocated handle; EXS/DAR/PRN reconnect in NEW state
 * - 1.2.0: add <stdint.h> for intptr_t/uintptr_t casts; move FRE to last
 *          position in dispatcher (lifecycle convention: NEW first, FRE last)
 */

/* z/OS: select code page for string literals */
#ifdef __ZOS__
#  pragma convert(1141)
#endif

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <stdint.h>   /* intptr_t, uintptr_t -- needed for SQLPOINTER casts */

#include "FSEG.h"

/**********************************************************************/
/* ODBC-specific code starts here                                     */
/**********************************************************************/
#ifdef __HAS_ODBC__

/* --- Platform dynamic loading (no DYL.h dependency) --- */
#if defined(_WIN32) || defined(_WIN64)
#  include <windows.h>
   typedef HMODULE TDylHdl;
   static TDylHdl     dyl_open(const char* p)               { return LoadLibraryA(p); }
   static void*       dyl_sym(TDylHdl h, const char* n)     { return (void*)GetProcAddress(h, n); }
   static void        dyl_close(TDylHdl h)                  { FreeLibrary(h); }
   static const char* dyl_err(void)                         { return "LoadLibrary failed"; }
#elif defined(__ZOS__)
#  include <dll.h>
   typedef dllhandle* TDylHdl;
   static TDylHdl     dyl_open(const char* p)               { return dllload(p); }
   static void*       dyl_sym(TDylHdl h, const char* n)     { return (void*)dllqueryfn(h, n); }
   static void        dyl_close(TDylHdl h)                  { dllfree(h); }
   static const char* dyl_err(void)                         { return "dllload failed"; }
#else
#  include <dlfcn.h>
   typedef void*      TDylHdl;
   static TDylHdl     dyl_open(const char* p)               { return dlopen(p, RTLD_NOW | RTLD_LOCAL); }
   static void*       dyl_sym(TDylHdl h, const char* n)     { return dlsym(h, n); }
   static void        dyl_close(TDylHdl h)                  { dlclose(h); }
   static const char* dyl_err(void)                         { return dlerror(); }
#endif

/* --- z/OS DB2 CLI vs. standard ODBC header selection --- */
/* z/OS DB2 CLI uses sqlcli1.h; standard ODBC uses sql.h + sqlext.h.
 * The function signatures are compatible; only headers differ. */
#if defined(__ZOS__)
#  include <sqlcli1.h>
#  define FSEGODBC_USE_SQLCONNECT   1   /* DB2 CLI has no SQLDriverConnect */
#  define FSEGODBC_BINARY_SQL_TYPE  SQL_BLOB
#else
#  include <sql.h>
#  include <sqlext.h>
#  define FSEGODBC_USE_SQLCONNECT   0
#  define FSEGODBC_BINARY_SQL_TYPE  SQL_BINARY
#endif

/* --- Default ODBC library name per platform --- */
#if defined(_WIN32) || defined(_WIN64)
#  define FSEGODBC_DEFAULT_LIB  "odbc32.dll"
#elif defined(__ZOS__)
#  define FSEGODBC_DEFAULT_LIB  "libdsnao64c.so"
#elif defined(__APPLE__)
#  define FSEGODBC_DEFAULT_LIB  "libiodbc.dylib"
#else
#  define FSEGODBC_DEFAULT_LIB  "libodbc.so.2"
#endif

/* --- Provider-specific return codes --- */
/** @brief Segment or archive not found. */
#define FSEGODBC_RTC_NFD  1
/** @brief Connection / SQL error. */
#define FSEGODBC_RTC_CON  2
/** @brief Memory allocation error. */
#define FSEGODBC_RTC_ALC  3
/** @brief Bad parameter string. */
#define FSEGODBC_RTC_PAR  4

/* --- Convenience macro: guard snprintf calls against NULL buffer --- */
#define FSEGODBC_MSG(pcMsg, uiLen, ...) \
   do { \
      if ((pcMsg) != NULL && (uiLen) > 0U) { \
         snprintf((pcMsg), (uiLen), __VA_ARGS__); \
      } \
   } while (0)

/**********************************************************************/
/* ODBC function pointer table                                        */
/**********************************************************************/

/**
 * @brief ODBC function pointer table loaded dynamically from the driver manager.
 *
 * All ODBC functions are called through these pointers so that the compiled
 * binary has no link-time dependency on any specific ODBC library.
 */
typedef struct FsegOdbcFuncs
{
   TDylHdl hLib;   /**< Handle returned by dyl_open(). */

   SQLRETURN (SQL_API *pSQLAllocHandle)(SQLSMALLINT, SQLHANDLE, SQLHANDLE*);
   SQLRETURN (SQL_API *pSQLSetEnvAttr)(SQLHENV, SQLINTEGER, SQLPOINTER, SQLINTEGER);
   SQLRETURN (SQL_API *pSQLSetConnectAttr)(SQLHDBC, SQLINTEGER, SQLPOINTER, SQLINTEGER);
   SQLRETURN (SQL_API *pSQLConnect)(SQLHDBC,
                                    SQLCHAR*, SQLSMALLINT,
                                    SQLCHAR*, SQLSMALLINT,
                                    SQLCHAR*, SQLSMALLINT);
#if !FSEGODBC_USE_SQLCONNECT
   SQLRETURN (SQL_API *pSQLDriverConnect)(SQLHDBC, SQLHWND,
                                          SQLCHAR*, SQLSMALLINT,
                                          SQLCHAR*, SQLSMALLINT,
                                          SQLSMALLINT*, SQLUSMALLINT);
#endif
   SQLRETURN (SQL_API *pSQLAllocStmt)(SQLHDBC, SQLHSTMT*);
   SQLRETURN (SQL_API *pSQLPrepare)(SQLHSTMT, SQLCHAR*, SQLINTEGER);
   SQLRETURN (SQL_API *pSQLBindParameter)(SQLHSTMT, SQLUSMALLINT, SQLSMALLINT,
                                          SQLSMALLINT, SQLSMALLINT, SQLULEN,
                                          SQLSMALLINT, SQLPOINTER,
                                          SQLLEN, SQLLEN*);
   SQLRETURN (SQL_API *pSQLExecute)(SQLHSTMT);
   SQLRETURN (SQL_API *pSQLExecDirect)(SQLHSTMT, SQLCHAR*, SQLINTEGER);
   SQLRETURN (SQL_API *pSQLFetch)(SQLHSTMT);
   SQLRETURN (SQL_API *pSQLGetData)(SQLHSTMT, SQLUSMALLINT, SQLSMALLINT,
                                    SQLPOINTER, SQLLEN, SQLLEN*);
   SQLRETURN (SQL_API *pSQLGetDiagRec)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT,
                                       SQLCHAR*, SQLINTEGER*, SQLCHAR*,
                                       SQLSMALLINT, SQLSMALLINT*);
   SQLRETURN (SQL_API *pSQLEndTran)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT);
   SQLRETURN (SQL_API *pSQLFreeStmt)(SQLHSTMT, SQLUSMALLINT);
   SQLRETURN (SQL_API *pSQLFreeHandle)(SQLSMALLINT, SQLHANDLE);
   SQLRETURN (SQL_API *pSQLDisconnect)(SQLHDBC);
   SQLRETURN (SQL_API *pSQLRowCount)(SQLHSTMT, SQLLEN*);
} TsFsegOdbcFuncs;

/**********************************************************************/
/* ODBC library loader                                                */
/**********************************************************************/

/** @brief Helper: load one function pointer; return 0 on success. */
static int fsegodbc_sym(
   TDylHdl     hLib,
   void**      ppFn,
   const char* pcName,
   char*       pcMsg,
   unsigned int uiMsgLen)
{
   *ppFn = dyl_sym(hLib, pcName);
   if (*ppFn == NULL) {
      FSEGODBC_MSG(pcMsg, uiMsgLen,
         "FSEGODB: symbol '%s' not found in ODBC library", pcName);
      return FSEGODBC_RTC_CON;
   }
   return 0;
}

/**
 * @brief Dynamically load the ODBC driver manager and resolve all function
 *        pointers into the provided @p pF table.
 *
 * @param pF        Output: populated function pointer table.
 * @param pcLib     Library filename (e.g. "libodbc.so.2").
 * @param pcMsg     Error message buffer (may be NULL).
 * @param uiMsgLen  Size of @p pcMsg.
 * @return 0 on success, #FSEGODBC_RTC_CON on error.
 */
static int fsegodbc_load(
   TsFsegOdbcFuncs* pF,
   const char*      pcLib,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   pF->hLib = dyl_open(pcLib);
   if (pF->hLib == NULL) {
      FSEGODBC_MSG(pcMsg, uiMsgLen,
         "FSEGODB: cannot open ODBC library '%s': %s", pcLib, dyl_err());
      return FSEGODBC_RTC_CON;
   }

#define LOADSYM(member, name) \
   if (fsegodbc_sym(pF->hLib, (void**)&pF->member, name, pcMsg, uiMsgLen) != 0) { \
      dyl_close(pF->hLib); pF->hLib = NULL; return FSEGODBC_RTC_CON; \
   }

   LOADSYM(pSQLAllocHandle,    "SQLAllocHandle")
   LOADSYM(pSQLSetEnvAttr,     "SQLSetEnvAttr")
   LOADSYM(pSQLSetConnectAttr, "SQLSetConnectAttr")
   LOADSYM(pSQLConnect,        "SQLConnect")
#if !FSEGODBC_USE_SQLCONNECT
   LOADSYM(pSQLDriverConnect,  "SQLDriverConnect")
#endif
   LOADSYM(pSQLAllocStmt,      "SQLAllocStmt")
   LOADSYM(pSQLPrepare,        "SQLPrepare")
   LOADSYM(pSQLBindParameter,  "SQLBindParameter")
   LOADSYM(pSQLExecute,        "SQLExecute")
   LOADSYM(pSQLExecDirect,     "SQLExecDirect")
   LOADSYM(pSQLFetch,          "SQLFetch")
   LOADSYM(pSQLGetData,        "SQLGetData")
   LOADSYM(pSQLGetDiagRec,     "SQLGetDiagRec")
   LOADSYM(pSQLEndTran,        "SQLEndTran")
   LOADSYM(pSQLFreeStmt,       "SQLFreeStmt")
   LOADSYM(pSQLFreeHandle,     "SQLFreeHandle")
   LOADSYM(pSQLDisconnect,     "SQLDisconnect")
   LOADSYM(pSQLRowCount,       "SQLRowCount")

#undef LOADSYM

   return 0;
}

/**********************************************************************/
/* Provider handle                                                    */
/**********************************************************************/

/**
 * @brief Provider handle; one instance per open archive.
 *
 * Allocated on #FSEG_FUNC_NEW; connected on #FSEG_FUNC_OPN.
 */
typedef struct FsegOdbcHdl
{
   TsFsegOdbcFuncs* pF;             /**< Loaded ODBC function pointers.       */
   SQLHENV          hEnv;           /**< ODBC environment handle.             */
   SQLHDBC          hDbc;           /**< ODBC connection handle.              */
   char             acNamStr[512];  /**< Archive namespace (copied from NEW). */
   char             acPar[1024];    /**< Provider param string (from NEW).    */
   long long        siVerId;        /**< Active version; -1 if newly created. */
   int              iWrite;         /**< 1 = write open, 0 = read-only.       */
   unsigned char*   pcGetBuf;       /**< GET locate buffer (provider-owned).  */
   unsigned int     uiGetBufLen;    /**< Allocated size of pcGetBuf.          */
   SQLHSTMT         hBatchStmt;     /**< Prepared INSERT for CBF/FLB batch.   */
   unsigned int     uiBufId;        /**< Active buffer ID (0 = none).         */
   char             acSchema[64];   /**< Schema prefix e.g. "FLAMDATA.".      */
   long long        siSegCnt;       /**< Segments written (for CLS stats).    */
   long long        siDatSiz;       /**< Bytes written    (for CLS stats).    */
} TsFsegOdbcHdl;

/**********************************************************************/
/* Parameter string parser                                            */
/**********************************************************************/

/**
 * @brief Parse the semicolon-separated @c key=value parameter string.
 *
 * All output buffers are zero-terminated even on partial parse.
 * Any key not present in the string leaves the corresponding output
 * buffer unchanged (the caller should pre-initialise with defaults).
 *
 * @param pcPar      Input parameter string (may be NULL).
 * @param pcDsn      Output: ODBC DSN name.
 * @param uiDsnSz    Size of pcDsn.
 * @param pcConnStr  Output: full ODBC connection string.
 * @param uiCsSz     Size of pcConnStr.
 * @param pcUid      Output: database user.
 * @param uiUidSz    Size of pcUid.
 * @param pcPwd      Output: database password.
 * @param uiPwdSz    Size of pcPwd.
 * @param pcSchema   Output: schema prefix (e.g. "FLAMDATA.").
 * @param uiSchSz    Size of pcSchema.
 * @param pcSeqtype  Output: version ID strategy ("max+1", "sequence", "identity").
 * @param uiSqtSz    Size of pcSeqtype.
 * @param pcSeqname  Output: sequence object name.
 * @param uiSqnSz    Size of pcSeqname.
 * @param pcLib      Output: ODBC library filename.
 * @param uiLibSz    Size of pcLib.
 */
static void fsegodbc_parse(
   const char*  pcPar,
   char*        pcDsn,     unsigned int uiDsnSz,
   char*        pcConnStr, unsigned int uiCsSz,
   char*        pcUid,     unsigned int uiUidSz,
   char*        pcPwd,     unsigned int uiPwdSz,
   char*        pcSchema,  unsigned int uiSchSz,
   char*        pcSeqtype, unsigned int uiSqtSz,
   char*        pcSeqname, unsigned int uiSqnSz,
   char*        pcLib,     unsigned int uiLibSz)
{
   char         acBuf[4096];
   char*        pcTok;
   char*        pcSave = NULL;
   char*        pcEq;

   if (pcPar == NULL || pcPar[0] == '\0') {
      return;
   }

   /* work on a local copy so we can tokenise destructively */
   strncpy(acBuf, pcPar, sizeof(acBuf) - 1U);
   acBuf[sizeof(acBuf) - 1U] = '\0';

   for (pcTok = strtok_r(acBuf, ";", &pcSave);
        pcTok != NULL;
        pcTok = strtok_r(NULL, ";", &pcSave)) {

      pcEq = strchr(pcTok, '=');
      if (pcEq == NULL) {
         continue;
      }
      *pcEq = '\0';
      pcEq++;

      if (strcmp(pcTok, "dsn") == 0) {
         strncpy(pcDsn, pcEq, uiDsnSz - 1U);
         pcDsn[uiDsnSz - 1U] = '\0';
      } else if (strcmp(pcTok, "connstr") == 0) {
         strncpy(pcConnStr, pcEq, uiCsSz - 1U);
         pcConnStr[uiCsSz - 1U] = '\0';
      } else if (strcmp(pcTok, "uid") == 0) {
         strncpy(pcUid, pcEq, uiUidSz - 1U);
         pcUid[uiUidSz - 1U] = '\0';
      } else if (strcmp(pcTok, "pwd") == 0) {
         strncpy(pcPwd, pcEq, uiPwdSz - 1U);
         pcPwd[uiPwdSz - 1U] = '\0';
      } else if (strcmp(pcTok, "schema") == 0) {
         strncpy(pcSchema, pcEq, uiSchSz - 1U);
         pcSchema[uiSchSz - 1U] = '\0';
      } else if (strcmp(pcTok, "seqtype") == 0) {
         strncpy(pcSeqtype, pcEq, uiSqtSz - 1U);
         pcSeqtype[uiSqtSz - 1U] = '\0';
      } else if (strcmp(pcTok, "seqname") == 0) {
         strncpy(pcSeqname, pcEq, uiSqnSz - 1U);
         pcSeqname[uiSqnSz - 1U] = '\0';
      } else if (strcmp(pcTok, "odbclib") == 0) {
         strncpy(pcLib, pcEq, uiLibSz - 1U);
         pcLib[uiLibSz - 1U] = '\0';
      }
      /* unknown keys are silently ignored */
   }
}

/**********************************************************************/
/* SQL error extraction                                               */
/**********************************************************************/

/**
 * @brief Extract the first diagnostic record from an ODBC handle and write
 *        the error message into @p pcMsg.
 *
 * @param pF        ODBC function pointer table.
 * @param siType    Handle type (SQL_HANDLE_ENV, SQL_HANDLE_DBC, SQL_HANDLE_STMT).
 * @param hHdl      The ODBC handle to diagnose.
 * @param pcMsg     Output buffer (may be NULL).
 * @param uiMsgLen  Size of @p pcMsg.
 */
static void fsegodbc_errmsg(
   TsFsegOdbcFuncs* pF,
   SQLSMALLINT      siType,
   SQLHANDLE        hHdl,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   SQLCHAR      acState[8];
   SQLCHAR      acText[512];
   SQLINTEGER   siNative;
   SQLSMALLINT  siTextLen;
   SQLRETURN    rc;

   if (pcMsg == NULL || uiMsgLen == 0U || pF->pSQLGetDiagRec == NULL) {
      return;
   }

   rc = pF->pSQLGetDiagRec(siType, hHdl, (SQLSMALLINT)1,
                            acState, &siNative, acText,
                            (SQLSMALLINT)sizeof(acText), &siTextLen);
   if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
      snprintf(pcMsg, uiMsgLen,
         "FSEGODB: SQL error [%s] (native %d): %s",
         (char*)acState, (int)siNative, (char*)acText);
   } else {
      snprintf(pcMsg, uiMsgLen,
         "FSEGODB: SQL error (SQLGetDiagRec failed, rc=%d)", (int)rc);
   }
}

/**********************************************************************/
/* Utility: check ODBC return code                                    */
/**********************************************************************/

/** @brief Return 1 if rc is SQL_SUCCESS or SQL_SUCCESS_WITH_INFO. */
static int fsegodbc_ok(SQLRETURN rc)
{
   return (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) ? 1 : 0;
}

/**********************************************************************/
/* Utility: allocate and execute a single-use statement               */
/**********************************************************************/

/**
 * @brief Allocate a statement handle, execute SQL, and free it again.
 *
 * Convenience wrapper for fire-and-forget DDL/DML that does not need
 * result binding.
 *
 * @param pHdl      Provider handle.
 * @param pcSql     SQL string (null-terminated).
 * @param pcMsg     Error buffer.
 * @param uiMsgLen  Size of pcMsg.
 * @return 0 on success, #FSEGODBC_RTC_CON on error.
 */
static int fsegodbc_exec(
   TsFsegOdbcHdl* pHdl,
   const char*    pcSql,
   char*          pcMsg,
   unsigned int   uiMsgLen)
{
   TsFsegOdbcFuncs* pF = pHdl->pF;
   SQLHSTMT         hStmt = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   int              siRet = 0;

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)pcSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
   }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return siRet;
}

/**********************************************************************/
/* FSEG_FUNC_NEW / FRE                                                */
/**********************************************************************/

/**
 * @brief FSEG_FUNC_NEW: allocate handle; store namespace and param string.
 */
static int fsegodbc_new(
   void**           ppHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcHdl* pHdl = (TsFsegOdbcHdl*)malloc(sizeof(TsFsegOdbcHdl));
   if (pHdl == NULL) {
      FSEGODBC_MSG(pcMsg, uiMsgLen, "FSEGODB-NEW: out of memory");
      return FSEGODBC_RTC_ALC;
   }
   memset(pHdl, 0, sizeof(TsFsegOdbcHdl));
   pHdl->siVerId    = -1LL;
   pHdl->hBatchStmt = SQL_NULL_HSTMT;
   if (puPara->stNew.pcNamStr != NULL) {
      strncpy(pHdl->acNamStr, puPara->stNew.pcNamStr,
              sizeof(pHdl->acNamStr) - 1U);
   }
   if (puPara->stNew.pcParStr != NULL) {
      strncpy(pHdl->acPar, puPara->stNew.pcParStr,
              sizeof(pHdl->acPar) - 1U);
   }
   *ppHdl = pHdl;
   return 0;
}

/**
 * @brief Reconnect using stored acPar.  Used by EXS/DAR/PRN in NEW state.
 * Assumes pF is already loaded.  Sets pHdl->hEnv and pHdl->hDbc.
 */
static int fsegodbc_reconnect(
   TsFsegOdbcHdl* pHdl,
   char*          pcMsg,
   unsigned int   uiMsgLen)
{
   TsFsegOdbcFuncs* pF = pHdl->pF;
   char acDsn[256]     = "";
   char acUid[128]     = "";
   char acPwd[128]     = "";
   char acConnStr[1024]= "";
   SQLRETURN rc;

   /* Parse connection params from stored acPar (ignore unused fields) */
   {
      char acIgnore[64] = "";
      fsegodbc_parse(pHdl->acPar,
                     acDsn,     sizeof(acDsn),
                     acConnStr, sizeof(acConnStr),
                     acUid,     sizeof(acUid),
                     acPwd,     sizeof(acPwd),
                     acIgnore,  sizeof(acIgnore),
                     acIgnore,  sizeof(acIgnore),
                     acIgnore,  sizeof(acIgnore),
                     acIgnore,  sizeof(acIgnore));
   }
   if (acDsn[0] == '\0' && acConnStr[0] == '\0') {
      FSEGODBC_MSG(pcMsg, uiMsgLen,
                   "FSEGODB: reconnect: no dsn/connstr in stored params");
      return FSEGODBC_RTC_PAR;
   }

   rc = pF->pSQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &pHdl->hEnv);
   if (!fsegodbc_ok(rc)) {
      FSEGODBC_MSG(pcMsg, uiMsgLen, "FSEGODB: reconnect: SQLAllocHandle(ENV) failed");
      return FSEGODBC_RTC_CON;
   }
   pF->pSQLSetEnvAttr(pHdl->hEnv, SQL_ATTR_ODBC_VERSION,
                      (SQLPOINTER)(intptr_t)SQL_OV_ODBC3, SQL_IS_INTEGER);
   rc = pF->pSQLAllocHandle(SQL_HANDLE_DBC, pHdl->hEnv, &pHdl->hDbc);
   if (!fsegodbc_ok(rc)) {
      pF->pSQLFreeHandle(SQL_HANDLE_ENV, pHdl->hEnv);
      pHdl->hEnv = SQL_NULL_HENV;
      FSEGODBC_MSG(pcMsg, uiMsgLen, "FSEGODB: reconnect: SQLAllocHandle(DBC) failed");
      return FSEGODBC_RTC_CON;
   }
   if (acConnStr[0] != '\0') {
      rc = pF->pSQLDriverConnect(pHdl->hDbc, NULL,
                                  (SQLCHAR*)acConnStr, SQL_NTS,
                                  NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
   } else {
      char acBuf[1024];
      if (acUid[0] != '\0') {
         snprintf(acBuf, sizeof(acBuf), "DSN=%s;UID=%s;PWD=%s", acDsn, acUid, acPwd);
      } else {
         snprintf(acBuf, sizeof(acBuf), "DSN=%s", acDsn);
      }
      rc = pF->pSQLDriverConnect(pHdl->hDbc, NULL,
                                  (SQLCHAR*)acBuf, SQL_NTS,
                                  NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
   }
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      pF->pSQLFreeHandle(SQL_HANDLE_DBC, pHdl->hDbc);
      pF->pSQLFreeHandle(SQL_HANDLE_ENV, pHdl->hEnv);
      pHdl->hDbc = SQL_NULL_HDBC;
      pHdl->hEnv = SQL_NULL_HENV;
      return FSEGODBC_RTC_CON;
   }
   pF->pSQLSetConnectAttr(pHdl->hDbc, SQL_ATTR_AUTOCOMMIT,
                           (SQLPOINTER)(intptr_t)SQL_AUTOCOMMIT_OFF,
                           SQL_IS_UINTEGER);
   return 0;
}

/** @brief Disconnect and free ODBC env+connection handles without freeing pF. */
static void fsegodbc_tmp_disconnect(TsFsegOdbcHdl* pHdl)
{
   TsFsegOdbcFuncs* pF = pHdl->pF;
   if (pHdl->hDbc != SQL_NULL_HDBC) {
      pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_ROLLBACK);
      pF->pSQLDisconnect(pHdl->hDbc);
      pF->pSQLFreeHandle(SQL_HANDLE_DBC, pHdl->hDbc);
      pHdl->hDbc = SQL_NULL_HDBC;
   }
   if (pHdl->hEnv != SQL_NULL_HENV) {
      pF->pSQLFreeHandle(SQL_HANDLE_ENV, pHdl->hEnv);
      pHdl->hEnv = SQL_NULL_HENV;
   }
}

/**
 * @brief FSEG_FUNC_FRE: disconnect (if needed), unload ODBC lib, free handle.
 */
static int fsegodbc_fre(
   void**           ppHdl,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcHdl*   pHdl = (TsFsegOdbcHdl*)*ppHdl;
   TsFsegOdbcFuncs* pF;
   (void)pcMsg; (void)uiMsgLen;
   if (pHdl == NULL) { return 0; }
   pF = pHdl->pF;
   if (pF != NULL) {
      if (pHdl->hBatchStmt != SQL_NULL_HSTMT) {
         pF->pSQLFreeHandle(SQL_HANDLE_STMT, pHdl->hBatchStmt);
         pHdl->hBatchStmt = SQL_NULL_HSTMT;
      }
      fsegodbc_tmp_disconnect(pHdl);
      if (pF->hLib != NULL) { dyl_close(pF->hLib); }
      free(pF);
      pHdl->pF = NULL;
   }
   if (pHdl->pcGetBuf != NULL) {
      free(pHdl->pcGetBuf);
      pHdl->pcGetBuf = NULL;
   }
   free(pHdl);
   *ppHdl = NULL;
   return 0;
}

/**********************************************************************/
/* FSEG_FUNC_OPN                                                      */
/**********************************************************************/

/**
 * @brief Connect to the database and open (or create) an archive version.
 *        Supports pre-allocated handle from FSEG_FUNC_NEW.
 */
static int fsegodbc_opn(
   void**           ppHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOpn*       pOpn   = &puPara->stOpn;
   TsFsegOdbcHdl*   pHdl   = NULL;
   TsFsegOdbcFuncs* pF     = NULL;
   char             acDsn[256];
   char             acConnStr[1024];
   char             acUid[128];
   char             acPwd[128];
   char             acSchema[64];
   char             acSeqtype[32];
   char             acSeqname[128];
   char             acLib[512];
   SQLRETURN        rc;
   char             acSql[2048];
   SQLHSTMT         hStmt  = SQL_NULL_HSTMT;
   SQLLEN           siInd;
   long long        siNewVer;
   int              siRet  = 0;

   /* --- Defaults --- */
   memset(acDsn,      0, sizeof(acDsn));
   memset(acConnStr,  0, sizeof(acConnStr));
   memset(acUid,      0, sizeof(acUid));
   memset(acPwd,      0, sizeof(acPwd));
   memset(acSchema,   0, sizeof(acSchema));
   memset(acSeqtype,  0, sizeof(acSeqtype));
   memset(acSeqname,  0, sizeof(acSeqname));
   memset(acLib,      0, sizeof(acLib));

   strncpy(acSeqtype, "max+1",            sizeof(acSeqtype) - 1U);
   strncpy(acSeqname, "flam_segidx_seq",  sizeof(acSeqname) - 1U);
   strncpy(acLib,     FSEGODBC_DEFAULT_LIB, sizeof(acLib)   - 1U);

   fsegodbc_parse(pOpn->pcParStr,
      acDsn,     sizeof(acDsn),
      acConnStr, sizeof(acConnStr),
      acUid,     sizeof(acUid),
      acPwd,     sizeof(acPwd),
      acSchema,  sizeof(acSchema),
      acSeqtype, sizeof(acSeqtype),
      acSeqname, sizeof(acSeqname),
      acLib,     sizeof(acLib));

   if (acDsn[0] == '\0' && acConnStr[0] == '\0') {
      FSEGODBC_MSG(pcMsg, uiMsgLen,
         "FSEGODB: parameter 'dsn' or 'connstr' is required");
      return FSEGODBC_RTC_PAR;
   }

   /* --- Allocate handle (skip if pre-allocated by NEW) --- */
   if (*ppHdl != NULL) {
      pHdl = (TsFsegOdbcHdl*)*ppHdl;
      /* Re-initialize connection state; keep acNamStr/acPar/acSchema */
      pHdl->siVerId    = -1LL;
      pHdl->hBatchStmt = SQL_NULL_HSTMT;
      pHdl->hEnv       = SQL_NULL_HENV;
      pHdl->hDbc       = SQL_NULL_HDBC;
      /* Use stored acPar if param string not in puPara->stOpn */
      if ((pOpn->pcParStr == NULL || pOpn->pcParStr[0] == '\0') &&
          pHdl->acPar[0] != '\0') {
         /* Re-parse using stored param string */
         fsegodbc_parse(pHdl->acPar,
                        acDsn,     sizeof(acDsn),
                        acConnStr, sizeof(acConnStr),
                        acUid,     sizeof(acUid),
                        acPwd,     sizeof(acPwd),
                        acSchema,  sizeof(acSchema),
                        acSeqtype, sizeof(acSeqtype),
                        acSeqname, sizeof(acSeqname),
                        acLib,     sizeof(acLib));
      }
      /* Use stored namespace if not in puPara->stOpn */
      if (pOpn->pcNamStr == NULL || pOpn->pcNamStr[0] == '\0') {
         /* acNamStr already set from NEW */
      } else {
         strncpy(pHdl->acNamStr, pOpn->pcNamStr, sizeof(pHdl->acNamStr) - 1U);
      }
   } else {
      pHdl = (TsFsegOdbcHdl*)malloc(sizeof(TsFsegOdbcHdl));
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, uiMsgLen, "FSEGODB: out of memory (handle)");
         return FSEGODBC_RTC_ALC;
      }
      memset(pHdl, 0, sizeof(TsFsegOdbcHdl));
      pHdl->siVerId     = -1LL;
      pHdl->hBatchStmt  = SQL_NULL_HSTMT;
   }

   /* --- Load ODBC library --- */
   pF = (TsFsegOdbcFuncs*)malloc(sizeof(TsFsegOdbcFuncs));
   if (pF == NULL) {
      FSEGODBC_MSG(pcMsg, uiMsgLen, "FSEGODB: out of memory (funcs)");
      free(pHdl);
      return FSEGODBC_RTC_ALC;
   }
   memset(pF, 0, sizeof(TsFsegOdbcFuncs));
   pHdl->pF = pF;

   siRet = fsegodbc_load(pF, acLib, pcMsg, uiMsgLen);
   if (siRet != 0) {
      free(pF);
      free(pHdl);
      return siRet;
   }

   /* --- Copy schema prefix --- */
   strncpy(pHdl->acSchema, acSchema, sizeof(pHdl->acSchema) - 1U);

   /* --- ODBC environment --- */
   rc = pF->pSQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &pHdl->hEnv);
   if (!fsegodbc_ok(rc)) {
      FSEGODBC_MSG(pcMsg, uiMsgLen, "FSEGODB: SQLAllocHandle(ENV) failed");
      siRet = FSEGODBC_RTC_CON;
      goto cleanup;
   }

   rc = pF->pSQLSetEnvAttr(pHdl->hEnv, SQL_ATTR_ODBC_VERSION,
                            (SQLPOINTER)(intptr_t)SQL_OV_ODBC3,
                            SQL_IS_INTEGER);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_ENV, pHdl->hEnv, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
      goto cleanup;
   }

   /* --- ODBC connection --- */
   rc = pF->pSQLAllocHandle(SQL_HANDLE_DBC, pHdl->hEnv, &pHdl->hDbc);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_ENV, pHdl->hEnv, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
      goto cleanup;
   }

#if FSEGODBC_USE_SQLCONNECT
   rc = pF->pSQLConnect(pHdl->hDbc,
                        (SQLCHAR*)acDsn, SQL_NTS,
                        (SQLCHAR*)acUid, SQL_NTS,
                        (SQLCHAR*)acPwd, SQL_NTS);
#else
   if (acConnStr[0] != '\0') {
      /* use supplied connection string */
      rc = pF->pSQLDriverConnect(pHdl->hDbc, NULL,
                                  (SQLCHAR*)acConnStr, SQL_NTS,
                                  NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
   } else {
      /* build connection string from dsn + optional uid/pwd */
      if (acUid[0] != '\0') {
         snprintf(acConnStr, sizeof(acConnStr),
            "DSN=%s;UID=%s;PWD=%s", acDsn, acUid, acPwd);
      } else {
         snprintf(acConnStr, sizeof(acConnStr), "DSN=%s", acDsn);
      }
      rc = pF->pSQLDriverConnect(pHdl->hDbc, NULL,
                                  (SQLCHAR*)acConnStr, SQL_NTS,
                                  NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
   }
#endif
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
      goto cleanup;
   }

   /* --- Disable autocommit --- */
   rc = pF->pSQLSetConnectAttr(pHdl->hDbc, SQL_ATTR_AUTOCOMMIT,
                                (SQLPOINTER)(intptr_t)SQL_AUTOCOMMIT_OFF,
                                SQL_IS_UINTEGER);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
      goto cleanup;
   }

   /* --- Store namespace --- */
   if (pOpn->pcNamStr != NULL) {
      strncpy(pHdl->acNamStr, pOpn->pcNamStr, sizeof(pHdl->acNamStr) - 1U);
   }
   pHdl->siVerId = pOpn->siVerId;
   pHdl->iWrite  = (pOpn->uiFlg & FSEG_FLAG_WRITE) ? 1 : 0;

   /* --- Version ID allocation for new write sessions --- */
   if ((pOpn->uiFlg & FSEG_FLAG_WRITE) && pOpn->siVerId == -1LL) {

      /* SELECT COALESCE(MAX(version),0)+1 FROM FLAM_ARCHIVE WHERE namespace=? */
      snprintf(acSql, sizeof(acSql),
         "SELECT COALESCE(MAX(version),0)+1 FROM %sFLAM_ARCHIVE WHERE namespace=?",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup;
      }

      siInd = SQL_NTS;
      rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                  SQL_C_CHAR, SQL_VARCHAR,
                                  (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                  pHdl->acNamStr, 0, &siInd);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      siNewVer = 1LL;
      if (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
         SQLLEN siLen = 0;
         pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siNewVer,
                         (SQLLEN)sizeof(siNewVer), &siLen);
      }

      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      hStmt = SQL_NULL_HSTMT;

      /* INSERT INTO FLAM_ARCHIVE */
      snprintf(acSql, sizeof(acSql),
         "INSERT INTO %sFLAM_ARCHIVE"
         " (namespace, version, mainidx, crtdat, segcnt, datsiz)"
         " VALUES(?, ?, -1, CURRENT_TIMESTAMP, 0, 0)",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup;
      }

      {
         SQLLEN siIndNam = SQL_NTS;
         SQLLEN siIndVer = 0;

         rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                     SQL_C_CHAR, SQL_VARCHAR,
                                     (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                     pHdl->acNamStr, 0, &siIndNam);
         if (!fsegodbc_ok(rc)) {
            fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
            siRet = FSEGODBC_RTC_CON;
            goto cleanup_stmt;
         }

         rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                                     SQL_C_SBIGINT, SQL_BIGINT,
                                     0, 0, &siNewVer, 0, &siIndVer);
         if (!fsegodbc_ok(rc)) {
            fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
            siRet = FSEGODBC_RTC_CON;
            goto cleanup_stmt;
         }

         rc = pF->pSQLExecute(hStmt);
         if (!fsegodbc_ok(rc)) {
            fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
            pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_ROLLBACK);
            siRet = FSEGODBC_RTC_CON;
            goto cleanup_stmt;
         }
      }

      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      hStmt = SQL_NULL_HSTMT;

      rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup;
      }

      pHdl->siVerId = siNewVer;

   } else if (pOpn->siVerId >= 0LL) {
      /* Read-only: verify the requested version exists */
      SQLLEN   siIndNam = SQL_NTS;
      SQLLEN   siIndVer = 0;
      long long siCheckVer = pOpn->siVerId;
      long long siCount    = 0LL;
      SQLLEN   siCountInd;

      snprintf(acSql, sizeof(acSql),
         "SELECT COUNT(*) FROM %sFLAM_ARCHIVE"
         " WHERE namespace=? AND version=?",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup;
      }

      rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                  SQL_C_CHAR, SQL_VARCHAR,
                                  (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                  pHdl->acNamStr, 0, &siIndNam);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                                  SQL_C_SBIGINT, SQL_BIGINT,
                                  0, 0, &siCheckVer, 0, &siIndVer);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      if (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
         pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siCount,
                         (SQLLEN)sizeof(siCount), &siCountInd);
      }

      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      hStmt = SQL_NULL_HSTMT;

      if (siCount == 0LL) {
         FSEGODBC_MSG(pcMsg, uiMsgLen,
            "FSEGODB: archive namespace='%s' version=%lld not found",
            pHdl->acNamStr, (long long)pOpn->siVerId);
         siRet = FSEGODBC_RTC_NFD;
         goto cleanup;
      }

   } else {
      /* siVerId == -1 and read-only: open the latest version */
      SQLLEN   siIndNam = SQL_NTS;
      long long siLatest = -1LL;
      SQLLEN   siLatestInd;

      snprintf(acSql, sizeof(acSql),
         "SELECT COALESCE(MAX(version),-1) FROM %sFLAM_ARCHIVE"
         " WHERE namespace=?",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup;
      }

      rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                  SQL_C_CHAR, SQL_VARCHAR,
                                  (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                  pHdl->acNamStr, 0, &siIndNam);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
      if (!fsegodbc_ok(rc)) {
         fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
         goto cleanup_stmt;
      }

      if (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
         pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siLatest,
                         (SQLLEN)sizeof(siLatest), &siLatestInd);
      }

      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      hStmt = SQL_NULL_HSTMT;

      if (siLatest < 0LL) {
         FSEGODBC_MSG(pcMsg, uiMsgLen,
            "FSEGODB: archive namespace='%s' has no versions",
            pHdl->acNamStr);
         siRet = FSEGODBC_RTC_NFD;
         goto cleanup;
      }

      pHdl->siVerId = siLatest;
   }

   /* --- Write back results --- */
   *ppHdl = (void*)pHdl;
   puPara->stOpn.siOpenedVerId = pHdl->siVerId;
   /* ppcName: point to our internal namespace string */
   if (puPara->stOpn.ppcName != NULL) {
      puPara->stOpn.ppcName[0] = pHdl->acNamStr;
   }
   return 0;

cleanup_stmt:
   if (hStmt != SQL_NULL_HSTMT) {
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   }

cleanup:
   if (pHdl->hDbc != SQL_NULL_HDBC) {
      pF->pSQLDisconnect(pHdl->hDbc);
      pF->pSQLFreeHandle(SQL_HANDLE_DBC, pHdl->hDbc);
      pHdl->hDbc = SQL_NULL_HDBC;
   }
   if (pHdl->hEnv != SQL_NULL_HENV) {
      pF->pSQLFreeHandle(SQL_HANDLE_ENV, pHdl->hEnv);
      pHdl->hEnv = SQL_NULL_HENV;
   }
   if (pF->hLib != NULL) {
      dyl_close(pF->hLib);
   }
   free(pF);
   pHdl->pF = NULL;
   if (*ppHdl == NULL) {
      free(pHdl);   /* only free if we allocated it here */
   }
   return siRet;
}

/**********************************************************************/
/* FSEG_FUNC_CLS                                                      */
/**********************************************************************/

/**
 * @brief Commit or rollback and disconnect.
 *
 * On commit: updates the statistics row in FLAM_ARCHIVE.
 * On rollback: deletes all segment rows and the FLAM_ARCHIVE row for the
 * current version, then commits (to ensure consistent state).
 */
static int fsegodbc_cls(
   void**           ppHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcHdl*   pHdl   = (TsFsegOdbcHdl*)*ppHdl;
   TsFsegOdbcFuncs* pF     = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt  = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   int              siRet  = 0;

   /* --- Free batch statement if still open --- */
   if (pHdl->hBatchStmt != SQL_NULL_HSTMT) {
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, pHdl->hBatchStmt);
      pHdl->hBatchStmt = SQL_NULL_HSTMT;
   }

   if (puPara->stCls.isCommit == 0) {
      /* Rollback: delete all segments and the archive version row */
      snprintf(acSql, sizeof(acSql),
         "DELETE FROM %sFLAM_SEGMENT WHERE namespace=? AND version=?",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (fsegodbc_ok(rc)) {
         pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                SQL_C_CHAR, SQL_VARCHAR,
                                (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                pHdl->acNamStr, 0, &siIndNam);
         pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                                SQL_C_SBIGINT, SQL_BIGINT,
                                0, 0, &pHdl->siVerId, 0, &siIndVer);
         pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
         pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
         hStmt = SQL_NULL_HSTMT;
      }

      snprintf(acSql, sizeof(acSql),
         "DELETE FROM %sFLAM_ARCHIVE WHERE namespace=? AND version=?",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (fsegodbc_ok(rc)) {
         pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                SQL_C_CHAR, SQL_VARCHAR,
                                (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                pHdl->acNamStr, 0, &siIndNam);
         pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                                SQL_C_SBIGINT, SQL_BIGINT,
                                0, 0, &pHdl->siVerId, 0, &siIndVer);
         pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
         pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
         hStmt = SQL_NULL_HSTMT;
      }

      pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);

   } else {
      /* Commit: update statistics row */
      snprintf(acSql, sizeof(acSql),
         "UPDATE %sFLAM_ARCHIVE SET segcnt=?, datsiz=?"
         " WHERE namespace=? AND version=?",
         pHdl->acSchema);

      rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
      if (fsegodbc_ok(rc)) {
         SQLLEN siIndCnt = 0;
         SQLLEN siIndSiz = 0;

         pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                                SQL_C_SBIGINT, SQL_BIGINT,
                                0, 0, &pHdl->siSegCnt, 0, &siIndCnt);
         pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                                SQL_C_SBIGINT, SQL_BIGINT,
                                0, 0, &pHdl->siDatSiz, 0, &siIndSiz);
         pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                                SQL_C_CHAR, SQL_VARCHAR,
                                (SQLULEN)sizeof(pHdl->acNamStr), 0,
                                pHdl->acNamStr, 0, &siIndNam);
         pF->pSQLBindParameter(hStmt, 4, SQL_PARAM_INPUT,
                                SQL_C_SBIGINT, SQL_BIGINT,
                                0, 0, &pHdl->siVerId, 0, &siIndVer);

         rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
         if (!fsegodbc_ok(rc)) {
            fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
            siRet = FSEGODBC_RTC_CON;
         }
         pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
         hStmt = SQL_NULL_HSTMT;
      }

      rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
      if (!fsegodbc_ok(rc) && siRet == 0) {
         fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
         siRet = FSEGODBC_RTC_CON;
      }

      /* Report statistics back to SEGSSE */
      puPara->stCls.uiSegmentBytesWritten =
         (unsigned long long)pHdl->siDatSiz;
      puPara->stCls.uiTotalBytesWritten   =
         (unsigned long long)pHdl->siDatSiz; /* no separate overhead tracked */
   }

   /* --- Release locate buffer --- */
   if (pHdl->pcGetBuf != NULL) {
      free(pHdl->pcGetBuf);
      pHdl->pcGetBuf    = NULL;
      pHdl->uiGetBufLen = 0U;
   }

   /* --- Disconnect ODBC (keep pF loaded for EXS/DAR/PRN in NEW state) --- */
   fsegodbc_tmp_disconnect(pHdl);
   /* *ppHdl stays non-NULL; handle is in NEW state; pF remains loaded.
    * Call FSEG_FUNC_FRE to release pF and free the handle.            */
   return siRet;
}

/**********************************************************************/
/* FSEG_FUNC_PUT                                                      */
/**********************************************************************/

/**
 * @brief Insert a new segment and return its assigned index.
 *
 * The segment index is allocated as MAX(segidx)+1 within the current
 * namespace/version inside an explicit transaction.
 */
static int fsegodbc_put(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF       = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt    = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   long long        siNewIdx = 1LL;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   SQLLEN           siIndIdx = 0;
   SQLLEN           siIndTyp = 0;
   SQLLEN           siIndDat;
   unsigned int     uiTyp    = puPara->stPut.uiSegTyp;
   unsigned int     uiLen    = puPara->stPut.uiSegLen;
   int              siRet    = 0;

   /* Determine next segment index */
   snprintf(acSql, sizeof(acSql),
      "SELECT COALESCE(MAX(segidx),0)+1 FROM %sFLAM_SEGMENT"
      " WHERE namespace=? AND version=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      return FSEGODBC_RTC_CON;
   }

   if (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
      SQLLEN siLen = 0;
      pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siNewIdx,
                      (SQLLEN)sizeof(siNewIdx), &siLen);
   }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   /* Insert segment */
   snprintf(acSql, sizeof(acSql),
      "INSERT INTO %sFLAM_SEGMENT"
      " (namespace, version, segidx, segtyp, segdat)"
      " VALUES(?, ?, ?, ?, ?)",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto put_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto put_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siNewIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto put_err; }

   rc = pF->pSQLBindParameter(hStmt, 4, SQL_PARAM_INPUT,
                               SQL_C_ULONG, SQL_INTEGER,
                               0, 0, &uiTyp, 0, &siIndTyp);
   if (!fsegodbc_ok(rc)) { goto put_err; }

   siIndDat = (SQLLEN)uiLen;
   rc = pF->pSQLBindParameter(hStmt, 5, SQL_PARAM_INPUT,
                               SQL_C_BINARY,
                               (SQLSMALLINT)FSEGODBC_BINARY_SQL_TYPE,
                               (SQLULEN)uiLen, 0,
                               (SQLPOINTER)(uintptr_t)puPara->stPut.pcSegDat,
                               (SQLLEN)uiLen, &siIndDat);
   if (!fsegodbc_ok(rc)) { goto put_err; }

   rc = pF->pSQLExecute(hStmt);
   if (!fsegodbc_ok(rc)) { goto put_err; }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);

   rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   puPara->stPut.siSegInd  = siNewIdx;
   pHdl->siSegCnt          += 1LL;
   pHdl->siDatSiz          += (long long)uiLen;
   return 0;

put_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_GET                                                      */
/**********************************************************************/

/**
 * @brief Fetch a segment blob from the database into the locate buffer.
 *
 * The locate buffer is reused across calls; it grows as needed but is
 * never shrunk.  The caller (FLAM) must not free the returned pointer.
 */
static int fsegodbc_get(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   SQLLEN           siIndIdx = 0;
   long long        siIdx   = puPara->stGet.siSegInd;
   SQLLEN           siDataLen;
   unsigned int     uiNeed;

   snprintf(acSql, sizeof(acSql),
      "SELECT segdat FROM %sFLAM_SEGMENT"
      " WHERE namespace=? AND version=? AND segidx=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLFetch(hStmt);
   if (rc == SQL_NO_DATA) {
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      FSEGODBC_MSG(pcMsg, uiMsgLen,
         "FSEGODB: segment %lld not found (namespace='%s' version=%lld)",
         (long long)siIdx, pHdl->acNamStr, pHdl->siVerId);
      return FSEGODBC_RTC_NFD;
   }
   if (!fsegodbc_ok(rc)) { goto get_err; }

   /* First SQLGetData call: obtain the data length without copying */
   siDataLen = 0;
   pF->pSQLGetData(hStmt, 1,
                   SQL_C_BINARY,
                   NULL, 0, &siDataLen);

   /* siDataLen now holds SQL_NO_TOTAL or the actual byte count */
   if (siDataLen == SQL_NULL_DATA) {
      siDataLen = 0;
   }
   if (siDataLen == SQL_NO_TOTAL) {
      /* Driver cannot pre-report length; use a safe default and grow later */
      siDataLen = 65536;
   }

   uiNeed = (unsigned int)siDataLen;

   /* Grow locate buffer if needed */
   if (uiNeed > pHdl->uiGetBufLen || pHdl->pcGetBuf == NULL) {
      unsigned char* pcNew =
         (unsigned char*)realloc(pHdl->pcGetBuf, (size_t)uiNeed + 1U);
      if (pcNew == NULL) {
         pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
         FSEGODBC_MSG(pcMsg, uiMsgLen,
            "FSEGODB: out of memory for GET buffer (%u bytes)", uiNeed);
         return FSEGODBC_RTC_ALC;
      }
      pHdl->pcGetBuf    = pcNew;
      pHdl->uiGetBufLen = uiNeed + 1U;
   }

   /* Second SQLGetData call: actually fetch the data */
   /* Re-execute because some drivers reset after the first partial read */
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto get_err; }

   rc = pF->pSQLFetch(hStmt);
   if (rc == SQL_NO_DATA || !fsegodbc_ok(rc)) { goto get_err; }

   siDataLen = 0;
   rc = pF->pSQLGetData(hStmt, 1, SQL_C_BINARY,
                         pHdl->pcGetBuf, (SQLLEN)pHdl->uiGetBufLen,
                         &siDataLen);
   if (!fsegodbc_ok(rc) && rc != SQL_NO_DATA) { goto get_err; }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);

   if (siDataLen < 0) {
      siDataLen = 0;
   }

   puPara->stGet.uiSegLen = (unsigned int)siDataLen;
   *puPara->stGet.ppSegDat = pHdl->pcGetBuf;
   return 0;

get_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_DEL                                                      */
/**********************************************************************/

/**
 * @brief Delete a segment; report its byte size in uiDelLen.
 *
 * A DELETE of a non-existent segment is treated as success (uiDelLen=0).
 */
static int fsegodbc_del(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   SQLLEN           siIndIdx = 0;
   long long        siIdx   = puPara->stDel.siSegInd;
   long long        siDlen  = 0LL;
   SQLLEN           siDlenInd;

   /* Get the data length before deleting */
   snprintf(acSql, sizeof(acSql),
      "SELECT COALESCE(LENGTH(segdat),0) FROM %sFLAM_SEGMENT"
      " WHERE namespace=? AND version=? AND segidx=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   if (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
      pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siDlen,
                      (SQLLEN)sizeof(siDlen), &siDlenInd);
   }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   /* Delete the segment row */
   snprintf(acSql, sizeof(acSql),
      "DELETE FROM %sFLAM_SEGMENT"
      " WHERE namespace=? AND version=? AND segidx=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto del_err; }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);

   rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   puPara->stDel.uiDelLen = (unsigned int)((siDlen > 0LL) ? siDlen : 0LL);
   return 0;

del_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_EXS                                                      */
/**********************************************************************/

/**
 * @brief Check whether a segment exists; write result to isExists.
 *
 * Always returns @c *piRetco = 0 (success); a non-existent segment is
 * not an error.
 */
static int fsegodbc_exs(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   SQLLEN           siIndIdx = 0;
   long long        siIdx   = puPara->stExs.siSegInd;
   long long        siCount = 0LL;
   SQLLEN           siCountInd;

   snprintf(acSql, sizeof(acSql),
      "SELECT COUNT(*) FROM %sFLAM_SEGMENT"
      " WHERE namespace=? AND version=? AND segidx=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto exs_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto exs_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto exs_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto exs_err; }

   if (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
      pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siCount,
                      (SQLLEN)sizeof(siCount), &siCountInd);
   }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   puPara->stExs.isExists = (siCount > 0LL) ? 1U : 0U;
   puPara->stExs.uiRes0   = 0U;
   return 0;

exs_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_GMN                                                      */
/**********************************************************************/

/**
 * @brief Read the main (root) segment index from FLAM_ARCHIVE.
 *
 * Writes #FSEG_IDX_INI (-1) if no row exists or mainidx == -1.
 */
static int fsegodbc_gmn(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   long long        siMain  = FSEG_IDX_INI;
   SQLLEN           siMainInd;

   snprintf(acSql, sizeof(acSql),
      "SELECT mainidx FROM %sFLAM_ARCHIVE WHERE namespace=? AND version=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto gmn_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto gmn_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto gmn_err; }

   rc = pF->pSQLFetch(hStmt);
   if (fsegodbc_ok(rc)) {
      pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT, &siMain,
                      (SQLLEN)sizeof(siMain), &siMainInd);
      if (siMainInd == SQL_NULL_DATA) {
         siMain = FSEG_IDX_INI;
      }
   }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   puPara->stGmn.siSegInd = siMain;
   return 0;

gmn_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_SMN                                                      */
/**********************************************************************/

/**
 * @brief Persist the main (root) segment index in FLAM_ARCHIVE.
 */
static int fsegodbc_smn(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   SQLLEN           siIndIdx = 0;
   long long        siIdx   = puPara->stSmn.siSegInd;

   snprintf(acSql, sizeof(acSql),
      "UPDATE %sFLAM_ARCHIVE SET mainidx=?"
      " WHERE namespace=? AND version=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siIdx, 0, &siIndIdx);
   if (!fsegodbc_ok(rc)) { goto smn_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto smn_err; }

   rc = pF->pSQLBindParameter(hStmt, 3, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &pHdl->siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto smn_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto smn_err; }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);

   rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   return 0;

smn_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_VLS -- internal helper (shared with PRN)                  */
/**********************************************************************/

/**
 * @brief Populate a malloc'd TsFsegVsnEnt array for the current namespace.
 *
 * The array is allocated with @c malloc(); the caller (FLAM) frees it with
 * @c free() after use.  On z/OS, both DLLs must share the same C runtime
 * heap for this to work (see FSEG.h VLS note).
 *
 * @param pHdl       Provider handle.
 * @param ppsEnt     Output: pointer to allocated array.
 * @param puiCnt     Output: number of entries.
 * @param pcMsg      Error buffer.
 * @param uiMsgLen   Size of pcMsg.
 * @return 0 on success, provider error code on failure.
 */
static int fsegodbc_vls_internal(
   TsFsegOdbcHdl*    pHdl,
   TsFsegVsnEnt**    ppsEnt,
   unsigned int*     puiCnt,
   char*             pcMsg,
   unsigned int      uiMsgLen)
{
   TsFsegOdbcFuncs* pF       = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt    = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   TsFsegVsnEnt*    psEnt    = NULL;
   unsigned int     uiAlloc  = 0U;
   unsigned int     uiCnt    = 0U;

   *ppsEnt = NULL;
   *puiCnt = 0U;

   snprintf(acSql, sizeof(acSql),
      "SELECT version, EXTRACT(EPOCH FROM crtdat)"
      " FROM %sFLAM_ARCHIVE WHERE namespace=?"
      " ORDER BY version ASC",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto vls_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto vls_err; }

   while (fsegodbc_ok(pF->pSQLFetch(hStmt))) {
      long long   siVer   = 0LL;
      double      dEpoch  = 0.0;
      SQLLEN      siVInd, siEInd;

      pF->pSQLGetData(hStmt, 1, SQL_C_SBIGINT,
                      &siVer, (SQLLEN)sizeof(siVer), &siVInd);
      pF->pSQLGetData(hStmt, 2, SQL_C_DOUBLE,
                      &dEpoch, (SQLLEN)sizeof(dEpoch), &siEInd);

      if (uiCnt >= uiAlloc) {
         unsigned int   uiNewAlloc = (uiAlloc == 0U) ? 8U : uiAlloc * 2U;
         TsFsegVsnEnt*  psNew      =
            (TsFsegVsnEnt*)realloc(psEnt,
                                   (size_t)uiNewAlloc * sizeof(TsFsegVsnEnt));
         if (psNew == NULL) {
            free(psEnt);
            pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
            FSEGODBC_MSG(pcMsg, uiMsgLen,
               "FSEGODB: out of memory for version list");
            return FSEGODBC_RTC_ALC;
         }
         psEnt   = psNew;
         uiAlloc = uiNewAlloc;
      }

      psEnt[uiCnt].siVerId = siVer;
      psEnt[uiCnt].uiDate  = (unsigned long long)((dEpoch > 0.0) ? dEpoch : 0.0);
      uiCnt++;
   }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   *ppsEnt = psEnt;
   *puiCnt = uiCnt;
   return 0;

vls_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   free(psEnt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_VLS                                                      */
/**********************************************************************/

/**
 * @brief Return all archive versions for the current namespace.
 *
 * The array is allocated with @c malloc(); FLAM frees it with @c free().
 */
static int fsegodbc_vls(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegVsnEnt* psEnt = NULL;
   unsigned int  uiCnt = 0U;
   int           siRet;

   siRet = fsegodbc_vls_internal(pHdl, &psEnt, &uiCnt, pcMsg, uiMsgLen);
   if (siRet != 0) {
      return siRet;
   }

   puPara->stVls.psVsnEnt = psEnt;
   puPara->stVls.uiVsnCnt = uiCnt;
   puPara->stVls.uiRes0   = 0U;
   return 0;
}

/**********************************************************************/
/* FSEG_FUNC_DVS -- internal helper (shared with PRN)                  */
/**********************************************************************/

/**
 * @brief Delete a specific archive version (internal helper).
 *
 * Deletes all FLAM_SEGMENT rows and the FLAM_ARCHIVE row for the given
 * version, then commits.
 *
 * @param pHdl       Provider handle.
 * @param siVerId    Version to delete.
 * @param puiVerDel  Output: 1 if the version existed, else 0.
 * @param pcMsg      Error buffer.
 * @param uiMsgLen   Size of pcMsg.
 * @return 0 on success, provider error code on failure.
 */
static int fsegodbc_dvs_one(
   TsFsegOdbcHdl*   pHdl,
   long long        siVerId,
   unsigned int*    puiVerDel,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siIndVer = 0;
   SQLLEN           siRowCnt;
   int              siRet   = 0;

   *puiVerDel = 0U;

   /* Delete all segments for this version */
   snprintf(acSql, sizeof(acSql),
      "DELETE FROM %sFLAM_SEGMENT WHERE namespace=? AND version=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto dvs_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto dvs_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto dvs_err; }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   /* Delete the archive version row */
   snprintf(acSql, sizeof(acSql),
      "DELETE FROM %sFLAM_ARCHIVE WHERE namespace=? AND version=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto dvs_err; }

   rc = pF->pSQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
                               SQL_C_SBIGINT, SQL_BIGINT,
                               0, 0, &siVerId, 0, &siIndVer);
   if (!fsegodbc_ok(rc)) { goto dvs_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto dvs_err; }

   siRowCnt = 0;
   pF->pSQLRowCount(hStmt, &siRowCnt);
   *puiVerDel = (siRowCnt > 0) ? 1U : 0U;

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
   }

   return siRet;

dvs_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_DVS                                                      */
/**********************************************************************/

/**
 * @brief Delete a specific archive version.
 */
static int fsegodbc_dvs(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   unsigned int uiDel = 0U;
   int          siRet;

   siRet = fsegodbc_dvs_one(pHdl, puPara->stDvs.siVerId, &uiDel,
                             pcMsg, uiMsgLen);
   puPara->stDvs.uiVerDel = uiDel;
   return siRet;
}

/**********************************************************************/
/* FSEG_FUNC_DAR                                                      */
/**********************************************************************/

/**
 * @brief Delete the entire archive (all versions) for the current namespace.
 */
static int fsegodbc_dar(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF      = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt   = SQL_NULL_HSTMT;
   SQLRETURN        rc;
   SQLLEN           siIndNam = SQL_NTS;
   SQLLEN           siRowCnt;
   int              siRet   = 0;

   /* Delete all segments in namespace */
   snprintf(acSql, sizeof(acSql),
      "DELETE FROM %sFLAM_SEGMENT WHERE namespace=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto dar_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto dar_err; }

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   /* Delete all archive version rows in namespace */
   snprintf(acSql, sizeof(acSql),
      "DELETE FROM %sFLAM_ARCHIVE WHERE namespace=?",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      return FSEGODBC_RTC_CON;
   }

   rc = pF->pSQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
                               SQL_C_CHAR, SQL_VARCHAR,
                               (SQLULEN)sizeof(pHdl->acNamStr), 0,
                               pHdl->acNamStr, 0, &siIndNam);
   if (!fsegodbc_ok(rc)) { goto dar_err; }

   rc = pF->pSQLExecDirect(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) { goto dar_err; }

   siRowCnt = 0;
   pF->pSQLRowCount(hStmt, &siRowCnt);

   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   hStmt = SQL_NULL_HSTMT;

   rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
   }

   puPara->stDar.uiVerDel = (siRowCnt > 0) ? (unsigned int)siRowCnt : 0U;
   return siRet;

dar_err:
   fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
   pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
   return FSEGODBC_RTC_CON;
}

/**********************************************************************/
/* FSEG_FUNC_PRN                                                      */
/**********************************************************************/

/**
 * @brief Prune old versions, keeping the @c uiKeepCnt most recent.
 *
 * Fetches the full version list via fsegodbc_vls_internal() and deletes
 * the oldest (count - uiKeepCnt) versions one by one.
 */
static int fsegodbc_prn(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegVsnEnt* psEnt    = NULL;
   unsigned int  uiCnt    = 0U;
   unsigned int  uiKeep   = puPara->stPrn.uiKeepCnt;
   unsigned int  uiDelTot = 0U;
   unsigned int  i;
   int           siRet;

   siRet = fsegodbc_vls_internal(pHdl, &psEnt, &uiCnt, pcMsg, uiMsgLen);
   if (siRet != 0) {
      return siRet;
   }

   if (uiCnt <= uiKeep) {
      /* Nothing to prune */
      free(psEnt);
      puPara->stPrn.uiVerDel = 0U;
      return 0;
   }

   /* Delete oldest (uiCnt - uiKeep) versions; list is sorted ASC */
   for (i = 0U; i < (uiCnt - uiKeep); i++) {
      unsigned int uiDel = 0U;
      siRet = fsegodbc_dvs_one(pHdl, psEnt[i].siVerId, &uiDel,
                                pcMsg, uiMsgLen);
      if (siRet != 0) {
         free(psEnt);
         return siRet;
      }
      uiDelTot += uiDel;
   }

   free(psEnt);
   puPara->stPrn.uiVerDel = uiDelTot;
   return 0;
}

/**********************************************************************/
/* FSEG_FUNC_CBF                                                      */
/**********************************************************************/

/**
 * @brief Create write buffer (batch hint).
 *
 * For the ODBC backend, "batching" means the provider prepares a
 * reusable INSERT statement and defers the COMMIT until FLB.  This
 * reduces the per-PUT transaction overhead.
 */
static int fsegodbc_cbf(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF  = pHdl->pF;
   char             acSql[1024];
   SQLHSTMT         hStmt = SQL_NULL_HSTMT;
   SQLRETURN        rc;

   /* Free any pre-existing batch statement */
   if (pHdl->hBatchStmt != SQL_NULL_HSTMT) {
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, pHdl->hBatchStmt);
      pHdl->hBatchStmt = SQL_NULL_HSTMT;
   }

   snprintf(acSql, sizeof(acSql),
      "INSERT INTO %sFLAM_SEGMENT"
      " (namespace, version, segidx, segtyp, segdat)"
      " VALUES(?, ?, ?, ?, ?)",
      pHdl->acSchema);

   rc = pF->pSQLAllocStmt(pHdl->hDbc, &hStmt);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      /* CBF failure is not fatal; return 0 so FLAM continues without batch */
      puPara->stCbf.uiBufId = 0U;
      return 0;
   }

   rc = pF->pSQLPrepare(hStmt, (SQLCHAR*)acSql, SQL_NTS);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_STMT, hStmt, pcMsg, uiMsgLen);
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      /* Non-fatal fallback */
      puPara->stCbf.uiBufId = 0U;
      return 0;
   }

   pHdl->hBatchStmt = hStmt;
   pHdl->uiBufId    = 1U;
   puPara->stCbf.uiBufId = 1U;
   return 0;
}

/**********************************************************************/
/* FSEG_FUNC_FLB                                                      */
/**********************************************************************/

/**
 * @brief Flush the write buffer and commit the pending transaction.
 */
static int fsegodbc_flb(
   TsFsegOdbcHdl*   pHdl,
   TuFsegPar*       puPara,
   char*            pcMsg,
   unsigned int     uiMsgLen)
{
   TsFsegOdbcFuncs* pF  = pHdl->pF;
   SQLRETURN        rc;
   int              siRet = 0;

   /* Commit all pending inserts */
   rc = pF->pSQLEndTran(SQL_HANDLE_DBC, pHdl->hDbc, SQL_COMMIT);
   if (!fsegodbc_ok(rc)) {
      fsegodbc_errmsg(pF, SQL_HANDLE_DBC, pHdl->hDbc, pcMsg, uiMsgLen);
      siRet = FSEGODBC_RTC_CON;
   }

   /* Release the prepared batch statement */
   if (pHdl->hBatchStmt != SQL_NULL_HSTMT) {
      pF->pSQLFreeHandle(SQL_HANDLE_STMT, pHdl->hBatchStmt);
      pHdl->hBatchStmt = SQL_NULL_HSTMT;
   }

   pHdl->uiBufId         = 0U;
   puPara->stFlb.uiBufId = 0U;
   return siRet;
}

/**********************************************************************/
/* Provider entry point                                               */
/**********************************************************************/

/**
 * @brief FSEG SPI entry point.
 *
 * This is the only symbol exported from @c libfsegodbc.  It dispatches on
 * @c *piFunc to the appropriate static helper and translates ODBC errors
 * to provider return codes.
 *
 * @param pvGbl     FLAM global handle (ignored by this provider).
 * @param ppHdl     In/out provider handle.  NULL before OPN; set at OPN,
 *                  freed at CLS.
 * @param piRetco   Return code output (0 = success).
 * @param piVersion Interface version pointer from SEGSSE.
 * @param piFunc    Function code (FSEG_FUNC_*).
 * @param puPara    Parameter union; active member determined by *piFunc.
 * @param piMsgSiz  Capacity of pcMsg.
 * @param pcMsg     Error/info message buffer.
 */
void FSEGUSR(
   void*               pvGbl,
   void**              ppHdl,
   int*                piRetco,
   const unsigned int* piVersion,
   const unsigned int* piFunc,
   TuFsegPar*          puPara,
   const unsigned int* piMsgSiz,
   char*               pcMsg)
{
   TsFsegOdbcHdl* pHdl = (ppHdl != NULL) ? (TsFsegOdbcHdl*)*ppHdl : NULL;
   int            siRet = 0;

   (void)pvGbl; /* external providers do not use the FLAM global handle */

   /* Guard message buffer */
   if (pcMsg != NULL && *piMsgSiz > 0U) {
      pcMsg[0] = '\0';
   }

   /* Interface version check: major version must match */
   if (FSEG_VER_MAJOR(*piVersion) != FSEG_VER_MAJOR(FSEG_VERSION)) {
      FSEGODBC_MSG(pcMsg, *piMsgSiz,
         "FSEGODB: interface version mismatch"
         " (got 0x%04X, built for 0x%04X)",
         *piVersion, FSEG_VERSION);
      *piRetco = FSEGODBC_RTC_PAR;
      return;
   }

   switch (*piFunc) {
   /* Lifecycle order: NEW first, FRE last (after all operational codes). */
   case FSEG_FUNC_NEW:
      siRet = fsegodbc_new(ppHdl, puPara, pcMsg, *piMsgSiz);
      break;
   case FSEG_FUNC_OPN:
      siRet = fsegodbc_opn(ppHdl, puPara, pcMsg, *piMsgSiz);
      break;
   case FSEG_FUNC_CLS:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: CLS called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_cls(ppHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_PUT:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: PUT called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_put(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_GET:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: GET called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_get(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_DEL:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: DEL called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_del(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_EXS:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: EXS called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else if (pHdl->hDbc == SQL_NULL_HDBC) {
         /* NEW state: temporarily connect for archive existence check */
         siRet = fsegodbc_reconnect(pHdl, pcMsg, *piMsgSiz);
         if (siRet == 0) {
            siRet = fsegodbc_exs(pHdl, puPara, pcMsg, *piMsgSiz);
            fsegodbc_tmp_disconnect(pHdl);
         }
      } else {
         siRet = fsegodbc_exs(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_GMN:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: GMN called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_gmn(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_SMN:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: SMN called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_smn(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_VLS:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: VLS called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_vls(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_DVS:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: DVS called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_dvs(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_DAR:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: DAR called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else if (pHdl->hDbc == SQL_NULL_HDBC) {
         /* NEW state: temporarily connect */
         siRet = fsegodbc_reconnect(pHdl, pcMsg, *piMsgSiz);
         if (siRet == 0) {
            siRet = fsegodbc_dar(pHdl, puPara, pcMsg, *piMsgSiz);
            fsegodbc_tmp_disconnect(pHdl);
         }
      } else {
         siRet = fsegodbc_dar(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_PRN:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: PRN called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else if (pHdl->hDbc == SQL_NULL_HDBC) {
         /* NEW state: temporarily connect */
         siRet = fsegodbc_reconnect(pHdl, pcMsg, *piMsgSiz);
         if (siRet == 0) {
            siRet = fsegodbc_prn(pHdl, puPara, pcMsg, *piMsgSiz);
            fsegodbc_tmp_disconnect(pHdl);
         }
      } else {
         siRet = fsegodbc_prn(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_CBF:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: CBF called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_cbf(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   case FSEG_FUNC_FLB:
      if (pHdl == NULL) {
         FSEGODBC_MSG(pcMsg, *piMsgSiz,
            "FSEGODB: FLB called with NULL handle");
         siRet = FSEGODBC_RTC_PAR;
      } else {
         siRet = fsegodbc_flb(pHdl, puPara, pcMsg, *piMsgSiz);
      }
      break;
   /* FRE last: releases handle allocated by NEW. */
   case FSEG_FUNC_FRE:
      siRet = fsegodbc_fre(ppHdl, pcMsg, *piMsgSiz);
      break;
   default:
      FSEGODBC_MSG(pcMsg, *piMsgSiz,
         "FSEGODB: unknown function code %u", *piFunc);
      siRet = FSEGODBC_RTC_PAR;
      break;
   }

   *piRetco = siRet;
}

#else /* __HAS_ODBC__ not defined */

/*
 * Stub entry point when compiled without -D__HAS_ODBC__.
 * Returns an error immediately so that accidental linkage is detected.
 */
void FSEGUSR(
   void*               pvGbl,
   void**              ppHdl,
   int*                piRetco,
   const unsigned int* piVersion,
   const unsigned int* piFunc,
   void*               puPara,
   const unsigned int* piMsgSiz,
   char*               pcMsg)
{
   (void)pvGbl;
   (void)ppHdl;
   (void)piVersion;
   (void)piFunc;
   (void)puPara;

   if (pcMsg != NULL && *piMsgSiz > 0U) {
      snprintf(pcMsg, *piMsgSiz,
         "FSEGODB: compiled without ODBC support"
         " (recompile with -D__HAS_ODBC__)");
   }
   *piRetco = 4; /* FSEGODBC_RTC_PAR equivalent */
}

#endif /* __HAS_ODBC__ */
