diff --git a/cpp/include/core/bound_datum.h b/cpp/include/core/bound_datum.h index ed0aaa43..4b4b114c 100644 --- a/cpp/include/core/bound_datum.h +++ b/cpp/include/core/bound_datum.h @@ -88,6 +88,10 @@ class BoundDatum { _params = nullptr; } + // a numeric or decimal result column bound as text: 38 digits, a sign, a point, a leading + // zero and a terminator + static constexpr size_t numeric_text_width = 64; + BoundDatum(shared_ptr params) : BoundDatum() { _params = params; } diff --git a/cpp/include/odbc/odbc_statement_legacy.h b/cpp/include/odbc/odbc_statement_legacy.h index e1f421f9..4d49cf71 100644 --- a/cpp/include/odbc/odbc_statement_legacy.h +++ b/cpp/include/odbc/odbc_statement_legacy.h @@ -172,6 +172,7 @@ class OdbcStatementLegacy : public IOdbcStatement { bool reserved_int(const size_t row_count, const size_t column) const; bool reserved_big_int(const size_t row_count, const size_t column) const; bool reserved_decimal(const size_t row_count, const size_t column) const; + bool reserved_numeric_text(const size_t row_count, const size_t column) const; bool reserved_time(const size_t row_count, const size_t column) const; bool reserved_timestamp(const size_t row_count, const size_t column) const; bool reserved_timestamp_offset(const size_t row_count, const size_t column) const; diff --git a/cpp/src/core/bound_datum.cpp b/cpp/src/core/bound_datum.cpp index 28e86e52..401835ce 100644 --- a/cpp/src/core/bound_datum.cpp +++ b/cpp/src/core/bound_datum.cpp @@ -2408,6 +2408,15 @@ void BoundDatum::reserve_column_type(const SQLSMALLINT type, size_t& len, const case SQL_DECIMAL: case SQL_NUMERIC: + // under numeric strings, the driver's exact text: as a double it lost what a double + // cannot hold (#498). the reader picks its path from c_type + if (_params && _params->numeric_string) { + reserve_w_var_char_array(numeric_text_width, row_count); + } else { + reserve_double(static_cast(row_count)); + } + break; + case SQL_REAL: case SQL_FLOAT: case SQL_DOUBLE: diff --git a/cpp/src/odbc/odbc_statement_legacy.cpp b/cpp/src/odbc/odbc_statement_legacy.cpp index 6862d3d2..23360dae 100644 --- a/cpp/src/odbc/odbc_statement_legacy.cpp +++ b/cpp/src/odbc/odbc_statement_legacy.cpp @@ -1358,6 +1358,14 @@ bool OdbcStatementLegacy::dispatch_prepared( case SQL_DECIMAL: case SQL_NUMERIC: + // bound as text at prepare when numeric strings were on + if (_preparedStorage->atIndex(static_cast(column))->c_type == SQL_C_WCHAR) { + res = reserved_numeric_text(rows_read, column); + } else { + res = reserved_decimal(rows_read, column); + } + break; + case SQL_REAL: case SQL_FLOAT: case SQL_DOUBLE: @@ -1978,6 +1986,14 @@ bool OdbcStatementLegacy::get_data_decimal(const size_t row_id, const size_t col return true; } +// the driver's text for a numeric leaves out the zero before the point ("-.50"): put it back +static void add_leading_zero(std::vector& text) { + const size_t sign = !text.empty() && text[0] == u'-' ? 1 : 0; + if (text.size() > sign && text[sign] == u'.') { + text.insert(text.begin() + static_cast(sign), u'0'); + } +} + // a numeric or decimal as the driver's text, which keeps every digit and the scale ("5.00"). // the driver leaves out the zero before the point ("-.50"); it is put back ("-0.50"). bool OdbcStatementLegacy::get_data_numeric_string(const size_t row_id, const size_t column) { @@ -2005,16 +2021,42 @@ bool OdbcStatementLegacy::get_data_numeric_string(const size_t row_id, const siz len = static_cast(capacity) - 1; } text.resize(len); - const auto sign = !text.empty() && text[0] == u'-' ? 1 : 0; - if (text.size() > static_cast(sign) && text[sign] == u'.') { - text.insert(text.begin() + sign, u'0'); - } + add_leading_zero(text); const auto size = text.size(); const auto storage = make_shared>(std::move(text)); _resultset->add_column(row_id, make_shared(column, storage, 0, size)); return true; } +// a prepared numeric or decimal column bound as text (numeric strings at prepare). an +// execution with numeric strings off reads the same text back as a number. +bool OdbcStatementLegacy::reserved_numeric_text(const size_t row_count, const size_t column) const { + const auto& bound_datum = _preparedStorage->atIndex(static_cast(column)); + const auto& ind = bound_datum->get_ind_vec(); + const auto& store = *bound_datum->get_storage()->uint16vec_ptr; + constexpr auto unit = sizeof(uint16_t); + for (size_t row_id = 0; row_id < row_count; ++row_id) { + if (ind[row_id] == SQL_NULL_DATA) { + _resultset->add_column(row_id, make_shared(column)); + continue; + } + const auto offset = BoundDatum::numeric_text_width * row_id; + const auto len = min(static_cast(ind[row_id]) / unit, BoundDatum::numeric_text_width - 1); + std::vector text(store.begin() + static_cast(offset), + store.begin() + static_cast(offset + len)); + add_leading_zero(text); + if (_numericStringEnabled) { + const auto size = text.size(); + const auto storage = make_shared>(std::move(text)); + _resultset->add_column(row_id, make_shared(column, storage, 0, size)); + } else { + const std::string narrow(text.begin(), text.end()); + _resultset->add_column(row_id, make_shared(column, std::strtod(narrow.c_str(), nullptr))); + } + } + return true; +} + bool OdbcStatementLegacy::get_data_binary(const size_t row_id, const size_t column) { auto storage = make_shared(); diff --git a/test/contract/HANDOVER.md b/test/contract/HANDOVER.md index 2161fffc..78bae12e 100644 --- a/test/contract/HANDOVER.md +++ b/test/contract/HANDOVER.md @@ -53,9 +53,13 @@ Repros in `repro/i497.js`, `repro/i498.js`; corpus `scripts/issue-497` (+ `-ns`, `money` and `smallmoney` are read as the driver's text, as `numeric` is - they are the same type to SQL Server, and gave different text ("5" against "5.00"). Both now get the leading zero the driver leaves out ("-0.50", not "-.50"). Corpus `scripts/issue-498` agreed. -- **#498 still open: prepared statements** bind `numeric`/`decimal` result columns as a double - (`bound_datum.cpp` reserve, `reserved_decimal`), so they still lose digits under numeric - strings. Pinned `known-bug` in `scripts/issue-498-prepared`; needs a text binding. +- **#498 prepared statements fixed:** with numeric strings on at prepare, `BoundDatum:: + reserve_column_type` binds `numeric`/`decimal` result columns as wide text (64 units a row) + instead of a double; `dispatch_prepared` picks the reader from the binding's `c_type` + (`reserved_numeric_text`). The text is the same as a query's, row for row + (`scripts/issue-498-prepared`, agreed). Seen on the way: a prepared `select top (?) …` is + described with no columns at prepare and returns no rows, numeric strings or not - the same + no-meta-at-prepare bug as `select 10 / ? as n`. - The replay keeps whole rows for scripts now (`run-one.js`), so a script's column values replay. A transcript stops at 400 lines: split a script that needs more (as `issue-497-ns`). diff --git a/test/contract/corpus/scripts/issue-498-prepared.txt b/test/contract/corpus/scripts/issue-498-prepared.txt index 5247eb1f..38e0d01f 100644 --- a/test/contract/corpus/scripts/issue-498-prepared.txt +++ b/test/contract/corpus/scripts/issue-498-prepared.txt @@ -1,20 +1,98 @@ # scenario: scripts/issue-498-prepared -# status: known-bug +# status: agreed # script: test/contract/scripts/issue-498-prepared.js -# #498: a prepared statement binds numeric and decimal result columns as a double, so column 1 -# loses digits (should be "12345678901234567.89") setup: submitted -setup: rowcount 1 +setup: rowcount 4 setup: cb ok rows=0 more=false setup: done setup: free +== query, numeric strings on +query: submitted +query: meta i,n,d,m,f +query: row +query: column 0 "1" +query: column 1 "1.0123456789" +query: column 2 "12345678901234567.89" +query: column 3 "922337203685477.5807" +query: column 4 "1e-10" +query: row +query: column 0 "2" +query: column 1 "-0.5000000000" +query: column 2 "0.00" +query: column 3 "-0.0001" +query: column 4 "-2.5" +query: row +query: column 0 "3" +query: column 1 null +query: column 2 null +query: column 3 null +query: column 4 null +query: row +query: column 0 "4" +query: column 1 "0.0000001000" +query: column 2 "5.00" +query: column 3 "1.2345" +query: column 4 "1.5e+300" +query: done +query: free == prepared, numeric strings on +ps-ns: submitted +ps-ns: info 01S02 0 Cursor type changed +ps-ns: meta i,n,d,m,f +ps-ns: row +ps-ns: column 0 "1" +ps-ns: column 1 "1.0123456789" +ps-ns: column 2 "12345678901234567.89" +ps-ns: column 3 "922337203685477.5807" +ps-ns: column 4 "1e-10" +ps-ns: row +ps-ns: column 0 "2" +ps-ns: column 1 "-0.5000000000" +ps-ns: column 2 "0.00" +ps-ns: column 3 "-0.0001" +ps-ns: column 4 "-2.5" +ps-ns: row +ps-ns: column 0 "3" +ps-ns: column 1 null +ps-ns: column 2 null +ps-ns: column 3 null +ps-ns: column 4 null +ps-ns: row +ps-ns: column 0 "4" +ps-ns: column 1 "0.0000001000" +ps-ns: column 2 "5.00" +ps-ns: column 3 "1.2345" +ps-ns: column 4 "1.5e+300" +ps-ns: done +ps-ns: free +== prepared, numeric strings off ps: submitted -ps: meta n,d,f +ps: info 01S02 0 Cursor type changed +ps: meta i,n,d,m,f ps: row -ps: column 0 "1.0123456789" -ps: column 1 "12345678901234568" -ps: column 2 "1e-10" +ps: column 0 1 +ps: column 1 1.0123456789 +ps: column 2 12345678901234568 +ps: column 3 922337203685477.6 +ps: column 4 1e-10 +ps: row +ps: column 0 2 +ps: column 1 -0.5 +ps: column 2 0 +ps: column 3 -0.0001 +ps: column 4 -2.5 +ps: row +ps: column 0 3 +ps: column 1 null +ps: column 2 null +ps: column 3 null +ps: column 4 null +ps: row +ps: column 0 4 +ps: column 1 1e-7 +ps: column 2 5 +ps: column 3 1.2345 +ps: column 4 1.5e+300 ps: done ps: free check ok diff --git a/test/contract/native/scripts/issue-498-prepared.win32.json b/test/contract/native/scripts/issue-498-prepared.win32.json index bd0db7a8..c4e7e5ae 100644 --- a/test/contract/native/scripts/issue-498-prepared.win32.json +++ b/test/contract/native/scripts/issue-498-prepared.win32.json @@ -1 +1 @@ -{"scenario":"scripts/issue-498-prepared","transcript":["setup: submitted","setup: rowcount 1","setup: cb ok rows=0 more=false","setup: done","setup: free","== prepared, numeric strings on","ps: submitted","ps: meta n,d,f","ps: row","ps: column 0 \"1.0123456789\"","ps: column 1 \"12345678901234568\"","ps: column 2 \"1e-10\"","ps: done","ps: free","check ok"],"log":[{"ev":"call","id":0,"t":11.7,"name":"open","args":["",0]},{"ev":"return","of":0,"t":31.7,"result":[null,{"success":true}]},{"ev":"call","id":2,"t":33.5,"name":"query","args":[0,{"query_str":"create table #p (n numeric(38,10), d decimal(38,2), f float); insert #p values (1.0123456789, 12345678901234567.89, 1e-10)","query_timeout":0,"query_polling":false,"query_tz_adjustment":0,"numeric_string":false,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}},[]]},{"ev":"state","of":2,"t":33.7,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_BINDING"}},{"ev":"state","of":2,"t":33.8,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_BINDING","newState":"STATEMENT_SUBMITTED"}},{"ev":"state","of":2,"t":35,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_READING"}},{"ev":"return","of":2,"t":35,"result":[null,{"meta":[],"handle":{"connectionId":0,"statementId":0},"endOfRows":true,"endOfResults":false,"rowCount":1},true]},{"ev":"call","id":7,"t":35.2,"name":"nextResultSet","args":[0,{"connectionId":0,"statementId":0}]},{"ev":"return","of":7,"t":35.3,"result":[null,{"meta":[],"handle":{"connectionId":0,"statementId":0},"endOfRows":true,"endOfResults":true,"rowCount":1}]},{"ev":"call","id":9,"t":35.6,"name":"releaseStatement","args":[0,{"connectionId":0,"statementId":0}]},{"ev":"state","of":2,"t":35.7,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_READING","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":9,"t":35.7,"result":[null,true]},{"ev":"call","id":12,"t":187.8,"name":"prepare","args":[1,{"query_str":"select n, d, f from #p where 1 = ?","numeric_string":true,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}}]},{"ev":"state","of":12,"t":189.5,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_PREPARED"}},{"ev":"return","of":12,"t":189.5,"result":[null,{"meta":[{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":1},"endOfRows":true,"endOfResults":false,"rowCount":0}]},{"ev":"call","id":15,"t":189.8,"name":"bindQuery","args":[1,[1]]},{"ev":"state","of":12,"t":189.9,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_PREPARED","newState":"STATEMENT_SUBMITTED"}},{"ev":"return","of":15,"t":193.5,"result":[null,{"meta":[{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":1},"endOfRows":false,"endOfResults":false,"rowCount":0}]},{"ev":"call","id":18,"t":193.7,"name":"fetchRows","args":[1,{"connectionId":0,"statementId":1},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":18,"t":194.5,"result":[null,{"endOfRows":false,"endOfResults":false,"rowCount":1,"data":[["1.0123456789","12345678901234568","1e-10"]]}]},{"ev":"call","id":20,"t":194.7,"name":"fetchRows","args":[1,{"connectionId":0,"statementId":1},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":20,"t":194.7,"result":[null,{"endOfRows":true,"endOfResults":false,"rowCount":0,"data":[]}]},{"ev":"call","id":22,"t":194.8,"name":"nextResultSet","args":[1,{"connectionId":0,"statementId":1}]},{"ev":"return","of":22,"t":194.9,"result":[null,{"meta":[{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":1},"endOfRows":true,"endOfResults":true,"rowCount":0}]},{"ev":"call","id":24,"t":195,"name":"releaseStatement","args":[1,{"connectionId":0,"statementId":1}]},{"ev":"state","of":12,"t":195,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":24,"t":195.5,"result":[null,true]},{"ev":"call","id":27,"t":346.3,"name":"query","args":[2,{"query_str":"select 1 as n","query_timeout":0,"query_polling":false,"query_tz_adjustment":0,"numeric_string":false,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}},[]]},{"ev":"state","of":27,"t":346.4,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_BINDING"}},{"ev":"state","of":27,"t":346.4,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_BINDING","newState":"STATEMENT_SUBMITTED"}},{"ev":"state","of":27,"t":347.4,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_READING"}},{"ev":"return","of":27,"t":347.5,"result":[null,{"meta":[{"name":"n","nullable":false,"type":"number","sqlType":"int","size":10}],"handle":{"connectionId":0,"statementId":2},"endOfRows":true,"endOfResults":false,"rowCount":0},true]},{"ev":"call","id":32,"t":347.6,"name":"fetchRows","args":[2,{"connectionId":0,"statementId":2},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":32,"t":347.6,"result":[null,{"endOfRows":true,"endOfResults":false,"rowCount":0,"data":[[1]]}]},{"ev":"call","id":34,"t":347.7,"name":"nextResultSet","args":[2,{"connectionId":0,"statementId":2}]},{"ev":"return","of":34,"t":347.8,"result":[null,{"meta":[{"name":"n","nullable":false,"type":"number","sqlType":"int","size":10}],"handle":{"connectionId":0,"statementId":2},"endOfRows":true,"endOfResults":true,"rowCount":0}]},{"ev":"call","id":36,"t":347.9,"name":"releaseStatement","args":[2,{"connectionId":0,"statementId":2}]},{"ev":"state","of":27,"t":347.9,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_READING","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":36,"t":347.9,"result":[null,true]},{"ev":"call","id":39,"t":348.1,"name":"close","args":[]},{"ev":"return","of":39,"t":348.2,"result":[null,true]}]} \ No newline at end of file +{"scenario":"scripts/issue-498-prepared","transcript":["setup: submitted","setup: rowcount 4","setup: cb ok rows=0 more=false","setup: done","setup: free","== query, numeric strings on","query: submitted","query: meta i,n,d,m,f","query: row","query: column 0 \"1\"","query: column 1 \"1.0123456789\"","query: column 2 \"12345678901234567.89\"","query: column 3 \"922337203685477.5807\"","query: column 4 \"1e-10\"","query: row","query: column 0 \"2\"","query: column 1 \"-0.5000000000\"","query: column 2 \"0.00\"","query: column 3 \"-0.0001\"","query: column 4 \"-2.5\"","query: row","query: column 0 \"3\"","query: column 1 null","query: column 2 null","query: column 3 null","query: column 4 null","query: row","query: column 0 \"4\"","query: column 1 \"0.0000001000\"","query: column 2 \"5.00\"","query: column 3 \"1.2345\"","query: column 4 \"1.5e+300\"","query: done","query: free","== prepared, numeric strings on","ps-ns: submitted","ps-ns: info 01S02 0 Cursor type changed","ps-ns: meta i,n,d,m,f","ps-ns: row","ps-ns: column 0 \"1\"","ps-ns: column 1 \"1.0123456789\"","ps-ns: column 2 \"12345678901234567.89\"","ps-ns: column 3 \"922337203685477.5807\"","ps-ns: column 4 \"1e-10\"","ps-ns: row","ps-ns: column 0 \"2\"","ps-ns: column 1 \"-0.5000000000\"","ps-ns: column 2 \"0.00\"","ps-ns: column 3 \"-0.0001\"","ps-ns: column 4 \"-2.5\"","ps-ns: row","ps-ns: column 0 \"3\"","ps-ns: column 1 null","ps-ns: column 2 null","ps-ns: column 3 null","ps-ns: column 4 null","ps-ns: row","ps-ns: column 0 \"4\"","ps-ns: column 1 \"0.0000001000\"","ps-ns: column 2 \"5.00\"","ps-ns: column 3 \"1.2345\"","ps-ns: column 4 \"1.5e+300\"","ps-ns: done","ps-ns: free","== prepared, numeric strings off","ps: submitted","ps: info 01S02 0 Cursor type changed","ps: meta i,n,d,m,f","ps: row","ps: column 0 1","ps: column 1 1.0123456789","ps: column 2 12345678901234568","ps: column 3 922337203685477.6","ps: column 4 1e-10","ps: row","ps: column 0 2","ps: column 1 -0.5","ps: column 2 0","ps: column 3 -0.0001","ps: column 4 -2.5","ps: row","ps: column 0 3","ps: column 1 null","ps: column 2 null","ps: column 3 null","ps: column 4 null","ps: row","ps: column 0 4","ps: column 1 1e-7","ps: column 2 5","ps: column 3 1.2345","ps: column 4 1.5e+300","ps: done","ps: free","check ok"],"log":[{"ev":"call","id":0,"t":10.7,"name":"open","args":["",0]},{"ev":"return","of":0,"t":31,"result":[null,{"success":true}]},{"ev":"call","id":2,"t":32.4,"name":"query","args":[0,{"query_str":"create table #p (i int, n numeric(38,10), d decimal(38,2), m money, f float); insert #p values (1, 1.0123456789, 12345678901234567.89, 922337203685477.5807, 1e-10), (2, -0.5, 0, -0.0001, -2.5), (3, null, null, null, null), (4, 0.0000001, 5, 1.2345, 1.5e300)","query_timeout":0,"query_polling":false,"query_tz_adjustment":0,"numeric_string":false,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}},[]]},{"ev":"state","of":2,"t":32.6,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_BINDING"}},{"ev":"state","of":2,"t":32.8,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_BINDING","newState":"STATEMENT_SUBMITTED"}},{"ev":"state","of":2,"t":33.8,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_READING"}},{"ev":"return","of":2,"t":33.8,"result":[null,{"meta":[],"handle":{"connectionId":0,"statementId":0},"endOfRows":true,"endOfResults":false,"rowCount":4},true]},{"ev":"call","id":7,"t":34.1,"name":"nextResultSet","args":[0,{"connectionId":0,"statementId":0}]},{"ev":"return","of":7,"t":34.1,"result":[null,{"meta":[],"handle":{"connectionId":0,"statementId":0},"endOfRows":true,"endOfResults":true,"rowCount":4}]},{"ev":"call","id":9,"t":34.5,"name":"releaseStatement","args":[0,{"connectionId":0,"statementId":0}]},{"ev":"state","of":2,"t":34.5,"change":{"handle":{"connectionId":0,"statementId":0},"oldState":"STATEMENT_READING","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":9,"t":34.5,"result":[null,true]},{"ev":"call","id":12,"t":200.4,"name":"query","args":[1,{"query_str":"select i, n, d, m, f from #p where i >= 1 order by i","numeric_string":true,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}},[]]},{"ev":"state","of":12,"t":200.5,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_BINDING"}},{"ev":"state","of":12,"t":200.5,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_BINDING","newState":"STATEMENT_SUBMITTED"}},{"ev":"state","of":12,"t":201.5,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_READING"}},{"ev":"return","of":12,"t":201.6,"result":[null,{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":1},"endOfRows":true,"endOfResults":false,"rowCount":0},true]},{"ev":"call","id":17,"t":201.7,"name":"fetchRows","args":[1,{"connectionId":0,"statementId":1},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":17,"t":201.9,"result":[null,{"endOfRows":true,"endOfResults":false,"rowCount":0,"data":[["1","1.0123456789","12345678901234567.89","922337203685477.5807","1e-10"],["2","-0.5000000000","0.00","-0.0001","-2.5"],["3",null,null,null,null],["4","0.0000001000","5.00","1.2345","1.5e+300"]]}]},{"ev":"call","id":19,"t":202.1,"name":"nextResultSet","args":[1,{"connectionId":0,"statementId":1}]},{"ev":"return","of":19,"t":202.2,"result":[null,{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":1},"endOfRows":true,"endOfResults":true,"rowCount":0}]},{"ev":"call","id":21,"t":202.2,"name":"releaseStatement","args":[1,{"connectionId":0,"statementId":1}]},{"ev":"state","of":12,"t":202.3,"change":{"handle":{"connectionId":0,"statementId":1},"oldState":"STATEMENT_READING","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":21,"t":202.3,"result":[null,true]},{"ev":"call","id":24,"t":356.4,"name":"prepare","args":[2,{"query_str":"select i, n, d, m, f from #p where i >= ? order by i","numeric_string":true,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}}]},{"ev":"state","of":24,"t":358.6,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_PREPARED"}},{"ev":"return","of":24,"t":358.6,"result":[null,{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":2},"endOfRows":true,"endOfResults":false,"rowCount":0}]},{"ev":"call","id":27,"t":358.9,"name":"bindQuery","args":[2,[1]]},{"ev":"state","of":24,"t":359,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_PREPARED","newState":"STATEMENT_SUBMITTED"}},{"ev":"return","of":27,"t":363.6,"result":[[{"$":"error","message":"[Microsoft][ODBC Driver 18 for SQL Server]Cursor type changed","sqlstate":"01S02","code":0,"severity":0,"serverName":"","procName":"","lineNumber":0}],{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":2},"endOfRows":false,"endOfResults":false,"rowCount":4}]},{"ev":"call","id":30,"t":363.8,"name":"fetchRows","args":[2,{"connectionId":0,"statementId":2},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":30,"t":364.5,"result":[null,{"endOfRows":false,"endOfResults":false,"rowCount":4,"data":[["1","1.0123456789","12345678901234567.89","922337203685477.5807","1e-10"],["2","-0.5000000000","0.00","-0.0001","-2.5"],["3",null,null,null,null],["4","0.0000001000","5.00","1.2345","1.5e+300"]]}]},{"ev":"call","id":32,"t":364.6,"name":"fetchRows","args":[2,{"connectionId":0,"statementId":2},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":32,"t":365.3,"result":[null,{"endOfRows":true,"endOfResults":false,"rowCount":0,"data":[]}]},{"ev":"call","id":34,"t":365.3,"name":"nextResultSet","args":[2,{"connectionId":0,"statementId":2}]},{"ev":"return","of":34,"t":365.4,"result":[null,{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":2},"endOfRows":true,"endOfResults":true,"rowCount":0}]},{"ev":"call","id":36,"t":365.5,"name":"releaseStatement","args":[2,{"connectionId":0,"statementId":2}]},{"ev":"state","of":24,"t":365.5,"change":{"handle":{"connectionId":0,"statementId":2},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":36,"t":366,"result":[null,true]},{"ev":"call","id":39,"t":366.1,"name":"prepare","args":[3,{"query_str":"select i, n, d, m, f from #p where i >= ? order by i","numeric_string":false,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}}]},{"ev":"state","of":39,"t":367.3,"change":{"handle":{"connectionId":0,"statementId":3},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_PREPARED"}},{"ev":"return","of":39,"t":367.3,"result":[null,{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":3},"endOfRows":true,"endOfResults":false,"rowCount":0}]},{"ev":"call","id":42,"t":367.3,"name":"bindQuery","args":[3,[1]]},{"ev":"state","of":39,"t":367.4,"change":{"handle":{"connectionId":0,"statementId":3},"oldState":"STATEMENT_PREPARED","newState":"STATEMENT_SUBMITTED"}},{"ev":"return","of":42,"t":368.5,"result":[[{"$":"error","message":"[Microsoft][ODBC Driver 18 for SQL Server]Cursor type changed","sqlstate":"01S02","code":0,"severity":0,"serverName":"","procName":"","lineNumber":0}],{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":3},"endOfRows":false,"endOfResults":false,"rowCount":4}]},{"ev":"call","id":45,"t":368.6,"name":"fetchRows","args":[3,{"connectionId":0,"statementId":3},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":45,"t":369.2,"result":[null,{"endOfRows":false,"endOfResults":false,"rowCount":4,"data":[[1,1.0123456789,12345678901234568,922337203685477.6,1e-10],[2,-0.5,0,-0.0001,-2.5],[3,null,null,null,null],[4,1e-7,5,1.2345,1.5e+300]]}]},{"ev":"call","id":47,"t":369.2,"name":"fetchRows","args":[3,{"connectionId":0,"statementId":3},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":47,"t":369.8,"result":[null,{"endOfRows":true,"endOfResults":false,"rowCount":0,"data":[]}]},{"ev":"call","id":49,"t":369.8,"name":"nextResultSet","args":[3,{"connectionId":0,"statementId":3}]},{"ev":"return","of":49,"t":369.8,"result":[null,{"meta":[{"name":"i","nullable":true,"type":"number","sqlType":"int","size":10},{"name":"n","nullable":true,"type":"number","sqlType":"numeric","size":38},{"name":"d","nullable":true,"type":"number","sqlType":"decimal","size":38},{"name":"m","nullable":true,"type":"number","sqlType":"money","size":19},{"name":"f","nullable":true,"type":"number","sqlType":"float","size":53}],"handle":{"connectionId":0,"statementId":3},"endOfRows":true,"endOfResults":true,"rowCount":0}]},{"ev":"call","id":51,"t":369.9,"name":"releaseStatement","args":[3,{"connectionId":0,"statementId":3}]},{"ev":"state","of":39,"t":369.9,"change":{"handle":{"connectionId":0,"statementId":3},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":51,"t":370.3,"result":[null,true]},{"ev":"call","id":54,"t":528.1,"name":"query","args":[4,{"query_str":"select 1 as n","query_timeout":0,"query_polling":false,"query_tz_adjustment":0,"numeric_string":false,"bigint_as_native":false,"stateChangeCallback":{"$":"function"}},[]]},{"ev":"state","of":54,"t":528.2,"change":{"handle":{"connectionId":0,"statementId":4},"oldState":"STATEMENT_CREATED","newState":"STATEMENT_BINDING"}},{"ev":"state","of":54,"t":528.2,"change":{"handle":{"connectionId":0,"statementId":4},"oldState":"STATEMENT_BINDING","newState":"STATEMENT_SUBMITTED"}},{"ev":"state","of":54,"t":529.2,"change":{"handle":{"connectionId":0,"statementId":4},"oldState":"STATEMENT_SUBMITTED","newState":"STATEMENT_READING"}},{"ev":"return","of":54,"t":529.2,"result":[null,{"meta":[{"name":"n","nullable":false,"type":"number","sqlType":"int","size":10}],"handle":{"connectionId":0,"statementId":4},"endOfRows":true,"endOfResults":false,"rowCount":0},true]},{"ev":"call","id":59,"t":529.3,"name":"fetchRows","args":[4,{"connectionId":0,"statementId":4},{"asArrays":true,"batchSize":50,"asObjects":false}]},{"ev":"return","of":59,"t":529.4,"result":[null,{"endOfRows":true,"endOfResults":false,"rowCount":0,"data":[[1]]}]},{"ev":"call","id":61,"t":529.5,"name":"nextResultSet","args":[4,{"connectionId":0,"statementId":4}]},{"ev":"return","of":61,"t":529.5,"result":[null,{"meta":[{"name":"n","nullable":false,"type":"number","sqlType":"int","size":10}],"handle":{"connectionId":0,"statementId":4},"endOfRows":true,"endOfResults":true,"rowCount":0}]},{"ev":"call","id":63,"t":529.5,"name":"releaseStatement","args":[4,{"connectionId":0,"statementId":4}]},{"ev":"state","of":54,"t":529.6,"change":{"handle":{"connectionId":0,"statementId":4},"oldState":"STATEMENT_READING","newState":"STATEMENT_CLOSED"}},{"ev":"return","of":63,"t":529.6,"result":[null,true]},{"ev":"call","id":66,"t":529.8,"name":"close","args":[]},{"ev":"return","of":66,"t":529.9,"result":[null,true]}]} \ No newline at end of file diff --git a/test/contract/scripts/issue-498-prepared.js b/test/contract/scripts/issue-498-prepared.js index 8eb3c280..bd74b508 100644 --- a/test/contract/scripts/issue-498-prepared.js +++ b/test/contract/scripts/issue-498-prepared.js @@ -1,18 +1,30 @@ 'use strict' -// #498 through a prepared statement: it binds numeric, decimal and float result columns as a -// double, so under numeric strings numeric and decimal lose what a double cannot hold. +// #498 through a prepared statement: with numeric strings on at prepare, numeric and decimal +// result columns are bound as text, so they keep every digit, as a query's do. several rows, +// so each row's text is read from its own place in the bound array; a null, values below 1, +// money, next to a query of the same rows; and the same statement prepared with numeric strings off still reads numbers. + +const ROWS = "(1, 1.0123456789, 12345678901234567.89, 922337203685477.5807, 1e-10), " + + '(2, -0.5, 0, -0.0001, -2.5), (3, null, null, null, null), (4, 0.0000001, 5, 1.2345, 1.5e300)' module.exports = async t => { - t.query('setup', 'create table #p (n numeric(38,10), d decimal(38,2), f float); ' + - 'insert #p values (1.0123456789, 12345678901234567.89, 1e-10)', null, { callback: true }) + t.query('setup', 'create table #p (i int, n numeric(38,10), d decimal(38,2), m money, f float); ' + + `insert #p values ${ROWS}`, null, { callback: true }) await t.settled('setup') - const ps = await new Promise((resolve, reject) => { - t.conn.prepare({ query_str: 'select n, d, f from #p where 1 = ?', numeric_string: true }, - (err, p) => err ? reject(err) : resolve(p)) - }) - t.section('prepared, numeric strings on') - t.watch('ps', ps.preparedQuery([1]), { columns: true }) - await t.next('ps', 'done') - await new Promise(resolve => ps.free(resolve)) + // what a query gives for the same rows: the prepared statement should read the same + t.section('query, numeric strings on') + t.query('query', { query_str: 'select i, n, d, m, f from #p where i >= 1 order by i', numeric_string: true }, null, { columns: true }) + await t.settled('query') + for (const numericString of [true, false]) { + const label = numericString ? 'ps-ns' : 'ps' + const ps = await new Promise((resolve, reject) => { + t.conn.prepare({ query_str: 'select i, n, d, m, f from #p where i >= ? order by i', numeric_string: numericString }, + (err, p) => err ? reject(err) : resolve(p)) + }) + t.section(`prepared, numeric strings ${numericString ? 'on' : 'off'}`) + t.watch(label, ps.preparedQuery([1]), { columns: true }) + await t.next(label, 'done') + await new Promise(resolve => ps.free(resolve)) + } } diff --git a/test/params-complete-refactored.test.js b/test/params-complete-refactored.test.js index 5a8e0cae..78b482d5 100644 --- a/test/params-complete-refactored.test.js +++ b/test/params-complete-refactored.test.js @@ -994,6 +994,28 @@ describe('Parameter Tests', function () { }) }) + // a prepared statement binds these columns as text under numeric strings; 120 rows is three + // fetch blocks of 50, so each row is read from its own place in the bound array + it('should return prepared decimal and numeric as their exact text', async function () { + const q = { + query_str: "select cast('12345678901234567.' + right('00' + cast(i as varchar(3)), 2) as decimal(38,2)) as d, " + + 'cast(-i / 1000.0 as numeric(10,4)) as n from (select row_number() over (order by object_id) as i from sys.all_objects) t ' + + 'where i <= ? order by i', + numeric_string: true + } + const prepared = await env.theConnection.promises.prepare(q) + try { + const res = await prepared.promises.query([120]) + const expected = Array.from({ length: 120 }, (_, k) => { + const i = k + 1 + return { d: `12345678901234567.${String(i % 100).padStart(2, '0')}`, n: `-${(i / 1000).toFixed(4)}` } + }) + assert.deepStrictEqual(res.first, expected) + } finally { + await prepared.promises.free() + } + }) + it('should return decimal and numeric with their scale and a leading zero', async function () { const result = await env.theConnection.promises.query({ query_str: 'select cast(5 as decimal(5,2)) as d1, cast(-0.5 as decimal(5,2)) as d2, cast(0 as decimal(5,2)) as d3, ' +