Skip to content

Commit

Permalink
timestamp conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
jraymakers committed Aug 27, 2024
1 parent 2c3e5a5 commit c1b886d
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 54 deletions.
193 changes: 140 additions & 53 deletions bindings/src/duckdb_node_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Napi::Number>().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<Napi::Number>().Int32Value();
int8_t month = date_parts_obj.Get("month").As<Napi::Number>().Int32Value();
int8_t day = date_parts_obj.Get("day").As<Napi::Number>().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<Napi::Number>().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<Napi::Number>().Int32Value();
int8_t min = time_parts_obj.Get("min").As<Napi::Number>().Int32Value();
int8_t sec = time_parts_obj.Get("sec").As<Napi::Number>().Int32Value();
int32_t micros = time_parts_obj.Get("micros").As<Napi::Number>().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<Napi::BigInt>().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<Napi::BigInt>().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<Napi::Object>());
auto time = GetTimePartsFromObject(timestamp_parts_obj.Get("time").As<Napi::Object>());
return { date, time };
}

// Externals

template<typename T>
Napi::External<T> CreateExternal(Napi::Env env, const napi_type_tag &type_tag, T *data) {
auto external = Napi::External<T>::New(env, data);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -525,6 +639,8 @@ Napi::Object CreateTypeEnum(Napi::Env env) {
return typeEnum;
}

// Addon

class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {

public:
Expand Down Expand Up @@ -1015,38 +1131,27 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
Napi::Value from_date(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto date_obj = info[0].As<Napi::Object>();
auto days = date_obj.Get("days").As<Napi::Number>().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);
// function to_date(parts: DateParts): Date_
Napi::Value to_date(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto date_parts_obj = info[0].As<Napi::Object>();
int32_t year = date_parts_obj.Get("year").As<Napi::Number>().Int32Value();
int8_t month = date_parts_obj.Get("month").As<Napi::Number>().Int32Value();
int8_t day = date_parts_obj.Get("day").As<Napi::Number>().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);
// function is_finite_date(date: Date_): boolean
Napi::Value is_finite_date(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto date_obj = info[0].As<Napi::Object>();
auto days = date_obj.Get("days").As<Napi::Number>().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);
}
Expand All @@ -1056,15 +1161,9 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
Napi::Value from_time(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto time_obj = info[0].As<Napi::Object>();
auto micros = time_obj.Get("micros").As<Napi::Number>().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);
Expand All @@ -1074,69 +1173,57 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
auto micros = info[0].As<Napi::Number>().Int64Value();
auto offset = info[1].As<Napi::Number>().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);
// function from_time_tz(time_tz: TimeTZ): TimeTZParts
Napi::Value from_time_tz(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto time_tz_obj = info[0].As<Napi::Object>();
bool lossless;
auto bits = time_tz_obj.Get("bits").As<Napi::BigInt>().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);
// function to_time(parts: TimeParts): Time
Napi::Value to_time(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto time_parts_obj = info[0].As<Napi::Object>();
int8_t hour = time_parts_obj.Get("hour").As<Napi::Number>().Int32Value();
int8_t min = time_parts_obj.Get("min").As<Napi::Number>().Int32Value();
int8_t sec = time_parts_obj.Get("sec").As<Napi::Number>().Int32Value();
int32_t micros = time_parts_obj.Get("micros").As<Napi::Number>().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<Napi::Object>();
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<Napi::Object>();
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<Napi::Object>();
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);
Expand Down
86 changes: 85 additions & 1 deletion bindings/test/conversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Expand All @@ -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);
});
});
});

0 comments on commit c1b886d

Please sign in to comment.