From 0fa556a84de0defa34f5fd5e08213c583e8831d1 Mon Sep 17 00:00:00 2001 From: Derek Perez Date: Tue, 14 Oct 2025 21:16:29 -0700 Subject: [PATCH 1/2] Updating module to use `cc` from bun:ffi --- .gitignore | 5 ++- bun.lock | 25 ++++++++++++ bun.lockb | Bin 3149 -> 0 bytes example.deno.ts | 18 --------- index.ts | 54 +++++++++++++++++--------- lib/README.md | 10 ----- lib/build.sh | 13 ------- lib/chdb.h | 28 -------------- lib/chdb_bun.c | 87 ++++++++++++++++++++++++++++++++++-------- lib/chdb_bun.h | 4 -- lib/update_libchdb.sh | 25 ++++++++---- mod.ts | 65 ------------------------------- package.json | 16 ++++---- 13 files changed, 161 insertions(+), 189 deletions(-) create mode 100644 bun.lock delete mode 100755 bun.lockb delete mode 100644 example.deno.ts delete mode 100644 lib/README.md delete mode 100755 lib/build.sh delete mode 100644 lib/chdb.h delete mode 100644 lib/chdb_bun.h delete mode 100644 mod.ts diff --git a/.gitignore b/.gitignore index 0b95b7c..dca5ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules *.so -/lib/chdb_bun.dylib -/lib/libchdb.so +*.dylib +/lib/chdb.h +/lib/chdb.hpp /lib/index.js /lib/index.d.ts diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..88d47e6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,25 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "chdb-bun", + "devDependencies": { + "bun-types": "^1.0.19", + "typescript": "^5.3.3", + }, + }, + }, + "packages": { + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index e574560f2c2d5c29eb2702e8f9971eb63f90f60b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3149 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_lPcYt16> zHXZ!Icw$D&<*fn>9^O%0KPNBY>(SimC)okde(&W5Dgpv_2*tpGMmIqD*)RnR$r&k0 zxG$0K$94>Co@{B$5(V3F@str2Yt*J@XZ_>|p z2)$O~%(prADsa~B#iwUf&d<7(IZ;2VS8c1UB}|+VXbvR+6bc|955zV=_pkzKW+(=928g-HMzbs}owIh0&!smG-miTxFz#oGoqTF#yR~mcy5E4{Z|oFV z^w-ezq$lR3rVEPxCvs;%v*=Brb_k(9eA8zm!{nvc=b6L`YFUpR7 z0h^~HnF|VQ7+{Is>!;7~>P+*N@=B9;D@>1`yZi9yAFm@Db|~-2@UOwztcE4a-(Oske-eHrJIr#tW{1&*_GIU}1QpJvq$DVP@2o8xF}gKGq1QLF(*e4R^#f06s4xxDHs_j6lYeY z=BMc>m?$LXWTxlkr@_kTfBzu>-H(gVF=Y9UwDcWi!aE1yD1>K$SfZaM?r~nZ=e`0DWr)cb6f$Zz1(R ztbXTUj5E|T&@;3IwFV76hEwFtI3=4rYs5Jp=9Ux?2 z?G0EvfeB=qk(r(e9@8-O)Is$ufco$O+^5J-+^k}~{GxPyga&D8l=?$Y`A`2 zeo88oSzcV4my(&B32`hQqmh*XO-JSeon~aAXGFkla0`LD%Zqg_^i1^(4T#eNaxYL9 dh|)CzCRziaUi<-p>^v;4HG>!cP0fRj0s#0}PO$(0 diff --git a/example.deno.ts b/example.deno.ts deleted file mode 100644 index a37de8b..0000000 --- a/example.deno.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Run with `deno run --unstable-ffi -A example.deno.ts` -import { query, Session } from "./mod.ts"; - -// Create a new session instance -const session = new Session("./chdb-bun-tmp"); -let result; - -// Test standalone query -result = query("SELECT version(), 'Hello chDB', chdb()", "CSV"); -console.log(result); - -// Test session query -session.query("CREATE FUNCTION IF NOT EXISTS hello AS () -> 'chDB'", "CSV"); -result = session.query("SELECT hello()", "CSV"); -console.log(result); - -// Clean up the session -session.cleanup(); diff --git a/index.ts b/index.ts index b6ca1aa..01992c6 100644 --- a/index.ts +++ b/index.ts @@ -1,25 +1,37 @@ -import { dlopen, FFIType } from "bun:ffi"; -import { rmdirSync, mkdtempSync } from 'fs'; +import { cc, FFIType, CString } from "bun:ffi"; +import { rmdirSync, mkdtempSync } from "fs"; +import source from "./lib/chdb_bun.c" with { type: "file" }; -const path = `chdb_bun.so`; - -const { symbols: chdb } = dlopen(path, { - Query: { - args: [FFIType.cstring, FFIType.cstring], - returns: FFIType.cstring, - }, - QuerySession: { - args: [FFIType.cstring, FFIType.cstring, FFIType.cstring], - returns: FFIType.cstring, - }, -}); +const { symbols: chdb } = cc({ + source, + library: ["chdb"], + flags: ["-L./lib"], + symbols: { + Query: { + args: [FFIType.cstring, FFIType.cstring], + returns: FFIType.ptr, + }, + QuerySession: { + args: [FFIType.cstring, FFIType.cstring, FFIType.cstring], + returns: FFIType.ptr, + }, + free_cstr: { + args: [FFIType.ptr], + returns: FFIType.void, + } + } +}); // Standalone exported query function export function query(query: string, format: string = "CSV") { - if (!query) { - return ""; + if (!query) return ""; + const resultPtr = chdb.Query(Buffer.from(query + "\0"), Buffer.from(format + "\0")); + if (resultPtr === null) return ""; + try { + return new CString(resultPtr).toString(); + } finally { + chdb.free_cstr(resultPtr); } - return chdb.Query(Buffer.from(query + "\0"), Buffer.from(format + "\0")); } // Session class with path handling @@ -29,11 +41,17 @@ class Session { query(query: string, format: string = "CSV") { if (!query) return ""; - return chdb.QuerySession( + const resultPtr = chdb.QuerySession( Buffer.from(query + "\0"), Buffer.from(format + "\0"), Buffer.from(this.path + "\0") ); + if (resultPtr === null) return ""; + try { + return new CString(resultPtr).toString(); + } finally { + chdb.free_cstr(resultPtr); + } } constructor(path: string = "") { diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index f38c410..0000000 --- a/lib/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## libchdb `Query` function binding - -```bash -# install dependencies -./update_libchdb.sh - -# build the dynamic library - -./build.sh -``` diff --git a/lib/build.sh b/lib/build.sh deleted file mode 100755 index e1110d2..0000000 --- a/lib/build.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ "$(uname)" == "Darwin" ]; then - clang -O3 -dynamiclib -o chdb_bun.so -L. -lchdb chdb_bun.c -elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then - clang -O3 -shared -fPIC -o chdb_bun.so -L. -lchdb chdb_bun.c -else - echo "Unsupported operating system" - exit 1 -fi - -mv chdb_bun.so ../chdb_bun.so -mv libchdb.so ../libchdb.so \ No newline at end of file diff --git a/lib/chdb.h b/lib/chdb.h deleted file mode 100644 index 48a8380..0000000 --- a/lib/chdb.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#ifdef __cplusplus -# include -# include -extern "C" { -#else -# include -# include -#endif - -#define CHDB_EXPORT __attribute__((visibility("default"))) -struct CHDB_EXPORT local_result -{ - char * buf; - size_t len; - void * _vec; // std::vector *, for freeing - double elapsed; - uint64_t rows_read; - uint64_t bytes_read; -}; - -CHDB_EXPORT struct local_result * query_stable(int argc, char ** argv); -CHDB_EXPORT void free_result(struct local_result * result); - -#ifdef __cplusplus -} -#endif diff --git a/lib/chdb_bun.c b/lib/chdb_bun.c index e64c74d..c840cc7 100644 --- a/lib/chdb_bun.c +++ b/lib/chdb_bun.c @@ -1,5 +1,4 @@ #include "chdb.h" -#include "chdb_bun.h" #include #include #include @@ -7,11 +6,23 @@ #define MAX_FORMAT_LENGTH 64 #define MAX_PATH_LENGTH 4096 #define MAX_ARG_COUNT 6 +#define QUERY_PREFIX "--query=" +#define FORMAT_PREFIX "--output-format=" +#define PATH_PREFIX "--path=" // Utility function to construct argument string -void construct_arg(char *dest, const char *prefix, const char *value, - size_t dest_size) { - snprintf(dest, dest_size, "%s%s", prefix, value); +// Returns 0 on success, -1 if truncated +int construct_arg(char *dest, const char *prefix, const char *value, + size_t dest_size) { + if (dest == NULL || prefix == NULL || value == NULL) { + return -1; + } + int written = snprintf(dest, dest_size, "%s%s", prefix, value); + // Check if output was truncated + if (written < 0 || (size_t)written >= dest_size) { + return -1; + } + return 0; } // Generalized query function @@ -20,27 +31,50 @@ char *general_query(int argc, char *args[]) { if (result == NULL) { return NULL; - } else { - return result->buf; } + + char *buf_copy = malloc(result->len + 1); + if (buf_copy == NULL) { + free_result(result); + return NULL; + } + memcpy(buf_copy, result->buf, result->len); + buf_copy[result->len] = '\0'; + + // Free the original result + free_result(result); + + return buf_copy; } // Query function without session char *Query(const char *query, const char *format) { + if (query == NULL || format == NULL) { + return NULL; + } + char dataFormat[MAX_FORMAT_LENGTH]; char *dataQuery; char *args[MAX_ARG_COUNT] = {"clickhouse", "--multiquery", NULL, NULL}; int argc = 4; - construct_arg(dataFormat, "--output-format=", format, MAX_FORMAT_LENGTH); + // Validate format string length + if (construct_arg(dataFormat, FORMAT_PREFIX, format, MAX_FORMAT_LENGTH) != 0) { + return NULL; + } args[2] = dataFormat; - dataQuery = (char *)malloc(strlen(query) + strlen("--query=") + 1); + // Allocate memory for query argument + size_t query_arg_len = strlen(query) + strlen(QUERY_PREFIX) + 1; + dataQuery = malloc(query_arg_len); if (dataQuery == NULL) { return NULL; } - construct_arg(dataQuery, "--query=", query, - strlen(query) + strlen("--query=") + 1); + + if (construct_arg(dataQuery, QUERY_PREFIX, query, query_arg_len) != 0) { + free(dataQuery); + return NULL; + } args[3] = dataQuery; char *result = general_query(argc, args); @@ -51,27 +85,50 @@ char *Query(const char *query, const char *format) { // QuerySession function will save the session to the path // queries with same path will use the same session char *QuerySession(const char *query, const char *format, const char *path) { + if (query == NULL || format == NULL || path == NULL) { + return NULL; + } + char dataFormat[MAX_FORMAT_LENGTH]; char dataPath[MAX_PATH_LENGTH]; char *dataQuery; char *args[MAX_ARG_COUNT] = {"clickhouse", "--multiquery", NULL, NULL, NULL}; int argc = 5; - construct_arg(dataFormat, "--output-format=", format, MAX_FORMAT_LENGTH); + // Validate format string length + if (construct_arg(dataFormat, FORMAT_PREFIX, format, MAX_FORMAT_LENGTH) != 0) { + return NULL; + } args[2] = dataFormat; - dataQuery = (char *)malloc(strlen(query) + strlen("--query=") + 1); + // Allocate memory for query argument + size_t query_arg_len = strlen(query) + strlen(QUERY_PREFIX) + 1; + dataQuery = malloc(query_arg_len); if (dataQuery == NULL) { return NULL; } - construct_arg(dataQuery, "--query=", query, - strlen(query) + strlen("--query=") + 1); + + if (construct_arg(dataQuery, QUERY_PREFIX, query, query_arg_len) != 0) { + free(dataQuery); + return NULL; + } args[3] = dataQuery; - construct_arg(dataPath, "--path=", path, MAX_PATH_LENGTH); + // Validate path string length + if (construct_arg(dataPath, PATH_PREFIX, path, MAX_PATH_LENGTH) != 0) { + free(dataQuery); + return NULL; + } args[4] = dataPath; char *result = general_query(argc, args); free(dataQuery); return result; } + +// Free function for FFI callers to clean up query results +void free_cstr(char *result) { + if (result != NULL) { + free(result); + } +} diff --git a/lib/chdb_bun.h b/lib/chdb_bun.h deleted file mode 100644 index 24594ad..0000000 --- a/lib/chdb_bun.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -char *Query(const char *query, const char *format); -char *QuerySession(const char *query, const char *format, const char *path); diff --git a/lib/update_libchdb.sh b/lib/update_libchdb.sh index ae8961e..5535b23 100755 --- a/lib/update_libchdb.sh +++ b/lib/update_libchdb.sh @@ -2,27 +2,31 @@ #!/bin/bash # 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.1.1 +LATEST_RELEASE=$(curl --silent "https://api.github.com/repos/chdb-io/chdb/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') # Download the correct version based on the platform -case "$(uname -s)" in - Linux) +OS_NAME="$(uname -s)" +echo "Detected OS: $OS_NAME" + +case "$OS_NAME" in + Linux*) if [[ $(uname -m) == "aarch64" ]]; then PLATFORM="linux-aarch64-libchdb.tar.gz" else PLATFORM="linux-x86_64-libchdb.tar.gz" fi + LIB_SUFFIX="so" ;; - Darwin) + Darwin*) if [[ $(uname -m) == "arm64" ]]; then PLATFORM="macos-arm64-libchdb.tar.gz" else PLATFORM="macos-x86_64-libchdb.tar.gz" fi + LIB_SUFFIX="dylib" ;; *) - echo "Unsupported platform" + echo "Unsupported platform: $OS_NAME" exit 1 ;; esac @@ -37,8 +41,13 @@ curl -L -o libchdb.tar.gz $DOWNLOAD_URL # Untar the file tar -xzf libchdb.tar.gz -# Set execute permission for libchdb.so -chmod +x libchdb.so +# Rename the library file if needed (tarball always contains .so) +if [ -f "libchdb.so" ] && [ "$LIB_SUFFIX" = "dylib" ]; then + mv libchdb.so libchdb.dylib +fi + +# Set execute permission for the library file +chmod +x libchdb.$LIB_SUFFIX # Clean up rm -f libchdb.tar.gz diff --git a/mod.ts b/mod.ts deleted file mode 100644 index b20e550..0000000 --- a/mod.ts +++ /dev/null @@ -1,65 +0,0 @@ -const path = "chdb_bun.so"; - -const { symbols: chdb } = Deno.dlopen(path, { - Query: { - parameters: ["buffer", "buffer"], - result: "buffer", - }, - QuerySession: { - parameters: ["buffer", "buffer", "buffer"], - result: "buffer", - }, -}); - -const enc = new TextEncoder(); -// Standalone exported query function -export function query(query: string, format: string = "CSV") { - if (!query) { - return ""; - } - const result = chdb.Query( - enc.encode(query + "\0"), - enc.encode(format + "\0"), - ); - if (result == null) { - return ""; - } - return new Deno.UnsafePointerView(result).getCString(); -} - -// Session class with path handling -class Session { - path: string; - isTemp: boolean; - - query(query: string, format: string = "CSV") { - if (!query) return ""; - const result = chdb.QuerySession( - enc.encode(query + "\0"), - enc.encode(format + "\0"), - enc.encode(this.path + "\0"), - ); - if (result == null) { - return ""; - } - return new Deno.UnsafePointerView(result).getCString(); - } - - constructor(path: string = "") { - if (path === "") { - // Create a temporary directory - this.path = Deno.makeTempDirSync(); - this.isTemp = true; - } else { - this.path = path; - this.isTemp = false; - } - } - - // Cleanup method to delete the temporary directory - cleanup() { - Deno.removeSync(this.path, { recursive: true }); - } -} - -export { chdb, Session }; diff --git a/package.json b/package.json index d2da4a2..b75ed86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chdb-bun", - "version": "1.1.0", + "version": "2.0.0", "author": "Farmer Sun ", "module": "lib/index.js", "devDependencies": { @@ -26,15 +26,15 @@ { "name": "Lorenzo Mangani", "email": "lorenzo.mangani@gmail.com" + }, + { + "name": "Derek Perez", + "email": "derek@perez.earth" } ], "scripts": { - "build:lib": "cd lib && ./update_libchdb.sh && ./build.sh", - "build:ts": "bun build index.ts --target=bun --outfile=lib/index.js --sourcemap=inline && tsc --declaration --emitDeclarationOnly --types bun-types --declarationDir lib index.ts", - "build": "bun run build:ts && bun run build:lib" + "build:lib": "cd lib && ./update_libchdb.sh", + "build": "bun run build:lib" }, - "type": "module", - "types": "lib/index.d.ts", - "dependencies": { - } + "type": "module" } From 7e7d80b88deb0d464621f3fce56a524d52a2f2cb Mon Sep 17 00:00:00 2001 From: Derek Perez Date: Tue, 14 Oct 2025 21:25:05 -0700 Subject: [PATCH 2/2] update example and README.md --- README.md | 15 +++++++-------- example.ts | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e054899..2bc9e87 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Experimental [chDB](https://github.com/chdb-io/chdb) FFI bindings for the [bun r - experimental, unstable, subject to changes - requires [`libchdb`](https://github.com/chdb-io/chdb) on the system -- requires `gcc` or `clang` #### Build binding ```bash @@ -25,23 +24,23 @@ bun run example.ts import { query } from 'chdb-bun'; // Query (ephemeral) -var result = query("SELECT version()", "CSV"); -console.log(result); // 23.10.1.1 +var result = query("SELECT version(), 'Hello chDB', chdb()", "CSV"); +console.log(result); // "25.5.2.1","Hello chDB","3.6.0" ``` #### Session.Query(query, *format) ```javascript import { Session } from 'chdb-bun'; -const sess = new Session('./chdb-bun-tmp'); +const session = new Session('./chdb-bun-tmp'); // Query Session (persistent) -sess.query("CREATE FUNCTION IF NOT EXISTS hello AS () -> 'Hello chDB'", "CSV"); -var result = sess.query("SELECT hello()", "CSV"); -console.log(result); +session.query("CREATE FUNCTION IF NOT EXISTS hello AS () -> 'chDB'", "CSV"); +var result = session.query("SELECT hello()", "CSV"); +console.log(result); //"chDB" // Before cleanup, you can find the database files in `./chdb-bun-tmp` -sess.cleanup(); // cleanup session, this will delete the database +session.cleanup(); // cleanup session, this will delete the database ```
diff --git a/example.ts b/example.ts index 61bdc96..14537f7 100644 --- a/example.ts +++ b/example.ts @@ -6,12 +6,12 @@ var result; // Test standalone query result = query("SELECT version(), 'Hello chDB', chdb()", "CSV"); -console.log(result); +console.log(result); // "25.5.2.1","Hello chDB","3.6.0" // Test session query session.query("CREATE FUNCTION IF NOT EXISTS hello AS () -> 'chDB'", "CSV"); result = session.query("SELECT hello()", "CSV"); -console.log(result); +console.log(result); //"chDB" // Clean up the session session.cleanup();