From 2663eda64b00ccf236ce6ea3aedc3f5a41bf350e Mon Sep 17 00:00:00 2001 From: wudidapaopao Date: Thu, 9 Oct 2025 20:23:55 +0800 Subject: [PATCH 1/5] Reuse connection --- index.js | 29 +++++++-- lib/chdb_node.cpp | 126 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- test.js => test_basic.js | 22 ++++--- test_connection.js | 93 +++++++++++++++++++++++++++++ 5 files changed, 257 insertions(+), 15 deletions(-) rename test.js => test_basic.js (82%) create mode 100644 test_connection.js diff --git a/index.js b/index.js index f171874..418e6e0 100644 --- a/index.js +++ b/index.js @@ -19,7 +19,7 @@ function queryBind(query, args = {}, format = "CSV") { return chdbNode.QueryBindSession(query, args, format); } -// Session class with path handling +// Session class with connection-based path handling class Session { constructor(path = "") { if (path === "") { @@ -30,21 +30,38 @@ class Session { this.path = path; this.isTemp = false; } + + // Create a connection for this session + this.connection = chdbNode.CreateConnection(this.path); + if (!this.connection) { + throw new Error("Failed to create connection"); + } } query(query, format = "CSV") { if (!query) return ""; - return chdbNode.QuerySession(query, format, this.path); + if (!this.connection) { + throw new Error("No active connection available"); + } + return chdbNode.QueryWithConnection(this.connection, query, format); } queryBind(query, args = {}, format = "CSV") { - if(!query) return ""; - return chdbNode.QueryBindSession(query, args, format, this.path) + throw new Error("QueryBind is not supported with connection-based sessions. Please use the standalone queryBind function instead."); } - // Cleanup method to delete the temporary directory + // Cleanup method to close connection and delete directory if temp cleanup() { - rmSync(this.path, { recursive: true }); // Replaced rmdirSync with rmSync + // Close the connection if it exists + if (this.connection) { + chdbNode.CloseConnection(this.connection); + this.connection = null; + } + + // Only delete directory if it's temporary + if (this.isTemp) { + rmSync(this.path, { recursive: true }); + } } } diff --git a/lib/chdb_node.cpp b/lib/chdb_node.cpp index 4dfc53f..b64c53a 100644 --- a/lib/chdb_node.cpp +++ b/lib/chdb_node.cpp @@ -1,11 +1,17 @@ #include "chdb.h" #include "chdb_node.h" +#include #include #include #include #include #include +typedef void * ChdbConnection; +ChdbConnection CreateConnection(const char * path); +void CloseConnection(ChdbConnection conn); +char * QueryWithConnection(ChdbConnection conn, const char * query, const char * format, char ** error_message); + #define MAX_FORMAT_LENGTH 64 #define MAX_PATH_LENGTH 4096 #define MAX_ARG_COUNT 6 @@ -189,6 +195,56 @@ char *QueryBindSession(const char *query, const char *format, const char *path, return query_stable_v2(static_cast(argv.size()), argv.data())->buf; } +ChdbConnection CreateConnection(const char * path) { + char dataPath[MAX_PATH_LENGTH]; + char * args[MAX_ARG_COUNT] = {"clickhouse", NULL}; + int argc = 1; + + if (path && path[0]) { + construct_arg(dataPath, "--path=", path, MAX_PATH_LENGTH); + args[1] = dataPath; + argc = 2; + } + + return static_cast(chdb_connect(argc, args)); +} + +void CloseConnection(ChdbConnection conn) { + if (conn) { + chdb_close_conn(static_cast(conn)); + } +} + +char * QueryWithConnection(ChdbConnection conn, const char * query, const char * format, char ** error_message) { + if (!conn || !query || !format) { + return nullptr; + } + + chdb_connection * inner_conn = static_cast(conn); + chdb_result * result = chdb_query(*inner_conn, query, format); + if (!result) { + return nullptr; + } + + const char * error = chdb_result_error(result); + if (error) { + if (error_message) { + *error_message = strdup(error); + } + chdb_destroy_query_result(result); + return nullptr; + } + + const char * buffer = chdb_result_buffer(result); + char * output = nullptr; + if (buffer) { + output = strdup(buffer); + } + + chdb_destroy_query_result(result); + return output; +} + Napi::String QueryWrapper(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); @@ -291,11 +347,81 @@ Napi::String QueryBindSessionWrapper(const Napi::CallbackInfo& info) { return Napi::String::New(env, out); } +Napi::Value CreateConnectionWrapper(const Napi::CallbackInfo & info) { + Napi::Env env = info.Env(); + + if (info.Length() < 1 || !info[0].IsString()) { + Napi::TypeError::New(env, "Path string expected").ThrowAsJavaScriptException(); + return env.Null(); + } + + std::string path = info[0].As().Utf8Value(); + ChdbConnection conn = CreateConnection(path.c_str()); + + if (!conn) { + Napi::Error::New(env, "Failed to create connection").ThrowAsJavaScriptException(); + return env.Null(); + } + + return Napi::External::New(env, conn); +} + +Napi::Value CloseConnectionWrapper(const Napi::CallbackInfo & info) { + Napi::Env env = info.Env(); + + if (info.Length() < 1 || !info[0].IsExternal()) { + Napi::TypeError::New(env, "Connection handle expected").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + ChdbConnection conn = info[0].As>().Data(); + CloseConnection(conn); + + return env.Undefined(); +} + +Napi::String QueryWithConnectionWrapper(const Napi::CallbackInfo & info) { + Napi::Env env = info.Env(); + + if (info.Length() < 3 || !info[0].IsExternal() || !info[1].IsString() || !info[2].IsString()) { + Napi::TypeError::New(env, "Usage: connection, query, format").ThrowAsJavaScriptException(); + return Napi::String::New(env, ""); + } + + ChdbConnection conn = info[0].As>().Data(); + std::string query = info[1].As().Utf8Value(); + std::string format = info[2].As().Utf8Value(); + + char * error_message = nullptr; + char * result = QueryWithConnection(conn, query.c_str(), format.c_str(), &error_message); + + if (error_message) { + std::string error_msg = std::string("Query failed: ") + error_message; + free(error_message); + Napi::Error::New(env, error_msg).ThrowAsJavaScriptException(); + return Napi::String::New(env, ""); + } + + if (!result) { + return Napi::String::New(env, ""); + } + + Napi::String output = Napi::String::New(env, result); + free(result); + return output; +} + Napi::Object Init(Napi::Env env, Napi::Object exports) { // Export the functions exports.Set("Query", Napi::Function::New(env, QueryWrapper)); exports.Set("QuerySession", Napi::Function::New(env, QuerySessionWrapper)); exports.Set("QueryBindSession", Napi::Function::New(env, QueryBindSessionWrapper)); + + // Export connection management functions + exports.Set("CreateConnection", Napi::Function::New(env, CreateConnectionWrapper)); + exports.Set("CloseConnection", Napi::Function::New(env, CloseConnectionWrapper)); + exports.Set("QueryWithConnection", Napi::Function::New(env, QueryWithConnectionWrapper)); + return exports; } diff --git a/package.json b/package.json index 92d5084..dc89c7c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "install": "npm run libchdb && npm run build", - "test": "mocha --timeout 15000", + "test": "mocha test_basic.js test_connection.js --timeout 15000", "libchdb": "bash ./update_libchdb.sh", "fixloaderpath": "bash ./fix_loader_path.sh", "build": "node-gyp configure build --verbose && npm run fixloaderpath" diff --git a/test.js b/test_basic.js similarity index 82% rename from test.js rename to test_basic.js index e5af13e..9858a8b 100644 --- a/test.js +++ b/test_basic.js @@ -56,8 +56,16 @@ describe('chDB Queries', function () { let session; before(function () { - // Create a new session instance before running the tests - session = new Session("./chdb-node-tmp"); + // Delete existing directory and create a new session instance + const fs = require('fs'); + const path = require('path'); + const tmpDir = "./chdb-node-tmp"; + + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + + session = new Session(tmpDir); }); after(function () { @@ -92,12 +100,10 @@ describe('chDB Queries', function () { }).to.throw(Error, /Unknown table expression identifier/); }); - it('should return result of the query made using bind parameters', () => { - const ret = session.queryBind("SELECT * from testdb.testtable where id > {id: UInt32}", { id: 2}, "CSV"); - console.log("Bind Session result:", ret); - expect(ret).to.not.include('1'); - expect(ret).to.not.include('2'); - expect(ret).to.include('3'); + it('should throw an error when using queryBind with session', () => { + expect(() => { + session.queryBind("SELECT * from testdb.testtable where id > {id: UInt32}", { id: 2}, "CSV"); + }).to.throw(Error, /QueryBind is not supported with connection-based sessions. Please use the standalone queryBind function instead./); }) }); diff --git a/test_connection.js b/test_connection.js new file mode 100644 index 0000000..66e4532 --- /dev/null +++ b/test_connection.js @@ -0,0 +1,93 @@ +const { expect } = require('chai'); +const { Session } = require("."); + +describe('chDB Connection Tests', function () { + + describe('Session Connection Management', function () { + let session; + + before(function () { + // Delete existing directory and create a new session instance + const fs = require('fs'); + const tmpDir = "./test-connection-tmp"; + + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + + session = new Session(tmpDir); + }); + + after(function () { + // Clean up the session after all tests are done + session.cleanup(); + }); + + it('should create session successfully with path and connection ID', function () { + expect(session.path).to.equal("./test-connection-tmp"); + expect(session.connection).to.not.be.null; + expect(session.connection).to.not.be.undefined; + console.log("✓ Session created successfully, path:", session.path); + console.log("✓ Connection:", session.connection); + }); + + it('should execute simple query and return correct result', function () { + const result = session.query("SELECT 1 as test_col", "CSV"); + console.log("Query result:", result.trim()); + expect(result).to.be.a('string'); + expect(result.trim()).to.equal('1'); + }); + + it('should return version information', function () { + const result = session.query("SELECT version()", "CSV"); + console.log("Version info:", result.trim()); + expect(result).to.be.a('string'); + expect(result).to.include('.'); + }); + + it('should create database and table successfully', function () { + // This should not throw an error + expect(() => { + session.query("CREATE DATABASE IF NOT EXISTS test_conn_db"); + session.query("CREATE TABLE IF NOT EXISTS test_conn_db.test_table (id UInt32, name String) ENGINE = MergeTree() ORDER BY id"); + }).to.not.throw(); + console.log("✓ Database and table created successfully"); + }); + + it('should insert data successfully', function () { + expect(() => { + session.query("INSERT INTO test_conn_db.test_table VALUES (1, 'Alice'), (2, 'Bob')"); + }).to.not.throw(); + console.log("✓ Data inserted successfully"); + }); + + it('should query inserted data and verify connection reuse', function () { + const result = session.query("SELECT * FROM test_conn_db.test_table ORDER BY id", "CSV"); + console.log("Query result:", result.trim()); + expect(result).to.be.a('string'); + expect(result).to.include('Alice'); + expect(result).to.include('Bob'); + expect(result).to.include('1'); + expect(result).to.include('2'); + }); + + it('should throw error when using queryBind with session', function () { + expect(() => { + session.queryBind("SELECT {id:UInt32}", {id: 42}); + }).to.throw(Error, /QueryBind is not supported with connection-based sessions. Please use the standalone queryBind function instead./); + console.log("✓ queryBind correctly throws error"); + }); + + it('should handle multiple queries in sequence (connection persistence)', function () { + const result1 = session.query("SELECT COUNT(*) FROM test_conn_db.test_table", "CSV"); + const result2 = session.query("SELECT MAX(id) FROM test_conn_db.test_table", "CSV"); + const result3 = session.query("SELECT name FROM test_conn_db.test_table WHERE id = 1", "CSV"); + + expect(result1.trim()).to.equal('2'); + expect(result2.trim()).to.equal('2'); + expect(result3.trim()).to.include('Alice'); + console.log("✓ Connection persistence test passed"); + }); + }); + +}); From d20507e04444328e3a9e9de72da607d4998f8ec2 Mon Sep 17 00:00:00 2001 From: wudidapaopao Date: Thu, 9 Oct 2025 20:24:05 +0800 Subject: [PATCH 2/5] 1.5.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f55b15..fd9d1dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chdb", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chdb", - "version": "1.4.0", + "version": "1.5.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index dc89c7c..a4822bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chdb", - "version": "1.4.0", + "version": "1.5.0", "description": "chDB bindings for nodejs", "main": "index.js", "repository": { From 40f9d02b8b35aaa99c3bb1c75c757ee127c64647 Mon Sep 17 00:00:00 2001 From: wudidapaopao Date: Thu, 9 Oct 2025 20:26:30 +0800 Subject: [PATCH 3/5] Remove unused header file --- lib/chdb_node.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/chdb_node.cpp b/lib/chdb_node.cpp index b64c53a..998d3f9 100644 --- a/lib/chdb_node.cpp +++ b/lib/chdb_node.cpp @@ -1,6 +1,5 @@ #include "chdb.h" #include "chdb_node.h" -#include #include #include #include From cd900f5aa3be65167fc7279b4809936b55da48e6 Mon Sep 17 00:00:00 2001 From: wudidapaopao Date: Thu, 9 Oct 2025 20:31:36 +0800 Subject: [PATCH 4/5] Add tests --- test_connection.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test_connection.js b/test_connection.js index 66e4532..f373d14 100644 --- a/test_connection.js +++ b/test_connection.js @@ -88,6 +88,25 @@ describe('chDB Connection Tests', function () { expect(result3.trim()).to.include('Alice'); console.log("✓ Connection persistence test passed"); }); + + it('should persist data after session cleanup and reopen', function () { + session.cleanup(); + + // Create a new session with the same path + session = new Session("./test-connection-tmp"); + session.query("USE test_conn_db") + + // Query the data to see if it persists + const result = session.query("SELECT * FROM test_table ORDER BY id", "CSV"); + console.log("Query result after session reopen:", result.trim()); + + expect(result).to.be.a('string'); + expect(result).to.include('Alice'); + expect(result).to.include('Bob'); + expect(result).to.include('1'); + expect(result).to.include('2'); + console.log("✓ Data persisted after session cleanup and reopen"); + }); }); }); From c809b8fa6cc4bc31d6c121297a332377ed3c461d Mon Sep 17 00:00:00 2001 From: wudidapaopao Date: Thu, 9 Oct 2025 20:38:24 +0800 Subject: [PATCH 5/5] Add tests --- test_connection.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test_connection.js b/test_connection.js index f373d14..86e2ae7 100644 --- a/test_connection.js +++ b/test_connection.js @@ -109,4 +109,67 @@ describe('chDB Connection Tests', function () { }); }); + describe('Session without Path (In-Memory)', function () { + let session; + + it('should create session successfully without path', function () { + session = new Session(); + expect(session.path).to.not.be.null; + expect(session.path).to.not.be.undefined; + expect(session.connection).to.not.be.null; + expect(session.connection).to.not.be.undefined; + console.log("✓ In-memory session created successfully"); + console.log("✓ Connection:", session.connection); + }); + + it('should execute simple query in memory session', function () { + const result = session.query("SELECT 1 as test_col", "CSV"); + console.log("Query result:", result.trim()); + expect(result).to.be.a('string'); + expect(result.trim()).to.equal('1'); + }); + + it('should create table and insert data in memory session', function () { + expect(() => { + session.query("CREATE TABLE memory_test (id UInt32, name String) ENGINE = Memory"); + session.query("INSERT INTO memory_test VALUES (1, 'MemoryAlice'), (2, 'MemoryBob')"); + }).to.not.throw(); + console.log("✓ Memory table created and data inserted successfully"); + }); + + it('should query data from memory table', function () { + const result = session.query("SELECT * FROM memory_test ORDER BY id", "CSV"); + console.log("Memory query result:", result.trim()); + expect(result).to.be.a('string'); + expect(result).to.include('MemoryAlice'); + expect(result).to.include('MemoryBob'); + expect(result).to.include('1'); + expect(result).to.include('2'); + }); + + it('should cleanup session and verify data is not accessible in new session', function () { + // Cleanup the current session + session.cleanup(); + console.log("✓ Memory session cleaned up"); + + // Create a new session without path + session = new Session(); + console.log("✓ New memory session created"); + + // Try to query the previous table - should fail or return empty + expect(() => { + const result = session.query("SELECT * FROM memory_test", "CSV"); + }).to.throw(); // Expected to throw error since table shouldn't exist + + console.log("✓ Data correctly cleaned up - table not accessible in new session"); + }); + + after(function () { + // Clean up the session after all tests are done + if (session) { + session.cleanup(); + } + }); + }); + });