Commit 7a6559c1 authored by Vadim Zeitlin's avatar Vadim Zeitlin

Merge branch 'odbc-string-len'

Fix handling of strings in bulk operations in ODBC and DB2 backends and
get_affected_rows() behaviour in ODBC.

Closes #561, also see #202, #495.
parents 82038018 c6aef12b
......@@ -16,8 +16,12 @@ It can be useful to know how many rows were affected by the last SQL statement,
---
##### Portability note:
This method is currently not supported by the Oracle backend. It is however
supported when using Oracle database via ODBC backend.
This method behaviour in case of partially executed update, i.e. when some
records were updated or inserted while some other have failed to be updated or
inserted, depends on the exact backend and, in the case of ODBC backend, on the
exact ODBC driver used. It can return `-1`, meaning that the number of rows is
unknown, the number of rows actually updated or the total number of affected
rows.
---
### Working with sequences
......@@ -71,4 +75,4 @@ The above example retrieves the `rowid` ("something" that identifies the row in
In order for any of the above to compile, you have to explicitly `#include` the appropriate backend's header file.
Please see the header file related to the given backend to learn what low-level handles and descriptors are available.
\ No newline at end of file
Please see the header file related to the given backend to learn what low-level handles and descriptors are available.
......@@ -194,11 +194,23 @@ void db2_vector_into_type_backend::post_fetch(bool gotData, indicator *ind)
std::vector<std::string> &v(*vp);
char *pos = buf;
const char *pos = buf;
std::size_t const vsize = v.size();
for (std::size_t i = 0; i != vsize; ++i)
{
v[i].assign(pos, strlen(pos));
// See ODBC backend for explanation, this code for determining
// the string length is exactly the same as there.
const char* end = pos + indVec[i];
while (end != pos)
{
if (*--end != ' ')
{
++end;
break;
}
}
v[i].assign(pos, end - pos);
pos += colSize;
}
}
......
......@@ -139,18 +139,20 @@ void db2_vector_use_type_backend::prepare_for_bind(void *&data, SQLUINTEGER &siz
prepare_indicators(vecSize);
for (std::size_t i = 0; i != vecSize; ++i)
{
std::size_t sz = v[i].length() + 1; // add one for null
std::size_t sz = v[i].length();
indVec[i] = static_cast<long>(sz);
maxSize = sz > maxSize ? sz : maxSize;
}
maxSize++; // For terminating nul.
buf = new char[maxSize * vecSize];
memset(buf, 0, maxSize * vecSize);
char *pos = buf;
for (std::size_t i = 0; i != vecSize; ++i)
{
strncpy(pos, v[i].c_str(), v[i].length());
memcpy(pos, v[i].c_str(), v[i].length());
pos += maxSize;
}
......
......@@ -150,20 +150,29 @@ odbc_statement_backend::execute(int number)
SQLRETURN rc = SQLExecute(hstmt_);
if (is_odbc_error(rc))
{
// Construct the error object immediately, before calling any other
// ODBC functions, in order to not lose the error message.
const odbc_soci_error err(SQL_HANDLE_STMT, hstmt_, "executing statement");
// There is no universal way to determine the number of affected rows
// after a failed update.
rowsAffected_ = -1LL;
// If executing bulk operation a partial
// number of rows affected may be available.
if (hasVectorUseElements_)
{
rowsAffected_ = 0;
do
{
SQLLEN res = 0;
// SQLRowCount will return error after a partially executed statement.
// SQL_DIAG_ROW_COUNT returns the same info but must be collected immediatelly after the execution.
rc = SQLGetDiagField(SQL_HANDLE_STMT, hstmt_, 0, SQL_DIAG_ROW_COUNT, &res, 0, NULL);
if (!is_odbc_error(rc) && res > 0) // 'res' will be -1 for the where the statement failed.
if (!is_odbc_error(rc) && res != -1)
{
if (rowsAffected_ == -1LL)
rowsAffected_ = res;
else
rowsAffected_ += res;
}
--rows_processed; // Avoid unnecessary calls to SQLGetDiagField
......@@ -171,7 +180,7 @@ odbc_statement_backend::execute(int number)
// Move forward to the next result while there are rows processed.
while (rows_processed > 0 && SQLMoreResults(hstmt_) == SQL_SUCCESS);
}
throw odbc_soci_error(SQL_HANDLE_STMT, hstmt_, "executing statement");
throw err;
}
else if (hasVectorUseElements_)
{
......@@ -180,20 +189,15 @@ odbc_statement_backend::execute(int number)
}
else // We need to retrieve the number of rows affected explicitly.
{
rowsAffected_ = 0;
do {
SQLLEN res = 0;
SQLRETURN rc = SQLRowCount(hstmt_, &res);
if (is_odbc_error(rc))
{
throw odbc_soci_error(SQL_HANDLE_STMT, hstmt_,
"getting number of affected rows");
}
rowsAffected_ += res;
SQLLEN res = 0;
rc = SQLRowCount(hstmt_, &res);
if (is_odbc_error(rc))
{
throw odbc_soci_error(SQL_HANDLE_STMT, hstmt_,
"getting number of affected rows");
}
// Move forward to the next result if executing a bulk operation.
while (hasVectorUseElements_ && SQLMoreResults(hstmt_) == SQL_SUCCESS);
rowsAffected_ = res;
}
SQLSMALLINT colCount;
SQLNumResultCols(hstmt_, &colCount);
......
......@@ -221,11 +221,33 @@ void odbc_vector_into_type_backend::post_fetch(bool gotData, indicator *ind)
std::vector<std::string> &v(*vp);
char *pos = buf_;
const char *pos = buf_;
std::size_t const vsize = v.size();
for (std::size_t i = 0; i != vsize; ++i)
{
v[i].assign(pos, strlen(pos));
// Find the actual length of the string: for a VARCHAR(N)
// column, it may be right-padded with spaces up to the length
// of the longest string in the result set. This happens with
// at least MS SQL (and the exact behaviour depends on the
// value of the ANSI_PADDING option) and it seems like some
// other ODBC drivers also have options like "PADVARCHAR", so
// it's probably not the only case when it does.
//
// So deal with this generically by just trimming all the
// spaces from the right hand-side.
const char* end = pos + indHolderVec_[i];
while (end != pos)
{
// Pre-decrement as "end" is one past the end, as usual.
if (*--end != ' ')
{
// We must count the last non-space character.
++end;
break;
}
}
v[i].assign(pos, end - pos);
pos += colSize_;
}
}
......
......@@ -165,18 +165,20 @@ void odbc_vector_use_type_backend::prepare_for_bind(void *&data, SQLUINTEGER &si
prepare_indicators(vecSize);
for (std::size_t i = 0; i != vecSize; ++i)
{
std::size_t sz = v[i].length() + 1; // add one for null
std::size_t sz = v[i].length();
indHolderVec_[i] = static_cast<long>(sz);
maxSize = sz > maxSize ? sz : maxSize;
}
maxSize++; // For terminating nul.
buf_ = new char[maxSize * vecSize];
memset(buf_, 0, maxSize * vecSize);
char *pos = buf_;
for (std::size_t i = 0; i != vecSize; ++i)
{
strncpy(pos, v[i].c_str(), v[i].length());
memcpy(pos, v[i].c_str(), v[i].length());
pos += maxSize;
}
......
......@@ -352,6 +352,11 @@ public:
// whatever we do.
virtual bool enable_std_char_padding(session&) const { return true; }
// Return the name of the function for determining the length of a string,
// i.e. "char_length" in standard SQL but often "len" or "length" in
// practice.
virtual std::string get_length_function_name() const = 0;
virtual ~test_context_base()
{
the_test_context_ = NULL;
......@@ -917,9 +922,9 @@ TEST_CASE_METHOD(common_tests, "Repeated and bulk fetch", "[core][bulk]")
st.execute();
while (st.fetch())
{
for (std::size_t i = 0; i != vec.size(); ++i)
for (std::size_t n = 0; n != vec.size(); ++n)
{
CHECK(i2 == vec[i]);
CHECK(i2 == vec[n]);
++i2;
}
......@@ -982,11 +987,11 @@ TEST_CASE_METHOD(common_tests, "Repeated and bulk fetch", "[core][bulk]")
int const rowsToTest = 100;
double d = 0.0;
statement st = (sql.prepare <<
statement sti = (sql.prepare <<
"insert into soci_test(d) values(:d)", use(d));
for (int i = 0; i != rowsToTest; ++i)
{
st.execute(true);
sti.execute(true);
d += 0.6;
}
......@@ -2013,7 +2018,6 @@ TEST_CASE_METHOD(common_tests, "Dynamic row binding", "[core][dynamic]")
// select into a row
{
row r;
statement st = (sql.prepare <<
"select * from soci_test", into(r));
st.execute(true);
......@@ -2039,9 +2043,7 @@ TEST_CASE_METHOD(common_tests, "Dynamic row binding", "[core][dynamic]")
ASSERT_EQUAL_APPROX(r.get<double>(0), 3.14);
CHECK(r.get<int>(1) == 123);
CHECK(r.get<std::string>(2) == "Johny");
std::tm t = std::tm();
t = r.get<std::tm>(3);
CHECK(t.tm_year == 105);
CHECK(r.get<std::tm>(3).tm_year == 105);
// again, type char is visible as string
CHECK_EQUAL_PADDED(r.get<std::string>(4), "a");
......@@ -2091,7 +2093,6 @@ TEST_CASE_METHOD(common_tests, "Dynamic row binding", "[core][dynamic]")
// additional test to check if the row object can be
// reused between queries
{
row r;
sql << "select * from soci_test", into(r);
CHECK(r.size() == 5);
......@@ -3895,8 +3896,15 @@ TEST_CASE_METHOD(common_tests, "Get affected rows", "[core][affected-rows]")
sql << "select count(val) from soci_test", into(val);
if(val != 0)
{
// test the preserved 'number of rows
// affected' after a potential failure.
// Notice that some ODBC drivers don't return the number of updated
// rows at all in the case of partially executed statement like this
// one, while MySQL ODBC driver wrongly returns 2 affected rows even
// though only one was actually inserted.
//
// So we can't check for "get_affected_rows() == val" here, it would
// fail in too many cases -- just check that the backend doesn't lie to
// us about no rows being affected at all (even if it just honestly
// admits that it has no idea by returning -1).
CHECK(st6.get_affected_rows() != 0);
}
}
......@@ -4086,6 +4094,22 @@ void check_for_exception_on_truncation(session& sql)
}
}
// And another helper for the test below.
void check_for_no_truncation(session& sql)
{
const std::string str20 = "exactly of length 20";
sql << "delete from soci_test";
// Also check that there is no truncation when inserting a string of
// the same length as the column size.
CHECK_NOTHROW( (sql << "insert into soci_test(name) values(:s)", use(str20)) );
std::string s;
sql << "select name from soci_test", into(s);
CHECK( s == str20 );
}
} // anonymous namespace
TEST_CASE_METHOD(common_tests, "Truncation error", "[core][insert][truncate][exception]")
......@@ -4112,6 +4136,8 @@ TEST_CASE_METHOD(common_tests, "Truncation error", "[core][insert][truncate][exc
tc_.on_after_ddl(sql);
check_for_exception_on_truncation(sql);
check_for_no_truncation(sql);
}
SECTION("Error given for varchar column")
......@@ -4120,6 +4146,8 @@ TEST_CASE_METHOD(common_tests, "Truncation error", "[core][insert][truncate][exc
auto_table_creator tableCreator(tc_.table_creator_1(sql));
check_for_exception_on_truncation(sql);
check_for_no_truncation(sql);
}
}
......@@ -4182,6 +4210,56 @@ TEST_CASE_METHOD(common_tests, "Select without table", "[core][select][dummy_fro
CHECK(plus17 == 17);
}
TEST_CASE_METHOD(common_tests, "String length", "[core][string][length]")
{
soci::session sql(backEndFactory_, connectString_);
auto_table_creator tableCreator(tc_.table_creator_1(sql));
std::string s("123");
sql << "insert into soci_test(str) values(:s)", use(s);
const std::string& len_func = tc_.get_length_function_name();
std::string sout;
size_t slen;
sql << "select str," + len_func + "(str)"
" from soci_test",
into(sout), into(slen);
CHECK(slen == 3);
CHECK(sout.length() == 3);
CHECK(sout == s);
sql << "delete from soci_test";
std::vector<std::string> v;
v.push_back("Hello");
v.push_back("");
v.push_back("whole of varchar(20)");
CHECK_NOTHROW( (sql << "insert into soci_test(str) values(:s)", use(v)) );
std::vector<std::string> vout(10);
std::vector<unsigned int> vlen(10);
sql << "select str," + len_func + "(str)"
" from soci_test"
" order by " + len_func + "(str)",
into(vout), into(vlen);
REQUIRE(vout.size() == 3);
REQUIRE(vlen.size() == 3);
CHECK(vlen[0] == 0);
CHECK(vout[0].length() == 0);
CHECK(vlen[1] == 5);
CHECK(vout[1].length() == 5);
CHECK(vlen[2] == 20);
CHECK(vout[2].length() == 20);
}
} // namespace test_cases
} // namespace tests
......
......@@ -95,6 +95,11 @@ public:
{
return "to_date('" + pi_datdt_string + "', 'YYYY-MM-DD HH24:MI:SS')";
}
virtual std::string get_length_function_name() const
{
return "length";
}
};
......
......@@ -1311,6 +1311,11 @@ class test_context : public tests::test_context_base
{
sql.commit();
}
virtual std::string get_length_function_name() const
{
return "char_length";
}
};
......
......@@ -127,6 +127,11 @@ public:
return false;
}
}
virtual std::string get_length_function_name() const
{
return "char_length";
}
};
#endif // SOCI_TESTS_MYSQL_H_INCLUDED
......@@ -107,6 +107,11 @@ test_context(backend_factory const &backEnd, std::string const &connectString)
{
return "#" + datdt_string + "#";
}
virtual std::string get_length_function_name() const
{
return "len";
}
};
int main(int argc, char** argv)
......
......@@ -93,6 +93,11 @@ public:
{
return "\'" + datdt_string + "\'";
}
virtual std::string get_length_function_name() const
{
return "length";
}
};
struct table_creator_bigint : table_creator_base
......
......@@ -160,6 +160,11 @@ public:
// on the side of caution and suppose that it's not supported.
return true;
}
virtual std::string get_length_function_name() const
{
return "len";
}
};
int main(int argc, char** argv)
......
......@@ -179,6 +179,11 @@ public:
return !m_verDriver.is_initialized() || m_verDriver < odbc_version(9, 3, 400);
}
virtual std::string get_length_function_name() const
{
return "char_length";
}
private:
odbc_version get_driver_version() const
{
......
......@@ -1555,6 +1555,11 @@ public:
{
return "to_date('" + datdt_string + "', 'YYYY-MM-DD HH24:MI:SS')";
}
virtual std::string get_length_function_name() const
{
return "length";
}
};
int main(int argc, char** argv)
......
......@@ -1172,6 +1172,11 @@ public:
{
return false;
}
virtual std::string get_length_function_name() const
{
return "char_length";
}
};
int main(int argc, char** argv)
......
......@@ -379,6 +379,11 @@ public:
// SQLite does not support right padded char type.
return false;
}
virtual std::string get_length_function_name() const
{
return "length";
}
};
int main(int argc, char** argv)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment