diff --git a/Cargo.lock b/Cargo.lock index 3ef6f0b0..40491524 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -935,9 +944,10 @@ dependencies = [ [[package]] name = "odbc-api" -version = "4.1.0" +version = "5.0.0" dependencies = [ "anyhow", + "atoi", "criterion", "csv", "env_logger", @@ -961,7 +971,7 @@ checksum = "6c219b6a4cd9eb167239d921d7348132cdff2eca05abbf1094e7f7ec9b8936b6" [[package]] name = "odbcsv" -version = "1.0.3" +version = "1.0.4" dependencies = [ "anyhow", "assert_cmd", diff --git a/Changelog.md b/Changelog.md index c23a3d33..73e98941 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,9 @@ # Changelog -## (next) +## 5.0.0 -* Removed reexport of force_send_sync +* removed reexport of `force_send_sync`. It is no longer used within `odbc-api` this should have been removed together with the methods which were promoting Connections to `Send`, but has been overlooked. +* Adds `decimal_text_to_i128` a useful function for downstream applications including `odbc2parquet` and `arrow-odbc`. ## 4.1.0 diff --git a/odbc-api/Cargo.toml b/odbc-api/Cargo.toml index 5ff77c19..3065b973 100644 --- a/odbc-api/Cargo.toml +++ b/odbc-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "odbc-api" -version = "4.1.0" +version = "5.0.0" authors = ["Markus Klein"] edition = "2021" license = "MIT" @@ -72,6 +72,7 @@ thiserror = "1.0.56" log = "0.4.20" # Interacting with UTF-16 texts for wide columns or wide function calls widestring = "1.0.2" +atoi = "2.0.0" [target.'cfg(windows)'.dependencies] # We use winit to display dialogs prompting for connection strings. We can deactivate default diff --git a/odbc-api/src/conversion.rs b/odbc-api/src/conversion.rs new file mode 100644 index 00000000..7007daca --- /dev/null +++ b/odbc-api/src/conversion.rs @@ -0,0 +1,63 @@ +use atoi::{FromRadix10, FromRadix10Signed}; + +/// Convert the text representation of a decimal into an integer representation. The integer +/// representation is not truncating the fraction, but is instead the value of the decimal times 10 +/// to the power of scale. E.g. 123.45 of a Decimal with scale 3 is thought of as 123.450 and +/// represented as 123450. This method will regard any non digit character as a radix character with +/// the exception of a `+` or `-` at the beginning of the string. +/// +/// This method is robust against representation which do not have trailing zeroes as well as +/// arbitrary radix character. If you do not write a generic application and now the specific way +/// your database formats decimals you may come up with faster methods to parse decimals. +pub fn decimal_text_to_i128(text: &[u8], scale: usize) -> i128 { + // High is now the number before the decimal point + let (mut high, num_digits_high) = i128::from_radix_10_signed(text); + let (low, num_digits_low) = if num_digits_high == text.len() { + (0, 0) + } else { + i128::from_radix_10(&text[(num_digits_high + 1)..]) + }; + // Left shift high so it is compatible with low + for _ in 0..num_digits_low { + high *= 10; + } + // We want to increase the absolute of high by low without changing highs sign + let mut n = if high < 0 { high - low } else { high + low }; + // We would be done now, if every database would include trailing zeroes, but they might choose + // to omit those. Therfore we see if we need to leftshift n further in order to meet scale. + for _ in 0..(scale - num_digits_low) { + n *= 10; + } + n +} + +#[cfg(test)] +mod tests { + use super::decimal_text_to_i128; + + /// An user of an Oracle database got invalid values from decimal after setting + /// `NLS_NUMERIC_CHARACTERS` to ",." instead of ".". + /// + /// See issue: + /// + #[test] + fn decimal_is_represented_with_comma_as_radix() { + let actual = decimal_text_to_i128(b"10,00000", 5); + assert_eq!(1_000_000, actual); + } + + /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle + /// seems to not emit trailing zeroes. Also see issue: + /// + #[test] + fn decimal_with_less_zeroes() { + let actual = decimal_text_to_i128(b"10.0", 5); + assert_eq!(1_000_000, actual); + } + + #[test] + fn negative_decimal() { + let actual = decimal_text_to_i128(b"-10.00000", 5); + assert_eq!(-1_000_000, actual); + } +} diff --git a/odbc-api/src/lib.rs b/odbc-api/src/lib.rs index 1dce522c..7cc87a85 100644 --- a/odbc-api/src/lib.rs +++ b/odbc-api/src/lib.rs @@ -6,6 +6,7 @@ mod columnar_bulk_inserter; mod connection; +mod conversion; mod cursor; mod driver_complete_option; mod environment; @@ -31,6 +32,7 @@ pub mod parameter; pub use self::{ columnar_bulk_inserter::{BoundInputSlice, ColumnarBulkInserter}, connection::{escape_attribute_value, Connection, ConnectionOptions}, + conversion::decimal_text_to_i128, cursor::{ BlockCursor, BlockCursorPolling, Cursor, CursorImpl, CursorPolling, CursorRow, RowSetBuffer, TruncationInfo, diff --git a/odbc-api/tests/integration.rs b/odbc-api/tests/integration.rs index 3540a07d..9eff8bd4 100644 --- a/odbc-api/tests/integration.rs +++ b/odbc-api/tests/integration.rs @@ -18,7 +18,7 @@ use odbc_api::{ parameter::{InputParameter, VarCharSliceMut}, sys, Bit, ColumnDescription, Connection, ConnectionOptions, Cursor, DataType, Error, InOut, IntoParameter, Narrow, Nullability, Nullable, Out, Preallocated, ResultSetMetadata, U16Str, - U16String, + U16String, decimal_text_to_i128, }; use std::{ ffi::CString, @@ -4045,6 +4045,40 @@ fn json_column_display_size(profile: &Profile, expected_display_size: Option