Skip to content

Commit

Permalink
Fixup/sqlserver datetime2 (#885)
Browse files Browse the repository at this point in the history
* nanodbc: statement: Add parameter_scale|type

upstream: nanodbc/nanodbc#424

* sql server: respect DATETIME2 precision

* NEWS: update

* tests: make tolerance explicit

* nanodbc: fixup cast
  • Loading branch information
detule authored Jan 27, 2025
1 parent 49b70ea commit 6b5635c
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 30 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# odbc (development version)

* SQL Server: Writing to DATETIME2 targets respects precision (#793).

* Addressed issue where error messages rethrown from some drivers would be
garbled when the raw error message contained curly brackets
(#859 by @simonpcouch).
Expand Down
87 changes: 63 additions & 24 deletions src/nanodbc/nanodbc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1870,35 +1870,44 @@ class statement::statement_impl
return params;
}

unsigned long parameter_size(short param_index) const
unsigned long parameter_size(short param_index)
{
if (!param_descr_data_.count(param_index))
if (param_descr_data_.count(param_index))
{
return static_cast<unsigned long>(param_descr_data_.at(param_index).size_);
return static_cast<unsigned long>(param_descr_data_.at(param_index).size_);
}
RETCODE rc;
SQLSMALLINT data_type;
SQLSMALLINT nullable;
SQLULEN parameter_size;

#if defined(NANODBC_DO_ASYNC_IMPL)
disable_async();
#endif

NANODBC_CALL_RC(
SQLDescribeParam,
rc,
stmt_,
param_index + 1,
&data_type,
&parameter_size,
0,
&nullable);
if (!success(rc))
NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
describe_parameters(param_index);
const SQLULEN& param_size = param_descr_data_.at(param_index).size_;
NANODBC_ASSERT(
parameter_size <= static_cast<SQLULEN>(std::numeric_limits<unsigned long>::max()));
return static_cast<unsigned long>(parameter_size);
param_size < static_cast<SQLULEN>(std::numeric_limits<unsigned long>::max()));
return static_cast<unsigned long>(param_size);
}

short parameter_scale(short param_index)
{
if (param_descr_data_.count(param_index))
{
return static_cast<short>(param_descr_data_.at(param_index).scale_);
}

describe_parameters(param_index);
const SQLSMALLINT& param_scale = param_descr_data_.at(param_index).scale_;
NANODBC_ASSERT(param_scale < static_cast<SQLULEN>(std::numeric_limits<short>::max()));
return static_cast<short>(param_scale);
}

short parameter_type(short param_index)
{
if (param_descr_data_.count(param_index))
{
return static_cast<short>(param_descr_data_.at(param_index).type_);
}

describe_parameters(param_index);
const SQLSMALLINT& param_type = param_descr_data_.at(param_index).type_;
NANODBC_ASSERT(param_type < static_cast<SQLULEN>(std::numeric_limits<short>::max()));
return static_cast<short>(param_type);
}

static SQLSMALLINT param_type_from_direction(param_direction direction)
Expand Down Expand Up @@ -2106,6 +2115,26 @@ class statement::statement_impl
NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
}

void describe_parameters(const short param_index)
{
RETCODE rc;
SQLSMALLINT nullable; // unused
#if defined(NANODBC_DO_ASYNC_IMPL)
disable_async();
#endif
NANODBC_CALL_RC(
SQLDescribeParam,
rc,
stmt_,
static_cast<SQLUSMALLINT>(param_index + 1),
&param_descr_data_[param_index].type_,
&param_descr_data_[param_index].size_,
&param_descr_data_[param_index].scale_,
&nullable);
if (!success(rc))
NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
}

void describe_parameters(
const std::vector<short>& idx,
const std::vector<short>& type,
Expand Down Expand Up @@ -4169,6 +4198,16 @@ unsigned long statement::parameter_size(short param_index) const
return impl_->parameter_size(param_index);
}

short statement::parameter_scale(short param_index) const
{
return impl_->parameter_scale(param_index);
}

short statement::parameter_type(short param_index) const
{
return impl_->parameter_type(param_index);
}

// We need to instantiate each form of bind() for each of our supported data types.
#define NANODBC_INSTANTIATE_BINDS(type) \
template void statement::bind(short, const type*, param_direction); /* 1-ary */ \
Expand Down
6 changes: 6 additions & 0 deletions src/nanodbc/nanodbc.h
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,12 @@ class statement
/// \brief Returns parameter size for indicated parameter placeholder in a prepared statement.
unsigned long parameter_size(short param_index) const;

/// \brief Returns parameter scale for indicated parameter placeholder in a prepared statement.
short parameter_scale(short param_index) const;

/// \brief Returns parameter type for indicated parameter placeholder in a prepared statement.
short parameter_type(short param_index) const;

/// \addtogroup binding Binding parameters
/// \brief These functions are used to bind values to ODBC parameters.
///
Expand Down
21 changes: 16 additions & 5 deletions src/odbc_result.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -355,17 +355,16 @@ void odbc_result::bind_raw(
column, raws_[column], reinterpret_cast<bool*>(nulls_[column].data()));
}

nanodbc::timestamp odbc_result::as_timestamp(double value) {
nanodbc::timestamp odbc_result::as_timestamp(double value, unsigned long long factor, unsigned long long pad) {
nanodbc::timestamp ts;
auto frac = modf(value, &value);

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

auto civil_time = cctz::convert(utc_time, c_->timezone());
// We are using a fixed precision of 3, as that is all we can be guaranteed
// to support in SQLServer
ts.fract = (std::int32_t)(frac * 1000) * 1000000;
ts.fract = (std::int32_t)(frac * factor) * pad;

ts.sec = civil_time.second();
ts.min = civil_time.minute();
ts.hour = civil_time.hour();
Expand Down Expand Up @@ -408,12 +407,24 @@ void odbc_result::bind_datetime(
auto d = REAL(data[column]);

nanodbc::timestamp ts;
short precision = 3;
try {
precision = statement.parameter_scale(column);
} catch (const nanodbc::database_error& e) {
raise_warning("Unable to discern datetime precision. Using default (3).");
};
// Sanity scrub
precision = std::min<short>(precision, 7);
unsigned long long prec_adj = std::pow(10, precision);
// The fraction field is expressed in billionths of
// a second.
unsigned long long pad = std::pow(10, 9 - precision);
for (size_t i = 0; i < size; ++i) {
auto value = d[start + i];
if (ISNA(value)) {
nulls_[column][i] = true;
} else {
ts = as_timestamp(value);
ts = as_timestamp(value, prec_adj, pad);
}
timestamps_[column].push_back(ts);
}
Expand Down
2 changes: 1 addition & 1 deletion src/odbc_result.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class odbc_result {
size_t start,
size_t size);

nanodbc::timestamp as_timestamp(double value);
nanodbc::timestamp as_timestamp(double value, unsigned long long factor, unsigned long long pad);

nanodbc::date as_date(double value);

Expand Down
13 changes: 13 additions & 0 deletions tests/testthat/test-driver-sql-server.R
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,16 @@ test_that("independent encoding of column entries and names (#834)", {
res <- DBI::dbReadTable(conn, tbl_id)
expect_identical(df, res)
})

test_that("DATETIME2 precision (#790)", {
con <- test_con("SQLSERVER")

seed <- as.POSIXlt("2025-01-25 18:45:39.395682")
val <- seed + runif(500, min = 0, max = 1)
df <- data.frame(dtm = val, dtm2 = val)

tbl <- local_table(con, "test_datetime2_precision", df,
field.types = list("dtm" = "DATETIME", "dtm2" = "DATETIME2(6)"))
res <- DBI::dbReadTable(con, tbl)
expect_equal(as.POSIXlt(df[[2]])$sec, as.POSIXlt(res[[2]])$sec, tolerance = 1E-7)
})

0 comments on commit 6b5635c

Please sign in to comment.