diff --git a/CHANGELOG.md b/CHANGELOG.md index 40d919a8..f29dff13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. # Unreleased +- add `TIME_NS` column support to `DuckDB::Result`. `TIME_NS` values are returned as `Time` objects with nanoseconds truncated to microseconds. + # 1.5.1.1 - 2026-04-04 - fix `DuckDB::ScalarFunction` to allow `HUGEINT` and `UHUGEINT` as `return_type` and parameter type (the C extension's vector write path was missing those cases). diff --git a/ext/duckdb/converter.h b/ext/duckdb/converter.h index 69a2c62c..d2db4bff 100644 --- a/ext/duckdb/converter.h +++ b/ext/duckdb/converter.h @@ -12,6 +12,7 @@ extern ID id__to_uuid_from_uhugeint; extern ID id__to_time_from_duckdb_timestamp_s; extern ID id__to_time_from_duckdb_timestamp_ms; extern ID id__to_time_from_duckdb_timestamp_ns; +extern ID id__to_time_from_duckdb_time_ns; extern ID id__to_time_from_duckdb_time_tz; extern ID id__to_time_from_duckdb_timestamp_tz; extern ID id__to_infinity; @@ -24,6 +25,7 @@ VALUE rbduckdb_uhugeint_to_ruby(duckdb_uhugeint h); VALUE rbduckdb_timestamp_s_to_ruby(duckdb_timestamp_s ts); VALUE rbduckdb_timestamp_ms_to_ruby(duckdb_timestamp_ms ts); VALUE rbduckdb_timestamp_ns_to_ruby(duckdb_timestamp_ns ts); +VALUE rbduckdb_time_ns_to_ruby(duckdb_time_ns ts); VALUE rbduckdb_time_tz_to_ruby(duckdb_time_tz tz); VALUE rbduckdb_timestamp_tz_to_ruby(duckdb_timestamp ts); VALUE rbduckdb_time_to_ruby(duckdb_time t); diff --git a/ext/duckdb/conveter.c b/ext/duckdb/conveter.c index 7537b9cc..732d6899 100644 --- a/ext/duckdb/conveter.c +++ b/ext/duckdb/conveter.c @@ -13,6 +13,7 @@ ID id__to_uuid_from_uhugeint; ID id__to_time_from_duckdb_timestamp_s; ID id__to_time_from_duckdb_timestamp_ms; ID id__to_time_from_duckdb_timestamp_ns; +ID id__to_time_from_duckdb_time_ns; ID id__to_time_from_duckdb_time_tz; ID id__to_time_from_duckdb_timestamp_tz; ID id__to_infinity; @@ -128,6 +129,12 @@ VALUE rbduckdb_timestamp_ns_to_ruby(duckdb_timestamp_ns ts) { ); } +VALUE rbduckdb_time_ns_to_ruby(duckdb_time_ns ts) { + return rb_funcall(mDuckDBConverter, id__to_time_from_duckdb_time_ns, 1, + LL2NUM(ts.nanos) + ); +} + VALUE rbduckdb_time_tz_to_ruby(duckdb_time_tz tz) { duckdb_time_tz_struct data = duckdb_from_time_tz(tz); return rb_funcall(mDuckDBConverter, id__to_time_from_duckdb_time_tz, 5, @@ -201,6 +208,7 @@ void rbduckdb_init_duckdb_converter(void) { id__to_time_from_duckdb_timestamp_s = rb_intern("_to_time_from_duckdb_timestamp_s"); id__to_time_from_duckdb_timestamp_ms = rb_intern("_to_time_from_duckdb_timestamp_ms"); id__to_time_from_duckdb_timestamp_ns = rb_intern("_to_time_from_duckdb_timestamp_ns"); + id__to_time_from_duckdb_time_ns = rb_intern("_to_time_from_duckdb_time_ns"); id__to_time_from_duckdb_time_tz = rb_intern("_to_time_from_duckdb_time_tz"); id__to_time_from_duckdb_timestamp_tz = rb_intern("_to_time_from_duckdb_timestamp_tz"); id__to_infinity = rb_intern("_to_infinity"); diff --git a/ext/duckdb/result.c b/ext/duckdb/result.c index 3148eccf..5a931ed3 100644 --- a/ext/duckdb/result.c +++ b/ext/duckdb/result.c @@ -37,6 +37,7 @@ static VALUE vector_decimal(duckdb_logical_type ty, void* vector_data, idx_t row static VALUE vector_timestamp_s(void* vector_data, idx_t row_idx); static VALUE vector_timestamp_ms(void* vector_data, idx_t row_idx); static VALUE vector_timestamp_ns(void* vector_data, idx_t row_idx); +static VALUE vector_time_ns(void* vector_data, idx_t row_idx); static VALUE vector_enum(duckdb_logical_type ty, void* vector_data, idx_t row_idx); static VALUE vector_array(duckdb_logical_type ty, duckdb_vector vector, idx_t row_idx); static VALUE vector_list(duckdb_logical_type ty, duckdb_vector vector, void* vector_data, idx_t row_idx); @@ -370,6 +371,10 @@ static VALUE vector_timestamp_ns(void* vector_data, idx_t row_idx) { return rbduckdb_timestamp_ns_to_ruby(((duckdb_timestamp_ns *)vector_data)[row_idx]); } +static VALUE vector_time_ns(void* vector_data, idx_t row_idx) { + return rbduckdb_time_ns_to_ruby(((duckdb_time_ns *)vector_data)[row_idx]); +} + static VALUE vector_enum(duckdb_logical_type ty, void* vector_data, idx_t row_idx) { duckdb_type type = duckdb_enum_internal_type(ty); uint8_t index; @@ -498,6 +503,9 @@ VALUE rbduckdb_vector_value_at(duckdb_vector vector, duckdb_logical_type element case DUCKDB_TYPE_TIMESTAMP_NS: obj = vector_timestamp_ns(vector_data, index); break; + case DUCKDB_TYPE_TIME_NS: + obj = vector_time_ns(vector_data, index); + break; case DUCKDB_TYPE_ENUM: obj = vector_enum(element_type, vector_data, index); break; diff --git a/ext/duckdb/value_impl.c b/ext/duckdb/value_impl.c index 12f60355..f35ff62f 100644 --- a/ext/duckdb/value_impl.c +++ b/ext/duckdb/value_impl.c @@ -106,6 +106,9 @@ VALUE rbduckdb_duckdb_value_to_ruby(duckdb_value val) { case DUCKDB_TYPE_TIMESTAMP_NS: result = rbduckdb_timestamp_ns_to_ruby(duckdb_get_timestamp_ns(val)); break; + case DUCKDB_TYPE_TIME_NS: + result = rbduckdb_time_ns_to_ruby(duckdb_get_time_ns(val)); + break; case DUCKDB_TYPE_TIME_TZ: result = rbduckdb_time_tz_to_ruby(duckdb_get_time_tz(val)); break; diff --git a/lib/duckdb/converter.rb b/lib/duckdb/converter.rb index cf8168fc..8f983983 100644 --- a/lib/duckdb/converter.rb +++ b/lib/duckdb/converter.rb @@ -76,6 +76,16 @@ def _to_time_from_duckdb_timestamp_ns(time) end end + def _to_time_from_duckdb_time_ns(nanos) + hour = nanos / 3_600_000_000_000 + nanos %= 3_600_000_000_000 + min = nanos / 60_000_000_000 + nanos %= 60_000_000_000 + sec = nanos / 1_000_000_000 + microsecond = (nanos % 1_000_000_000) / 1_000 + _to_time_from_duckdb_time(hour, min, sec, microsecond) + end + def _to_time_from_duckdb_time_tz(hour, min, sec, micro, timezone) tz_offset = format_timezone_offset(timezone) time_str = format('%02d:%02d:%02d.%06d%s', diff --git a/test/duckdb_test/result_each_test.rb b/test/duckdb_test/result_each_test.rb index e3061ccc..25b40f5c 100644 --- a/test/duckdb_test/result_each_test.rb +++ b/test/duckdb_test/result_each_test.rb @@ -26,6 +26,11 @@ def teardown # rubocop:disable Style/NumericLiterals, Layout/ExtraSpacing # rubocop:disable Layout/SpaceInsideHashLiteralBraces, Layout/SpaceAfterComma # rubocop:disable Style/TrailingCommaInArrayLiteral + # Minimum DuckDB library version required for each type. + MINIMUM_DUCKDB_VERSION = { + 'TIME_NS' => '1.5.0' + }.freeze + TEST_TABLES = [ # DB Type , DB declartion String Rep Ruby Type Ruby Value [:ok, 'BOOLEAN', 'BOOLEAN', 'true', TrueClass, true ], @@ -107,6 +112,7 @@ def teardown # set TIMEZONE to Asia/Kabul to test TIMETZ and TIMESTAMPTZ [:ok, 'TIMETZ', 'TIMETZ', "'2019-11-03 12:34:56.123456789'", Time, timetz_expected ], [:ok, 'TIMESTAMPTZ', 'TIMESTAMPTZ', "'2019-11-03 12:34:56.123456789'", Time, Time.parse('2019-11-03 08:04:56.123456+0000') ], + [:ok, 'TIME_NS', 'TIME_NS', "'12:34:56.789123456'", Time, Time.parse('12:34:56.789123') ], ].freeze # rubocop:enable Layout/LineLength, Layout/SpaceInsideArrayLiteralBrackets # rubocop:enable Style/NumericLiterals, Layout/ExtraSpacing @@ -141,7 +147,7 @@ def async_query_test_data end def do_query_result_assertions(res, ruby_val, db_type, klass) - if %w[TIME TIMETZ].include?(db_type) + if %w[TIME TIMETZ TIME_NS].include?(db_type) assert_equal( [ruby_val.hour, ruby_val.min, ruby_val.sec, ruby_val.usec, ruby_val.utc_offset], [res.hour, res.min, res.sec, res.usec, res.utc_offset] @@ -157,6 +163,10 @@ def do_query_result_assertions(res, ruby_val, db_type, klass) do_test, db_type, db_declaration, string_rep, klass, ruby_val = *spec define_method :"test_#{db_type}_type#{i}" do skip spec.to_s if do_test == :ng + min_ver = MINIMUM_DUCKDB_VERSION[db_type] + if min_ver && ::DuckDBTest.duckdb_library_version < Gem::Version.new(min_ver) + skip "#{db_type} requires DuckDB >= #{min_ver}" + end prepare_test_table_and_data(db_declaration, db_type, string_rep) @@ -167,6 +177,10 @@ def do_query_result_assertions(res, ruby_val, db_type, klass) define_method :"test_stream_#{db_type}_type#{i}" do skip spec.to_s if do_test == :ng + min_ver = MINIMUM_DUCKDB_VERSION[db_type] + if min_ver && ::DuckDBTest.duckdb_library_version < Gem::Version.new(min_ver) + skip "#{db_type} requires DuckDB >= #{min_ver}" + end prepare_test_table_and_data(db_declaration, db_type, string_rep)