From c1b886d56873a31f3061252156b78da8baa381f6 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Mon, 26 Aug 2024 21:41:41 -0700 Subject: [PATCH] timestamp conversion --- bindings/src/duckdb_node_bindings.cpp | 193 +++++++++++++++++++------- bindings/test/conversion.test.ts | 86 +++++++++++- 2 files changed, 225 insertions(+), 54 deletions(-) diff --git a/bindings/src/duckdb_node_bindings.cpp b/bindings/src/duckdb_node_bindings.cpp index b44e7b2e..4a8d975e 100644 --- a/bindings/src/duckdb_node_bindings.cpp +++ b/bindings/src/duckdb_node_bindings.cpp @@ -8,6 +8,116 @@ #include "duckdb.h" +// Conversion betweeen structs and objects + +Napi::Object MakeDateObject(Napi::Env env, duckdb_date date) { + auto date_obj = Napi::Object::New(env); + date_obj.Set("days", Napi::Number::New(env, date.days)); + return date_obj; +} + +duckdb_date GetDateFromObject(Napi::Object date_obj) { + auto days = date_obj.Get("days").As().Int32Value(); + return { days }; +} + +Napi::Object MakeDatePartsObject(Napi::Env env, duckdb_date_struct date_parts) { + auto date_parts_obj = Napi::Object::New(env); + date_parts_obj.Set("year", Napi::Number::New(env, date_parts.year)); + date_parts_obj.Set("month", Napi::Number::New(env, date_parts.month)); + date_parts_obj.Set("day", Napi::Number::New(env, date_parts.day)); + return date_parts_obj; +} + +duckdb_date_struct GetDatePartsFromObject(Napi::Object date_parts_obj) { + int32_t year = date_parts_obj.Get("year").As().Int32Value(); + int8_t month = date_parts_obj.Get("month").As().Int32Value(); + int8_t day = date_parts_obj.Get("day").As().Int32Value(); + return { year, month, day }; +} + +Napi::Object MakeTimeObject(Napi::Env env, duckdb_time time) { + auto time_obj = Napi::Object::New(env); + time_obj.Set("micros", Napi::Number::New(env, time.micros)); + return time_obj; +} + +duckdb_time GetTimeFromObject(Napi::Object time_obj) { + auto micros = time_obj.Get("micros").As().Int64Value(); + return { micros }; +} + +Napi::Object MakeTimePartsObject(Napi::Env env, duckdb_time_struct time_parts) { + auto time_parts_obj = Napi::Object::New(env); + time_parts_obj.Set("hour", Napi::Number::New(env, time_parts.hour)); + time_parts_obj.Set("min", Napi::Number::New(env, time_parts.min)); + time_parts_obj.Set("sec", Napi::Number::New(env, time_parts.sec)); + time_parts_obj.Set("micros", Napi::Number::New(env, time_parts.micros)); + return time_parts_obj; +} + +duckdb_time_struct GetTimePartsFromObject(Napi::Object time_parts_obj) { + int8_t hour = time_parts_obj.Get("hour").As().Int32Value(); + int8_t min = time_parts_obj.Get("min").As().Int32Value(); + int8_t sec = time_parts_obj.Get("sec").As().Int32Value(); + int32_t micros = time_parts_obj.Get("micros").As().Int32Value(); + return { hour, min, sec, micros }; +} + +Napi::Object MakeTimeTZObject(Napi::Env env, duckdb_time_tz time_tz) { + auto time_tz_obj = Napi::Object::New(env); + time_tz_obj.Set("bits", Napi::BigInt::New(env, time_tz.bits)); + return time_tz_obj; +} + +duckdb_time_tz GetTimeTZFromObject(Napi::Env env, Napi::Object time_tz_obj) { + bool lossless; + auto bits = time_tz_obj.Get("bits").As().Uint64Value(&lossless); + if (!lossless) { + throw Napi::Error::New(env, "bits out of uint64 range"); + } + return { bits }; +} + +Napi::Object MakeTimeTZPartsObject(Napi::Env env, duckdb_time_tz_struct time_tz_parts) { + auto time_tz_parts_obj = Napi::Object::New(env); + time_tz_parts_obj.Set("time", MakeTimePartsObject(env, time_tz_parts.time)); + time_tz_parts_obj.Set("offset", Napi::Number::New(env, time_tz_parts.offset)); + return time_tz_parts_obj; +} + +// GetTimeTZFromObject not used + +Napi::Object MakeTimestampObject(Napi::Env env, duckdb_timestamp timestamp) { + auto timestamp_obj = Napi::Object::New(env); + timestamp_obj.Set("micros", Napi::BigInt::New(env, timestamp.micros)); + return timestamp_obj; +} + +duckdb_timestamp GetTimestampFromObject(Napi::Env env, Napi::Object timestamp_obj) { + bool lossless; + auto micros = timestamp_obj.Get("micros").As().Int64Value(&lossless); + if (!lossless) { + throw Napi::Error::New(env, "micros out of int64 range"); + } + return { micros }; +} + +Napi::Object MakeTimestampPartsObject(Napi::Env env, duckdb_timestamp_struct timestamp_parts) { + auto timestamp_parts_obj = Napi::Object::New(env); + timestamp_parts_obj.Set("date", MakeDatePartsObject(env, timestamp_parts.date)); + timestamp_parts_obj.Set("time", MakeTimePartsObject(env, timestamp_parts.time)); + return timestamp_parts_obj; +} + +duckdb_timestamp_struct GetTimestampPartsFromObject(Napi::Object timestamp_parts_obj) { + auto date = GetDatePartsFromObject(timestamp_parts_obj.Get("date").As()); + auto time = GetTimePartsFromObject(timestamp_parts_obj.Get("time").As()); + return { date, time }; +} + +// Externals + template Napi::External CreateExternal(Napi::Env env, const napi_type_tag &type_tag, T *data) { auto external = Napi::External::New(env, data); @@ -134,6 +244,8 @@ duckdb_vector GetVectorFromExternal(Napi::Env env, Napi::Value value) { return GetDataFromExternal<_duckdb_vector>(env, VectorTypeTag, value, "Invalid vector argument"); } +// Promise workers + class PromiseWorker : public Napi::AsyncWorker { public: @@ -430,6 +542,8 @@ class FetchWorker : public PromiseWorker { }; +// Enums + void DefineEnumMember(Napi::Object enumObj, const char *key, uint32_t value) { enumObj.Set(key, value); enumObj.Set(value, key); @@ -525,6 +639,8 @@ Napi::Object CreateTypeEnum(Napi::Env env) { return typeEnum; } +// Addon + class DuckDBNodeAddon : public Napi::Addon { public: @@ -1015,14 +1131,9 @@ class DuckDBNodeAddon : public Napi::Addon { Napi::Value from_date(const Napi::CallbackInfo& info) { auto env = info.Env(); auto date_obj = info[0].As(); - auto days = date_obj.Get("days").As().Int32Value(); - duckdb_date date = { days }; + auto date = GetDateFromObject(date_obj); auto date_parts = duckdb_from_date(date); - auto result = Napi::Object::New(env); - result.Set("year", Napi::Number::New(env, date_parts.year)); - result.Set("month", Napi::Number::New(env, date_parts.month)); - result.Set("day", Napi::Number::New(env, date_parts.day)); - return result; + return MakeDatePartsObject(env, date_parts); } // DUCKDB_API duckdb_date duckdb_to_date(duckdb_date_struct date); @@ -1030,14 +1141,9 @@ class DuckDBNodeAddon : public Napi::Addon { Napi::Value to_date(const Napi::CallbackInfo& info) { auto env = info.Env(); auto date_parts_obj = info[0].As(); - int32_t year = date_parts_obj.Get("year").As().Int32Value(); - int8_t month = date_parts_obj.Get("month").As().Int32Value(); - int8_t day = date_parts_obj.Get("day").As().Int32Value(); - duckdb_date_struct date_parts = { year, month, day }; + auto date_parts = GetDatePartsFromObject(date_parts_obj); auto date = duckdb_to_date(date_parts); - auto result = Napi::Object::New(env); - result.Set("days", Napi::Number::New(env, date.days)); - return result; + return MakeDateObject(env, date); } // DUCKDB_API bool duckdb_is_finite_date(duckdb_date date); @@ -1045,8 +1151,7 @@ class DuckDBNodeAddon : public Napi::Addon { Napi::Value is_finite_date(const Napi::CallbackInfo& info) { auto env = info.Env(); auto date_obj = info[0].As(); - auto days = date_obj.Get("days").As().Int32Value(); - duckdb_date date = { days }; + auto date = GetDateFromObject(date_obj); auto is_finite = duckdb_is_finite_date(date); return Napi::Boolean::New(env, is_finite); } @@ -1056,15 +1161,9 @@ class DuckDBNodeAddon : public Napi::Addon { Napi::Value from_time(const Napi::CallbackInfo& info) { auto env = info.Env(); auto time_obj = info[0].As(); - auto micros = time_obj.Get("micros").As().Int64Value(); - duckdb_time time = { micros }; + auto time = GetTimeFromObject(time_obj); auto time_parts = duckdb_from_time(time); - auto result = Napi::Object::New(env); - result.Set("hour", Napi::Number::New(env, time_parts.hour)); - result.Set("min", Napi::Number::New(env, time_parts.min)); - result.Set("sec", Napi::Number::New(env, time_parts.sec)); - result.Set("micros", Napi::Number::New(env, time_parts.micros)); - return result; + return MakeTimePartsObject(env, time_parts); } // DUCKDB_API duckdb_time_tz duckdb_create_time_tz(int64_t micros, int32_t offset); @@ -1074,9 +1173,7 @@ class DuckDBNodeAddon : public Napi::Addon { auto micros = info[0].As().Int64Value(); auto offset = info[1].As().Int32Value(); auto time_tz = duckdb_create_time_tz(micros, offset); - auto result = Napi::Object::New(env); - result.Set("bits", Napi::BigInt::New(env, time_tz.bits)); - return result; + return MakeTimeTZObject(env, time_tz); } // DUCKDB_API duckdb_time_tz_struct duckdb_from_time_tz(duckdb_time_tz micros); @@ -1084,22 +1181,9 @@ class DuckDBNodeAddon : public Napi::Addon { Napi::Value from_time_tz(const Napi::CallbackInfo& info) { auto env = info.Env(); auto time_tz_obj = info[0].As(); - bool lossless; - auto bits = time_tz_obj.Get("bits").As().Uint64Value(&lossless); - if (!lossless) { - throw Napi::Error::New(env, "bits out of uint64 range"); - } - duckdb_time_tz time_tz = { bits }; + auto time_tz = GetTimeTZFromObject(env, time_tz_obj); auto time_tz_parts = duckdb_from_time_tz(time_tz); - auto result = Napi::Object::New(env); - auto time = Napi::Object::New(env); - time.Set("hour", Napi::Number::New(env, time_tz_parts.time.hour)); - time.Set("min", Napi::Number::New(env, time_tz_parts.time.min)); - time.Set("sec", Napi::Number::New(env, time_tz_parts.time.sec)); - time.Set("micros", Napi::Number::New(env, time_tz_parts.time.micros)); - result.Set("time", time); - result.Set("offset", Napi::Number::New(env, time_tz_parts.offset)); - return result; + return MakeTimeTZPartsObject(env, time_tz_parts); } // DUCKDB_API duckdb_time duckdb_to_time(duckdb_time_struct time); @@ -1107,36 +1191,39 @@ class DuckDBNodeAddon : public Napi::Addon { Napi::Value to_time(const Napi::CallbackInfo& info) { auto env = info.Env(); auto time_parts_obj = info[0].As(); - int8_t hour = time_parts_obj.Get("hour").As().Int32Value(); - int8_t min = time_parts_obj.Get("min").As().Int32Value(); - int8_t sec = time_parts_obj.Get("sec").As().Int32Value(); - int32_t micros = time_parts_obj.Get("micros").As().Int32Value(); - duckdb_time_struct time_parts = { hour, min, sec, micros }; + auto time_parts = GetTimePartsFromObject(time_parts_obj); auto time = duckdb_to_time(time_parts); - auto result = Napi::Object::New(env); - result.Set("micros", Napi::Number::New(env, time.micros)); - return result; + return MakeTimeObject(env, time); } // DUCKDB_API duckdb_timestamp_struct duckdb_from_timestamp(duckdb_timestamp ts); // function from_timestamp(timestamp: Timestamp): TimestampParts Napi::Value from_timestamp(const Napi::CallbackInfo& info) { auto env = info.Env(); - throw Napi::Error::New(env, "Not implemented yet"); + auto timestamp_obj = info[0].As(); + auto timestamp = GetTimestampFromObject(env, timestamp_obj); + auto timestamp_parts = duckdb_from_timestamp(timestamp); + return MakeTimestampPartsObject(env, timestamp_parts); } // DUCKDB_API duckdb_timestamp duckdb_to_timestamp(duckdb_timestamp_struct ts); // function to_timestamp(parts: TimestampParts): Timestamp Napi::Value to_timestamp(const Napi::CallbackInfo& info) { auto env = info.Env(); - throw Napi::Error::New(env, "Not implemented yet"); + auto timestamp_parts_obj = info[0].As(); + auto timestamp_parts = GetTimestampPartsFromObject(timestamp_parts_obj); + auto timestamp = duckdb_to_timestamp(timestamp_parts); + return MakeTimestampObject(env, timestamp); } // DUCKDB_API bool duckdb_is_finite_timestamp(duckdb_timestamp ts); // function is_finite_timestamp(timestamp: Timestamp): boolean Napi::Value is_finite_timestamp(const Napi::CallbackInfo& info) { auto env = info.Env(); - throw Napi::Error::New(env, "Not implemented yet"); + auto timestamp_obj = info[0].As(); + auto timestamp = GetTimestampFromObject(env, timestamp_obj); + auto is_finite = duckdb_is_finite_timestamp(timestamp); + return Napi::Boolean::New(env, is_finite); } // DUCKDB_API double duckdb_hugeint_to_double(duckdb_hugeint val); diff --git a/bindings/test/conversion.test.ts b/bindings/test/conversion.test.ts index d9b6969c..7ec8aa0c 100644 --- a/bindings/test/conversion.test.ts +++ b/bindings/test/conversion.test.ts @@ -79,7 +79,7 @@ suite('conversion', () => { test('max', () => { expect(duckdb.from_time_tz({ bits: 1449551462400115198n })).toStrictEqual({ time: { hour: 24, min: 0, sec: 0, micros: 0 }, offset: -57599 }); }); - test('out of range', () => { + test('out of uint64 range', () => { expect(() => duckdb.from_time_tz({ bits: 2n ** 64n })).toThrowError('bits out of uint64 range'); }); }); @@ -94,4 +94,88 @@ suite('conversion', () => { expect(duckdb.to_time({ hour: 24, min: 0, sec: 0, micros: 0 })).toStrictEqual({ micros: 86400000000 }); }); }); + suite('from_timestamp', () => { + test('mid-range', () => { + // 1717418096789123n = 19877n * 86400000000n + 45296789123n + expect(duckdb.from_timestamp({ micros: 1717418096789123n })).toStrictEqual({ + date: { year: 2024, month: 6, day: 3 }, + time: { hour: 12, min: 34, sec: 56, micros: 789123 }, + }); + }); + test('epoch', () => { + expect(duckdb.from_timestamp({ micros: 0n })).toStrictEqual({ + date: { year: 1970, month: 1, day: 1 }, + time: { hour: 0, min: 0, sec: 0, micros: 0 }, + }); + }); + test('min', () => { + // min timestamp = 290309-12-22 (BC) 00:00:00 + expect(duckdb.from_timestamp({ micros: -9223372022400000000n })).toStrictEqual({ + date: { year: -290308, month: 12, day: 22 }, + time: { hour: 0, min: 0, sec: 0, micros: 0 }, + }); + }); + test('max', () => { + // max timestamp = 294247-01-10 04:00:54.775806 + expect(duckdb.from_timestamp({ micros: 9223372036854775806n })).toStrictEqual({ + date: { year: 294247, month: 1, day: 10 }, + time: { hour: 4, min: 0, sec: 54, micros: 775806 }, + }); + }); + test('out of int64 range (positive)', () => { + expect(() => duckdb.from_timestamp({ micros: 2n ** 63n })).toThrowError('micros out of int64 range'); + }); + test('out of int64 range (negative)', () => { + expect(() => duckdb.from_timestamp({ micros: -(2n ** 63n + 1n) })).toThrowError('micros out of int64 range'); + }); + }); + suite('to_timestamp', () => { + test('mid-range', () => { + // 1717418096789123n = 19877n * 86400000000n + 45296789123n + expect(duckdb.to_timestamp({ + date: { year: 2024, month: 6, day: 3 }, + time: { hour: 12, min: 34, sec: 56, micros: 789123 }, + })).toStrictEqual({ micros: 1717418096789123n }); + }); + test('epoch', () => { + expect(duckdb.to_timestamp({ + date: { year: 1970, month: 1, day: 1 }, + time: { hour: 0, min: 0, sec: 0, micros: 0 }, + })).toStrictEqual({ micros: 0n }); + }); + test('min', () => { + // min timestamp = 290309-12-22 (BC) 00:00:00 + expect(duckdb.to_timestamp({ + date: { year: -290308, month: 12, day: 22 }, + time: { hour: 0, min: 0, sec: 0, micros: 0 }, + })).toStrictEqual({ micros: -9223372022400000000n }); + }); + test('max', () => { + // max timestamp = 294247-01-10 04:00:54.775806 + expect(duckdb.to_timestamp({ + date: { year: 294247, month: 1, day: 10 }, + time: { hour: 4, min: 0, sec: 54, micros: 775806 }, + })).toStrictEqual({ micros: 9223372036854775806n }); + }); + }); + suite('is_finite_timestamp', () => { + test('mid-range', () => { + expect(duckdb.is_finite_timestamp({ micros: 1717418096789123n })).toBe(true); + }); + test('epoch', () => { + expect(duckdb.is_finite_timestamp({ micros: 0n })).toBe(true); + }); + test('min', () => { + expect(duckdb.is_finite_timestamp({ micros: -9223372022400000000n })).toBe(true); + }); + test('max', () => { + expect(duckdb.is_finite_timestamp({ micros: 9223372036854775806n })).toBe(true); + }); + test('infinity', () => { + expect(duckdb.is_finite_timestamp({ micros: 2n ** 63n - 1n })).toBe(false); + }); + test('-infinity', () => { + expect(duckdb.is_finite_timestamp({ micros: -(2n ** 63n - 1n) })).toBe(false); + }); + }); });