Skip to content

Commit 0a8689a

Browse files
committed
tests: cover sibling numeric→VARCHAR conversions
The Issue #161 fix touched the entire conv*ToString family — conv{TinyInt, Short,Long,Bigint,Float,Double}ToString plus conv{Date,Time,DateTime}ToString — but only the convLongToString path was exercised by the existing test suite (Issue161_SLongToVarcharViaStoredProcedure / …ViaDml). Any regression in the other conversion helpers would slip through CI. Add NumericToVarcharTest with one compact, loop-based test per helper that is safe to exercise on CI: - SShortToVarcharLoopDml — convShortToString - SBigintToVarcharLoopDml — convBigintToString - FloatToVarcharLoopDml — convFloatToString - DoubleToVarcharLoopDml — convDoubleToString Each binds the relevant SQL_C_* type to a VARCHAR column, executes the same prepared INSERT across a small set of values chosen to exercise 1-, 2- and 3-digit widths plus a negative, and verifies every row committed with no NUL-byte corruption plus a round-trip cast. DML (no stored procedure) is used so nothing in FB 3 / 4 / 5 trips the parameterized-SP handling; the FB 6 master parameterized-query regression is skipped via SKIP_ON_FIREBIRD6() — same rationale as the existing Issue161 tests. SQL_C_TYPE_DATE and SQL_C_TYPE_TIMESTAMP → VARCHAR are deliberately NOT covered here: they expose a separate pre-existing bug (the driver routes ODBC SQL_DATE_STRUCT / SQL_TIMESTAMP_STRUCT through conv helpers that expect Firebird's internal ISC_DATE / ISC_TIMESTAMP encoding) that is unrelated to the length-prefix fix in Issue #161 and should be tracked separately.
1 parent 38a2655 commit 0a8689a

2 files changed

Lines changed: 254 additions & 0 deletions

File tree

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ add_executable(firebird_odbc_tests
6565
test_data_types.cpp
6666
test_result_conversions.cpp
6767
test_param_conversions.cpp
68+
test_numeric_to_varchar.cpp
6869
test_prepare.cpp
6970
test_cursors.cpp
7071
test_cursor_commit.cpp

tests/test_numeric_to_varchar.cpp

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// tests/test_numeric_to_varchar.cpp — Numeric/datetime C-type → Firebird VARCHAR
2+
//
3+
// The Issue #161 driver fix (duckdb/odbc-scanner#161) lives in the
4+
// conv<Numeric|DateTime>ToString family of conversion helpers, each of which
5+
// writes an ASCII representation into a Firebird-side VARYING buffer. Before
6+
// the fix every one of those helpers wrote raw bytes at offset 0 of the
7+
// buffer, stomping on the VARYING length prefix. The pre-existing suite only
8+
// covered SLONG → INTEGER/SMALLINT round-trips, so none of the broken paths
9+
// were tested.
10+
//
11+
// This file adds one compact, loop-based test per conversion helper. Each
12+
// binds the relevant C-type to a VARCHAR column, executes the same prepared
13+
// INSERT many times with values of varying widths, and verifies:
14+
// - every row committed,
15+
// - no stored value contains an embedded NUL byte,
16+
// - CAST(val AS <original-type>) round-trips for the boundary values.
17+
//
18+
// DML (no stored procedure) is deliberately used so the tests run on every
19+
// server in the matrix — FB 6 master still rejects parameterized EXECUTE
20+
// PROCEDURE via SKIP_ON_FIREBIRD6(), but `UPDATE OR INSERT` / `INSERT` are
21+
// accepted.
22+
23+
#include "test_helpers.h"
24+
#include <cstring>
25+
#include <string>
26+
#include <vector>
27+
28+
class NumericToVarcharTest : public OdbcConnectedTest {
29+
protected:
30+
void SetUp() override {
31+
OdbcConnectedTest::SetUp();
32+
if (::testing::Test::IsSkipped()) return;
33+
34+
ExecIgnoreError("DROP TABLE ODBC_NUM_VAR");
35+
Commit();
36+
ReallocStmt();
37+
38+
// 40 bytes is plenty for any decimal representation produced by the
39+
// conv*ToString helpers (longest case is a DOUBLE ≈ 23 chars).
40+
ExecDirect("CREATE TABLE ODBC_NUM_VAR ("
41+
"ID INTEGER NOT NULL PRIMARY KEY, "
42+
"VAL VARCHAR(40))");
43+
Commit();
44+
ReallocStmt();
45+
}
46+
47+
void TearDown() override {
48+
if (hDbc != SQL_NULL_HDBC) {
49+
ExecIgnoreError("DROP TABLE ODBC_NUM_VAR");
50+
SQLEndTran(SQL_HANDLE_DBC, hDbc, SQL_COMMIT);
51+
}
52+
OdbcConnectedTest::TearDown();
53+
}
54+
55+
// Verifies no stored VAL contains a NUL byte (the classic pre-fix symptom
56+
// is the first-row value being stored as two bytes, e.g. "1\0") and that
57+
// exactly `expectedRowCount` rows were persisted.
58+
void verifyIntactness(int expectedRowCount) {
59+
ExecDirect("SELECT COUNT(*) FROM ODBC_NUM_VAR");
60+
SQLRETURN ret = SQLFetch(hStmt);
61+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
62+
SQLINTEGER cnt = 0;
63+
SQLLEN ind = 0;
64+
SQLGetData(hStmt, 1, SQL_C_SLONG, &cnt, sizeof(cnt), &ind);
65+
SQLCloseCursor(hStmt);
66+
EXPECT_EQ(cnt, expectedRowCount) << "rows stored";
67+
68+
ExecDirect("SELECT COUNT(*) FROM ODBC_NUM_VAR "
69+
"WHERE POSITION(_OCTETS x'00' IN VAL) > 0");
70+
ret = SQLFetch(hStmt);
71+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
72+
SQLINTEGER nulCount = -1;
73+
SQLGetData(hStmt, 1, SQL_C_SLONG, &nulCount, sizeof(nulCount), &ind);
74+
SQLCloseCursor(hStmt);
75+
EXPECT_EQ(nulCount, 0) << "rows with NUL-byte corruption";
76+
}
77+
};
78+
79+
// ===== SQL_C_SSHORT (→ convShortToString) =====
80+
81+
TEST_F(NumericToVarcharTest, SShortToVarcharLoopDml) {
82+
// FB 6 master crashes with "Stack overflow" / SEGFAULT on loop-prepared
83+
// parameterized INSERT — see SKIP_ON_FIREBIRD6 note in test_helpers.h.
84+
SKIP_ON_FIREBIRD6();
85+
86+
const std::vector<SQLSMALLINT> vals = {1, 10, 100, 999, -1, -999};
87+
88+
SQLRETURN ret = SQLPrepare(hStmt,
89+
(SQLCHAR*)"INSERT INTO ODBC_NUM_VAR (ID, VAL) VALUES (?, ?)", SQL_NTS);
90+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
91+
92+
SQLINTEGER idVal = 0; SQLLEN idInd = sizeof(idVal);
93+
SQLSMALLINT val = 0; SQLLEN valInd = sizeof(val);
94+
SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &idVal, sizeof(idVal), &idInd);
95+
SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_SSHORT, SQL_VARCHAR, 40, 0, &val, sizeof(val), &valInd);
96+
97+
for (size_t i = 0; i < vals.size(); ++i) {
98+
idVal = static_cast<SQLINTEGER>(i + 1);
99+
val = vals[i];
100+
ret = SQLExecute(hStmt);
101+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "row " << i + 1 << " value " << vals[i];
102+
}
103+
Commit();
104+
ReallocStmt();
105+
106+
verifyIntactness(static_cast<int>(vals.size()));
107+
108+
// Round-trip spot checks.
109+
ExecDirect("SELECT CAST(VAL AS SMALLINT) FROM ODBC_NUM_VAR ORDER BY ID");
110+
for (auto expected : vals) {
111+
ret = SQLFetch(hStmt);
112+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
113+
SQLSMALLINT got = 0;
114+
SQLLEN ind = 0;
115+
SQLGetData(hStmt, 1, SQL_C_SSHORT, &got, sizeof(got), &ind);
116+
EXPECT_EQ(got, expected);
117+
}
118+
SQLCloseCursor(hStmt);
119+
}
120+
121+
// ===== SQL_C_SBIGINT (→ convBigintToString) =====
122+
123+
TEST_F(NumericToVarcharTest, SBigintToVarcharLoopDml) {
124+
SKIP_ON_FIREBIRD6();
125+
126+
const std::vector<SQLBIGINT> vals = {
127+
1LL, 9LL, 10LL, 99LL, 100LL, 999999LL,
128+
9999999999LL, 1234567890123LL,
129+
-1LL, -999999LL, -1234567890123LL,
130+
};
131+
132+
SQLRETURN ret = SQLPrepare(hStmt,
133+
(SQLCHAR*)"INSERT INTO ODBC_NUM_VAR (ID, VAL) VALUES (?, ?)", SQL_NTS);
134+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
135+
136+
SQLINTEGER idVal = 0; SQLLEN idInd = sizeof(idVal);
137+
SQLBIGINT val = 0; SQLLEN valInd = sizeof(val);
138+
SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &idVal, sizeof(idVal), &idInd);
139+
SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_VARCHAR, 40, 0, &val, sizeof(val), &valInd);
140+
141+
for (size_t i = 0; i < vals.size(); ++i) {
142+
idVal = static_cast<SQLINTEGER>(i + 1);
143+
val = vals[i];
144+
ret = SQLExecute(hStmt);
145+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "row " << i + 1 << " value " << vals[i];
146+
}
147+
Commit();
148+
ReallocStmt();
149+
150+
verifyIntactness(static_cast<int>(vals.size()));
151+
152+
ExecDirect("SELECT CAST(VAL AS BIGINT) FROM ODBC_NUM_VAR ORDER BY ID");
153+
for (auto expected : vals) {
154+
ret = SQLFetch(hStmt);
155+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
156+
SQLBIGINT got = 0;
157+
SQLLEN ind = 0;
158+
SQLGetData(hStmt, 1, SQL_C_SBIGINT, &got, sizeof(got), &ind);
159+
EXPECT_EQ(got, expected);
160+
}
161+
SQLCloseCursor(hStmt);
162+
}
163+
164+
// ===== SQL_C_FLOAT (→ convFloatToString) =====
165+
166+
TEST_F(NumericToVarcharTest, FloatToVarcharLoopDml) {
167+
SKIP_ON_FIREBIRD6();
168+
169+
const std::vector<SQLREAL> vals = {
170+
1.5f, 10.25f, 100.125f, 0.5f, -1.5f, -100.0f,
171+
};
172+
173+
SQLRETURN ret = SQLPrepare(hStmt,
174+
(SQLCHAR*)"INSERT INTO ODBC_NUM_VAR (ID, VAL) VALUES (?, ?)", SQL_NTS);
175+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
176+
177+
SQLINTEGER idVal = 0; SQLLEN idInd = sizeof(idVal);
178+
SQLREAL val = 0; SQLLEN valInd = sizeof(val);
179+
SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &idVal, sizeof(idVal), &idInd);
180+
SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_FLOAT, SQL_VARCHAR, 40, 0, &val, sizeof(val), &valInd);
181+
182+
for (size_t i = 0; i < vals.size(); ++i) {
183+
idVal = static_cast<SQLINTEGER>(i + 1);
184+
val = vals[i];
185+
ret = SQLExecute(hStmt);
186+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "row " << i + 1;
187+
}
188+
Commit();
189+
ReallocStmt();
190+
191+
verifyIntactness(static_cast<int>(vals.size()));
192+
193+
ExecDirect("SELECT CAST(VAL AS DOUBLE PRECISION) FROM ODBC_NUM_VAR ORDER BY ID");
194+
for (auto expected : vals) {
195+
ret = SQLFetch(hStmt);
196+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
197+
SQLDOUBLE got = 0;
198+
SQLLEN ind = 0;
199+
SQLGetData(hStmt, 1, SQL_C_DOUBLE, &got, sizeof(got), &ind);
200+
EXPECT_NEAR(got, expected, 1e-4);
201+
}
202+
SQLCloseCursor(hStmt);
203+
}
204+
205+
// ===== SQL_C_DOUBLE (→ convDoubleToString) =====
206+
207+
TEST_F(NumericToVarcharTest, DoubleToVarcharLoopDml) {
208+
SKIP_ON_FIREBIRD6();
209+
210+
const std::vector<SQLDOUBLE> vals = {
211+
1.5, 10.25, 100.125, 0.5, 123456789.0, -1.5, -100.0,
212+
};
213+
214+
SQLRETURN ret = SQLPrepare(hStmt,
215+
(SQLCHAR*)"INSERT INTO ODBC_NUM_VAR (ID, VAL) VALUES (?, ?)", SQL_NTS);
216+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
217+
218+
SQLINTEGER idVal = 0; SQLLEN idInd = sizeof(idVal);
219+
SQLDOUBLE val = 0; SQLLEN valInd = sizeof(val);
220+
SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &idVal, sizeof(idVal), &idInd);
221+
SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_VARCHAR, 40, 0, &val, sizeof(val), &valInd);
222+
223+
for (size_t i = 0; i < vals.size(); ++i) {
224+
idVal = static_cast<SQLINTEGER>(i + 1);
225+
val = vals[i];
226+
ret = SQLExecute(hStmt);
227+
ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "row " << i + 1;
228+
}
229+
Commit();
230+
ReallocStmt();
231+
232+
verifyIntactness(static_cast<int>(vals.size()));
233+
234+
ExecDirect("SELECT CAST(VAL AS DOUBLE PRECISION) FROM ODBC_NUM_VAR ORDER BY ID");
235+
for (auto expected : vals) {
236+
ret = SQLFetch(hStmt);
237+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
238+
SQLDOUBLE got = 0;
239+
SQLLEN ind = 0;
240+
SQLGetData(hStmt, 1, SQL_C_DOUBLE, &got, sizeof(got), &ind);
241+
EXPECT_NEAR(got, expected, 1e-6);
242+
}
243+
SQLCloseCursor(hStmt);
244+
}
245+
246+
// NOTE — SQL_C_TYPE_DATE / SQL_C_TYPE_TIMESTAMP → VARCHAR tests intentionally
247+
// omitted. The driver routes those C-types through convDateToString /
248+
// convDateTimeToString which expect Firebird's internal ISC_DATE / ISC_TIMESTAMP
249+
// buffers (a 32-bit int and a QUAD, respectively), NOT the ODBC-side
250+
// SQL_DATE_STRUCT / SQL_TIMESTAMP_STRUCT the application actually binds. The
251+
// resulting "VARCHAR" string is garbage (year 2043, etc.) independent of the
252+
// Issue #161 length-prefix fix. That's a separate pre-existing bug and should
253+
// be tracked on its own.

0 commit comments

Comments
 (0)