Skip to content

Commit c1b886d

Browse files
committed
timestamp conversion
1 parent 2c3e5a5 commit c1b886d

File tree

2 files changed

+225
-54
lines changed

2 files changed

+225
-54
lines changed

bindings/src/duckdb_node_bindings.cpp

Lines changed: 140 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,116 @@
88

99
#include "duckdb.h"
1010

11+
// Conversion betweeen structs and objects
12+
13+
Napi::Object MakeDateObject(Napi::Env env, duckdb_date date) {
14+
auto date_obj = Napi::Object::New(env);
15+
date_obj.Set("days", Napi::Number::New(env, date.days));
16+
return date_obj;
17+
}
18+
19+
duckdb_date GetDateFromObject(Napi::Object date_obj) {
20+
auto days = date_obj.Get("days").As<Napi::Number>().Int32Value();
21+
return { days };
22+
}
23+
24+
Napi::Object MakeDatePartsObject(Napi::Env env, duckdb_date_struct date_parts) {
25+
auto date_parts_obj = Napi::Object::New(env);
26+
date_parts_obj.Set("year", Napi::Number::New(env, date_parts.year));
27+
date_parts_obj.Set("month", Napi::Number::New(env, date_parts.month));
28+
date_parts_obj.Set("day", Napi::Number::New(env, date_parts.day));
29+
return date_parts_obj;
30+
}
31+
32+
duckdb_date_struct GetDatePartsFromObject(Napi::Object date_parts_obj) {
33+
int32_t year = date_parts_obj.Get("year").As<Napi::Number>().Int32Value();
34+
int8_t month = date_parts_obj.Get("month").As<Napi::Number>().Int32Value();
35+
int8_t day = date_parts_obj.Get("day").As<Napi::Number>().Int32Value();
36+
return { year, month, day };
37+
}
38+
39+
Napi::Object MakeTimeObject(Napi::Env env, duckdb_time time) {
40+
auto time_obj = Napi::Object::New(env);
41+
time_obj.Set("micros", Napi::Number::New(env, time.micros));
42+
return time_obj;
43+
}
44+
45+
duckdb_time GetTimeFromObject(Napi::Object time_obj) {
46+
auto micros = time_obj.Get("micros").As<Napi::Number>().Int64Value();
47+
return { micros };
48+
}
49+
50+
Napi::Object MakeTimePartsObject(Napi::Env env, duckdb_time_struct time_parts) {
51+
auto time_parts_obj = Napi::Object::New(env);
52+
time_parts_obj.Set("hour", Napi::Number::New(env, time_parts.hour));
53+
time_parts_obj.Set("min", Napi::Number::New(env, time_parts.min));
54+
time_parts_obj.Set("sec", Napi::Number::New(env, time_parts.sec));
55+
time_parts_obj.Set("micros", Napi::Number::New(env, time_parts.micros));
56+
return time_parts_obj;
57+
}
58+
59+
duckdb_time_struct GetTimePartsFromObject(Napi::Object time_parts_obj) {
60+
int8_t hour = time_parts_obj.Get("hour").As<Napi::Number>().Int32Value();
61+
int8_t min = time_parts_obj.Get("min").As<Napi::Number>().Int32Value();
62+
int8_t sec = time_parts_obj.Get("sec").As<Napi::Number>().Int32Value();
63+
int32_t micros = time_parts_obj.Get("micros").As<Napi::Number>().Int32Value();
64+
return { hour, min, sec, micros };
65+
}
66+
67+
Napi::Object MakeTimeTZObject(Napi::Env env, duckdb_time_tz time_tz) {
68+
auto time_tz_obj = Napi::Object::New(env);
69+
time_tz_obj.Set("bits", Napi::BigInt::New(env, time_tz.bits));
70+
return time_tz_obj;
71+
}
72+
73+
duckdb_time_tz GetTimeTZFromObject(Napi::Env env, Napi::Object time_tz_obj) {
74+
bool lossless;
75+
auto bits = time_tz_obj.Get("bits").As<Napi::BigInt>().Uint64Value(&lossless);
76+
if (!lossless) {
77+
throw Napi::Error::New(env, "bits out of uint64 range");
78+
}
79+
return { bits };
80+
}
81+
82+
Napi::Object MakeTimeTZPartsObject(Napi::Env env, duckdb_time_tz_struct time_tz_parts) {
83+
auto time_tz_parts_obj = Napi::Object::New(env);
84+
time_tz_parts_obj.Set("time", MakeTimePartsObject(env, time_tz_parts.time));
85+
time_tz_parts_obj.Set("offset", Napi::Number::New(env, time_tz_parts.offset));
86+
return time_tz_parts_obj;
87+
}
88+
89+
// GetTimeTZFromObject not used
90+
91+
Napi::Object MakeTimestampObject(Napi::Env env, duckdb_timestamp timestamp) {
92+
auto timestamp_obj = Napi::Object::New(env);
93+
timestamp_obj.Set("micros", Napi::BigInt::New(env, timestamp.micros));
94+
return timestamp_obj;
95+
}
96+
97+
duckdb_timestamp GetTimestampFromObject(Napi::Env env, Napi::Object timestamp_obj) {
98+
bool lossless;
99+
auto micros = timestamp_obj.Get("micros").As<Napi::BigInt>().Int64Value(&lossless);
100+
if (!lossless) {
101+
throw Napi::Error::New(env, "micros out of int64 range");
102+
}
103+
return { micros };
104+
}
105+
106+
Napi::Object MakeTimestampPartsObject(Napi::Env env, duckdb_timestamp_struct timestamp_parts) {
107+
auto timestamp_parts_obj = Napi::Object::New(env);
108+
timestamp_parts_obj.Set("date", MakeDatePartsObject(env, timestamp_parts.date));
109+
timestamp_parts_obj.Set("time", MakeTimePartsObject(env, timestamp_parts.time));
110+
return timestamp_parts_obj;
111+
}
112+
113+
duckdb_timestamp_struct GetTimestampPartsFromObject(Napi::Object timestamp_parts_obj) {
114+
auto date = GetDatePartsFromObject(timestamp_parts_obj.Get("date").As<Napi::Object>());
115+
auto time = GetTimePartsFromObject(timestamp_parts_obj.Get("time").As<Napi::Object>());
116+
return { date, time };
117+
}
118+
119+
// Externals
120+
11121
template<typename T>
12122
Napi::External<T> CreateExternal(Napi::Env env, const napi_type_tag &type_tag, T *data) {
13123
auto external = Napi::External<T>::New(env, data);
@@ -134,6 +244,8 @@ duckdb_vector GetVectorFromExternal(Napi::Env env, Napi::Value value) {
134244
return GetDataFromExternal<_duckdb_vector>(env, VectorTypeTag, value, "Invalid vector argument");
135245
}
136246

247+
// Promise workers
248+
137249
class PromiseWorker : public Napi::AsyncWorker {
138250

139251
public:
@@ -430,6 +542,8 @@ class FetchWorker : public PromiseWorker {
430542

431543
};
432544

545+
// Enums
546+
433547
void DefineEnumMember(Napi::Object enumObj, const char *key, uint32_t value) {
434548
enumObj.Set(key, value);
435549
enumObj.Set(value, key);
@@ -525,6 +639,8 @@ Napi::Object CreateTypeEnum(Napi::Env env) {
525639
return typeEnum;
526640
}
527641

642+
// Addon
643+
528644
class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
529645

530646
public:
@@ -1015,38 +1131,27 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
10151131
Napi::Value from_date(const Napi::CallbackInfo& info) {
10161132
auto env = info.Env();
10171133
auto date_obj = info[0].As<Napi::Object>();
1018-
auto days = date_obj.Get("days").As<Napi::Number>().Int32Value();
1019-
duckdb_date date = { days };
1134+
auto date = GetDateFromObject(date_obj);
10201135
auto date_parts = duckdb_from_date(date);
1021-
auto result = Napi::Object::New(env);
1022-
result.Set("year", Napi::Number::New(env, date_parts.year));
1023-
result.Set("month", Napi::Number::New(env, date_parts.month));
1024-
result.Set("day", Napi::Number::New(env, date_parts.day));
1025-
return result;
1136+
return MakeDatePartsObject(env, date_parts);
10261137
}
10271138

10281139
// DUCKDB_API duckdb_date duckdb_to_date(duckdb_date_struct date);
10291140
// function to_date(parts: DateParts): Date_
10301141
Napi::Value to_date(const Napi::CallbackInfo& info) {
10311142
auto env = info.Env();
10321143
auto date_parts_obj = info[0].As<Napi::Object>();
1033-
int32_t year = date_parts_obj.Get("year").As<Napi::Number>().Int32Value();
1034-
int8_t month = date_parts_obj.Get("month").As<Napi::Number>().Int32Value();
1035-
int8_t day = date_parts_obj.Get("day").As<Napi::Number>().Int32Value();
1036-
duckdb_date_struct date_parts = { year, month, day };
1144+
auto date_parts = GetDatePartsFromObject(date_parts_obj);
10371145
auto date = duckdb_to_date(date_parts);
1038-
auto result = Napi::Object::New(env);
1039-
result.Set("days", Napi::Number::New(env, date.days));
1040-
return result;
1146+
return MakeDateObject(env, date);
10411147
}
10421148

10431149
// DUCKDB_API bool duckdb_is_finite_date(duckdb_date date);
10441150
// function is_finite_date(date: Date_): boolean
10451151
Napi::Value is_finite_date(const Napi::CallbackInfo& info) {
10461152
auto env = info.Env();
10471153
auto date_obj = info[0].As<Napi::Object>();
1048-
auto days = date_obj.Get("days").As<Napi::Number>().Int32Value();
1049-
duckdb_date date = { days };
1154+
auto date = GetDateFromObject(date_obj);
10501155
auto is_finite = duckdb_is_finite_date(date);
10511156
return Napi::Boolean::New(env, is_finite);
10521157
}
@@ -1056,15 +1161,9 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
10561161
Napi::Value from_time(const Napi::CallbackInfo& info) {
10571162
auto env = info.Env();
10581163
auto time_obj = info[0].As<Napi::Object>();
1059-
auto micros = time_obj.Get("micros").As<Napi::Number>().Int64Value();
1060-
duckdb_time time = { micros };
1164+
auto time = GetTimeFromObject(time_obj);
10611165
auto time_parts = duckdb_from_time(time);
1062-
auto result = Napi::Object::New(env);
1063-
result.Set("hour", Napi::Number::New(env, time_parts.hour));
1064-
result.Set("min", Napi::Number::New(env, time_parts.min));
1065-
result.Set("sec", Napi::Number::New(env, time_parts.sec));
1066-
result.Set("micros", Napi::Number::New(env, time_parts.micros));
1067-
return result;
1166+
return MakeTimePartsObject(env, time_parts);
10681167
}
10691168

10701169
// DUCKDB_API duckdb_time_tz duckdb_create_time_tz(int64_t micros, int32_t offset);
@@ -1074,69 +1173,57 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
10741173
auto micros = info[0].As<Napi::Number>().Int64Value();
10751174
auto offset = info[1].As<Napi::Number>().Int32Value();
10761175
auto time_tz = duckdb_create_time_tz(micros, offset);
1077-
auto result = Napi::Object::New(env);
1078-
result.Set("bits", Napi::BigInt::New(env, time_tz.bits));
1079-
return result;
1176+
return MakeTimeTZObject(env, time_tz);
10801177
}
10811178

10821179
// DUCKDB_API duckdb_time_tz_struct duckdb_from_time_tz(duckdb_time_tz micros);
10831180
// function from_time_tz(time_tz: TimeTZ): TimeTZParts
10841181
Napi::Value from_time_tz(const Napi::CallbackInfo& info) {
10851182
auto env = info.Env();
10861183
auto time_tz_obj = info[0].As<Napi::Object>();
1087-
bool lossless;
1088-
auto bits = time_tz_obj.Get("bits").As<Napi::BigInt>().Uint64Value(&lossless);
1089-
if (!lossless) {
1090-
throw Napi::Error::New(env, "bits out of uint64 range");
1091-
}
1092-
duckdb_time_tz time_tz = { bits };
1184+
auto time_tz = GetTimeTZFromObject(env, time_tz_obj);
10931185
auto time_tz_parts = duckdb_from_time_tz(time_tz);
1094-
auto result = Napi::Object::New(env);
1095-
auto time = Napi::Object::New(env);
1096-
time.Set("hour", Napi::Number::New(env, time_tz_parts.time.hour));
1097-
time.Set("min", Napi::Number::New(env, time_tz_parts.time.min));
1098-
time.Set("sec", Napi::Number::New(env, time_tz_parts.time.sec));
1099-
time.Set("micros", Napi::Number::New(env, time_tz_parts.time.micros));
1100-
result.Set("time", time);
1101-
result.Set("offset", Napi::Number::New(env, time_tz_parts.offset));
1102-
return result;
1186+
return MakeTimeTZPartsObject(env, time_tz_parts);
11031187
}
11041188

11051189
// DUCKDB_API duckdb_time duckdb_to_time(duckdb_time_struct time);
11061190
// function to_time(parts: TimeParts): Time
11071191
Napi::Value to_time(const Napi::CallbackInfo& info) {
11081192
auto env = info.Env();
11091193
auto time_parts_obj = info[0].As<Napi::Object>();
1110-
int8_t hour = time_parts_obj.Get("hour").As<Napi::Number>().Int32Value();
1111-
int8_t min = time_parts_obj.Get("min").As<Napi::Number>().Int32Value();
1112-
int8_t sec = time_parts_obj.Get("sec").As<Napi::Number>().Int32Value();
1113-
int32_t micros = time_parts_obj.Get("micros").As<Napi::Number>().Int32Value();
1114-
duckdb_time_struct time_parts = { hour, min, sec, micros };
1194+
auto time_parts = GetTimePartsFromObject(time_parts_obj);
11151195
auto time = duckdb_to_time(time_parts);
1116-
auto result = Napi::Object::New(env);
1117-
result.Set("micros", Napi::Number::New(env, time.micros));
1118-
return result;
1196+
return MakeTimeObject(env, time);
11191197
}
11201198

11211199
// DUCKDB_API duckdb_timestamp_struct duckdb_from_timestamp(duckdb_timestamp ts);
11221200
// function from_timestamp(timestamp: Timestamp): TimestampParts
11231201
Napi::Value from_timestamp(const Napi::CallbackInfo& info) {
11241202
auto env = info.Env();
1125-
throw Napi::Error::New(env, "Not implemented yet");
1203+
auto timestamp_obj = info[0].As<Napi::Object>();
1204+
auto timestamp = GetTimestampFromObject(env, timestamp_obj);
1205+
auto timestamp_parts = duckdb_from_timestamp(timestamp);
1206+
return MakeTimestampPartsObject(env, timestamp_parts);
11261207
}
11271208

11281209
// DUCKDB_API duckdb_timestamp duckdb_to_timestamp(duckdb_timestamp_struct ts);
11291210
// function to_timestamp(parts: TimestampParts): Timestamp
11301211
Napi::Value to_timestamp(const Napi::CallbackInfo& info) {
11311212
auto env = info.Env();
1132-
throw Napi::Error::New(env, "Not implemented yet");
1213+
auto timestamp_parts_obj = info[0].As<Napi::Object>();
1214+
auto timestamp_parts = GetTimestampPartsFromObject(timestamp_parts_obj);
1215+
auto timestamp = duckdb_to_timestamp(timestamp_parts);
1216+
return MakeTimestampObject(env, timestamp);
11331217
}
11341218

11351219
// DUCKDB_API bool duckdb_is_finite_timestamp(duckdb_timestamp ts);
11361220
// function is_finite_timestamp(timestamp: Timestamp): boolean
11371221
Napi::Value is_finite_timestamp(const Napi::CallbackInfo& info) {
11381222
auto env = info.Env();
1139-
throw Napi::Error::New(env, "Not implemented yet");
1223+
auto timestamp_obj = info[0].As<Napi::Object>();
1224+
auto timestamp = GetTimestampFromObject(env, timestamp_obj);
1225+
auto is_finite = duckdb_is_finite_timestamp(timestamp);
1226+
return Napi::Boolean::New(env, is_finite);
11401227
}
11411228

11421229
// DUCKDB_API double duckdb_hugeint_to_double(duckdb_hugeint val);

bindings/test/conversion.test.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ suite('conversion', () => {
7979
test('max', () => {
8080
expect(duckdb.from_time_tz({ bits: 1449551462400115198n })).toStrictEqual({ time: { hour: 24, min: 0, sec: 0, micros: 0 }, offset: -57599 });
8181
});
82-
test('out of range', () => {
82+
test('out of uint64 range', () => {
8383
expect(() => duckdb.from_time_tz({ bits: 2n ** 64n })).toThrowError('bits out of uint64 range');
8484
});
8585
});
@@ -94,4 +94,88 @@ suite('conversion', () => {
9494
expect(duckdb.to_time({ hour: 24, min: 0, sec: 0, micros: 0 })).toStrictEqual({ micros: 86400000000 });
9595
});
9696
});
97+
suite('from_timestamp', () => {
98+
test('mid-range', () => {
99+
// 1717418096789123n = 19877n * 86400000000n + 45296789123n
100+
expect(duckdb.from_timestamp({ micros: 1717418096789123n })).toStrictEqual({
101+
date: { year: 2024, month: 6, day: 3 },
102+
time: { hour: 12, min: 34, sec: 56, micros: 789123 },
103+
});
104+
});
105+
test('epoch', () => {
106+
expect(duckdb.from_timestamp({ micros: 0n })).toStrictEqual({
107+
date: { year: 1970, month: 1, day: 1 },
108+
time: { hour: 0, min: 0, sec: 0, micros: 0 },
109+
});
110+
});
111+
test('min', () => {
112+
// min timestamp = 290309-12-22 (BC) 00:00:00
113+
expect(duckdb.from_timestamp({ micros: -9223372022400000000n })).toStrictEqual({
114+
date: { year: -290308, month: 12, day: 22 },
115+
time: { hour: 0, min: 0, sec: 0, micros: 0 },
116+
});
117+
});
118+
test('max', () => {
119+
// max timestamp = 294247-01-10 04:00:54.775806
120+
expect(duckdb.from_timestamp({ micros: 9223372036854775806n })).toStrictEqual({
121+
date: { year: 294247, month: 1, day: 10 },
122+
time: { hour: 4, min: 0, sec: 54, micros: 775806 },
123+
});
124+
});
125+
test('out of int64 range (positive)', () => {
126+
expect(() => duckdb.from_timestamp({ micros: 2n ** 63n })).toThrowError('micros out of int64 range');
127+
});
128+
test('out of int64 range (negative)', () => {
129+
expect(() => duckdb.from_timestamp({ micros: -(2n ** 63n + 1n) })).toThrowError('micros out of int64 range');
130+
});
131+
});
132+
suite('to_timestamp', () => {
133+
test('mid-range', () => {
134+
// 1717418096789123n = 19877n * 86400000000n + 45296789123n
135+
expect(duckdb.to_timestamp({
136+
date: { year: 2024, month: 6, day: 3 },
137+
time: { hour: 12, min: 34, sec: 56, micros: 789123 },
138+
})).toStrictEqual({ micros: 1717418096789123n });
139+
});
140+
test('epoch', () => {
141+
expect(duckdb.to_timestamp({
142+
date: { year: 1970, month: 1, day: 1 },
143+
time: { hour: 0, min: 0, sec: 0, micros: 0 },
144+
})).toStrictEqual({ micros: 0n });
145+
});
146+
test('min', () => {
147+
// min timestamp = 290309-12-22 (BC) 00:00:00
148+
expect(duckdb.to_timestamp({
149+
date: { year: -290308, month: 12, day: 22 },
150+
time: { hour: 0, min: 0, sec: 0, micros: 0 },
151+
})).toStrictEqual({ micros: -9223372022400000000n });
152+
});
153+
test('max', () => {
154+
// max timestamp = 294247-01-10 04:00:54.775806
155+
expect(duckdb.to_timestamp({
156+
date: { year: 294247, month: 1, day: 10 },
157+
time: { hour: 4, min: 0, sec: 54, micros: 775806 },
158+
})).toStrictEqual({ micros: 9223372036854775806n });
159+
});
160+
});
161+
suite('is_finite_timestamp', () => {
162+
test('mid-range', () => {
163+
expect(duckdb.is_finite_timestamp({ micros: 1717418096789123n })).toBe(true);
164+
});
165+
test('epoch', () => {
166+
expect(duckdb.is_finite_timestamp({ micros: 0n })).toBe(true);
167+
});
168+
test('min', () => {
169+
expect(duckdb.is_finite_timestamp({ micros: -9223372022400000000n })).toBe(true);
170+
});
171+
test('max', () => {
172+
expect(duckdb.is_finite_timestamp({ micros: 9223372036854775806n })).toBe(true);
173+
});
174+
test('infinity', () => {
175+
expect(duckdb.is_finite_timestamp({ micros: 2n ** 63n - 1n })).toBe(false);
176+
});
177+
test('-infinity', () => {
178+
expect(duckdb.is_finite_timestamp({ micros: -(2n ** 63n - 1n) })).toBe(false);
179+
});
180+
});
97181
});

0 commit comments

Comments
 (0)