From f0e40e749739de03de08a98965b58d667c3bf87d Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 2 Nov 2023 10:56:47 +0100 Subject: [PATCH 1/6] feat: Expose LogicalType(s) for columns in QueryResult, fix #24 --- lib/duckdb.d.ts | 6 ++++++ lib/duckdb.js | 14 +++++++++----- src/duckdb_node.hpp | 1 + src/statement.cpp | 23 ++++++++++++++++++++++- test/query_result.test.ts | 10 +++++++++- test/typescript_decls.test.ts | 2 +- 6 files changed, 48 insertions(+), 8 deletions(-) diff --git a/lib/duckdb.d.ts b/lib/duckdb.d.ts index c613c097..93c2304f 100644 --- a/lib/duckdb.d.ts +++ b/lib/duckdb.d.ts @@ -108,7 +108,13 @@ export class Connection { unregister_buffer(name: string, callback?: Callback): void; } +export type LogicalType = { + id: number, + name: string, +} + export class QueryResult implements AsyncIterable { + getColumns(): Record; [Symbol.asyncIterator](): AsyncIterator; } diff --git a/lib/duckdb.js b/lib/duckdb.js index fc760d2f..c08b04ca 100644 --- a/lib/duckdb.js +++ b/lib/duckdb.js @@ -84,6 +84,13 @@ QueryResult.prototype.nextChunk; */ QueryResult.prototype.nextIpcBuffer; +/** + * Function to return logical types for columns + * + * @method + */ +QueryResult.prototype.getColumns; + /** * @name asyncIterator * @memberof module:duckdb~QueryResult @@ -218,12 +225,9 @@ Connection.prototype.each = function (sql) { * @param {...*} params * @yields row chunks */ -Connection.prototype.stream = async function* (sql) { +Connection.prototype.stream = async function (sql) { const statement = new Statement(this, sql); - const queryResult = await statement.stream.apply(statement, arguments); - for await (const result of queryResult) { - yield result; - } + return statement.stream.apply(statement, arguments); } /** diff --git a/src/duckdb_node.hpp b/src/duckdb_node.hpp index 614f32cb..4fe8e3fe 100644 --- a/src/duckdb_node.hpp +++ b/src/duckdb_node.hpp @@ -208,6 +208,7 @@ class QueryResult : public Napi::ObjectWrap { public: Napi::Value NextChunk(const Napi::CallbackInfo &info); Napi::Value NextIpcBuffer(const Napi::CallbackInfo &info); + Napi::Value GetColumns(const Napi::CallbackInfo &info); duckdb::shared_ptr cschema; private: diff --git a/src/statement.cpp b/src/statement.cpp index 3d813f8a..3bbeae27 100644 --- a/src/statement.cpp +++ b/src/statement.cpp @@ -636,7 +636,8 @@ Napi::FunctionReference QueryResult::Init(Napi::Env env, Napi::Object exports) { Napi::Function t = DefineClass(env, "QueryResult", {InstanceMethod("nextChunk", &QueryResult::NextChunk), - InstanceMethod("nextIpcBuffer", &QueryResult::NextIpcBuffer)}); + InstanceMethod("nextIpcBuffer", &QueryResult::NextIpcBuffer), + InstanceMethod("getColumns", &QueryResult::GetColumns)}); exports.Set("QueryResult", t); @@ -742,6 +743,26 @@ Napi::Value QueryResult::NextIpcBuffer(const Napi::CallbackInfo &info) { return deferred.Promise(); } +Napi::Value QueryResult::GetColumns(const Napi::CallbackInfo &info) +{ + auto env = info.Env(); + auto result = Napi::Object::New(env); + + for (duckdb::idx_t column_idx = 0; column_idx < this->result->ColumnCount(); column_idx++) + { + auto column_name = this->result->ColumnName(column_idx); + auto column_type = this->result->types[column_idx]; + + auto logic_type = Napi::Object::New(env); + logic_type.Set("id", Napi::Number::New(env, (double)column_type.id())); + logic_type.Set("name", Napi::String::New(env, column_type.ToString())); + + result.Set(column_name, logic_type); + } + + return result; +} + Napi::Object QueryResult::NewInstance(const Napi::Object &db) { return NodeDuckDB::GetData(db.Env())->query_result_constructor.New({db}); } diff --git a/test/query_result.test.ts b/test/query_result.test.ts index 16eb231c..dc4ebccb 100644 --- a/test/query_result.test.ts +++ b/test/query_result.test.ts @@ -14,7 +14,15 @@ describe('QueryResult', () => { it('streams results', async () => { let retrieved = 0; - const stream = conn.stream('SELECT * FROM range(0, ?)', total); + + const stream = await conn.stream("SELECT * FROM range(0, ?)", total); + assert.deepEqual(stream.getColumns(), { + range: { + id: 14, + name: "1", + }, + }); + for await (const row of stream) { retrieved++; } diff --git a/test/typescript_decls.test.ts b/test/typescript_decls.test.ts index ffdb6125..d0fe3167 100644 --- a/test/typescript_decls.test.ts +++ b/test/typescript_decls.test.ts @@ -222,7 +222,7 @@ describe("typescript: stream and QueryResult", function () { it("streams results", async () => { let retrieved = 0; - const stream = conn.stream("SELECT * FROM range(0, ?)", total); + const stream = await conn.stream("SELECT * FROM range(0, ?)", total); for await (const row of stream) { retrieved++; } From 693e2a49f3122319047e110e5e9629ff9beeac6b Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 2 Nov 2023 11:08:23 +0100 Subject: [PATCH 2/6] chore: type --- test/query_result.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/query_result.test.ts b/test/query_result.test.ts index dc4ebccb..cb6395c0 100644 --- a/test/query_result.test.ts +++ b/test/query_result.test.ts @@ -19,7 +19,7 @@ describe('QueryResult', () => { assert.deepEqual(stream.getColumns(), { range: { id: 14, - name: "1", + name: "BIGINT", }, }); From 6169c7bfc0a414ef7433cff4956ebd305f73368f Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 2 Nov 2023 11:11:42 +0100 Subject: [PATCH 3/6] chore: correct type --- lib/duckdb.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/duckdb.d.ts b/lib/duckdb.d.ts index 93c2304f..a3f86d45 100644 --- a/lib/duckdb.d.ts +++ b/lib/duckdb.d.ts @@ -101,7 +101,7 @@ export class Connection { ): void; unregister_udf(name: string, callback: Callback): void; - stream(sql: any, ...args: any[]): QueryResult; + stream(sql: any, ...args: any[]): Promise; arrowIPCStream(sql: any, ...args: any[]): Promise; register_buffer(name: string, array: ArrowIterable, force: boolean, callback?: Callback): void; @@ -172,7 +172,7 @@ export class Database { ): void; unregister_udf(name: string, callback: Callback): void; - stream(sql: any, ...args: any[]): QueryResult; + stream(sql: any, ...args: any[]): Promise; arrowIPCStream(sql: any, ...args: any[]): Promise; serialize(done?: Callback): void; From dd42ca957b08fc34f0e64d39a42503c76ce188b3 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 2 Nov 2023 12:07:28 +0100 Subject: [PATCH 4/6] chore: review, same api as Statement.columns() --- lib/duckdb.d.ts | 7 +------ lib/duckdb.js | 5 +++-- src/duckdb_node.hpp | 2 +- src/statement.cpp | 14 +++++++------- test/query_result.test.ts | 15 +++++++++------ 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/duckdb.d.ts b/lib/duckdb.d.ts index a3f86d45..b7e31318 100644 --- a/lib/duckdb.d.ts +++ b/lib/duckdb.d.ts @@ -108,13 +108,8 @@ export class Connection { unregister_buffer(name: string, callback?: Callback): void; } -export type LogicalType = { - id: number, - name: string, -} - export class QueryResult implements AsyncIterable { - getColumns(): Record; + columns(): ColumnInfo[] | null; [Symbol.asyncIterator](): AsyncIterator; } diff --git a/lib/duckdb.js b/lib/duckdb.js index c08b04ca..5c04ccd1 100644 --- a/lib/duckdb.js +++ b/lib/duckdb.js @@ -88,8 +88,9 @@ QueryResult.prototype.nextIpcBuffer; * Function to return logical types for columns * * @method + * @return {ColumnInfo[]} - Array of column names and types */ -QueryResult.prototype.getColumns; +QueryResult.prototype.columns; /** * @name asyncIterator @@ -717,7 +718,7 @@ Statement.prototype.sql; /** * @method - * @return {ColumnInfo[]} - Array of column names and types + * @return {ColumnInfo[] | null} - Array of column names and types */ Statement.prototype.columns; diff --git a/src/duckdb_node.hpp b/src/duckdb_node.hpp index 4fe8e3fe..aaa81937 100644 --- a/src/duckdb_node.hpp +++ b/src/duckdb_node.hpp @@ -208,7 +208,7 @@ class QueryResult : public Napi::ObjectWrap { public: Napi::Value NextChunk(const Napi::CallbackInfo &info); Napi::Value NextIpcBuffer(const Napi::CallbackInfo &info); - Napi::Value GetColumns(const Napi::CallbackInfo &info); + Napi::Value Columns(const Napi::CallbackInfo &info); duckdb::shared_ptr cschema; private: diff --git a/src/statement.cpp b/src/statement.cpp index 3bbeae27..500fa8df 100644 --- a/src/statement.cpp +++ b/src/statement.cpp @@ -637,7 +637,7 @@ Napi::FunctionReference QueryResult::Init(Napi::Env env, Napi::Object exports) { Napi::Function t = DefineClass(env, "QueryResult", {InstanceMethod("nextChunk", &QueryResult::NextChunk), InstanceMethod("nextIpcBuffer", &QueryResult::NextIpcBuffer), - InstanceMethod("getColumns", &QueryResult::GetColumns)}); + InstanceMethod("columns", &QueryResult::Columns)}); exports.Set("QueryResult", t); @@ -743,21 +743,21 @@ Napi::Value QueryResult::NextIpcBuffer(const Napi::CallbackInfo &info) { return deferred.Promise(); } -Napi::Value QueryResult::GetColumns(const Napi::CallbackInfo &info) +Napi::Value QueryResult::Columns(const Napi::CallbackInfo &info) { auto env = info.Env(); - auto result = Napi::Object::New(env); + auto result = Napi::Array::New(env, this->result->ColumnCount()); for (duckdb::idx_t column_idx = 0; column_idx < this->result->ColumnCount(); column_idx++) { auto column_name = this->result->ColumnName(column_idx); auto column_type = this->result->types[column_idx]; - auto logic_type = Napi::Object::New(env); - logic_type.Set("id", Napi::Number::New(env, (double)column_type.id())); - logic_type.Set("name", Napi::String::New(env, column_type.ToString())); + auto obj = Napi::Object::New(env); + obj.Set("name", Napi::String::New(env, column_name)); + obj.Set("type", TypeToObject(env, column_type)); - result.Set(column_name, logic_type); + result.Set(column_idx, obj); } return result; diff --git a/test/query_result.test.ts b/test/query_result.test.ts index cb6395c0..0dccdc29 100644 --- a/test/query_result.test.ts +++ b/test/query_result.test.ts @@ -16,12 +16,15 @@ describe('QueryResult', () => { let retrieved = 0; const stream = await conn.stream("SELECT * FROM range(0, ?)", total); - assert.deepEqual(stream.getColumns(), { - range: { - id: 14, - name: "BIGINT", - }, - }); + assert.deepEqual(stream.columns(), [ + { + name: 'range', + type: { + id: 'BIGINT', + sql_type: 'BIGINT', + } + } + ]); for await (const row of stream) { retrieved++; From 39f4c4fc7dc2a8fa4decc6344c239e2113d4d11c Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 2 Nov 2023 12:16:50 +0100 Subject: [PATCH 5/6] chore: column can return null only for Statement --- lib/duckdb.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/duckdb.d.ts b/lib/duckdb.d.ts index b7e31318..d7696473 100644 --- a/lib/duckdb.d.ts +++ b/lib/duckdb.d.ts @@ -109,7 +109,7 @@ export class Connection { } export class QueryResult implements AsyncIterable { - columns(): ColumnInfo[] | null; + columns(): ColumnInfo[]; [Symbol.asyncIterator](): AsyncIterator; } @@ -262,7 +262,7 @@ export class Statement { run(...args: [...any, Callback] | any[]): Statement; - columns(): ColumnInfo[]; + columns(): ColumnInfo[] | null; } export const ERROR: number; From 7a66f671da6e598d290b09202d3e80070fb35299 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 2 Nov 2023 13:00:23 +0100 Subject: [PATCH 6/6] chore: assert --- test/columns.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/columns.test.ts b/test/columns.test.ts index 55dabce2..29371cd7 100644 --- a/test/columns.test.ts +++ b/test/columns.test.ts @@ -10,7 +10,7 @@ describe('Column Types', function() { var stmt = db.prepare("SELECT * EXCLUDE(medium_enum, large_enum) FROM test_all_types()", function(err: null | Error) { if (err) throw err; - let cols = stmt.columns(); + let cols = stmt.columns() as duckdb.ColumnInfo[]; assert.equal(cols.length, 42);