Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bind non-standard timestamps #158

Merged
merged 1 commit into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions api/src/DuckDBPreparedStatement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
DuckDBListType,
DuckDBStructType,
DuckDBType,
TIMESTAMP_MS,
TIMESTAMP_NS,
TIMESTAMP_S,
TIMESTAMPTZ,
TIMETZ,
VARINT,
Expand All @@ -24,6 +27,9 @@ import {
DuckDBIntervalValue,
DuckDBListValue,
DuckDBStructValue,
DuckDBTimestampMillisecondsValue,
DuckDBTimestampNanosecondsValue,
DuckDBTimestampSecondsValue,
DuckDBTimestampTZValue,
DuckDBTimestampValue,
DuckDBTimeTZValue,
Expand Down Expand Up @@ -52,10 +58,9 @@ export class DuckDBPreparedStatement {
) as number as DuckDBTypeId;
}
public parameterType(parameterIndex: number): DuckDBType {
return DuckDBLogicalType.create(duckdb.param_logical_type(
this.prepared_statement,
parameterIndex
)).asType();
return DuckDBLogicalType.create(
duckdb.param_logical_type(this.prepared_statement, parameterIndex)
).asType();
}
public clearBindings() {
duckdb.clear_bindings(this.prepared_statement);
Expand Down Expand Up @@ -126,7 +131,24 @@ export class DuckDBPreparedStatement {
) {
this.bindValue(parameterIndex, value, TIMESTAMPTZ);
}
// TODO: bind TIMESTAMPS_S/_MS/_NS
public bindTimestampSeconds(
parameterIndex: number,
value: DuckDBTimestampSecondsValue
) {
this.bindValue(parameterIndex, value, TIMESTAMP_S);
}
public bindTimestampMilliseconds(
parameterIndex: number,
value: DuckDBTimestampMillisecondsValue
) {
this.bindValue(parameterIndex, value, TIMESTAMP_MS);
}
public bindTimestampNanoseconds(
parameterIndex: number,
value: DuckDBTimestampNanosecondsValue
) {
this.bindValue(parameterIndex, value, TIMESTAMP_NS);
}
public bindInterval(parameterIndex: number, value: DuckDBIntervalValue) {
duckdb.bind_interval(this.prepared_statement, parameterIndex, value);
}
Expand Down
33 changes: 25 additions & 8 deletions api/src/createValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
DuckDBIntervalValue,
DuckDBListValue,
DuckDBStructValue,
DuckDBTimestampMillisecondsValue,
DuckDBTimestampNanosecondsValue,
DuckDBTimestampSecondsValue,
DuckDBTimestampTZValue,
DuckDBTimestampValue,
DuckDBTimeTZValue,
Expand All @@ -17,7 +20,8 @@ import {
} from './values';

export function createValue(type: DuckDBType, input: DuckDBValue): Value {
switch (type.typeId) {
const { typeId } = type;
switch (typeId) {
case DuckDBTypeId.BOOLEAN:
if (typeof input === 'boolean') {
return duckdb.create_bool(input);
Expand Down Expand Up @@ -119,11 +123,20 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value {
}
throw new Error(`input is not a DuckDBDecimalValue`);
case DuckDBTypeId.TIMESTAMP_S:
throw new Error(`not yet implemented for TIMESTAMP_S`); // TODO: implement when available in 1.2.0
if (input instanceof DuckDBTimestampSecondsValue) {
return duckdb.create_timestamp_s(input);
}
throw new Error(`input is not a DuckDBTimestampSecondsValue`);
case DuckDBTypeId.TIMESTAMP_MS:
throw new Error(`not yet implemented for TIMESTAMP_MS`); // TODO: implement when available in 1.2.0
if (input instanceof DuckDBTimestampMillisecondsValue) {
return duckdb.create_timestamp_ms(input);
}
throw new Error(`input is not a DuckDBTimestampMillisecondsValue`);
case DuckDBTypeId.TIMESTAMP_NS:
throw new Error(`not yet implemented for TIMESTAMP_NS`); // TODO: implement when available in 1.2.0
if (input instanceof DuckDBTimestampNanosecondsValue) {
return duckdb.create_timestamp_ns(input);
}
throw new Error(`input is not a DuckDBTimestampNanosecondsValue`);
case DuckDBTypeId.ENUM:
throw new Error(`not yet implemented for ENUM`); // TODO: implement when available in 1.2.0
case DuckDBTypeId.LIST:
Expand All @@ -141,7 +154,11 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value {
throw new Error(`input is not a DuckDBListValue`);
case DuckDBTypeId.STRUCT:
if (input instanceof DuckDBStructValue) {
if (type.entryTypes.find((type) => type.typeId === DuckDBTypeId.ANY)) {
if (
type.entryTypes.find(
(entryType) => entryType.typeId === DuckDBTypeId.ANY
)
) {
throw new Error(
'Cannot create structs with an entry type of ANY. Specify a specific type.'
);
Expand Down Expand Up @@ -173,7 +190,7 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value {
throw new Error(`not yet implemented for UUID`); // TODO: implement when available in 1.2.0
case DuckDBTypeId.UNION:
throw new Error(`not yet implemented for UNION`); // TODO: implement when available, hopefully in 1.2.0
case DuckDBTypeId.UNION:
case DuckDBTypeId.BIT:
throw new Error(`not yet implemented for BIT`); // TODO: implement when available in 1.2.0
case DuckDBTypeId.TIME_TZ:
if (input instanceof DuckDBTimeTZValue) {
Expand All @@ -182,7 +199,7 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value {
throw new Error(`input is not a DuckDBTimeTZValue`);
case DuckDBTypeId.TIMESTAMP_TZ:
if (input instanceof DuckDBTimestampTZValue) {
return duckdb.create_timestamp(input); // TODO: change to create_timestamp_tz when available in 1.2.0
return duckdb.create_timestamp_tz(input);
}
throw new Error(`input is not a DuckDBTimestampTZValue`);
case DuckDBTypeId.ANY:
Expand All @@ -197,6 +214,6 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value {
case DuckDBTypeId.SQLNULL:
throw new Error(`not yet implemented for SQLNUll`); // TODO: implement when available in 1.2.0
default:
throw new Error(`unrecognized type id ${type.typeId}`);
throw new Error(`unrecognized type id ${typeId}`);
}
}
164 changes: 86 additions & 78 deletions api/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,120 +399,128 @@ describe('api', () => {
});
test('should support running prepared statements', async () => {
await withConnection(async (connection) => {
const prepared = await connection.prepare(
'select \
$num as num, \
$str as str, \
$bool as bool, \
$timetz as timetz, \
$varint as varint, \
$list as list, \
$list_dec as list_dec, \
$struct as struct, \
$array as array, \
$null as null_value'
);
assert.strictEqual(prepared.parameterCount, 10);
assert.strictEqual(prepared.parameterName(1), 'num');
assert.strictEqual(prepared.parameterName(2), 'str');
assert.strictEqual(prepared.parameterName(3), 'bool');
assert.strictEqual(prepared.parameterName(4), 'timetz');
assert.strictEqual(prepared.parameterName(5), 'varint');
assert.strictEqual(prepared.parameterName(6), 'list');
assert.strictEqual(prepared.parameterName(7), 'list_dec');
assert.strictEqual(prepared.parameterName(8), 'struct');
assert.strictEqual(prepared.parameterName(9), 'array');
assert.strictEqual(prepared.parameterName(10), 'null');
prepared.bindInteger(1, 10);
prepared.bindVarchar(2, 'abc');
prepared.bindBoolean(3, true);
prepared.bindTimeTZ(4, TIMETZ.max);
prepared.bindVarInt(5, VARINT.max);
prepared.bindList(6, listValue([100, 200, 300]), LIST(INTEGER));
const params: ColumnNameAndType[] = [
{ name: 'num', type: INTEGER },
{ name: 'str', type: VARCHAR },
{ name: 'bool', type: BOOLEAN },
{ name: 'timetz', type: TIMETZ },
{ name: 'timestamptz', type: TIMESTAMPTZ },
{ name: 'timestamp_s', type: TIMESTAMP_S },
{ name: 'timestamp_ms', type: TIMESTAMP_MS },
{ name: 'timestamp_ns', type: TIMESTAMP_NS },
{ name: 'varint', type: VARINT },
{ name: 'list_int', type: LIST(INTEGER) },
{ name: 'list_dec', type: LIST(DECIMAL(4, 1)) },
{ name: 'struct', type: STRUCT({ 'a': INTEGER, 'b': VARCHAR }) },
{ name: 'array', type: ARRAY(INTEGER, 3) },
{ name: 'null_value', type: SQLNULL },
];

const sql = `select ${params
.map((p) => `$${p.name} as ${p.name}`)
.join(', ')}`;
const prepared = await connection.prepare(sql);

assert.strictEqual(prepared.parameterCount, params.length);
for (let i = 0; i < params.length; i++) {
assert.strictEqual(prepared.parameterName(i + 1), params[i].name, `param ${i} name mismatch`);
}

let i = 1;
prepared.bindInteger(i++, 10);
prepared.bindVarchar(i++, 'abc');
prepared.bindBoolean(i++, true);
prepared.bindTimeTZ(i++, TIMETZ.max);
prepared.bindTimestampTZ(i++, TIMESTAMPTZ.max);
prepared.bindTimestampSeconds(i++, TIMESTAMP_S.max);
prepared.bindTimestampMilliseconds(i++, TIMESTAMP_MS.max);
prepared.bindTimestampNanoseconds(i++, TIMESTAMP_NS.max);
prepared.bindVarInt(i++, VARINT.max);
prepared.bindList(i++, listValue([100, 200, 300]), LIST(INTEGER));
prepared.bindList(
7,
i++,
listValue([decimalValue(9876n, 4, 1), decimalValue(5432n, 4, 1)]),
LIST(DECIMAL(4, 1))
);
prepared.bindStruct(
8,
i++,
structValue({ 'a': 42, 'b': 'duck' }),
STRUCT({ 'a': INTEGER, 'b': VARCHAR })
);
prepared.bindArray(9, arrayValue([100, 200, 300]), ARRAY(INTEGER, 3));
prepared.bindNull(10);
assert.equal(prepared.parameterTypeId(1), DuckDBTypeId.INTEGER);
assert.deepEqual(prepared.parameterType(1), INTEGER);
// See https://github.com/duckdb/duckdb/issues/16137
// assert.equal(prepared.parameterTypeId(2), DuckDBTypeId.VARCHAR);
// assert.deepEqual(prepared.parameterType(2), VARCHAR);
assert.equal(prepared.parameterTypeId(3), DuckDBTypeId.BOOLEAN);
assert.deepEqual(prepared.parameterType(3), BOOLEAN);
assert.equal(prepared.parameterTypeId(4), DuckDBTypeId.TIME_TZ);
assert.deepEqual(prepared.parameterType(4), TIMETZ);
assert.equal(prepared.parameterTypeId(5), DuckDBTypeId.VARINT);
assert.deepEqual(prepared.parameterType(5), VARINT);
assert.equal(prepared.parameterTypeId(6), DuckDBTypeId.LIST);
assert.deepEqual(prepared.parameterType(6), LIST(INTEGER));
assert.equal(prepared.parameterTypeId(7), DuckDBTypeId.LIST);
assert.deepEqual(prepared.parameterType(7), LIST(DECIMAL(4, 1)));
assert.equal(prepared.parameterTypeId(8), DuckDBTypeId.STRUCT);
assert.deepEqual(prepared.parameterType(8), STRUCT({ 'a': INTEGER, 'b': VARCHAR }));
assert.equal(prepared.parameterTypeId(9), DuckDBTypeId.ARRAY);
assert.deepEqual(prepared.parameterType(9), ARRAY(INTEGER, 3));
assert.equal(prepared.parameterTypeId(10), DuckDBTypeId.SQLNULL);
assert.deepEqual(prepared.parameterType(10), SQLNULL);
prepared.bindArray(i++, arrayValue([100, 200, 300]), ARRAY(INTEGER, 3));
prepared.bindNull(i++);

for (let i = 0; i < params.length; i++) {
let type = params[i].type;
if (i === 1) {
// VARCHAR type is reported incorrectly; see https://github.com/duckdb/duckdb/issues/16137
continue;
}
assert.equal(prepared.parameterTypeId(i + 1), type.typeId, `param ${i} type id mismatch`);
assert.deepEqual(prepared.parameterType(i + 1), type, `param ${i} type mismatch`);
}

const result = await prepared.run();
assertColumns(result, [
{ name: 'num', type: INTEGER },
{ name: 'str', type: VARCHAR },
{ name: 'bool', type: BOOLEAN },
{ name: 'timetz', type: TIMETZ },
{ name: 'varint', type: VARINT },
{ name: 'list', type: LIST(INTEGER) },
{ name: 'list_dec', type: LIST(DECIMAL(4, 1)) },
{ name: 'struct', type: STRUCT({ 'a': INTEGER, 'b': VARCHAR }) },
{ name: 'array', type: ARRAY(INTEGER, 3) },
{ name: 'null_value', type: INTEGER },
]);

// In the result, SQLNULL params get type INTEGER.
const expectedColumns = params.map((p) =>
p.type.typeId === DuckDBTypeId.SQLNULL ? { ...p, type: INTEGER } : p
);

assertColumns(result, expectedColumns);

const chunk = await result.fetchChunk();

assert.isDefined(chunk);
if (chunk) {
assert.strictEqual(chunk.columnCount, 10);
assert.strictEqual(chunk.columnCount, expectedColumns.length);
assert.strictEqual(chunk.rowCount, 1);
let i = 0;
assertValues<number, DuckDBIntegerVector>(
chunk,
0,
i++,
DuckDBIntegerVector,
[10]
);
assertValues<string, DuckDBVarCharVector>(
chunk,
1,
i++,
DuckDBVarCharVector,
['abc']
);
assertValues<boolean, DuckDBBooleanVector>(
chunk,
2,
i++,
DuckDBBooleanVector,
[true]
);
assertValues(chunk, 3, DuckDBTimeTZVector, [TIMETZ.max]);
assertValues(chunk, 4, DuckDBVarIntVector, [VARINT.max]);
assertValues(chunk, 5, DuckDBListVector, [listValue([100, 200, 300])]);
assertValues(chunk, 6, DuckDBListVector, [
assertValues(chunk, i++, DuckDBTimeTZVector, [TIMETZ.max]);
assertValues(chunk, i++, DuckDBTimestampTZVector, [TIMESTAMPTZ.max]);
assertValues(chunk, i++, DuckDBTimestampSecondsVector, [
TIMESTAMP_S.max,
]);
assertValues(chunk, i++, DuckDBTimestampMillisecondsVector, [
TIMESTAMP_MS.max,
]);
assertValues(chunk, i++, DuckDBTimestampNanosecondsVector, [
TIMESTAMP_NS.max,
]);
assertValues(chunk, i++, DuckDBVarIntVector, [VARINT.max]);
assertValues(chunk, i++, DuckDBListVector, [
listValue([100, 200, 300]),
]);
assertValues(chunk, i++, DuckDBListVector, [
listValue([decimalValue(9876n, 4, 1), decimalValue(5432n, 4, 1)]),
]);
assertValues(chunk, 7, DuckDBStructVector, [
assertValues(chunk, i++, DuckDBStructVector, [
structValue({ 'a': 42, 'b': 'duck' }),
]);
assertValues(chunk, 8, DuckDBArrayVector, [
assertValues(chunk, i++, DuckDBArrayVector, [
arrayValue([100, 200, 300]),
]);
assertValues<number, DuckDBIntegerVector>(
chunk,
9,
i++,
DuckDBIntegerVector,
[null]
);
Expand Down
14 changes: 14 additions & 0 deletions bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,9 +661,16 @@ export function create_time_tz_value(input: TimeTZ): Value;
export function create_timestamp(input: Timestamp): Value;

// DUCKDB_API duckdb_value duckdb_create_timestamp_tz(duckdb_timestamp input);
export function create_timestamp_tz(input: Timestamp): Value;

// DUCKDB_API duckdb_value duckdb_create_timestamp_s(duckdb_timestamp_s input);
export function create_timestamp_s(input: TimestampSeconds): Value;

// DUCKDB_API duckdb_value duckdb_create_timestamp_ms(duckdb_timestamp_ms input);
export function create_timestamp_ms(input: TimestampMilliseconds): Value;

// DUCKDB_API duckdb_value duckdb_create_timestamp_ns(duckdb_timestamp_ns input);
export function create_timestamp_ns(input: TimestampNanoseconds): Value;

// DUCKDB_API duckdb_value duckdb_create_interval(duckdb_interval input);
export function create_interval(input: Interval): Value;
Expand Down Expand Up @@ -732,9 +739,16 @@ export function get_time_tz(value: Value): TimeTZ;
export function get_timestamp(value: Value): Timestamp;

// DUCKDB_API duckdb_timestamp duckdb_get_timestamp_tz(duckdb_value val);
export function get_timestamp_tz(value: Value): Timestamp;

// DUCKDB_API duckdb_timestamp_s duckdb_get_timestamp_s(duckdb_value val);
export function get_timestamp_s(value: Value): TimestampSeconds;

// DUCKDB_API duckdb_timestamp_ms duckdb_get_timestamp_ms(duckdb_value val);
export function get_timestamp_ms(value: Value): TimestampMilliseconds;

// DUCKDB_API duckdb_timestamp_ns duckdb_get_timestamp_ns(duckdb_value val);
export function get_timestamp_ns(value: Value): TimestampNanoseconds;

// DUCKDB_API duckdb_interval duckdb_get_interval(duckdb_value val);
export function get_interval(value: Value): Interval;
Expand Down
Loading