Skip to content

Commit 6b5635c

Browse files
authored
Fixup/sqlserver datetime2 (#885)
* nanodbc: statement: Add parameter_scale|type upstream: nanodbc/nanodbc#424 * sql server: respect DATETIME2 precision * NEWS: update * tests: make tolerance explicit * nanodbc: fixup cast
1 parent 49b70ea commit 6b5635c

File tree

6 files changed

+101
-30
lines changed

6 files changed

+101
-30
lines changed

Diff for: NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# odbc (development version)
22

3+
* SQL Server: Writing to DATETIME2 targets respects precision (#793).
4+
35
* Addressed issue where error messages rethrown from some drivers would be
46
garbled when the raw error message contained curly brackets
57
(#859 by @simonpcouch).

Diff for: src/nanodbc/nanodbc.cpp

+63-24
Original file line numberDiff line numberDiff line change
@@ -1870,35 +1870,44 @@ class statement::statement_impl
18701870
return params;
18711871
}
18721872

1873-
unsigned long parameter_size(short param_index) const
1873+
unsigned long parameter_size(short param_index)
18741874
{
1875-
if (!param_descr_data_.count(param_index))
1875+
if (param_descr_data_.count(param_index))
18761876
{
1877-
return static_cast<unsigned long>(param_descr_data_.at(param_index).size_);
1877+
return static_cast<unsigned long>(param_descr_data_.at(param_index).size_);
18781878
}
1879-
RETCODE rc;
1880-
SQLSMALLINT data_type;
1881-
SQLSMALLINT nullable;
1882-
SQLULEN parameter_size;
1883-
1884-
#if defined(NANODBC_DO_ASYNC_IMPL)
1885-
disable_async();
1886-
#endif
18871879

1888-
NANODBC_CALL_RC(
1889-
SQLDescribeParam,
1890-
rc,
1891-
stmt_,
1892-
param_index + 1,
1893-
&data_type,
1894-
&parameter_size,
1895-
0,
1896-
&nullable);
1897-
if (!success(rc))
1898-
NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
1880+
describe_parameters(param_index);
1881+
const SQLULEN& param_size = param_descr_data_.at(param_index).size_;
18991882
NANODBC_ASSERT(
1900-
parameter_size <= static_cast<SQLULEN>(std::numeric_limits<unsigned long>::max()));
1901-
return static_cast<unsigned long>(parameter_size);
1883+
param_size < static_cast<SQLULEN>(std::numeric_limits<unsigned long>::max()));
1884+
return static_cast<unsigned long>(param_size);
1885+
}
1886+
1887+
short parameter_scale(short param_index)
1888+
{
1889+
if (param_descr_data_.count(param_index))
1890+
{
1891+
return static_cast<short>(param_descr_data_.at(param_index).scale_);
1892+
}
1893+
1894+
describe_parameters(param_index);
1895+
const SQLSMALLINT& param_scale = param_descr_data_.at(param_index).scale_;
1896+
NANODBC_ASSERT(param_scale < static_cast<SQLULEN>(std::numeric_limits<short>::max()));
1897+
return static_cast<short>(param_scale);
1898+
}
1899+
1900+
short parameter_type(short param_index)
1901+
{
1902+
if (param_descr_data_.count(param_index))
1903+
{
1904+
return static_cast<short>(param_descr_data_.at(param_index).type_);
1905+
}
1906+
1907+
describe_parameters(param_index);
1908+
const SQLSMALLINT& param_type = param_descr_data_.at(param_index).type_;
1909+
NANODBC_ASSERT(param_type < static_cast<SQLULEN>(std::numeric_limits<short>::max()));
1910+
return static_cast<short>(param_type);
19021911
}
19031912

19041913
static SQLSMALLINT param_type_from_direction(param_direction direction)
@@ -2106,6 +2115,26 @@ class statement::statement_impl
21062115
NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
21072116
}
21082117

2118+
void describe_parameters(const short param_index)
2119+
{
2120+
RETCODE rc;
2121+
SQLSMALLINT nullable; // unused
2122+
#if defined(NANODBC_DO_ASYNC_IMPL)
2123+
disable_async();
2124+
#endif
2125+
NANODBC_CALL_RC(
2126+
SQLDescribeParam,
2127+
rc,
2128+
stmt_,
2129+
static_cast<SQLUSMALLINT>(param_index + 1),
2130+
&param_descr_data_[param_index].type_,
2131+
&param_descr_data_[param_index].size_,
2132+
&param_descr_data_[param_index].scale_,
2133+
&nullable);
2134+
if (!success(rc))
2135+
NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
2136+
}
2137+
21092138
void describe_parameters(
21102139
const std::vector<short>& idx,
21112140
const std::vector<short>& type,
@@ -4169,6 +4198,16 @@ unsigned long statement::parameter_size(short param_index) const
41694198
return impl_->parameter_size(param_index);
41704199
}
41714200

4201+
short statement::parameter_scale(short param_index) const
4202+
{
4203+
return impl_->parameter_scale(param_index);
4204+
}
4205+
4206+
short statement::parameter_type(short param_index) const
4207+
{
4208+
return impl_->parameter_type(param_index);
4209+
}
4210+
41724211
// We need to instantiate each form of bind() for each of our supported data types.
41734212
#define NANODBC_INSTANTIATE_BINDS(type) \
41744213
template void statement::bind(short, const type*, param_direction); /* 1-ary */ \

Diff for: src/nanodbc/nanodbc.h

+6
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,12 @@ class statement
697697
/// \brief Returns parameter size for indicated parameter placeholder in a prepared statement.
698698
unsigned long parameter_size(short param_index) const;
699699

700+
/// \brief Returns parameter scale for indicated parameter placeholder in a prepared statement.
701+
short parameter_scale(short param_index) const;
702+
703+
/// \brief Returns parameter type for indicated parameter placeholder in a prepared statement.
704+
short parameter_type(short param_index) const;
705+
700706
/// \addtogroup binding Binding parameters
701707
/// \brief These functions are used to bind values to ODBC parameters.
702708
///

Diff for: src/odbc_result.cpp

+16-5
Original file line numberDiff line numberDiff line change
@@ -355,17 +355,16 @@ void odbc_result::bind_raw(
355355
column, raws_[column], reinterpret_cast<bool*>(nulls_[column].data()));
356356
}
357357

358-
nanodbc::timestamp odbc_result::as_timestamp(double value) {
358+
nanodbc::timestamp odbc_result::as_timestamp(double value, unsigned long long factor, unsigned long long pad) {
359359
nanodbc::timestamp ts;
360360
auto frac = modf(value, &value);
361361

362362
using namespace std::chrono;
363363
auto utc_time = system_clock::from_time_t(static_cast<std::time_t>(value));
364364

365365
auto civil_time = cctz::convert(utc_time, c_->timezone());
366-
// We are using a fixed precision of 3, as that is all we can be guaranteed
367-
// to support in SQLServer
368-
ts.fract = (std::int32_t)(frac * 1000) * 1000000;
366+
ts.fract = (std::int32_t)(frac * factor) * pad;
367+
369368
ts.sec = civil_time.second();
370369
ts.min = civil_time.minute();
371370
ts.hour = civil_time.hour();
@@ -408,12 +407,24 @@ void odbc_result::bind_datetime(
408407
auto d = REAL(data[column]);
409408

410409
nanodbc::timestamp ts;
410+
short precision = 3;
411+
try {
412+
precision = statement.parameter_scale(column);
413+
} catch (const nanodbc::database_error& e) {
414+
raise_warning("Unable to discern datetime precision. Using default (3).");
415+
};
416+
// Sanity scrub
417+
precision = std::min<short>(precision, 7);
418+
unsigned long long prec_adj = std::pow(10, precision);
419+
// The fraction field is expressed in billionths of
420+
// a second.
421+
unsigned long long pad = std::pow(10, 9 - precision);
411422
for (size_t i = 0; i < size; ++i) {
412423
auto value = d[start + i];
413424
if (ISNA(value)) {
414425
nulls_[column][i] = true;
415426
} else {
416-
ts = as_timestamp(value);
427+
ts = as_timestamp(value, prec_adj, pad);
417428
}
418429
timestamps_[column].push_back(ts);
419430
}

Diff for: src/odbc_result.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class odbc_result {
135135
size_t start,
136136
size_t size);
137137

138-
nanodbc::timestamp as_timestamp(double value);
138+
nanodbc::timestamp as_timestamp(double value, unsigned long long factor, unsigned long long pad);
139139

140140
nanodbc::date as_date(double value);
141141

Diff for: tests/testthat/test-driver-sql-server.R

+13
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,16 @@ test_that("independent encoding of column entries and names (#834)", {
360360
res <- DBI::dbReadTable(conn, tbl_id)
361361
expect_identical(df, res)
362362
})
363+
364+
test_that("DATETIME2 precision (#790)", {
365+
con <- test_con("SQLSERVER")
366+
367+
seed <- as.POSIXlt("2025-01-25 18:45:39.395682")
368+
val <- seed + runif(500, min = 0, max = 1)
369+
df <- data.frame(dtm = val, dtm2 = val)
370+
371+
tbl <- local_table(con, "test_datetime2_precision", df,
372+
field.types = list("dtm" = "DATETIME", "dtm2" = "DATETIME2(6)"))
373+
res <- DBI::dbReadTable(con, tbl)
374+
expect_equal(as.POSIXlt(df[[2]])$sec, as.POSIXlt(res[[2]])$sec, tolerance = 1E-7)
375+
})

0 commit comments

Comments
 (0)