Skip to content

Commit e40dfa8

Browse files
committed
tests: cover rebind and direct shapes for Issue #161
The two existing Issue161 tests (ViaStoredProcedure, ViaDml) both use the prepare-once + bind-once + execute-N shape, which only exercises Bug A (conv<Numeric>ToString writing over the VARYING length prefix). A regression that reintroduced Bug B — Sqlda::getPrecision reading from the mutated sqlvar — would slip past them entirely, because preponce_slong never re-enters defFromMetaDataIn and so never re-reads getPrecision after the first execute. Add two sibling tests, both plain DML, same table layout: - Issue161_SLongToVarcharViaDmlRebind: SQLPrepare once, then per row SQLFreeStmt(SQL_RESET_PARAMS) + SQLBindParameter + SQLExecute. This is the shape DuckDB's odbc-scanner odbc_copy_from uses internally and the one that originally turned Issue #161 from silent NUL corruption into outright row loss. Verified locally: with Sqlda::getPrecision reverted to master (Bug B reintroduced) this test fails with min=0 / max=9 / count<500, while the two preponce tests pass. - Issue161_SLongToVarcharViaDmlDirect: SQLExecDirect per row with SQL_C_SLONG. Exercises the same conv path as preponce but with no persistent prepared-statement context between iterations, guarding against any state-leak regression where the per-execute reset path diverges from the per-prepare path. Both carry SKIP_ON_FIREBIRD6() for the same reason as the existing Issue161 tests — see 3e267bf.
1 parent 538a993 commit e40dfa8

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

tests/test_param_conversions.cpp

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,186 @@ TEST_F(ParamConversionsTest, Issue161_SLongToVarcharViaDml) {
475475
ReallocStmt();
476476
}
477477

478+
// Rebind-per-row variant: SQLPrepare once, then for every row call
479+
// SQLFreeStmt(SQL_RESET_PARAMS) + SQLBindParameter(...) + SQLExecute.
480+
// This is the shape DuckDB's odbc-scanner odbc_copy_from uses internally,
481+
// and the one that originally exposed issue #161 as row *loss* (500 sent,
482+
// 11 stored) rather than silent NUL corruption.
483+
//
484+
// Why a separate test: this shape is the only one that exercises the
485+
// Sqlda::getPrecision / orgVarSqlProperties fix. The two preponce_slong
486+
// tests above (Via{StoredProcedure,Dml}) bind once and never re-enter
487+
// defFromMetaDataIn, so a regression that reintroduced
488+
// `record->length = getPrecision(mutated_sqlvar)` would slip past them.
489+
TEST_F(ParamConversionsTest, Issue161_SLongToVarcharViaDmlRebind) {
490+
SKIP_ON_FIREBIRD6();
491+
492+
ExecIgnoreError("DROP TABLE ODBC_ISSUE161_T");
493+
Commit();
494+
ReallocStmt();
495+
496+
ExecDirect("CREATE TABLE ODBC_ISSUE161_T ("
497+
"ID VARCHAR(20) NOT NULL PRIMARY KEY, "
498+
"NAME VARCHAR(100))");
499+
Commit();
500+
ReallocStmt();
501+
502+
constexpr int kRowCount = 500;
503+
SQLINTEGER idVal = 0;
504+
SQLLEN idInd = sizeof(idVal);
505+
SQLCHAR nameBuf[32] = {};
506+
SQLLEN nameInd = SQL_NTS;
507+
508+
SQLRETURN ret = SQLPrepare(hStmt,
509+
(SQLCHAR*)"UPDATE OR INSERT INTO ODBC_ISSUE161_T (ID, NAME) "
510+
"VALUES (?, ?) MATCHING (ID)", SQL_NTS);
511+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
512+
<< "SQLPrepare failed: " << GetOdbcError(SQL_HANDLE_STMT, hStmt);
513+
514+
for (int i = 1; i <= kRowCount; ++i) {
515+
ret = SQLFreeStmt(hStmt, SQL_RESET_PARAMS);
516+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
517+
<< "SQLFreeStmt(RESET_PARAMS) failed on row " << i << ": "
518+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
519+
520+
idVal = i;
521+
snprintf((char*)nameBuf, sizeof(nameBuf), "name-%d", i);
522+
523+
ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
524+
SQL_C_SLONG, SQL_INTEGER, 0, 0, &idVal, sizeof(idVal), &idInd);
525+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
526+
<< "SQLBindParameter(1) failed on row " << i << ": "
527+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
528+
529+
ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
530+
SQL_C_CHAR, SQL_VARCHAR, 100, 0, nameBuf, sizeof(nameBuf), &nameInd);
531+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
532+
<< "SQLBindParameter(2) failed on row " << i << ": "
533+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
534+
535+
ret = SQLExecute(hStmt);
536+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
537+
<< "SQLExecute failed on row " << i << ": "
538+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
539+
}
540+
Commit();
541+
ReallocStmt();
542+
543+
ExecDirect("SELECT COUNT(*), MIN(CAST(ID AS INTEGER)), MAX(CAST(ID AS INTEGER)) "
544+
"FROM ODBC_ISSUE161_T");
545+
ret = SQLFetch(hStmt);
546+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "SQLFetch on aggregate failed";
547+
548+
SQLINTEGER cnt = 0, minId = 0, maxId = 0;
549+
SQLLEN ind = 0;
550+
SQLGetData(hStmt, 1, SQL_C_SLONG, &cnt, sizeof(cnt), &ind);
551+
SQLGetData(hStmt, 2, SQL_C_SLONG, &minId, sizeof(minId), &ind);
552+
SQLGetData(hStmt, 3, SQL_C_SLONG, &maxId, sizeof(maxId), &ind);
553+
SQLCloseCursor(hStmt);
554+
555+
EXPECT_EQ(cnt, kRowCount);
556+
EXPECT_EQ(minId, 1);
557+
EXPECT_EQ(maxId, kRowCount);
558+
559+
ExecDirect("SELECT COUNT(*) FROM ODBC_ISSUE161_T "
560+
"WHERE POSITION(_OCTETS x'00' IN ID) > 0");
561+
ret = SQLFetch(hStmt);
562+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "SQLFetch on NUL check failed";
563+
SQLINTEGER nulCount = -1;
564+
SQLGetData(hStmt, 1, SQL_C_SLONG, &nulCount, sizeof(nulCount), &ind);
565+
SQLCloseCursor(hStmt);
566+
EXPECT_EQ(nulCount, 0) << "rows with embedded NUL bytes were stored";
567+
568+
ExecIgnoreError("DROP TABLE ODBC_ISSUE161_T");
569+
Commit();
570+
ReallocStmt();
571+
}
572+
573+
// Direct-per-row variant: no SQLPrepare cache, each row is bound and executed
574+
// via SQLExecDirect. Exercises the same conv*ToString path as the preponce
575+
// shape but without a persistent prepared-statement context between
576+
// iterations, guarding against any state-leak regression where per-execute
577+
// reset paths diverge from per-prepare paths.
578+
TEST_F(ParamConversionsTest, Issue161_SLongToVarcharViaDmlDirect) {
579+
SKIP_ON_FIREBIRD6();
580+
581+
ExecIgnoreError("DROP TABLE ODBC_ISSUE161_T");
582+
Commit();
583+
ReallocStmt();
584+
585+
ExecDirect("CREATE TABLE ODBC_ISSUE161_T ("
586+
"ID VARCHAR(20) NOT NULL PRIMARY KEY, "
587+
"NAME VARCHAR(100))");
588+
Commit();
589+
ReallocStmt();
590+
591+
constexpr int kRowCount = 500;
592+
SQLINTEGER idVal = 0;
593+
SQLLEN idInd = sizeof(idVal);
594+
SQLCHAR nameBuf[32] = {};
595+
SQLLEN nameInd = SQL_NTS;
596+
597+
const char* kInsertSql =
598+
"UPDATE OR INSERT INTO ODBC_ISSUE161_T (ID, NAME) "
599+
"VALUES (?, ?) MATCHING (ID)";
600+
601+
for (int i = 1; i <= kRowCount; ++i) {
602+
idVal = i;
603+
snprintf((char*)nameBuf, sizeof(nameBuf), "name-%d", i);
604+
605+
SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
606+
SQL_C_SLONG, SQL_INTEGER, 0, 0, &idVal, sizeof(idVal), &idInd);
607+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
608+
<< "SQLBindParameter(1) failed on row " << i << ": "
609+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
610+
611+
ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
612+
SQL_C_CHAR, SQL_VARCHAR, 100, 0, nameBuf, sizeof(nameBuf), &nameInd);
613+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
614+
<< "SQLBindParameter(2) failed on row " << i << ": "
615+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
616+
617+
ret = SQLExecDirect(hStmt, (SQLCHAR*)kInsertSql, SQL_NTS);
618+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
619+
<< "SQLExecDirect failed on row " << i << ": "
620+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
621+
622+
SQLFreeStmt(hStmt, SQL_CLOSE);
623+
SQLFreeStmt(hStmt, SQL_RESET_PARAMS);
624+
}
625+
Commit();
626+
ReallocStmt();
627+
628+
ExecDirect("SELECT COUNT(*), MIN(CAST(ID AS INTEGER)), MAX(CAST(ID AS INTEGER)) "
629+
"FROM ODBC_ISSUE161_T");
630+
SQLRETURN ret = SQLFetch(hStmt);
631+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "SQLFetch on aggregate failed";
632+
633+
SQLINTEGER cnt = 0, minId = 0, maxId = 0;
634+
SQLLEN ind = 0;
635+
SQLGetData(hStmt, 1, SQL_C_SLONG, &cnt, sizeof(cnt), &ind);
636+
SQLGetData(hStmt, 2, SQL_C_SLONG, &minId, sizeof(minId), &ind);
637+
SQLGetData(hStmt, 3, SQL_C_SLONG, &maxId, sizeof(maxId), &ind);
638+
SQLCloseCursor(hStmt);
639+
640+
EXPECT_EQ(cnt, kRowCount);
641+
EXPECT_EQ(minId, 1);
642+
EXPECT_EQ(maxId, kRowCount);
643+
644+
ExecDirect("SELECT COUNT(*) FROM ODBC_ISSUE161_T "
645+
"WHERE POSITION(_OCTETS x'00' IN ID) > 0");
646+
ret = SQLFetch(hStmt);
647+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "SQLFetch on NUL check failed";
648+
SQLINTEGER nulCount = -1;
649+
SQLGetData(hStmt, 1, SQL_C_SLONG, &nulCount, sizeof(nulCount), &ind);
650+
SQLCloseCursor(hStmt);
651+
EXPECT_EQ(nulCount, 0) << "rows with embedded NUL bytes were stored";
652+
653+
ExecIgnoreError("DROP TABLE ODBC_ISSUE161_T");
654+
Commit();
655+
ReallocStmt();
656+
}
657+
478658
// ===== Already-covered round-trip tests from test_data_types.cpp =====
479659
// (IntegerParamInsertAndSelect, VarcharParamInsertAndSelect,
480660
// DoubleParamInsertAndSelect, DateParamInsertAndSelect,

0 commit comments

Comments
 (0)