From 4b9346affad3720c21f38c98a62bb470108a35bd Mon Sep 17 00:00:00 2001 From: auxten Date: Wed, 28 Aug 2024 18:02:26 +0800 Subject: [PATCH 1/6] Bump to v1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 845ca2c..d5301b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chdb", - "version": "1.2.0", + "version": "1.2.1", "description": "chDB bindings for nodejs", "main": "index.js", "repository": { From 6460f570247357c5c37dad58377008a34cf185f1 Mon Sep 17 00:00:00 2001 From: Auxten Wang Date: Mon, 24 Feb 2025 21:48:52 +0800 Subject: [PATCH 2/6] Update update_libchdb.sh --- update_libchdb.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/update_libchdb.sh b/update_libchdb.sh index ce7f608..1acfe95 100755 --- a/update_libchdb.sh +++ b/update_libchdb.sh @@ -9,8 +9,8 @@ cd "$(dirname "$0")" # Get the newest release version -LATEST_RELEASE=$(curl --silent "https://api.github.com/repos/chdb-io/chdb/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') -# LATEST_RELEASE=v2.0.0b0 +# LATEST_RELEASE=$(curl --silent "https://api.github.com/repos/chdb-io/chdb/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +LATEST_RELEASE=v2.2.0b1 # Download the correct version based on the platform case "$(uname -s)" in @@ -48,4 +48,4 @@ tar -xzf libchdb.tar.gz chmod +x libchdb.so # Clean up -rm -f libchdb.tar.gz \ No newline at end of file +rm -f libchdb.tar.gz From 1dd873ee15d301ab4939f964cc2eceea66398b88 Mon Sep 17 00:00:00 2001 From: Auxten Wang Date: Mon, 24 Feb 2025 21:54:27 +0800 Subject: [PATCH 3/6] LATEST_RELEASE=v2.0.4 --- update_libchdb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update_libchdb.sh b/update_libchdb.sh index 1acfe95..372583f 100755 --- a/update_libchdb.sh +++ b/update_libchdb.sh @@ -10,7 +10,7 @@ cd "$(dirname "$0")" # Get the newest release version # LATEST_RELEASE=$(curl --silent "https://api.github.com/repos/chdb-io/chdb/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') -LATEST_RELEASE=v2.2.0b1 +LATEST_RELEASE=v2.0.4 # Download the correct version based on the platform case "$(uname -s)" in From 469372ce7095cd886253d56b29572c9ea8ce1552 Mon Sep 17 00:00:00 2001 From: Auxten Wang Date: Wed, 19 Mar 2025 21:35:46 +0800 Subject: [PATCH 4/6] Update update_libchdb.sh v2.1.1 --- update_libchdb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update_libchdb.sh b/update_libchdb.sh index 372583f..404fac0 100755 --- a/update_libchdb.sh +++ b/update_libchdb.sh @@ -10,7 +10,7 @@ cd "$(dirname "$0")" # Get the newest release version # LATEST_RELEASE=$(curl --silent "https://api.github.com/repos/chdb-io/chdb/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') -LATEST_RELEASE=v2.0.4 +LATEST_RELEASE=v2.1.1 # Download the correct version based on the platform case "$(uname -s)" in From 9b7bd29ad0026d64eac1b0585fab811076b14bae Mon Sep 17 00:00:00 2001 From: Rajesh Sharma Date: Fri, 30 May 2025 23:29:36 +0200 Subject: [PATCH 5/6] Extending cpp bindings to support Clickhouse params thereby enabling parameterized query support --- index.d.ts | 21 +++++++ index.js | 14 ++++- lib/chdb_node.cpp | 149 ++++++++++++++++++++++++++++++++++++++++++++-- test.js | 46 +++++++++++++- 4 files changed, 223 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 095eec4..9d16329 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,16 @@ */ export function query(query: string, format?: string): string; +/** + * Executes a query with parameters using the chdb addon. + * + * @param query The query string to execute. + * @param binding arguments for parameters defined in the query. + * @param format The format for the query result, default is "CSV". + * @returns The query result as a string. + */ +export function queryBind(query:string, args: object, format?:string): string; + /** * Session class for managing queries and temporary paths. */ @@ -37,6 +47,17 @@ export class Session { */ query(query: string, format?: string): string; + /** + * Executes a query with parameters using the chdb addon. + * + * @param query The query string to execute. + * @param binding arguments for parameters defined in the query. + * @param format The format for the query result, default is "CSV". + * @returns The query result as a string. + */ + + queryBind(query:string, args: object, format?: string): string; + /** * Cleans up the session, deleting the temporary directory if one was created. */ diff --git a/index.js b/index.js index 29e5a57..f171874 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,13 @@ function query(query, format = "CSV") { return chdbNode.Query(query, format); } +function queryBind(query, args = {}, format = "CSV") { + if(!query) { + return ""; + } + return chdbNode.QueryBindSession(query, args, format); +} + // Session class with path handling class Session { constructor(path = "") { @@ -30,10 +37,15 @@ class Session { return chdbNode.QuerySession(query, format, this.path); } + queryBind(query, args = {}, format = "CSV") { + if(!query) return ""; + return chdbNode.QueryBindSession(query, args, format, this.path) + } + // Cleanup method to delete the temporary directory cleanup() { rmSync(this.path, { recursive: true }); // Replaced rmdirSync with rmSync } } -module.exports = { query, Session }; +module.exports = { query, queryBind, Session }; diff --git a/lib/chdb_node.cpp b/lib/chdb_node.cpp index 4af15e3..4dfc53f 100644 --- a/lib/chdb_node.cpp +++ b/lib/chdb_node.cpp @@ -10,6 +10,76 @@ #define MAX_PATH_LENGTH 4096 #define MAX_ARG_COUNT 6 + +static std::string toCHLiteral(const Napi::Env& env, const Napi::Value& v); + + +static std::string chEscape(const std::string& s) +{ + std::string out; + out.reserve(s.size() + 4); + out += '\''; + for (char c : s) { + if (c == '\'') out += "\\'"; + else out += c; + } + out += '\''; + return out; +} + +static std::string toCHLiteral(const Napi::Env& env, const Napi::Value& v) +{ + if (v.IsNumber() || v.IsBoolean() || v.IsString()) + return v.ToString().Utf8Value(); + + if (v.IsDate()) { + double ms = v.As().ValueOf(); + std::time_t t = static_cast(ms / 1000); + std::tm tm{}; + gmtime_r(&t, &tm); + char buf[32]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + return std::string(&buf[0], sizeof(buf)); + } + + if (v.IsTypedArray()) { + Napi::Object arr = env.Global().Get("Array").As(); + Napi::Function from = arr.Get("from").As(); + return toCHLiteral(env, from.Call(arr, { v })); + } + + if (v.IsArray()) { + Napi::Array a = v.As(); + size_t n = a.Length(); + std::string out = "["; + for (size_t i = 0; i < n; ++i) { + if (i) out += ","; + out += toCHLiteral(env, a.Get(i)); + } + out += "]"; + return out; + } + + if (v.IsObject()) { + Napi::Object o = v.As(); + Napi::Array keys = o.GetPropertyNames(); + size_t n = keys.Length(); + std::string out = "{"; + for (size_t i = 0; i < n; ++i) { + if (i) out += ","; + std::string k = keys.Get(i).ToString().Utf8Value(); + out += chEscape(k); // escape the map key with single-qoutes for click house query to work i.e 'key' not "key" + out += ":"; + out += toCHLiteral(env, o.Get(keys.Get(i))); + } + out += "}"; + return out; + } + + /* Fallback – stringify & quote */ + return chEscape(v.ToString().Utf8Value()); +} + // Utility function to construct argument string void construct_arg(char *dest, const char *prefix, const char *value, size_t dest_size) { @@ -92,21 +162,46 @@ char *QuerySession(const char *query, const char *format, const char *path, return result; } +char *QueryBindSession(const char *query, const char *format, const char *path, + const std::vector& params, char **error_message) { + + std::vector store; + store.reserve(4 + params.size() + (path && path[0] ? 1 : 0)); + + store.emplace_back("clickhouse"); + store.emplace_back("--multiquery"); + store.emplace_back(std::string("--output-format=") + format); + store.emplace_back(std::string("--query=") + query); + + for (const auto& p : params) store.emplace_back(p); + if (path && path[0]) store.emplace_back(std::string("--path=") + path); + + std::vector argv; + argv.reserve(store.size()); + for (auto& s : store) + argv.push_back(const_cast(s.c_str())); + + #ifdef CHDB_DEBUG + std::cerr << "=== chdb argv (" << argv.size() << ") ===\n"; + for (char* a : argv) std::cerr << a << '\n'; + #endif + + return query_stable_v2(static_cast(argv.size()), argv.data())->buf; +} + Napi::String QueryWrapper(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); - // Check argument types and count if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) { Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException(); return Napi::String::New(env, ""); } - // Get the arguments std::string query = info[0].As().Utf8Value(); std::string format = info[1].As().Utf8Value(); char *error_message = nullptr; - // Call the native function + char *result = Query(query.c_str(), format.c_str(), &error_message); if (result == NULL) { @@ -117,7 +212,6 @@ Napi::String QueryWrapper(const Napi::CallbackInfo &info) { return Napi::String::New(env, ""); } - // Return the result return Napi::String::New(env, result); } @@ -153,11 +247,56 @@ Napi::String QuerySessionWrapper(const Napi::CallbackInfo &info) { return Napi::String::New(env, result); } +static std::string jsToParam(const Napi::Env& env, const Napi::Value& v) { + return toCHLiteral(env, v); +} + +Napi::String QueryBindSessionWrapper(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsString() || !info[1].IsObject()) + Napi::TypeError::New(env,"Usage: sql, params, [format]").ThrowAsJavaScriptException(); + + std::string sql = info[0].As(); + Napi::Object obj = info[1].As(); + std::string format = (info.Length() > 2 && info[2].IsString()) + ? info[2].As() : std::string("CSV"); + std::string path = (info.Length() > 3 && info[3].IsString()) + ? info[3].As() : std::string(""); + + // Build param vector + std::vector cliParams; + Napi::Array keys = obj.GetPropertyNames(); + int len = keys.Length(); + for (int i = 0; i < len; i++) { + Napi::Value k = keys.Get(i); + if(!k.IsString()) continue; + + std::string key = k.As(); + std::string val = jsToParam(env, obj.Get(k)); + cliParams.emplace_back("--param_" + key + "=" + val); + } + + #ifdef CHDB_DEBUG + std::cerr << "=== cliParams ===\n"; + for (const auto& s : cliParams) + std::cerr << s << '\n'; + #endif + + char* err = nullptr; + char* out = QueryBindSession(sql.c_str(), format.c_str(), path.c_str(), cliParams, &err); + if (!out) { + Napi::Error::New(env, err ? err : "unknown error").ThrowAsJavaScriptException(); + return Napi::String::New(env,""); + } + return Napi::String::New(env, out); +} + 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)); return exports; } -NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) \ No newline at end of file +NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test.js b/test.js index 42c2a16..5968af3 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ const { expect } = require('chai'); -const { query, Session } = require("."); +const { query, queryBind, Session } = require("."); describe('chDB Queries', function () { @@ -16,6 +16,42 @@ describe('chDB Queries', function () { }).to.throw(Error, /Unknown table expression identifier/); }); + it('should return version, greeting message, and chDB() using bind query', () => { + const ret = queryBind("SELECT version(), 'Hello chDB', chDB()", {}, "CSV"); + console.log("Bind Query Result:", ret); + expect(ret).to.be.a('string'); + expect(ret).to.include('Hello chDB'); + }); + + it('binds a numeric parameter (stand-alone query)', () => { + const out = queryBind('SELECT {id:UInt32}', { id: 42 }, 'CSV').trim(); + console.log(out) + expect(out).to.equal('42'); + }); + + it('binds a string parameter (stand-alone query)', () => { + const out = queryBind( + `SELECT concat('Hello ', {name:String})`, + { name: 'Alice' }, + 'CSV' + ).trim(); + console.log(out) + expect(out).to.equal('"Hello Alice"'); + }); + + it('binds Date and Map correctly', () => { + const res = queryBind("SELECT {t: DateTime} AS t, {m: Map(String, Array(UInt8))} AS m", + { + t: new Date('2025-05-29T12:00:00Z'), + m: { "abc": Uint8Array.from([1, 2, 3]) } + }, + 'JSONEachRow' + ); + const row = JSON.parse(res.trim()); + expect(row.t).to.equal('2025-05-29 12:00:00'); + expect(row.m).to.deep.equal({ abc: [1, 2, 3] }); + }); + describe('Session Queries', function () { let session; @@ -55,6 +91,14 @@ describe('chDB Queries', function () { session.query("SELECT * FROM non_existent_table;", "CSV"); }).to.throw(Error, /Unknown table expression identifier/); }); + + it('should return result of the query made using bind parameters', () => { + const ret = session.queryBind("SELECT * from 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'); + }) }); }); From 5118123515d0ec56b723270849d56e590ae002bc Mon Sep 17 00:00:00 2001 From: Auxten Wang Date: Tue, 3 Jun 2025 10:46:27 +0800 Subject: [PATCH 6/6] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5301b6..4c466e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chdb", - "version": "1.2.1", + "version": "1.3.0", "description": "chDB bindings for nodejs", "main": "index.js", "repository": {