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/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/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 e574560..0000000
Binary files a/bun.lockb and /dev/null differ
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/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();
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"
}