From d381e10b2fc1a03c6acd53e7e36bce8218fca9e3 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 27 Mar 2026 16:16:07 -0700 Subject: [PATCH 01/10] ci: use precompiled libs for java --- .github/workflows/build-and-deploy.yml | 2 + .github/workflows/java-workflow.yml | 81 ++++++++++++++++++++++++-- .github/workflows/nodejs-workflow.yml | 4 +- CMakeLists.txt | 18 +++++- 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 307ec7adf..27669940d 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -207,9 +207,11 @@ jobs: build-java: if: ${{ github.event_name == 'schedule' || github.event.inputs.skipJava != 'true' }} + needs: [build-precompiled-bin] uses: ./.github/workflows/java-workflow.yml with: isNightly: ${{ github.event_name == 'schedule' || github.event.inputs.isNightly == 'true' }} + precompiledRunId: ${{ github.run_id }} secrets: inherit deploy-java: diff --git a/.github/workflows/java-workflow.yml b/.github/workflows/java-workflow.yml index f746cb591..d297531df 100644 --- a/.github/workflows/java-workflow.yml +++ b/.github/workflows/java-workflow.yml @@ -1,24 +1,47 @@ name: Build Java Lib on: workflow_dispatch: + inputs: + precompiledRunId: + description: "Run ID of the precompiled-bin job whose artifacts should be used" + required: true + type: string workflow_call: inputs: isNightly: type: boolean required: true default: false + precompiledRunId: + type: string + required: true env: PIP_BREAK_SYSTEM_PACKAGES: 1 jobs: build-linux-java-x86_64: runs-on: ubuntu-latest + env: + LBUG_LINUX_VARIANT: compat + LBUG_JAVA_PRECOMPILED_DIR: ${{ github.workspace }}/precompiled-liblbug + LBUG_JAVA_PRECOMPILED_LIB_PATH: ${{ github.workspace }}/precompiled-liblbug/liblbug.a steps: - uses: actions/checkout@v4 - name: Update submodules run: git submodule update --init --recursive tools/java_api + - name: Download precompiled liblbug + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LBUG_LIB_KIND: static + LBUG_PRECOMPILED_RUN_ID: ${{ inputs.precompiledRunId }} + LBUG_TARGET_DIR: ${{ env.LBUG_JAVA_PRECOMPILED_DIR }} + run: | + rm -rf "$LBUG_TARGET_DIR" + ./scripts/download-liblbug.sh + - name: Install uv run: python3 -m pip install uv @@ -32,6 +55,8 @@ jobs: working-directory: scripts - name: Build Java lib for Linux + env: + EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_API_USE_PRECOMPILED_LIB=TRUE -DLBUG_API_PRECOMPILED_LIB_PATH=${{ env.LBUG_JAVA_PRECOMPILED_LIB_PATH }} run: | make GEN=Ninja java @@ -41,13 +66,28 @@ jobs: path: tools/java_api/build/liblbug_java_native* build-linux-java-aarch64: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm + env: + LBUG_LINUX_VARIANT: compat + LBUG_JAVA_PRECOMPILED_DIR: ${{ github.workspace }}/precompiled-liblbug + LBUG_JAVA_PRECOMPILED_LIB_PATH: ${{ github.workspace }}/precompiled-liblbug/liblbug.a steps: - uses: actions/checkout@v4 - name: Update submodules run: git submodule update --init --recursive tools/java_api + - name: Download precompiled liblbug + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LBUG_LIB_KIND: static + LBUG_PRECOMPILED_RUN_ID: ${{ inputs.precompiledRunId }} + LBUG_TARGET_DIR: ${{ env.LBUG_JAVA_PRECOMPILED_DIR }} + run: | + rm -rf "$LBUG_TARGET_DIR" + ./scripts/download-liblbug.sh + - name: Install uv run: python3 -m pip install uv @@ -61,6 +101,8 @@ jobs: working-directory: scripts - name: Build Java lib for Linux + env: + EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_API_USE_PRECOMPILED_LIB=TRUE -DLBUG_API_PRECOMPILED_LIB_PATH=${{ env.LBUG_JAVA_PRECOMPILED_LIB_PATH }} run: make GEN=Ninja java - uses: actions/upload-artifact@v4 @@ -70,12 +112,26 @@ jobs: build-mac-java-arm: runs-on: macos-latest + env: + LBUG_JAVA_PRECOMPILED_DIR: ${{ github.workspace }}/precompiled-liblbug + LBUG_JAVA_PRECOMPILED_LIB_PATH: ${{ github.workspace }}/precompiled-liblbug/liblbug.a steps: - uses: actions/checkout@v4 - name: Update submodules run: git submodule update --init --recursive tools/java_api + - name: Download precompiled liblbug + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LBUG_LIB_KIND: static + LBUG_PRECOMPILED_RUN_ID: ${{ inputs.precompiledRunId }} + LBUG_TARGET_DIR: ${{ env.LBUG_JAVA_PRECOMPILED_DIR }} + run: | + rm -rf "$LBUG_TARGET_DIR" + ./scripts/download-liblbug.sh + - name: Install uv run: pip3 install uv @@ -89,11 +145,12 @@ jobs: working-directory: scripts - name: Build Java lib for Apple Silicon - run: | - arch -arm64 env JAVA_HOME=$(/usr/libexec/java_home) make GEN=Ninja java env: + EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_API_USE_PRECOMPILED_LIB=TRUE -DLBUG_API_PRECOMPILED_LIB_PATH=${{ env.LBUG_JAVA_PRECOMPILED_LIB_PATH }} MACOSX_DEPLOYMENT_TARGET: 13.3 ARCHFLAGS: "-arch arm64" + run: | + arch -arm64 env JAVA_HOME=$(/usr/libexec/java_home) make GEN=Ninja java - uses: actions/upload-artifact@v4 with: @@ -102,12 +159,26 @@ jobs: build-windows-java: runs-on: windows-latest + env: + LBUG_JAVA_PRECOMPILED_DIR: ${{ github.workspace }}/precompiled-liblbug + LBUG_JAVA_PRECOMPILED_LIB_PATH: ${{ github.workspace }}/precompiled-liblbug/lbug.lib steps: - uses: actions/checkout@v4 - name: Update submodules run: git submodule update --init --recursive tools/java_api + - name: Download precompiled liblbug + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LBUG_LIB_KIND: static + LBUG_PRECOMPILED_RUN_ID: ${{ inputs.precompiledRunId }} + LBUG_TARGET_DIR: ${{ env.LBUG_JAVA_PRECOMPILED_DIR }} + run: | + rm -rf "$LBUG_TARGET_DIR" + ./scripts/download-liblbug.sh + - name: Install uv run: pip3 install uv @@ -127,6 +198,8 @@ jobs: - name: Build Java lib for Windows shell: cmd + env: + EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_API_USE_PRECOMPILED_LIB=TRUE -DLBUG_API_PRECOMPILED_LIB_PATH=${{ env.LBUG_JAVA_PRECOMPILED_LIB_PATH }} run: | powershell.exe -Command "Add-MpPreference -ExclusionPath '${{ github.workspace }}'" call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" @@ -135,4 +208,4 @@ jobs: - uses: actions/upload-artifact@v4 with: name: java-lib-win-x86_64 - path: tools/java_api/build/liblbug_java_native* \ No newline at end of file + path: tools/java_api/build/liblbug_java_native* diff --git a/.github/workflows/nodejs-workflow.yml b/.github/workflows/nodejs-workflow.yml index 5b6c92deb..7329669f4 100644 --- a/.github/workflows/nodejs-workflow.yml +++ b/.github/workflows/nodejs-workflow.yml @@ -162,7 +162,7 @@ jobs: if: ${{ matrix.platform == 'win32' }} shell: cmd env: - EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_NODEJS_USE_PRECOMPILED_LIB=TRUE -DLBUG_NODEJS_PRECOMPILED_LIB_PATH=${{ env.LBUG_NODEJS_PRECOMPILED_LIB_PATH }} + EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_API_USE_PRECOMPILED_LIB=TRUE -DLBUG_API_PRECOMPILED_LIB_PATH=${{ env.LBUG_NODEJS_PRECOMPILED_LIB_PATH }} run: | powershell.exe -Command "Add-MpPreference -ExclusionPath '${{ github.workspace }}'" call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" @@ -175,7 +175,7 @@ jobs: env: CMAKE_C_COMPILER_LAUNCHER: ccache CMAKE_CXX_COMPILER_LAUNCHER: ccache - EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_NODEJS_USE_PRECOMPILED_LIB=TRUE -DLBUG_NODEJS_PRECOMPILED_LIB_PATH=${{ env.LBUG_NODEJS_PRECOMPILED_LIB_PATH }} + EXTRA_CMAKE_FLAGS: -DBUILD_LBUG=FALSE -DBUILD_SHELL=FALSE -DLBUG_API_USE_PRECOMPILED_LIB=TRUE -DLBUG_API_PRECOMPILED_LIB_PATH=${{ env.LBUG_NODEJS_PRECOMPILED_LIB_PATH }} run: | ccache -s make GEN=Ninja nodejs diff --git a/CMakeLists.txt b/CMakeLists.txt index a4172c1c2..52ecd17d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,14 +331,26 @@ option(BUILD_PYTHON "Build Python API." FALSE) option(BUILD_SHELL "Build Interactive Shell" TRUE) option(BUILD_SINGLE_FILE_HEADER "Build single file header. Requires Python >= 3.9." TRUE) option(BUILD_LBUG "Build Lbug." TRUE) -option(LBUG_NODEJS_USE_PRECOMPILED_LIB "Link the Node.js addon against a precompiled static liblbug archive." FALSE) -set(LBUG_NODEJS_PRECOMPILED_LIB_PATH "" CACHE FILEPATH "Path to the precompiled static liblbug archive used by the Node.js addon.") +option(LBUG_API_USE_PRECOMPILED_LIB "Link language bindings against a precompiled static liblbug archive." FALSE) +set(LBUG_API_PRECOMPILED_LIB_PATH "" CACHE FILEPATH "Path to the precompiled static liblbug archive used by language bindings.") +option(LBUG_NODEJS_USE_PRECOMPILED_LIB "Deprecated alias for LBUG_API_USE_PRECOMPILED_LIB." FALSE) +set(LBUG_NODEJS_PRECOMPILED_LIB_PATH "" CACHE FILEPATH "Deprecated alias for LBUG_API_PRECOMPILED_LIB_PATH.") option(ENABLE_BACKTRACES "Enable backtrace printing for exceptions and segfaults" FALSE) option(PREFER_SYSTEM_DEPS "Only download certain deps if not found on the system" TRUE) if(LBUG_NODEJS_USE_PRECOMPILED_LIB) + set(LBUG_API_USE_PRECOMPILED_LIB TRUE) +endif() +if(LBUG_NODEJS_PRECOMPILED_LIB_PATH AND NOT LBUG_API_PRECOMPILED_LIB_PATH) + set(LBUG_API_PRECOMPILED_LIB_PATH "${LBUG_NODEJS_PRECOMPILED_LIB_PATH}") +endif() +if(LBUG_API_USE_PRECOMPILED_LIB) + set(LBUG_NODEJS_USE_PRECOMPILED_LIB TRUE CACHE BOOL "Deprecated alias for LBUG_API_USE_PRECOMPILED_LIB." FORCE) set(BUILD_LBUG FALSE CACHE BOOL "Build Lbug." FORCE) endif() +if(LBUG_API_PRECOMPILED_LIB_PATH) + set(LBUG_NODEJS_PRECOMPILED_LIB_PATH "${LBUG_API_PRECOMPILED_LIB_PATH}" CACHE FILEPATH "Deprecated alias for LBUG_API_PRECOMPILED_LIB_PATH." FORCE) +endif() option(BUILD_LCOV "Build coverage report." FALSE) if(${BUILD_LCOV}) @@ -405,7 +417,7 @@ function(add_lbug_api_test TEST_NAME) endfunction() # Windows doesn't support dynamic lookup, so we have to link extensions against lbug. -if (MSVC AND (NOT BUILD_EXTENSIONS EQUAL "") AND NOT LBUG_NODEJS_USE_PRECOMPILED_LIB) +if (MSVC AND (NOT BUILD_EXTENSIONS EQUAL "") AND NOT LBUG_API_USE_PRECOMPILED_LIB) set(BUILD_LBUG TRUE) endif () From f14c04f091f8f2ab1b695cdc25650f2760a9b32e Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 27 Mar 2026 18:01:15 -0700 Subject: [PATCH 02/10] Extend public C API for Java bindings --- src/c_api/data_type.cpp | 18 ++++++++++++++++++ src/c_api/database.cpp | 5 ++++- src/c_api/value.cpp | 28 ++++++++++++++++++++++++++++ src/include/c_api/lbug.h | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/c_api/data_type.cpp b/src/c_api/data_type.cpp index 37c57ca84..c8b40c224 100644 --- a/src/c_api/data_type.cpp +++ b/src/c_api/data_type.cpp @@ -55,6 +55,24 @@ lbug_data_type_id lbug_data_type_get_id(lbug_logical_type* data_type) { return static_cast(data_type_id_u8); } +lbug_state lbug_data_type_get_child_type(lbug_logical_type* data_type, lbug_logical_type* out_result) { + auto* parent_type = static_cast(data_type->_data_type); + try { + switch (parent_type->getLogicalTypeID()) { + case LogicalTypeID::ARRAY: + out_result->_data_type = new LogicalType(ArrayType::getChildType(*parent_type).copy()); + return LbugSuccess; + case LogicalTypeID::LIST: + out_result->_data_type = new LogicalType(ListType::getChildType(*parent_type).copy()); + return LbugSuccess; + default: + return LbugError; + } + } catch (Exception& e) { + return LbugError; + } +} + lbug_state lbug_data_type_get_num_elements_in_array(lbug_logical_type* data_type, uint64_t* out_result) { auto parent_type = static_cast(data_type->_data_type); diff --git a/src/c_api/database.cpp b/src/c_api/database.cpp index 80c60689d..54da8e210 100644 --- a/src/c_api/database.cpp +++ b/src/c_api/database.cpp @@ -10,7 +10,8 @@ lbug_state lbug_database_init(const char* database_path, lbug_system_config conf std::string database_path_str = database_path; auto systemConfig = SystemConfig(config.buffer_pool_size, config.max_num_threads, config.enable_compression, config.read_only, config.max_db_size, config.auto_checkpoint, - config.checkpoint_threshold); + config.checkpoint_threshold, true, config.throw_on_wal_replay_failure, + config.enable_checksums); #if defined(__APPLE__) systemConfig.threadQos = config.thread_qos; @@ -42,6 +43,8 @@ lbug_system_config lbug_default_system_config() { cSystemConfig.max_db_size = config.maxDBSize; cSystemConfig.auto_checkpoint = config.autoCheckpoint; cSystemConfig.checkpoint_threshold = config.checkpointThreshold; + cSystemConfig.throw_on_wal_replay_failure = config.throwOnWalReplayFailure; + cSystemConfig.enable_checksums = config.enableChecksums; #if defined(__APPLE__) cSystemConfig.thread_qos = config.threadQos; #endif diff --git a/src/c_api/value.cpp b/src/c_api/value.cpp index 3cbc358e8..343ae2d7b 100644 --- a/src/c_api/value.cpp +++ b/src/c_api/value.cpp @@ -113,6 +113,12 @@ lbug_value* lbug_value_create_double(double val_) { return c_value; } +lbug_value* lbug_value_create_decimal(const char* val_, uint32_t precision, uint32_t scale) { + auto* c_value = (lbug_value*)calloc(1, sizeof(lbug_value)); + c_value->_value = new Value(LogicalType::DECIMAL(precision, scale), val_); + return c_value; +} + lbug_value* lbug_value_create_internal_id(lbug_internal_id_t val_) { auto* c_value = (lbug_value*)calloc(1, sizeof(lbug_value)); internalID_t id(val_.offset, val_.table_id); @@ -175,6 +181,12 @@ lbug_value* lbug_value_create_string(const char* val_) { return c_value; } +lbug_value* lbug_value_create_uuid(const char* val_) { + auto* c_value = (lbug_value*)calloc(1, sizeof(lbug_value)); + c_value->_value = new Value(uuid{UUID::fromCString(val_, strlen(val_))}); + return c_value; +} + lbug_state lbug_value_create_list(uint64_t num_elements, lbug_value** elements, lbug_value** out_value) { if (num_elements == 0) { @@ -345,6 +357,22 @@ lbug_state lbug_value_get_struct_field_name(lbug_value* value, uint64_t index, c return LbugSuccess; } +lbug_state lbug_value_get_struct_field_index(lbug_value* value, const char* field_name, + uint64_t* out_result) { + auto physical_type_id = static_cast(value->_value)->getDataType().getPhysicalType(); + if (physical_type_id != PhysicalTypeID::STRUCT) { + return LbugError; + } + auto val = static_cast(value->_value); + const auto& data_type = val->getDataType(); + auto index = StructType::getFieldIdx(data_type, field_name); + if (index == INVALID_STRUCT_FIELD_IDX) { + return LbugError; + } + *out_result = index; + return LbugSuccess; +} + lbug_state lbug_value_get_struct_field_value(lbug_value* value, uint64_t index, lbug_value* out_value) { return lbug_value_get_list_element(value, index, out_value); diff --git a/src/include/c_api/lbug.h b/src/include/c_api/lbug.h index c5d6cbac1..1fc72d0e9 100644 --- a/src/include/c_api/lbug.h +++ b/src/include/c_api/lbug.h @@ -132,6 +132,10 @@ typedef struct { // The threshold of the WAL file size in bytes. When the size of the // WAL file exceeds this threshold, the database will checkpoint if auto_checkpoint is true. uint64_t checkpoint_threshold; + // If true, any WAL replay failure when loading the database will raise an error. + bool throw_on_wal_replay_failure; + // If true, checksums are enabled for WAL and storage pages. + bool enable_checksums; #if defined(__APPLE__) // The thread quality of service (QoS) for the worker threads. @@ -807,6 +811,14 @@ LBUG_C_API bool lbug_data_type_equals(lbug_logical_type* data_type1, lbug_logica * @param data_type The data type instance to return. */ LBUG_C_API lbug_data_type_id lbug_data_type_get_id(lbug_logical_type* data_type); +/** + * @brief Returns the child type of the given ARRAY or LIST data type. + * @param data_type The ARRAY or LIST data type instance. + * @param[out] out_result The output parameter that will hold the child type. + * @return The state indicating the success or failure of the operation. + */ +LBUG_C_API lbug_state lbug_data_type_get_child_type(lbug_logical_type* data_type, + lbug_logical_type* out_result); /** * @brief Returns the number of elements for array. * @param data_type The data type instance to return. @@ -916,6 +928,15 @@ LBUG_C_API lbug_value* lbug_value_create_float(float val_); * @param val_ The double value of the value to create. */ LBUG_C_API lbug_value* lbug_value_create_double(double val_); +/** + * @brief Creates a value with decimal type and the given string representation. + * Caller is responsible for destroying the returned value. + * @param val_ The decimal value to create. + * @param precision The decimal precision. + * @param scale The decimal scale. + */ +LBUG_C_API lbug_value* lbug_value_create_decimal(const char* val_, uint32_t precision, + uint32_t scale); /** * @brief Creates a value with internal_id type and the given internal_id value. Caller is * responsible for destroying the returned value. @@ -970,6 +991,12 @@ LBUG_C_API lbug_value* lbug_value_create_interval(lbug_interval_t val_); * @param val_ The string value of the value to create. */ LBUG_C_API lbug_value* lbug_value_create_string(const char* val_); +/** + * @brief Creates a value with UUID type and the given string representation. + * Caller is responsible for destroying the returned value. + * @param val_ The UUID string value to create. + */ +LBUG_C_API lbug_value* lbug_value_create_uuid(const char* val_); /** * @brief Creates a list value with the given number of elements and the given elements. * The caller needs to make sure that all elements have the same type. @@ -1062,6 +1089,15 @@ LBUG_C_API lbug_state lbug_value_get_struct_num_fields(lbug_value* value, uint64 */ LBUG_C_API lbug_state lbug_value_get_struct_field_name(lbug_value* value, uint64_t index, char** out_result); +/** + * @brief Returns the field index for the given field name in the given struct value. + * @param value The STRUCT value to inspect. + * @param field_name The field name to look up. + * @param[out] out_result The output parameter that will hold the field index. + * @return The state indicating the success or failure of the operation. + */ +LBUG_C_API lbug_state lbug_value_get_struct_field_index(lbug_value* value, const char* field_name, + uint64_t* out_result); /** * @brief Returns the field value at index of the given struct value. The value must be of physical * type STRUCT (STRUCT, NODE, REL, RECURSIVE_REL, UNION). From be95824afed129fe3eba0151de93fee27b6f9f3e Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 27 Mar 2026 18:07:51 -0700 Subject: [PATCH 03/10] Fix MSVC UUID name collision in C API --- src/c_api/value.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/c_api/value.cpp b/src/c_api/value.cpp index 343ae2d7b..01009cc5f 100644 --- a/src/c_api/value.cpp +++ b/src/c_api/value.cpp @@ -183,7 +183,8 @@ lbug_value* lbug_value_create_string(const char* val_) { lbug_value* lbug_value_create_uuid(const char* val_) { auto* c_value = (lbug_value*)calloc(1, sizeof(lbug_value)); - c_value->_value = new Value(uuid{UUID::fromCString(val_, strlen(val_))}); + c_value->_value = new Value( + lbug::common::uuid{lbug::common::UUID::fromCString(val_, strlen(val_))}); return c_value; } From 47d0f8e66032232d9a4ff8bd540f59ac24bee9f1 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sat, 28 Mar 2026 00:22:55 -0700 Subject: [PATCH 04/10] c_api: add thread-local error message infrastructure Add setLastCAPIErrorMessage, clearLastCAPIErrorMessage, and takeLastCAPIErrorMessage to helpers.h/helpers.cpp using a thread_local string. These will be used by callers (connection, database, query result) to surface exception messages back through the C API boundary. --- src/c_api/helpers.cpp | 21 +++++++++++++++++++++ src/include/c_api/helpers.h | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/src/c_api/helpers.cpp b/src/c_api/helpers.cpp index 4eb8955dd..ec666ce03 100644 --- a/src/c_api/helpers.cpp +++ b/src/c_api/helpers.cpp @@ -2,6 +2,10 @@ #include +namespace { +thread_local std::string lastCAPIErrorMessage; +} + #ifdef _WIN32 const uint64_t NS_TO_SEC = 10000000ULL; const uint64_t SEC_TO_UNIX_EPOCH = 11644473600ULL; @@ -45,6 +49,23 @@ int32_t convertTimeToTm(time_t time, struct tm* out_tm) { } #endif +void setLastCAPIErrorMessage(const std::string& message) { + lastCAPIErrorMessage = message; +} + +void clearLastCAPIErrorMessage() { + lastCAPIErrorMessage.clear(); +} + +char* takeLastCAPIErrorMessage() { + if (lastCAPIErrorMessage.empty()) { + return nullptr; + } + auto* message = convertToOwnedCString(lastCAPIErrorMessage); + lastCAPIErrorMessage.clear(); + return message; +} + char* convertToOwnedCString(const std::string& str) { size_t src_len = str.size(); auto* c_str = (char*)malloc(sizeof(char) * (src_len + 1)); diff --git a/src/include/c_api/helpers.h b/src/include/c_api/helpers.h index 21b386ab4..4d3e7cdba 100644 --- a/src/include/c_api/helpers.h +++ b/src/include/c_api/helpers.h @@ -12,4 +12,10 @@ time_t convertTmToTime(struct tm tm); int32_t convertTimeToTm(time_t time, struct tm* out_tm); #endif +void setLastCAPIErrorMessage(const std::string& message); + +void clearLastCAPIErrorMessage(); + +char* takeLastCAPIErrorMessage(); + char* convertToOwnedCString(const std::string& str); From 87e7875dc04fa29d47322f6e0378fc5fe72b0837 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sat, 28 Mar 2026 00:23:00 -0700 Subject: [PATCH 05/10] c_api: propagate error messages through connection, database, and query result Clear the last error before each operation and set it on exception so callers can retrieve the message via takeLastCAPIErrorMessage(). Also remove the now-redundant !isSuccess() early-return in lbug_connection_query and lbug_connection_execute: a failed query is returned to the caller as a QueryResult object (with its own isSuccess/getErrorMessage), so the C API layer should not treat it as a hard error. --- src/c_api/connection.cpp | 13 +++++++------ src/c_api/database.cpp | 3 +++ src/c_api/query_result.cpp | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/c_api/connection.cpp b/src/c_api/connection.cpp index ab7035317..80820497c 100644 --- a/src/c_api/connection.cpp +++ b/src/c_api/connection.cpp @@ -1,4 +1,5 @@ #include "c_api/lbug.h" +#include "c_api/helpers.h" #include "common/exception/exception.h" #include "main/lbug.h" @@ -66,6 +67,7 @@ lbug_state lbug_connection_query(lbug_connection* connection, const char* query, return LbugError; } try { + clearLastCAPIErrorMessage(); auto query_result = static_cast(connection->_connection)->query(query).release(); if (query_result == nullptr) { @@ -73,11 +75,9 @@ lbug_state lbug_connection_query(lbug_connection* connection, const char* query, } out_query_result->_query_result = query_result; out_query_result->_is_owned_by_cpp = false; - if (!query_result->isSuccess()) { - return LbugError; - } return LbugSuccess; } catch (Exception& e) { + setLastCAPIErrorMessage(e.what()); return LbugError; } } @@ -88,6 +88,7 @@ lbug_state lbug_connection_prepare(lbug_connection* connection, const char* quer return LbugError; } try { + clearLastCAPIErrorMessage(); auto prepared_statement = static_cast(connection->_connection)->prepare(query).release(); if (prepared_statement == nullptr) { @@ -98,6 +99,7 @@ lbug_state lbug_connection_prepare(lbug_connection* connection, const char* quer new std::unordered_map>; return LbugSuccess; } catch (Exception& e) { + setLastCAPIErrorMessage(e.what()); return LbugError; } return LbugSuccess; @@ -111,6 +113,7 @@ lbug_state lbug_connection_execute(lbug_connection* connection, return LbugError; } try { + clearLastCAPIErrorMessage(); auto prepared_statement_ptr = static_cast(prepared_statement->_prepared_statement); auto bound_values = static_cast>*>( @@ -132,11 +135,9 @@ lbug_state lbug_connection_execute(lbug_connection* connection, } out_query_result->_query_result = query_result; out_query_result->_is_owned_by_cpp = false; - if (!query_result->isSuccess()) { - return LbugError; - } return LbugSuccess; } catch (Exception& e) { + setLastCAPIErrorMessage(e.what()); return LbugError; } } diff --git a/src/c_api/database.cpp b/src/c_api/database.cpp index 54da8e210..43c229cc0 100644 --- a/src/c_api/database.cpp +++ b/src/c_api/database.cpp @@ -1,4 +1,5 @@ #include "c_api/lbug.h" +#include "c_api/helpers.h" #include "common/exception/exception.h" #include "main/lbug.h" using namespace lbug::main; @@ -7,6 +8,7 @@ using namespace lbug::common; lbug_state lbug_database_init(const char* database_path, lbug_system_config config, lbug_database* out_database) { try { + clearLastCAPIErrorMessage(); std::string database_path_str = database_path; auto systemConfig = SystemConfig(config.buffer_pool_size, config.max_num_threads, config.enable_compression, config.read_only, config.max_db_size, config.auto_checkpoint, @@ -19,6 +21,7 @@ lbug_state lbug_database_init(const char* database_path, lbug_system_config conf out_database->_database = new Database(database_path_str, systemConfig); } catch (Exception& e) { out_database->_database = nullptr; + setLastCAPIErrorMessage(e.what()); return LbugError; } return LbugSuccess; diff --git a/src/c_api/query_result.cpp b/src/c_api/query_result.cpp index bb3f77aae..d6108f457 100644 --- a/src/c_api/query_result.cpp +++ b/src/c_api/query_result.cpp @@ -96,11 +96,13 @@ lbug_state lbug_query_result_get_next_query_result(lbug_query_result* query_resu lbug_state lbug_query_result_get_next(lbug_query_result* query_result, lbug_flat_tuple* out_flat_tuple) { try { + clearLastCAPIErrorMessage(); auto flat_tuple = static_cast(query_result->_query_result)->getNext(); out_flat_tuple->_flat_tuple = flat_tuple.get(); out_flat_tuple->_is_owned_by_cpp = true; return LbugSuccess; } catch (Exception& e) { + setLastCAPIErrorMessage(e.what()); return LbugError; } } From c20cc13a895802359c5dfec8c4c131d26cf3627f Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sat, 28 Mar 2026 00:23:07 -0700 Subject: [PATCH 06/10] c_api: fix decimal value creation and UUID string retrieval lbug_value_create_decimal previously passed a raw string to the Value constructor which does not perform decimal casting. Switch to Value::createDefaultValue + decimalCast dispatched on the physical type (INT16/INT32/INT64/INT128). lbug_value_get_uuid previously called getValue() which reads the raw binary blob rather than producing a formatted UUID string. Use toString() instead. --- src/c_api/value.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/c_api/value.cpp b/src/c_api/value.cpp index 01009cc5f..6a7167104 100644 --- a/src/c_api/value.cpp +++ b/src/c_api/value.cpp @@ -115,7 +115,26 @@ lbug_value* lbug_value_create_double(double val_) { lbug_value* lbug_value_create_decimal(const char* val_, uint32_t precision, uint32_t scale) { auto* c_value = (lbug_value*)calloc(1, sizeof(lbug_value)); - c_value->_value = new Value(LogicalType::DECIMAL(precision, scale), val_); + auto decimalType = LogicalType::DECIMAL(precision, scale); + auto value = Value::createDefaultValue(decimalType); + switch (decimalType.getPhysicalType()) { + case PhysicalTypeID::INT16: + lbug::function::decimalCast(val_, strlen(val_), value.val.int16Val, decimalType); + break; + case PhysicalTypeID::INT32: + lbug::function::decimalCast(val_, strlen(val_), value.val.int32Val, decimalType); + break; + case PhysicalTypeID::INT64: + lbug::function::decimalCast(val_, strlen(val_), value.val.int64Val, decimalType); + break; + case PhysicalTypeID::INT128: + lbug::function::decimalCast(val_, strlen(val_), value.val.int128Val, decimalType); + break; + default: + free(c_value); + return nullptr; + } + c_value->_value = new Value(std::move(value)); return c_value; } @@ -784,8 +803,7 @@ lbug_state lbug_value_get_uuid(lbug_value* value, char** out_result) { return LbugError; } try { - *out_result = - convertToOwnedCString(static_cast(value->_value)->getValue()); + *out_result = convertToOwnedCString(static_cast(value->_value)->toString()); } catch (Exception& e) { return LbugError; } From 1c098443ea3898b75d83ec1e54a0ce4c7e9b3558 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 27 Mar 2026 22:28:52 -0700 Subject: [PATCH 07/10] Upload Java test diagnostics in CI --- .github/workflows/build-and-deploy.yml | 12 +++++++++++- tools/java_api | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 27669940d..4197692ec 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -266,7 +266,17 @@ jobs: - name: Create Java JAR working-directory: tools/java_api run: | - ./gradlew build + ./gradlew build --stacktrace --info --warning-mode all --no-parallel --max-workers=1 + + - name: Upload Java test diagnostics + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: java-test-diagnostics + path: | + tools/java_api/build/test-results/test + tools/java_api/build/reports/tests/test + tools/java_api/build/hs_err_pid*.log - name: Upload Java JAR uses: actions/upload-artifact@v4 diff --git a/tools/java_api b/tools/java_api index 9db246545..673c38c6a 160000 --- a/tools/java_api +++ b/tools/java_api @@ -1 +1 @@ -Subproject commit 9db2465450b5a36e6cad275de6cdc477acc35423 +Subproject commit 673c38c6a19ba574e9aede894e948078ccad5ddf From 1831997efe3ae57133b20d56e345acf1da9ba080 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sat, 28 Mar 2026 09:26:32 -0700 Subject: [PATCH 08/10] Expose lbug_get_last_error() in public C API Add lbug_get_last_error() to lbug.h so callers outside the library can retrieve the thread-local last-error message without depending on the internal c_api/helpers.h header. Implemented as a thin wrapper around takeLastCAPIErrorMessage() in helpers.cpp. Bump tools/java_api submodule to use the new public API. --- src/c_api/helpers.cpp | 5 +++++ src/include/c_api/lbug.h | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/c_api/helpers.cpp b/src/c_api/helpers.cpp index ec666ce03..c7839f79e 100644 --- a/src/c_api/helpers.cpp +++ b/src/c_api/helpers.cpp @@ -1,4 +1,5 @@ #include "c_api/helpers.h" +#include "c_api/lbug.h" #include @@ -73,3 +74,7 @@ char* convertToOwnedCString(const std::string& str) { c_str[src_len] = '\0'; return c_str; } + +char* lbug_get_last_error() { + return takeLastCAPIErrorMessage(); +} diff --git a/src/include/c_api/lbug.h b/src/include/c_api/lbug.h index 1fc72d0e9..1e0a92430 100644 --- a/src/include/c_api/lbug.h +++ b/src/include/c_api/lbug.h @@ -1621,4 +1621,12 @@ LBUG_C_API char* lbug_get_version(); * @brief Returns the storage version of the Lbug library. */ LBUG_C_API uint64_t lbug_get_storage_version(); + +// Error handling +/** + * @brief Returns the last error message set by the C API, consuming it (subsequent calls return + * nullptr until another error occurs). The caller is responsible for freeing the returned string + * using lbug_destroy_string(). Returns nullptr if no error has been recorded. + */ +LBUG_C_API char* lbug_get_last_error(); #undef LBUG_C_API From c8f0f7114c37e9592cde9a9756980ea025aaea27 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sat, 28 Mar 2026 11:02:01 -0700 Subject: [PATCH 09/10] cpp: run clang-format --- src/c_api/connection.cpp | 2 +- src/c_api/data_type.cpp | 3 ++- src/c_api/database.cpp | 2 +- src/c_api/helpers.cpp | 3 ++- src/c_api/value.cpp | 4 ++-- tools/java_api | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/c_api/connection.cpp b/src/c_api/connection.cpp index 80820497c..85d3f1c0f 100644 --- a/src/c_api/connection.cpp +++ b/src/c_api/connection.cpp @@ -1,5 +1,5 @@ -#include "c_api/lbug.h" #include "c_api/helpers.h" +#include "c_api/lbug.h" #include "common/exception/exception.h" #include "main/lbug.h" diff --git a/src/c_api/data_type.cpp b/src/c_api/data_type.cpp index c8b40c224..1110f1368 100644 --- a/src/c_api/data_type.cpp +++ b/src/c_api/data_type.cpp @@ -55,7 +55,8 @@ lbug_data_type_id lbug_data_type_get_id(lbug_logical_type* data_type) { return static_cast(data_type_id_u8); } -lbug_state lbug_data_type_get_child_type(lbug_logical_type* data_type, lbug_logical_type* out_result) { +lbug_state lbug_data_type_get_child_type(lbug_logical_type* data_type, + lbug_logical_type* out_result) { auto* parent_type = static_cast(data_type->_data_type); try { switch (parent_type->getLogicalTypeID()) { diff --git a/src/c_api/database.cpp b/src/c_api/database.cpp index 43c229cc0..c759f4caa 100644 --- a/src/c_api/database.cpp +++ b/src/c_api/database.cpp @@ -1,5 +1,5 @@ -#include "c_api/lbug.h" #include "c_api/helpers.h" +#include "c_api/lbug.h" #include "common/exception/exception.h" #include "main/lbug.h" using namespace lbug::main; diff --git a/src/c_api/helpers.cpp b/src/c_api/helpers.cpp index c7839f79e..9f0ded2b5 100644 --- a/src/c_api/helpers.cpp +++ b/src/c_api/helpers.cpp @@ -1,8 +1,9 @@ #include "c_api/helpers.h" -#include "c_api/lbug.h" #include +#include "c_api/lbug.h" + namespace { thread_local std::string lastCAPIErrorMessage; } diff --git a/src/c_api/value.cpp b/src/c_api/value.cpp index 6a7167104..73f0f2cfe 100644 --- a/src/c_api/value.cpp +++ b/src/c_api/value.cpp @@ -202,8 +202,8 @@ lbug_value* lbug_value_create_string(const char* val_) { lbug_value* lbug_value_create_uuid(const char* val_) { auto* c_value = (lbug_value*)calloc(1, sizeof(lbug_value)); - c_value->_value = new Value( - lbug::common::uuid{lbug::common::UUID::fromCString(val_, strlen(val_))}); + c_value->_value = + new Value(lbug::common::uuid{lbug::common::UUID::fromCString(val_, strlen(val_))}); return c_value; } diff --git a/tools/java_api b/tools/java_api index 673c38c6a..166d5e2f1 160000 --- a/tools/java_api +++ b/tools/java_api @@ -1 +1 @@ -Subproject commit 673c38c6a19ba574e9aede894e948078ccad5ddf +Subproject commit 166d5e2f139f5075be6974c24ab8e286810713ff From b14e3d6186cec025ecfb6517ee89aa3c748f06ce Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Mon, 30 Mar 2026 16:00:45 -0700 Subject: [PATCH 10/10] c-api: return LbugError on failure --- src/c_api/connection.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/c_api/connection.cpp b/src/c_api/connection.cpp index 85d3f1c0f..66e884313 100644 --- a/src/c_api/connection.cpp +++ b/src/c_api/connection.cpp @@ -75,6 +75,9 @@ lbug_state lbug_connection_query(lbug_connection* connection, const char* query, } out_query_result->_query_result = query_result; out_query_result->_is_owned_by_cpp = false; + if (!query_result->isSuccess()) { + return LbugError; + } return LbugSuccess; } catch (Exception& e) { setLastCAPIErrorMessage(e.what()); @@ -135,6 +138,9 @@ lbug_state lbug_connection_execute(lbug_connection* connection, } out_query_result->_query_result = query_result; out_query_result->_is_owned_by_cpp = false; + if (!query_result->isSuccess()) { + return LbugError; + } return LbugSuccess; } catch (Exception& e) { setLastCAPIErrorMessage(e.what());