diff --git a/api/src/DuckDBAppender.ts b/api/src/DuckDBAppender.ts index 2388ca38..b17fb777 100644 --- a/api/src/DuckDBAppender.ts +++ b/api/src/DuckDBAppender.ts @@ -1,6 +1,7 @@ import duckdb from '@duckdb/node-bindings'; import { createValue } from './createValue'; import { DuckDBDataChunk } from './DuckDBDataChunk'; +import { DuckDBErrorData } from './DuckDBErrorData'; import { DuckDBLogicalType } from './DuckDBLogicalType'; import { BIGNUM, @@ -64,6 +65,9 @@ export class DuckDBAppender { duckdb.appender_column_type(this.appender, columnIndex) ).asType(); } + public get errorData(): DuckDBErrorData { + return new DuckDBErrorData(duckdb.appender_error_data(this.appender)); + } public endRow() { duckdb.appender_end_row(this.appender); } diff --git a/api/src/DuckDBErrorData.ts b/api/src/DuckDBErrorData.ts new file mode 100644 index 00000000..c3ecf56a --- /dev/null +++ b/api/src/DuckDBErrorData.ts @@ -0,0 +1,28 @@ +import duckdb from '@duckdb/node-bindings'; + +export class DuckDBErrorData { + private readonly error_data: duckdb.ErrorData; + + constructor(error_data: duckdb.ErrorData) { + this.error_data = error_data; + } + + public get errorType(): duckdb.ErrorType { + return duckdb.error_data_error_type(this.error_data); + } + + public get message(): string | null { + if (!this.hasError) { + return null; + } + return duckdb.error_data_message(this.error_data); + } + + public get hasError(): boolean { + return duckdb.error_data_has_error(this.error_data); + } + + public toString(): string { + return this.message || ''; + } +} diff --git a/api/src/duckdb.ts b/api/src/duckdb.ts index 4d3ddbb6..e2b95c2a 100644 --- a/api/src/duckdb.ts +++ b/api/src/duckdb.ts @@ -8,6 +8,7 @@ export * from './configurationOptionDescriptions'; export * from './createDuckDBValueConverter'; export * from './DuckDBAppender'; export * from './DuckDBConnection'; +export * from './DuckDBErrorData'; export * from './DuckDBDataChunk'; export * from './DuckDBExtractedStatements'; export * from './DuckDBFunctionInfo'; diff --git a/api/src/enums.ts b/api/src/enums.ts index 48e93182..9164e7c3 100644 --- a/api/src/enums.ts +++ b/api/src/enums.ts @@ -5,3 +5,6 @@ export const ResultReturnType = duckdb.ResultType; export type StatementType = duckdb.StatementType; export const StatementType = duckdb.StatementType; + +export type ErrorType = duckdb.ErrorType; +export const ErrorType = duckdb.ErrorType; diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 44847ee2..e980c6c0 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -67,6 +67,7 @@ import { DuckDBVarCharVector, DuckDBVector, ENUM, + ErrorType, FLOAT, HUGEINT, INTEGER, @@ -2577,3 +2578,51 @@ ORDER BY name }); }); }); + +describe('DuckDBErrorData', () => { + test('appender errorData property', async () => { + await withConnection(async (connection) => { + await connection.run('create table test_error_data(i integer)'); + const appender = await connection.createAppender('test_error_data'); + + // Get error data - should indicate no error on successful append + const errorData = appender.errorData; + assert(errorData !== null); + assert(errorData.hasError === false); + assert(errorData.message === null); + assert(errorData.toString() === ''); + + // Append a value successfully + appender.appendInteger(42); + appender.endRow(); + appender.flushSync(); + + // Error data should still indicate no error after flush + const errorDataAfterFlush = appender.errorData; + assert(errorDataAfterFlush.hasError === false); + assert(errorDataAfterFlush.message === null); + }); + }); + + test('errorData captures invalid input details', async () => { + await withConnection(async (connection) => { + await connection.run('create table test_error_type(i integer)'); + const appender = await connection.createAppender('test_error_type'); + + const expectedMessage = "Could not convert string 'not an int' to INT32"; + assert.throws(() => { + appender.appendVarchar('not an int'); + appender.endRow(); + }, expectedMessage); + + const errorData = appender.errorData; + assert.strictEqual(errorData.hasError, true); + assert.strictEqual(errorData.errorType, ErrorType.INVALID_INPUT); + + const message = errorData.message; + assert(message !== null); + assert.ok(message.includes(expectedMessage)); + assert.strictEqual(errorData.toString(), message); + }); + }); +}); diff --git a/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts b/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts index 155beaf2..5fc24e78 100644 --- a/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts +++ b/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts @@ -47,6 +47,52 @@ export enum StatementType { MULTI = 27, } +export enum ErrorType { + INVALID = 0, + OUT_OF_RANGE = 1, + CONVERSION = 2, + UNKNOWN_TYPE = 3, + DECIMAL = 4, + MISMATCH_TYPE = 5, + DIVIDE_BY_ZERO = 6, + OBJECT_SIZE = 7, + INVALID_TYPE = 8, + SERIALIZATION = 9, + TRANSACTION = 10, + NOT_IMPLEMENTED = 11, + EXPRESSION = 12, + CATALOG = 13, + PARSER = 14, + PLANNER = 15, + SCHEDULER = 16, + EXECUTOR = 17, + CONSTRAINT = 18, + INDEX = 19, + STAT = 20, + CONNECTION = 21, + SYNTAX = 22, + SETTINGS = 23, + BINDER = 24, + NETWORK = 25, + OPTIMIZER = 26, + NULL_POINTER = 27, + IO = 28, + INTERRUPT = 29, + FATAL = 30, + INTERNAL = 31, + INVALID_INPUT = 32, + OUT_OF_MEMORY = 33, + PERMISSION = 34, + PARAMETER_NOT_RESOLVED = 35, + PARAMETER_NOT_ALLOWED = 36, + DEPENDENCY = 37, + HTTP = 38, + MISSING_EXTENSION = 39, + AUTOLOAD = 40, + SEQUENCE = 41, + INVALID_CONFIGURATION = 42, +} + export enum Type { INVALID = 0, BOOLEAN = 1, @@ -208,6 +254,10 @@ export interface DataChunk { __duckdb_type: 'duckdb_data_chunk'; } +export interface ErrorData { + __duckdb_type: 'duckdb_error_data'; +} + export interface ExtractedStatements { __duckdb_type: 'duckdb_extracted_statements'; } @@ -323,8 +373,13 @@ export function set_config(config: Config, name: string, option: string): void; // DUCKDB_C_API duckdb_error_data duckdb_create_error_data(duckdb_error_type type, const char *message); // DUCKDB_C_API void duckdb_destroy_error_data(duckdb_error_data *error_data); // DUCKDB_C_API duckdb_error_type duckdb_error_data_error_type(duckdb_error_data error_data); +export function error_data_error_type(error_data: ErrorData): ErrorType; + // DUCKDB_C_API const char *duckdb_error_data_message(duckdb_error_data error_data); +export function error_data_message(error_data: ErrorData): string; + // DUCKDB_C_API bool duckdb_error_data_has_error(duckdb_error_data error_data); +export function error_data_has_error(error_data: ErrorData): boolean; // DUCKDB_C_API duckdb_state duckdb_query(duckdb_connection connection, const char *query, duckdb_result *out_result); export function query(connection: Connection, query: string): Promise; @@ -1190,6 +1245,7 @@ export function appender_column_type(appender: Appender, column_index: number): // #endif // DUCKDB_C_API duckdb_error_data duckdb_appender_error_data(duckdb_appender appender); +export function appender_error_data(appender: Appender): ErrorData; // DUCKDB_C_API duckdb_state duckdb_appender_flush(duckdb_appender appender); export function appender_flush_sync(appender: Appender): void; diff --git a/bindings/src/duckdb_node_bindings.cpp b/bindings/src/duckdb_node_bindings.cpp index 5d54eb20..1fe58e4c 100644 --- a/bindings/src/duckdb_node_bindings.cpp +++ b/bindings/src/duckdb_node_bindings.cpp @@ -724,6 +724,25 @@ duckdb_vector GetVectorFromExternal(Napi::Env env, Napi::Value value) { return GetDataFromExternal<_duckdb_vector>(env, VectorTypeTag, value, "Invalid vector argument"); } +static const napi_type_tag ErrorDataTypeTag = { + 0xA3B5C7D9E1F3A5B7, 0xC9D1E3F5A7B9C1D3 +}; + +void FinalizeErrorData(Napi::BasicEnv, duckdb_error_data error_data) { + if (error_data) { + duckdb_destroy_error_data(&error_data); + error_data = nullptr; + } +} + +Napi::External<_duckdb_error_data> CreateExternalForErrorData(Napi::Env env, duckdb_error_data error_data) { + return CreateExternal<_duckdb_error_data>(env, ErrorDataTypeTag, error_data, FinalizeErrorData); +} + +duckdb_error_data GetErrorDataFromExternal(Napi::Env env, Napi::Value value) { + return GetDataFromExternal<_duckdb_error_data>(env, ErrorDataTypeTag, value, "Invalid error_data argument"); +} + // Scalar functions struct ScalarFunctionMainTSFNData { @@ -1343,6 +1362,54 @@ Napi::Object CreateTypeEnum(Napi::Env env) { return typeEnum; } +Napi::Object CreateErrorTypeEnum(Napi::Env env) { + auto errorTypeEnum = Napi::Object::New(env); + DefineEnumMember(errorTypeEnum, "INVALID", 0); + DefineEnumMember(errorTypeEnum, "OUT_OF_RANGE", 1); + DefineEnumMember(errorTypeEnum, "CONVERSION", 2); + DefineEnumMember(errorTypeEnum, "UNKNOWN_TYPE", 3); + DefineEnumMember(errorTypeEnum, "DECIMAL", 4); + DefineEnumMember(errorTypeEnum, "MISMATCH_TYPE", 5); + DefineEnumMember(errorTypeEnum, "DIVIDE_BY_ZERO", 6); + DefineEnumMember(errorTypeEnum, "OBJECT_SIZE", 7); + DefineEnumMember(errorTypeEnum, "INVALID_TYPE", 8); + DefineEnumMember(errorTypeEnum, "SERIALIZATION", 9); + DefineEnumMember(errorTypeEnum, "TRANSACTION", 10); + DefineEnumMember(errorTypeEnum, "NOT_IMPLEMENTED", 11); + DefineEnumMember(errorTypeEnum, "EXPRESSION", 12); + DefineEnumMember(errorTypeEnum, "CATALOG", 13); + DefineEnumMember(errorTypeEnum, "PARSER", 14); + DefineEnumMember(errorTypeEnum, "PLANNER", 15); + DefineEnumMember(errorTypeEnum, "SCHEDULER", 16); + DefineEnumMember(errorTypeEnum, "EXECUTOR", 17); + DefineEnumMember(errorTypeEnum, "CONSTRAINT", 18); + DefineEnumMember(errorTypeEnum, "INDEX", 19); + DefineEnumMember(errorTypeEnum, "STAT", 20); + DefineEnumMember(errorTypeEnum, "CONNECTION", 21); + DefineEnumMember(errorTypeEnum, "SYNTAX", 22); + DefineEnumMember(errorTypeEnum, "SETTINGS", 23); + DefineEnumMember(errorTypeEnum, "BINDER", 24); + DefineEnumMember(errorTypeEnum, "NETWORK", 25); + DefineEnumMember(errorTypeEnum, "OPTIMIZER", 26); + DefineEnumMember(errorTypeEnum, "NULL_POINTER", 27); + DefineEnumMember(errorTypeEnum, "IO", 28); + DefineEnumMember(errorTypeEnum, "INTERRUPT", 29); + DefineEnumMember(errorTypeEnum, "FATAL", 30); + DefineEnumMember(errorTypeEnum, "INTERNAL", 31); + DefineEnumMember(errorTypeEnum, "INVALID_INPUT", 32); + DefineEnumMember(errorTypeEnum, "OUT_OF_MEMORY", 33); + DefineEnumMember(errorTypeEnum, "PERMISSION", 34); + DefineEnumMember(errorTypeEnum, "PARAMETER_NOT_RESOLVED", 35); + DefineEnumMember(errorTypeEnum, "PARAMETER_NOT_ALLOWED", 36); + DefineEnumMember(errorTypeEnum, "DEPENDENCY", 37); + DefineEnumMember(errorTypeEnum, "HTTP", 38); + DefineEnumMember(errorTypeEnum, "MISSING_EXTENSION", 39); + DefineEnumMember(errorTypeEnum, "AUTOLOAD", 40); + DefineEnumMember(errorTypeEnum, "SEQUENCE", 41); + DefineEnumMember(errorTypeEnum, "INVALID_CONFIGURATION", 42); + return errorTypeEnum; +} + // Addon class DuckDBNodeAddon : public Napi::Addon { @@ -1356,6 +1423,7 @@ class DuckDBNodeAddon : public Napi::Addon { InstanceValue("PendingState", CreatePendingStateEnum(env)), InstanceValue("ResultType", CreateResultTypeEnum(env)), InstanceValue("StatementType", CreateStatementTypeEnum(env)), + InstanceValue("ErrorType", CreateErrorTypeEnum(env)), InstanceValue("Type", CreateTypeEnum(env)), InstanceMethod("create_instance_cache", &DuckDBNodeAddon::create_instance_cache), @@ -1374,6 +1442,9 @@ class DuckDBNodeAddon : public Napi::Addon { InstanceMethod("config_count", &DuckDBNodeAddon::config_count), InstanceMethod("get_config_flag", &DuckDBNodeAddon::get_config_flag), InstanceMethod("set_config", &DuckDBNodeAddon::set_config), + InstanceMethod("error_data_error_type", &DuckDBNodeAddon::error_data_error_type), + InstanceMethod("error_data_message", &DuckDBNodeAddon::error_data_message), + InstanceMethod("error_data_has_error", &DuckDBNodeAddon::error_data_has_error), InstanceMethod("query", &DuckDBNodeAddon::query), InstanceMethod("column_name", &DuckDBNodeAddon::column_name), @@ -1605,6 +1676,7 @@ class DuckDBNodeAddon : public Napi::Addon { InstanceMethod("appender_flush_sync", &DuckDBNodeAddon::appender_flush_sync), InstanceMethod("appender_close_sync", &DuckDBNodeAddon::appender_close_sync), InstanceMethod("appender_end_row", &DuckDBNodeAddon::appender_end_row), + InstanceMethod("appender_error_data", &DuckDBNodeAddon::appender_error_data), InstanceMethod("append_default", &DuckDBNodeAddon::append_default), InstanceMethod("append_bool", &DuckDBNodeAddon::append_bool), InstanceMethod("append_int8", &DuckDBNodeAddon::append_int8), @@ -1829,13 +1901,31 @@ class DuckDBNodeAddon : public Napi::Addon { // TODO error data // DUCKDB_C_API duckdb_error_type duckdb_error_data_error_type(duckdb_error_data error_data); - // TODO error data + Napi::Value error_data_error_type(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto error_data = GetErrorDataFromExternal(env, info[0]); + auto error_type = duckdb_error_data_error_type(error_data); + return Napi::Number::New(env, static_cast(error_type)); + } // DUCKDB_C_API const char *duckdb_error_data_message(duckdb_error_data error_data); - // TODO error data + Napi::Value error_data_message(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto error_data = GetErrorDataFromExternal(env, info[0]); + if (!duckdb_error_data_has_error(error_data)) { + return Napi::String::New(env, ""); + } + auto message = duckdb_error_data_message(error_data); + return Napi::String::New(env, message ? message : ""); + } // DUCKDB_C_API bool duckdb_error_data_has_error(duckdb_error_data error_data); - // TODO error data + Napi::Value error_data_has_error(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto error_data = GetErrorDataFromExternal(env, info[0]); + auto has_error = duckdb_error_data_has_error(error_data); + return Napi::Boolean::New(env, has_error); + } // DUCKDB_C_API duckdb_state duckdb_query(duckdb_connection connection, const char *query, duckdb_result *out_result); // function query(connection: Connection, query: string): Promise @@ -4603,7 +4693,12 @@ class DuckDBNodeAddon : public Napi::Addon { // #endif // DUCKDB_C_API duckdb_error_data duckdb_appender_error_data(duckdb_appender appender); - // TODO appender error data + Napi::Value appender_error_data(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto appender = GetAppenderFromExternal(env, info[0]); + auto error_data = duckdb_appender_error_data(appender); + return CreateExternalForErrorData(env, error_data); + } // DUCKDB_C_API duckdb_state duckdb_appender_flush(duckdb_appender appender); // function appender_flush(appender: Appender): void diff --git a/bindings/test/appender.test.ts b/bindings/test/appender.test.ts index 2cf5a75e..83523e22 100644 --- a/bindings/test/appender.test.ts +++ b/bindings/test/appender.test.ts @@ -341,4 +341,61 @@ suite('appender', () => { }); }); }); + + test('error_data: basic error access', async () => { + await withConnection(async (connection) => { + await duckdb.query( + connection, + 'create table test_error_data(i integer)' + ); + + const appender = duckdb.appender_create_ext( + connection, + 'memory', + 'main', + 'test_error_data' + ); + + // Get error data even when there's no error + const error_data = duckdb.appender_error_data(appender); + expect(duckdb.error_data_has_error(error_data)).toBe(false); + expect(duckdb.error_data_message(error_data)).toBe(''); + + // Successful append operations should not have errors + duckdb.append_int32(appender, 42); + duckdb.appender_end_row(appender); + duckdb.appender_flush_sync(appender); + + // Error data should still indicate no error after successful operations + const error_data_after = duckdb.appender_error_data(appender); + expect(duckdb.error_data_has_error(error_data_after)).toBe(false); + }); + }); + + test('error_data: invalid input', async () => { + await withConnection(async (connection) => { + await duckdb.query( + connection, + 'create table test_error_type(i integer)' + ); + + const appender = duckdb.appender_create_ext( + connection, + 'memory', + 'main', + 'test_error_type' + ); + + expect(() => { + duckdb.append_varchar(appender, 'not an int'); + duckdb.appender_end_row(appender); + }).toThrowError("Could not convert string 'not an int' to INT32"); + + const error_data = duckdb.appender_error_data(appender); + expect(duckdb.error_data_has_error(error_data)).toBe(true); + expect(duckdb.error_data_error_type(error_data)).toBe( + duckdb.ErrorType.INVALID_INPUT + ); + }); + }); });