diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml
index cefb4480b5..2a38be7551 100644
--- a/.github/actions/setup-rust/action.yml
+++ b/.github/actions/setup-rust/action.yml
@@ -34,60 +34,19 @@ runs:
steps:
- name: Install Rust toolchain
shell: bash
- working-directory: ${{ github.workspace }}/rust
+ env:
+ SEEKDB_REPO_ROOT: ${{ github.workspace }}
+ SEEKDB_CARGO_RSProxy_MIRROR: ${{ inputs.crates-mirror }}
run: |
set -euo pipefail
-
- if ! command -v rustup >/dev/null 2>&1 || ! command -v cargo >/dev/null 2>&1; then
- install_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/seekdb-rust"
- export CARGO_HOME="${CARGO_HOME:-$install_root/cargo}"
- export RUSTUP_HOME="${RUSTUP_HOME:-$install_root/rustup}"
- export PATH="$CARGO_HOME/bin:$PATH"
- mkdir -p "$install_root" "$CARGO_HOME" "$RUSTUP_HOME"
-
- rustup_init="$install_root/rustup-init.sh"
- rustup_init_url="${RUSTUP_INIT_URL:-https://sh.rustup.rs}"
- if command -v curl >/dev/null 2>&1; then
- curl --proto '=https' --tlsv1.2 --retry 3 --fail --silent \
- --show-error --location "$rustup_init_url" --output "$rustup_init"
- elif command -v wget >/dev/null 2>&1; then
- wget --tries=3 --quiet -O "$rustup_init" "$rustup_init_url"
- else
- echo "setup-rust: curl or wget is required" >&2
- exit 1
- fi
- sh "$rustup_init" -y --profile minimal --default-toolchain none \
- --no-modify-path
-
- fi
-
- # Export the homes unconditionally so later steps (mirror config,
- # registry cache) can rely on them even when rustup pre-existed.
- export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
- export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}"
+ # shellcheck source=/dev/null
+ source "${{ github.workspace }}/.github/script/install-rust.sh"
{
- echo "CARGO_HOME=$CARGO_HOME"
- echo "RUSTUP_HOME=$RUSTUP_HOME"
+ echo "CARGO_HOME=${CARGO_HOME}"
+ echo "RUSTUP_HOME=${RUSTUP_HOME}"
} >> "$GITHUB_ENV"
-
rustup_bin_dir="$(dirname "$(command -v rustup)")"
- export PATH="$rustup_bin_dir:$PATH"
echo "$rustup_bin_dir" >> "$GITHUB_PATH"
- RUSTUP_AUTO_INSTALL=1 cargo --version
- rustc --version
-
- - name: Route cargo through the rsproxy crates mirror
- if: inputs.crates-mirror == 'true'
- shell: bash
- run: |
- set -euo pipefail
- mkdir -p "$CARGO_HOME"
- cat >> "$CARGO_HOME/config.toml" <<'EOF'
- [source.crates-io]
- replace-with = "rsproxy-sparse"
- [source.rsproxy-sparse]
- registry = "sparse+https://rsproxy.cn/index/"
- EOF
# NOTE: needs a node20-capable runner (same constraint as the ccache action
# pin in buildbase); disable via `cache: 'false'` on runners that cannot.
diff --git a/.github/script/install-rust.sh b/.github/script/install-rust.sh
new file mode 100755
index 0000000000..5871256720
--- /dev/null
+++ b/.github/script/install-rust.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+# Install the Rust toolchain required by the seekdb build (see rust/rust-toolchain.toml).
+# Used by .github/actions/setup-rust and CI jobs that run inside Docker containers.
+set -euo pipefail
+
+repo_root="${SEEKDB_REPO_ROOT:-${GITHUB_WORKSPACE:-}}"
+if [[ -z "$repo_root" ]]; then
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+fi
+
+install_root="${SEEKDB_RUST_INSTALL_ROOT:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/seekdb-rust}"
+export CARGO_HOME="${CARGO_HOME:-$install_root/cargo}"
+export RUSTUP_HOME="${RUSTUP_HOME:-$install_root/rustup}"
+
+if ! command -v rustup >/dev/null 2>&1 || ! command -v cargo >/dev/null 2>&1; then
+ mkdir -p "$install_root" "$CARGO_HOME" "$RUSTUP_HOME"
+ export PATH="$CARGO_HOME/bin:$PATH"
+
+ rustup_init="$install_root/rustup-init.sh"
+ rustup_init_url="${RUSTUP_INIT_URL:-https://sh.rustup.rs}"
+ if command -v curl >/dev/null 2>&1; then
+ curl --proto '=https' --tlsv1.2 --retry 3 --fail --silent \
+ --show-error --location "$rustup_init_url" --output "$rustup_init"
+ elif command -v wget >/dev/null 2>&1; then
+ wget --tries=3 --quiet -O "$rustup_init" "$rustup_init_url"
+ else
+ echo "install-rust: curl or wget is required" >&2
+ exit 1
+ fi
+ sh "$rustup_init" -y --profile minimal --default-toolchain none --no-modify-path
+fi
+
+export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
+export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}"
+rustup_bin_dir="$(dirname "$(command -v rustup)")"
+export PATH="$rustup_bin_dir:$PATH"
+
+if [[ "${SEEKDB_CARGO_RSProxy_MIRROR:-}" == "true" ]]; then
+ mkdir -p "$CARGO_HOME"
+ cat >> "$CARGO_HOME/config.toml" <<'EOF'
+[source.crates-io]
+replace-with = "rsproxy-sparse"
+[source.rsproxy-sparse]
+registry = "sparse+https://rsproxy.cn/index/"
+EOF
+fi
+
+# The version check triggers rustup's auto-install of the pinned toolchain
+# (rust-toolchain.toml). Run it in a subshell: this script is sourced by CI
+# steps that call `bash build.sh ...` right afterwards, and the cd must not
+# leak into the caller's working directory.
+(
+ cd "$repo_root/rust"
+ RUSTUP_AUTO_INSTALL=1 cargo --version
+ rustc --version
+)
+
+# Binding-test crates (e.g. unittest/include/rust) have no rust-toolchain.toml
+# in their ancestor chain, so with --default-toolchain none (above) a bare
+# `cargo` there fails: "no default is configured". Pin the default to the
+# workspace's pinned channel, read from rust-toolchain.toml so the version
+# stays in one place.
+pinned_channel="$(sed -n 's/^channel = "\(.*\)"/\1/p' "$repo_root/rust/rust-toolchain.toml" | head -n 1)"
+if [[ -z "$pinned_channel" ]]; then
+ echo "install-rust: failed to read channel from $repo_root/rust/rust-toolchain.toml" >&2
+ exit 1
+fi
+rustup default "$pinned_channel"
diff --git a/.github/workflows/build-libseekdb.yml b/.github/workflows/build-libseekdb.yml
new file mode 100644
index 0000000000..bfa3004881
--- /dev/null
+++ b/.github/workflows/build-libseekdb.yml
@@ -0,0 +1,920 @@
+# Build, pack and upload libseekdb for multiple platforms (linux x64/arm64, macos arm64, windows x64, android arm64-v8a) to S3
+#
+# Reference build environments (use these systems/env as the standard):
+# linux-x64: runner ubuntu-22.04, container quay.io/pypa/manylinux2014_x86_64 (glibc 2.17, CentOS 7+), zip libseekdb-linux-x64.zip
+# linux-arm64: runner ubuntu-22.04-arm, container quay.io/pypa/manylinux2014_aarch64 (glibc 2.17), zip libseekdb-linux-arm64.zip
+# darwin-arm64: runner macos-15, native, zip libseekdb-darwin-arm64.zip (min macOS 11.0)
+# windows-x64: runner windows-2022, .\build.ps1 + libseekdb-build.ps1, zip libseekdb-windows-x64.zip (seekdb.dll + libs/*.dll)
+# android-arm64-v8a: runner macos-15, NDK + ./build.sh --android, zip libseekdb-android-arm64-v8a.zip
+#
+# macOS builds use runner macos-15 and set CMAKE_OSX_DEPLOYMENT_TARGET=11.0 so the dylib runs on macOS 11+ (Big Sur and later).
+# On macOS, dylibs are signed in libseekdb-build.sh: ad-hoc when no cert; when repo is oceanbase/seekdb and secrets are set,
+# use Developer ID (secrets: OSX_CODESIGN_BUILD_CERTIFICATE_BASE64, OSX_CODESIGN_P12_PASSWORD, OSX_CODESIGN_KEYCHAIN_PASSWORD, OSX_CODESIGN_IDENTITY).
+# Optional: add notarization step and APPLE_ID/PASSWORD/TEAM_ID secrets to notarize the zip.
+# Artifacts: platform zips including libseekdb-android-arm64-v8a.zip; combined artifact libseekdb-all-platforms; optional S3 upload when DESTINATION_TARGET_PATH or AWS_S3_BUCKET and credentials are set.
+#
+# Job step order (unified across linux/macos/android/windows): Checkout → caches → compile → setup Node/Rust/Go/Java →
+# FFI binding tests (same step names/IDs on every platform; continue-on-error per language so all languages run; the final
+# "Binding tests outcome" step fails the job if any binding test failed) → pack → verify packed artifact → upload artifact
+# → save caches (always).
+# Verify packed artifact: linux/macos run test-packed-artifact-smoke.sh (hard gate); windows runs the equivalent
+# test-packed-artifact-smoke.ps1 (hard gate); android runs unzip -t.
+# Platform differences (build invocation, toolchain install, ccache provisioning) are confined to step bodies/scripts.
+# macOS ccache keys include COMMIT_SHA so object files from other commits are not reused (see package/libseekdb/README.md).
+# Android: compile → setup toolchains → host-skip FFI notice → pack → verify zip → upload → save caches → summary (see job comment).
+name: Build libseekdb
+run-name: Build libseekdb for ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+
+# PRs: pull_request. Direct pushes: only long-lived / release lines — avoids duplicate runs when the same commit
+# is both "push" to a feature branch and "pull_request" sync (open PR to upstream from that branch).
+# S3 upload still only on main, master, develop, *.*.x, integration/* (UPLOAD_S3; see release-artifacts job).
+on:
+ push:
+ branches:
+ - main
+ - master
+ - develop
+ - "integration/**"
+ - "release/**"
+ # e.g. 1.0.x; aligns with S3 / UPLOAD_S3 for dot-x lines
+ - "*.*.x"
+ paths-ignore:
+ - "*.md"
+ - "LICENSE"
+ - "CODEOWNERS"
+ - "docs/**"
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: "Branch, tag or commit SHA to build (empty = use default branch)"
+ required: false
+ type: string
+ default: ""
+ pull_request:
+ paths-ignore:
+ - "*.md"
+ - "LICENSE"
+ - "CODEOWNERS"
+ - "docs/**"
+
+env:
+ COMMIT_SHA: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+ AWS_REGION: ${{ vars.AWS_REGION || 'ap-southeast-1' }}
+ UPLOAD_S3: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/integration/') || contains(github.ref, '.x'))) }}
+ BUCKET_NAME: ${{ vars.AWS_S3_BUCKET || 'oceanbase-seekdb-builds' }}
+ DESTINATION_TARGET_PATH: ${{ vars.DESTINATION_TARGET_PATH || format('s3://oceanbase-seekdb-builds/libseekdb/all_commits/{0}', github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha)) }}
+ S3_BUCKET: ${{ vars.AWS_S3_BUCKET || 'oceanbase-seekdb-builds' }}
+ S3_PREFIX: libseekdb/all_commits/${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+
+jobs:
+ # ---------- Build libseekdb on Linux / macOS ----------
+ build:
+ name: Build libseekdb (${{ matrix.platform }})
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux-x64
+ runner: ubuntu-22.04
+ artifact_name: libseekdb-linux-x64
+ container_image: quay.io/pypa/manylinux2014_x86_64
+ deps_file: oceanbase.el7.x86_64.deps
+ - platform: linux-arm64
+ runner: ubuntu-22.04-arm
+ artifact_name: libseekdb-linux-arm64
+ container_image: quay.io/pypa/manylinux2014_aarch64
+ deps_file: oceanbase.el7.aarch64.deps
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # workflow_dispatch: use inputs.ref if set; PR: head sha; push: event sha
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+
+ - name: Cache deps
+ uses: actions/cache@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-${{ matrix.platform }}-el7-${{ hashFiles(format('deps/init/{0}', matrix.deps_file)) }}
+ restore-keys: |
+ ${{ runner.os }}-libseekdb-deps-${{ matrix.platform }}-el7-
+
+ - name: Cache ccache
+ uses: actions/cache@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}-${{ env.COMMIT_SHA }}
+ restore-keys: |
+ ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}-
+
+ # Run build inside manylinux2014 so Node/actions run on host (glibc 2.28+), build runs in CentOS 7 (glibc 2.17) for compatibility
+ - name: Build libseekdb
+ env:
+ BUILD_TYPE: release
+ run: |
+ docker run --rm -u root \
+ -v "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE" -w "$GITHUB_WORKSPACE" \
+ -e BUILD_TYPE -e GITHUB_WORKSPACE \
+ ${{ matrix.container_image }} \
+ bash -c '
+ set -e
+ # Install deps in two steps: base first (wget required for dep_create.sh), then ccache if available
+ yum install -y git wget rpm cpio make glibc-devel glibc-headers binutils m4 python3 python3-devel libtool libaio ncurses-devel which zlib-devel
+ yum install -y epel-release 2>/dev/null || true
+ yum install -y ccache 2>/dev/null || true
+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
+ # CMake includes cmake/Rust.cmake at configure time; cargo must exist before build.sh.
+ # shellcheck source=/dev/null
+ source .github/script/install-rust.sh
+ export PATH="$CARGO_HOME/bin:$PATH"
+ CARGO_CMAKE="-DCARGO=$(command -v cargo)"
+ export CCACHE_DIR="$GITHUB_WORKSPACE/.ccache"
+ export CCACHE_COMPILERCHECK=content
+ export CCACHE_NOHASHDIR=1
+ mkdir -p deps/3rd/usr/local/oceanbase/devtools/bin
+ CCACHE_SRC=$(command -v ccache 2>/dev/null || true)
+ if [ -n "$CCACHE_SRC" ] && [ -x "$CCACHE_SRC" ]; then
+ ln -sf "$CCACHE_SRC" deps/3rd/usr/local/oceanbase/devtools/bin/ccache
+ USE_CCACHE="-DOB_USE_CCACHE=ON"
+ else
+ USE_CCACHE="-DOB_USE_CCACHE=OFF"
+ fi
+ export PATH="$GITHUB_WORKSPACE/deps/3rd/usr/local/oceanbase/devtools/bin:$PATH"
+ bash build.sh init
+ if [ -x /opt/python/cp39-cp39/bin/python3.9 ]; then
+ export PATH="/opt/python/cp39-cp39/bin:$PATH"
+ PYVER=3.9
+ else
+ PYVER=$(python3 -c "import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")")
+ fi
+ bash build.sh release --init $USE_CCACHE -DBUILD_EMBED_MODE=ON -DPYTHON_VERSION=$PYVER $CARGO_CMAKE --make libseekdb
+ [ -n "$USE_CCACHE" ] && ccache -s || true
+ '
+ - name: Fix ownership (container writes as root)
+ run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE"
+
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Test Python binding
+ id: binding_python
+ continue-on-error: true
+ run: |
+ cd unittest/include/python
+ bash test.sh
+
+ - name: Test Node.js FFI binding
+ id: binding_node_ffi
+ continue-on-error: true
+ run: |
+ cd unittest/include/nodejs
+ npm install
+ bash test.sh
+
+ - name: Test Node.js N-API binding
+ id: binding_node_napi
+ continue-on-error: true
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/nodejs_napi
+ npm install
+ bash test.sh
+
+ - name: Test Rust binding
+ id: binding_rust
+ continue-on-error: true
+ run: |
+ cd unittest/include/rust
+ bash test.sh
+
+ - name: Test Go binding
+ id: binding_go
+ continue-on-error: true
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/go
+ bash test.sh
+
+ - name: Test Java binding
+ id: binding_java
+ continue-on-error: true
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/java
+ bash test.sh
+
+ - name: Pack libseekdb
+ run: |
+ chmod +x package/libseekdb/libseekdb-build.sh
+ cd package/libseekdb && bash libseekdb-build.sh "${GITHUB_WORKSPACE}/build_release/src/include"
+
+ - name: Verify packed artifact
+ run: |
+ chmod +x package/libseekdb/test-packed-artifact-smoke.sh
+ package/libseekdb/test-packed-artifact-smoke.sh "package/libseekdb/libseekdb-${{ matrix.platform }}.zip"
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.artifact_name }}
+ path: package/libseekdb/libseekdb-*.zip
+
+ - name: Save Cache deps
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-${{ matrix.platform }}-el7-${{ hashFiles(format('deps/init/{0}', matrix.deps_file)) }}
+ - name: Save Cache ccache
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}-${{ env.COMMIT_SHA }}
+
+ - name: Binding tests outcome
+ if: always()
+ run: |
+ failed=0
+ for o in \
+ "${{ steps.binding_node_ffi.outcome }}" \
+ "${{ steps.binding_node_napi.outcome }}" \
+ "${{ steps.binding_python.outcome }}" \
+ "${{ steps.binding_rust.outcome }}" \
+ "${{ steps.binding_go.outcome }}" \
+ "${{ steps.binding_java.outcome }}"; do
+ [ "$o" = "failure" ] && failed=1
+ done
+ if [ "$failed" -ne 0 ]; then
+ echo "::error::One or more libseekdb binding tests failed on ${{ matrix.platform }}"
+ exit 1
+ fi
+ echo "All binding test steps succeeded (or were skipped)."
+
+ # ---------- Build on macOS (no container) ----------
+ build-macos:
+ name: Build libseekdb (${{ matrix.platform }})
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: darwin-arm64
+ runner: macos-15
+ artifact_name: libseekdb-darwin-arm64
+ arch: arm64
+ cmake_arch: arm64
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+
+ - name: Install macOS dependencies (pinned Homebrew)
+ run: |
+ chmod +x package/libseekdb/install-macos-brew-deps.sh
+ package/libseekdb/install-macos-brew-deps.sh
+
+ - name: Cache deps
+ uses: actions/cache@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-${{ matrix.platform }}-${{ hashFiles('deps/init/oceanbase.macos.arm64.deps') }}
+ restore-keys: |
+ ${{ runner.os }}-libseekdb-deps-${{ matrix.platform }}-
+
+ - name: Cache ccache
+ uses: actions/cache@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}-${{ env.COMMIT_SHA }}
+ restore-keys: |
+ ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}-
+
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+
+ - name: Build init (macOS)
+ run: bash build.sh init
+
+ - name: Build libseekdb
+ env:
+ BUILD_TYPE: release
+ ARCH: ${{ matrix.arch }}
+ CMAKE_OSX_ARCHITECTURES: ${{ matrix.cmake_arch }}
+ CCACHE_DIR: ${{ github.workspace }}/.ccache
+ CCACHE_COMPILERCHECK: content
+ CCACHE_NOHASHDIR: 1
+ run: |
+ # Env.cmake looks for ccache in deps/3rd/.../devtools/bin; put it there so no Env.cmake change is needed
+ mkdir -p deps/3rd/usr/local/oceanbase/devtools/bin
+ ln -sf "$(which ccache)" deps/3rd/usr/local/oceanbase/devtools/bin/ccache
+ # Use runner's Python (macOS has 3.x, not 3.8 by default) so embed CMake finds it
+ PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
+ bash build.sh release --init -DOB_USE_CCACHE=ON -DBUILD_EMBED_MODE=ON -DPYTHON_VERSION=$PYVER -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 --make libseekdb
+ ccache -s
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Test Python binding
+ id: binding_python
+ continue-on-error: true
+ run: |
+ cd unittest/include/python
+ bash test.sh
+
+ - name: Test Node.js FFI binding
+ id: binding_node_ffi
+ continue-on-error: true
+ run: |
+ cd unittest/include/nodejs
+ npm install
+ bash test.sh
+
+ - name: Test Node.js N-API binding
+ id: binding_node_napi
+ continue-on-error: true
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/nodejs_napi
+ npm install
+ bash test.sh
+
+ - name: Test Rust binding
+ id: binding_rust
+ continue-on-error: true
+ run: |
+ cd unittest/include/rust
+ bash test.sh
+
+ - name: Test Go binding
+ id: binding_go
+ continue-on-error: true
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/go
+ bash test.sh
+
+ - name: Test Java binding
+ id: binding_java
+ continue-on-error: true
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/java
+ bash test.sh
+
+ # macOS code signing. Entitlements allow loading embedded dylib.
+ - name: Create entitlements (macOS)
+ run: |
+ echo -e '\n\n\n\n com.apple.security.cs.disable-library-validation\n \n\n' > package/libseekdb/entitlements.plist
+
+ # Only import certificate on main repo; script skips when cert empty (ad-hoc signing used).
+ - name: Import certificate (macOS)
+ if: github.repository == 'oceanbase/seekdb'
+ env:
+ BUILD_CERTIFICATE_BASE64: ${{ secrets.OSX_CODESIGN_BUILD_CERTIFICATE_BASE64 }}
+ P12_PASSWORD: ${{ secrets.OSX_CODESIGN_P12_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.OSX_CODESIGN_KEYCHAIN_PASSWORD }}
+ run: . package/libseekdb/osx_import_codesign_certificate.sh
+
+ - name: Pack libseekdb
+ env:
+ ARCH: ${{ matrix.arch }}
+ CODESIGN_IDENTITY: ${{ secrets.OSX_CODESIGN_IDENTITY }}
+ CODESIGN_ENTITLEMENTS: ${{ github.workspace }}/package/libseekdb/entitlements.plist
+ run: cd package/libseekdb && bash libseekdb-build.sh
+
+ - name: Verify packed artifact
+ run: |
+ chmod +x package/libseekdb/test-packed-artifact-smoke.sh
+ package/libseekdb/test-packed-artifact-smoke.sh package/libseekdb/libseekdb-darwin-arm64.zip
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.artifact_name }}
+ path: package/libseekdb/libseekdb-*.zip
+
+ # Save caches even on failure so next run can resume (deps/ccache)
+ - name: Save Cache deps
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-${{ matrix.platform }}-${{ hashFiles('deps/init/oceanbase.macos.arm64.deps') }}
+ - name: Save Cache ccache
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}-${{ env.COMMIT_SHA }}
+
+ - name: Binding tests outcome
+ if: always()
+ run: |
+ failed=0
+ for o in \
+ "${{ steps.binding_node_ffi.outcome }}" \
+ "${{ steps.binding_node_napi.outcome }}" \
+ "${{ steps.binding_python.outcome }}" \
+ "${{ steps.binding_rust.outcome }}" \
+ "${{ steps.binding_go.outcome }}" \
+ "${{ steps.binding_java.outcome }}"; do
+ [ "$o" = "failure" ] && failed=1
+ done
+ if [ "$failed" -ne 0 ]; then
+ echo "::error::One or more libseekdb binding tests failed on ${{ matrix.platform }}"
+ exit 1
+ fi
+ echo "All binding test steps succeeded (or were skipped)."
+
+ # ---------- Android NDK cross-compile (macOS arm64 host; target arm64-v8a) ----------
+ # ubuntu-* runners are x86_64 by default. macOS-15 uses Apple silicon + NDK darwin-arm64 prebuilts (see docs).
+ # Alternative: runs-on: ubuntu-24.04-arm for Linux ARM64.
+ #
+ # Why no FFI binding tests here: libseekdb.so is Android arm64-v8a / ELF for Bionic — the dynamic linker,
+ # libc, and JNI/NDK ABI differ from macOS dyld. CI cannot dlopen/load that .so on the host like Linux/macOS/Windows jobs.
+ # Running the same unittest/include/*/test.sh would require an Android emulator, rooted device, or adb push + adb shell test.
+ build-android:
+ name: Build libseekdb (android-arm64-v8a)
+ runs-on: macos-15
+ timeout-minutes: 180
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+
+ - name: Install macOS packages
+ run: |
+ brew install cmake ccache wget || true
+
+ - name: Install Android NDK
+ uses: nttld/setup-ndk@v1
+ with:
+ # Use rYYx form (e.g. r27d); full build numbers (27.3.13750724) 404 on the action download URL
+ ndk-version: r27d
+
+ - name: Cache deps
+ uses: actions/cache@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-android-arm64-v8a-${{ hashFiles('deps/init/oceanbase.android.arm64.deps') }}
+ restore-keys: |
+ ${{ runner.os }}-libseekdb-deps-android-arm64-v8a-
+
+ - name: Cache ccache
+ uses: actions/cache@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-android-arm64-v8a
+ restore-keys: |
+ ${{ runner.os }}-ccache-libseekdb-android-arm64-v8a-
+
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+
+ - name: Build libseekdb
+ env:
+ BUILD_TYPE: release
+ CCACHE_DIR: ${{ github.workspace }}/.ccache
+ CCACHE_COMPILERCHECK: content
+ CCACHE_NOHASHDIR: 1
+ run: |
+ set -e
+ # Do not symlink ccache into deps/3rd before --init: dep_create.sh rm -rf deps/3rd.
+ # CMake finds ccache on PATH when devtools has none (Android deps omit obdevtools-ccache).
+ # ANDROID_NDK_HOME is set by the NDK install step; build.sh fixes ANDROID_ABI=arm64-v8a.
+ bash build.sh release --android -DOB_USE_CCACHE=ON -DBUILD_EMBED_MODE=ON --init --make libseekdb
+ ccache -s
+
+ # Same toolchain install order as other jobs (no host FFI below).
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Test bindings (host skip)
+ id: android_binding_notice
+ run: |
+ echo "::notice::Skipping native FFI binding tests on this runner: libseekdb.so targets Android arm64-v8a / Bionic and cannot be dlopen()'d on the macOS host."
+ echo "Zip verification runs after pack; device or emulator CI would be required for full FFI tests."
+
+ - name: Pack libseekdb
+ run: cd package/libseekdb && bash libseekdb-build.sh --android
+
+ - name: Verify packed artifact
+ id: android_verify_zip
+ continue-on-error: true
+ run: unzip -t package/libseekdb/libseekdb-android-arm64-v8a.zip
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: libseekdb-android-arm64-v8a
+ path: package/libseekdb/libseekdb-android-arm64-v8a.zip
+
+ - name: Save Cache deps
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-android-arm64-v8a-${{ hashFiles('deps/init/oceanbase.android.arm64.deps') }}
+
+ - name: Save Cache ccache
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-android-arm64-v8a
+
+ - name: Pack / zip verification outcome
+ if: always()
+ run: |
+ if [ "${{ steps.android_verify_zip.outcome }}" = "failure" ]; then
+ echo "::error::Packaged zip verification failed (unzip -t)"
+ exit 1
+ fi
+ echo "Android pack and zip verification OK (or verify step skipped)."
+
+ # ---------- Build on Windows x64 (native, embed DLL) ----------
+ build-windows:
+ name: Build libseekdb (windows-x64)
+ runs-on: windows-2022
+ timeout-minutes: 360
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Cache deps
+ uses: actions/cache@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-windows-x64-${{ hashFiles('deps/init/oceanbase.windows.x86_64.deps') }}
+ restore-keys: |
+ ${{ runner.os }}-libseekdb-deps-windows-x64-
+
+ - name: Cache ccache
+ uses: actions/cache@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-windows-x64
+ restore-keys: |
+ ${{ runner.os }}-ccache-libseekdb-windows-x64-
+
+ - name: Install ccache (Windows)
+ shell: pwsh
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ function Refresh-Path {
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ }
+ Refresh-Path
+ if (Get-Command ccache -ErrorAction SilentlyContinue) {
+ Write-Host "ccache already on PATH"
+ Get-Command ccache | Out-Host
+ exit 0
+ }
+ choco install ccache -y --no-progress 2>&1 | Out-Host
+ Refresh-Path
+ if ($LASTEXITCODE -ne 0 -or -not (Get-Command ccache -ErrorAction SilentlyContinue)) {
+ Write-Host "::notice::choco install ccache failed or ccache not on PATH; installing from ccache GitHub releases."
+ $ver = "4.13.2"
+ $zipName = "ccache-$ver-windows-x86_64.zip"
+ $url = "https://github.com/ccache/ccache/releases/download/v$ver/$zipName"
+ $tools = Join-Path $env:GITHUB_WORKSPACE ".tools"
+ $dest = Join-Path $tools "ccache-$ver-win64"
+ New-Item -ItemType Directory -Force -Path $dest | Out-Null
+ $zipPath = Join-Path $dest $zipName
+ Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
+ Expand-Archive -Path $zipPath -DestinationPath $dest -Force
+ $exe = Get-ChildItem -Path $dest -Filter ccache.exe -Recurse | Select-Object -First 1
+ if (-not $exe) { throw "ccache.exe not found after extracting $zipName under $dest" }
+ $binDir = $exe.Directory.FullName
+ Add-Content -Path $env:GITHUB_PATH -Value $binDir -Encoding utf8
+ $env:PATH = "$binDir;$env:PATH"
+ }
+ Get-Command ccache | Out-Host
+
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+
+ - name: Build libseekdb
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ CCACHE_DIR: ${{ github.workspace }}/.ccache
+ CCACHE_COMPILERCHECK: content
+ CCACHE_NOHASHDIR: 1
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ # Pick up Chocolatey / GITHUB_PATH / fallback-installed ccache
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ $py = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
+ if (-not (Test-Path "deps/3rd/DONE")) {
+ .\build.ps1 init
+ }
+ # Prepend deps LLVM to PATH; pass lld-link so CMake does not pick GNU ld (MinGW) for clang-cl.
+ $ws = if ($env:GITHUB_WORKSPACE) { $env:GITHUB_WORKSPACE } else { (Get-Location).Path }
+ # CMake -G Ninja requires ninja on PATH at configure time (otherwise CMake may fall back or leave a stale non-Ninja cache).
+ foreach ($rel in @("deps/3rd/tools/ninja", "deps/3rd/tools/cmake/bin")) {
+ $tp = Join-Path $ws $rel
+ if (Test-Path -LiteralPath $tp) { $env:PATH = "$tp;$env:PATH" }
+ }
+ $llvmRoot = if ($env:OB_LLVM_DIR) { $env:OB_LLVM_DIR } else { Join-Path $ws "deps/3rd/tools/llvm18" }
+ $llvmBin = Join-Path $llvmRoot "bin"
+ $lldLink = Join-Path $llvmBin "lld-link.exe"
+ if (-not (Test-Path $lldLink)) { throw "lld-link.exe not found: $lldLink" }
+ $lldFwd = $lldLink.Replace("\", "/")
+ $env:PATH = "$llvmBin;$env:PATH"
+ $ccacheOpt = "-DOB_USE_CCACHE=OFF"
+ if (Get-Command ccache -ErrorAction SilentlyContinue) {
+ $ccacheOpt = "-DOB_USE_CCACHE=ON"
+ } else {
+ Write-Host "::warning::ccache not found; building without compiler cache."
+ }
+ # Map RelWithDebInfo/Debug/MinSizeRel -> Release for IMPORTED targets (e.g. Python3::Module .lib/.dll on Windows).
+ # Ensures cache has these on first cmake run; complements cmake/Env.cmake for CI runners where Python stubs omit RelWithDebInfo.
+ .\build.ps1 release --ninja --target libseekdb "-DBUILD_EMBED_MODE=ON" "-DPYTHON_VERSION=$py" "-DCMAKE_LINKER=$lldFwd" "-DCMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO=Release" "-DCMAKE_MAP_IMPORTED_CONFIG_DEBUG=Release" "-DCMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL=Release" $ccacheOpt
+ if (Get-Command ccache -ErrorAction SilentlyContinue) { ccache -s }
+
+ - name: Debug facts (Windows)
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ run: |
+ $ErrorActionPreference = "Continue"
+ $ws = if ($env:GITHUB_WORKSPACE) { $env:GITHUB_WORKSPACE } else { (Get-Location).Path }
+ foreach ($rel in @("deps/3rd/tools/ninja", "deps/3rd/tools/cmake/bin")) {
+ $tp = Join-Path $ws $rel
+ if (Test-Path -LiteralPath $tp) { $env:PATH = "$tp;$env:PATH" }
+ }
+ . ./unittest/include/seekdb-windows-dll-resolve.ps1
+ . ./unittest/include/debug-libseekdb-windows.ps1
+ Write-LibseekdbWindowsBuildFacts -RepoRoot "$PWD"
+
+ - name: Verify libseekdb DLL (Windows)
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ . ./unittest/include/seekdb-windows-dll-resolve.ps1
+ $bdn = Get-SeekDbWindowsBuildDirNameFromEnv
+ $r = Find-SeekDbWindowsDll -RepoRoot "$PWD" -BuildDirName $bdn
+ if (-not $r) {
+ Write-SeekDbWindowsDllDiagnostics -RepoRoot "$PWD" -BuildDirName $bdn
+ throw "libseekdb build did not produce seekdb.dll under build_$bdn (see diagnostics above)."
+ }
+ Write-Host "libseekdb DLL: $($r.DllPath)"
+ "SEEKDB_LIB_PATH=$($r.DllPath)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Install MinGW and Maven (Go CGO + Java)
+ shell: pwsh
+ run: choco install mingw maven -y --no-progress
+
+ # One job step per language: if something hangs, the in-progress step name in the Actions UI shows
+ # whether the stall is in Python, npm, node-gyp, cargo, go, or Java (not “whole binding” vs global timeout).
+ - name: Test Python binding
+ id: binding_python
+ continue-on-error: true
+ shell: pwsh
+ env:
+ SEEKDB_BINDING_SECTION: Python
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ Get-Command gcc | Out-Host
+ Get-Command mvn | Out-Host
+ .\unittest\include\run-libseekdb-binding-tests.ps1 -RepoRoot "$PWD" -ContinueOnError
+
+ - name: Test Node.js FFI binding
+ id: binding_node_ffi
+ continue-on-error: true
+ shell: pwsh
+ env:
+ SEEKDB_BINDING_SECTION: NodeFfi
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ .\unittest\include\run-libseekdb-binding-tests.ps1 -RepoRoot "$PWD" -ContinueOnError
+
+ - name: Test Node.js N-API binding
+ id: binding_node_napi
+ continue-on-error: true
+ shell: pwsh
+ env:
+ SEEKDB_BINDING_SECTION: NodeNapi
+ # VECTOR + DBMS_HYBRID_SEARCH cases can stall native code on Windows runners; core N-API still covered.
+ SEEKDB_NODE_NAPI_SKIP_HEAVY: "1"
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ .\unittest\include\run-libseekdb-binding-tests.ps1 -RepoRoot "$PWD" -ContinueOnError
+
+ - name: Test Rust binding
+ id: binding_rust
+ continue-on-error: true
+ shell: pwsh
+ env:
+ SEEKDB_BINDING_SECTION: Rust
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ .\unittest\include\run-libseekdb-binding-tests.ps1 -RepoRoot "$PWD" -ContinueOnError
+
+ - name: Test Go binding
+ id: binding_go
+ continue-on-error: true
+ shell: pwsh
+ env:
+ SEEKDB_BINDING_SECTION: Go
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ .\unittest\include\run-libseekdb-binding-tests.ps1 -RepoRoot "$PWD" -ContinueOnError
+
+ - name: Test Java binding
+ id: binding_java
+ continue-on-error: true
+ shell: pwsh
+ env:
+ SEEKDB_BINDING_SECTION: Java
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+ .\unittest\include\run-libseekdb-binding-tests.ps1 -RepoRoot "$PWD" -ContinueOnError
+
+ - name: Pack libseekdb
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ run: .\package\libseekdb\libseekdb-build.ps1
+
+ # Hard gate: mirrors linux/macos (Verify packed artifact); fails the job if the packed zip does not load.
+ # Same step name as linux/macos (Verify packed artifact); platform script differs (see package/libseekdb/test-packed-artifact-smoke.ps1).
+ - name: Verify packed artifact
+ shell: pwsh
+ run: .\package\libseekdb\test-packed-artifact-smoke.ps1 -Zip "package\libseekdb\libseekdb-windows-x64.zip"
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: libseekdb-windows-x64
+ path: package/libseekdb/libseekdb-windows-x64.zip
+
+ - name: Save Cache deps
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: deps/3rd
+ key: ${{ runner.os }}-libseekdb-deps-windows-x64-${{ hashFiles('deps/init/oceanbase.windows.x86_64.deps') }}
+
+ - name: Save Cache ccache
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-windows-x64
+
+ - name: Binding tests outcome
+ if: always()
+ shell: pwsh
+ run: |
+ $failed = $false
+ if ("${{ steps.binding_python.outcome }}" -eq "failure") { Write-Host "::error::Binding section failed: Python"; $failed = $true }
+ if ("${{ steps.binding_node_ffi.outcome }}" -eq "failure") { Write-Host "::error::Binding section failed: Node FFI (koffi)"; $failed = $true }
+ if ("${{ steps.binding_node_napi.outcome }}" -eq "failure") { Write-Host "::error::Binding section failed: Node N-API"; $failed = $true }
+ if ("${{ steps.binding_rust.outcome }}" -eq "failure") { Write-Host "::error::Binding section failed: Rust"; $failed = $true }
+ if ("${{ steps.binding_go.outcome }}" -eq "failure") { Write-Host "::error::Binding section failed: Go"; $failed = $true }
+ if ("${{ steps.binding_java.outcome }}" -eq "failure") { Write-Host "::error::Binding section failed: Java"; $failed = $true }
+ if ($failed) { exit 1 }
+ Write-Host "All Windows binding test sections succeeded."
+
+ # ---------- Collect libseekdb artifacts and upload to S3 (runs only when all needed build jobs succeed, including binding tests) ----------
+ release-artifacts:
+ name: Collect artifacts and upload to S3
+ runs-on: ubuntu-22.04
+ needs:
+ - build
+ - build-macos
+ - build-android
+ - build-windows
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: release-artifacts
+ merge-multiple: true
+
+ - name: List all artifacts
+ run: |
+ echo "=== All artifacts ==="
+ ls -la release-artifacts/
+
+ - name: Upload combined artifact (for workflow download)
+ uses: actions/upload-artifact@v4
+ with:
+ name: libseekdb-all-platforms
+ path: release-artifacts/
+
+ - name: Configure AWS credentials
+ if: env.UPLOAD_S3 == 'true'
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ - name: Upload to S3
+ if: env.UPLOAD_S3 == 'true'
+ run: |
+ set -e
+ if [ -n "${{ env.DESTINATION_TARGET_PATH }}" ]; then
+ S3_TARGET="${{ env.DESTINATION_TARGET_PATH }}"
+ else
+ S3_TARGET="s3://${{ env.S3_BUCKET }}/${{ env.S3_PREFIX }}/"
+ fi
+ [ "${S3_TARGET: -1}" != "/" ] && S3_TARGET="${S3_TARGET}/"
+ echo "Uploading to $S3_TARGET"
+ aws s3 cp release-artifacts/ "$S3_TARGET" --recursive --exclude "*" --include "*.zip" --no-progress
+ echo "Uploaded:"
+ aws s3 ls "$S3_TARGET" --recursive
+ echo "Done."
+ continue-on-error: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 8f332e50c1..fdedf450ac 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -26,6 +26,9 @@ jobs:
- uses: actions/checkout@v3
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+
- name: Cache deps
id: cache-deps
uses: actions/cache@v3
diff --git a/.github/workflows/rust-checks.yml b/.github/workflows/rust-checks.yml
index 2dc47c2a5d..3f5ee90adf 100644
--- a/.github/workflows/rust-checks.yml
+++ b/.github/workflows/rust-checks.yml
@@ -59,5 +59,12 @@ jobs:
- name: cargo audit
working-directory: rust
run: |
- command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --locked
+ # The setup-rust cache restores $CARGO_HOME/bin as-is; a stale or
+ # half-written cargo-audit binary can pass `command -v` yet fail to
+ # run, and cargo install then refuses to overwrite it. Verify by
+ # executing, and reinstall from scratch when the binary is broken.
+ if ! cargo audit --version >/dev/null 2>&1; then
+ rm -f "$(command -v cargo-audit)" 2>/dev/null || true
+ cargo install cargo-audit --locked
+ fi
cargo audit
diff --git a/.gitignore b/.gitignore
index 50b0751c9b..bb883b6687 100644
--- a/.gitignore
+++ b/.gitignore
@@ -187,6 +187,14 @@ src/pl/parser/pl_parser_mysql_mode.output
src/pl/parser/pl_parser_oracle_mode.output
src/share/inner_table/sys_package/syspack_source.cpp
src/share/inner_table/sys_package/*.plw
+# generate_inner_table_schema.py (run at cmake configure)
+src/share/inner_table/ob_inner_table_schema.h
+src/share/inner_table/ob_inner_table_schema.*.cpp
+src/share/inner_table/ob_inner_table_schema_constants.h
+src/share/inner_table/ob_inner_table_schema_misc.ipp
+src/share/inner_table/table_id_to_name
+src/observer/virtual_table/ob_all_virtual_sqlite_tables.cpp
+src/observer/virtual_table/ob_all_virtual_sqlite_tables.h
src/share/parameter/standalone_default_parameter.json
src/share/parameter/shared_storage_default_parameter.json
src/share/system_variable/standalone_default_system_variable.json
@@ -381,6 +389,9 @@ tools/ob-configserver/bin/*
tools/ob-configserver/tests/*.log
tools/ob-configserver/tests/*.out
+############# package/libseekdb #############
+package/libseekdb/libseekdb-*.zip
+
## .NET Configurator build output (rebuilt by dotnet publish)
tools/windows/seekdbConfigurator/bin/
tools/windows/seekdbConfigurator/obj/
@@ -396,6 +407,9 @@ deps/3rd_party
deps/usr
win_deps_archives/
win_deps.zip
+
+############# rust #############
+rust/target/
win_deps/
############# test #############
@@ -404,3 +418,4 @@ test/var
.worktrees/
.claude/
CLAUDE.md
+.codegraph/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index abecc0511f..4235fe4d3e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,6 +12,12 @@ project("OceanBase"
HOMEPAGE_URL "https://www.oceanbase.ai"
LANGUAGES CXX C ASM)
+# Android NDK toolchain / defaults may pin C++17 after project(); enforce C++20 for language features (e.g. consteval).
+if(ANDROID)
+ set(CMAKE_CXX_STANDARD 20)
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
+ set(CMAKE_CXX_EXTENSIONS ON)
+endif()
option(OB_ENABLE_STANDBY "Build physical standby support" ON)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
@@ -91,6 +97,12 @@ if(NOT SEEKDB_CMAKE_INVENTORY_RESULT EQUAL 0)
endif()
include("${SEEKDB_CMAKE_INVENTORY}")
+# OB_BUILD_PACKAGE gates source-level packaging paths (#ifdef OB_BUILD_PACKAGE)
+# and enables the Windows cpack (WIX/ZIP) or Linux RPM packaging surface.
+if(OB_BUILD_PACKAGE)
+ add_definitions(-DOB_BUILD_PACKAGE)
+endif()
+
if(NOT CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
message(FATAL_ERROR
"The compatibility CMake build supports only release "
@@ -120,7 +132,7 @@ if((ENABLE_AUTO_FDO OR ENABLE_THIN_LTO OR ENABLE_HOTFUNC OR
message(FATAL_ERROR
"Package optimization options are supported only by the CMake RPM profile")
endif()
-if((OB_BUILD_PACKAGE OR OB_BUILD_RPM) AND NOT SEEKDB_CMAKE_RPM_BUILD)
+if((OB_BUILD_PACKAGE OR OB_BUILD_RPM) AND NOT SEEKDB_CMAKE_RPM_BUILD AND NOT WIN32)
message(FATAL_ERROR
"The compatibility CMake packaging surface supports Linux RPM only")
endif()
@@ -195,6 +207,9 @@ endif()
include(cmake/Rust.cmake)
include_directories("${CMAKE_SOURCE_DIR}/rust/sql-nio/include")
+# LLVM imports may reference BuildTools\DIA SDK\...\diaguids.lib; ensure it
+# exists before any add_subdirectory pulls in LLVM-based targets (Windows only).
+include(${CMAKE_SOURCE_DIR}/cmake/EnsureWindowsDiaGuids.cmake)
add_subdirectory(src)
# Farm compile and pretest still publish and consume liboberror.so. Keep the
diff --git a/build.ps1 b/build.ps1
index db6da0c68b..7995b6ea78 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,172 +1,575 @@
<#
.SYNOPSIS
- Configure or build the seekdb CMake compatibility release on Windows x64.
+ OceanBase Lite Windows build script - mirrors build.sh on Linux/macOS.
.EXAMPLE
- .\build.ps1 release --init --ninja -j 16
+ .\build.ps1 -h
+ .\build.ps1 init
+ .\build.ps1 release
+ .\build.ps1 release --ninja
+ .\build.ps1 release --ninja -j 16
+ .\build.ps1 release --ninja --init
+ .\build.ps1 debug
+ .\build.ps1 clean
#>
-$ErrorActionPreference = "Stop"
-$TOPDIR = $PSScriptRoot
-$Action = "release"
-$Build = $false
-$Init = $false
-$Jobs = 0
-$Help = $false
-$ExtraCMakeArgs = @()
+# Manual arg parsing to support both -flag and --flag styles (like build.sh)
+$Action = "debug"
+$Ninja = $false
+$Init = $false
+$Jobs = 0
+$h = $false
+$NinjaTarget = "observer"
+$ExtraCmake = @()
$i = 0
while ($i -lt $args.Count) {
- $arg = "$($args[$i])"
- switch -Wildcard ($arg) {
- { $_ -in "-h", "--help", "-help" } { $Help = $true }
- { $_ -in "--ninja", "-ninja", "--make" } { $Build = $true }
- { $_ -in "--init", "-init" } { $Init = $true }
+ $a = "$($args[$i])"
+ switch -Wildcard ($a) {
+ { $_ -in "-h", "--help", "-help" } { $h = $true }
+ { $_ -in "--ninja", "-ninja" } { $Ninja = $true }
+ { $_ -in "--init", "-init" } { $Init = $true }
+ { $_ -in "--target" } {
+ $i++
+ if ($i -lt $args.Count) { $NinjaTarget = "$($args[$i])" }
+ }
{ $_ -in "-j", "--jobs" } {
$i++
- if ($i -ge $args.Count) { throw "$arg requires a job count" }
- $Jobs = [int]$args[$i]
+ if ($i -lt $args.Count) { $Jobs = [int]$args[$i] }
}
- { $_.StartsWith("-D") } { $ExtraCMakeArgs += $arg }
default {
- if ($arg.StartsWith("-")) { throw "unsupported option: $arg" }
- $Action = $arg
+ if (-not $a.StartsWith("-")) { $Action = $a }
+ elseif ($a.StartsWith("-D")) { $ExtraCmake += $a }
+ else { Write-Host "[build.ps1][WARN] Unknown flag: $a" -ForegroundColor Yellow }
}
}
$i++
}
-function Write-Log { param([string]$Message) Write-Host "[build.ps1] $Message" }
-function Write-Err { param([string]$Message) Write-Host "[build.ps1][ERROR] $Message" -ForegroundColor Red }
+$ErrorActionPreference = "Stop"
+$TOPDIR = $PSScriptRoot
+
+# Force all tool output to English (avoids encoding issues on non-English systems)
+$env:DOTNET_CLI_UI_LANGUAGE = "en"
+$env:VSLANG = "1033"
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+$OutputEncoding = [System.Text.Encoding]::UTF8
+
+# -- Dependency paths -------------------------------------------------
+# Priority: env var > deps/3rd (if initialized via `init`) > system defaults
+$DEPS_3RD = "$TOPDIR\deps\3rd"
+$TOOLS_DIR = "$DEPS_3RD\tools"
+$DepsInitialized = Test-Path "$DEPS_3RD\DONE"
+
+$DefaultVcpkgDir = if ($env:OB_VCPKG_DIR) { $env:OB_VCPKG_DIR }
+ elseif ($DepsInitialized) { "$DEPS_3RD\vcpkg\x64-windows" }
+ else { "C:/VcpkgInstalled/x64-windows" }
+
+$DefaultOpenSSLDir = if ($env:OB_OPENSSL_DIR) { $env:OB_OPENSSL_DIR }
+ elseif ($DepsInitialized) { "$DEPS_3RD\openssl" }
+ else { "C:/Program Files/OpenSSL-Win64" }
+
+$DefaultLLVMDir = if ($env:OB_LLVM_DIR) { $env:OB_LLVM_DIR }
+ elseif ($DepsInitialized) { "$TOOLS_DIR\llvm18" }
+ else { "C:/Program Files/LLVM18" }
+
+# When deps/3rd is initialized, add tools to PATH
+if ($DepsInitialized) {
+ $toolPaths = @(
+ "$TOOLS_DIR\cmake\bin",
+ "$TOOLS_DIR\ninja",
+ "$TOOLS_DIR\llvm18\bin",
+ "$TOOLS_DIR\win_flex_bison"
+ )
+ foreach ($tp in $toolPaths) {
+ if ((Test-Path $tp) -and ($env:PATH -notlike "*$tp*")) {
+ $env:PATH = "$tp;$env:PATH"
+ }
+ }
+}
+
+# -- Helpers ---------------------------------------------------------
+function Write-Log { param([string]$msg) Write-Host "[build.ps1] $msg" }
+function Write-Err { param([string]$msg) Write-Host "[build.ps1][ERROR] $msg" -ForegroundColor Red }
+
+# -- Code signing (DigiCert Software Trust Manager) -----------------
+# Enabled automatically when SM_API_KEY env var is present.
+# Required env vars: SM_API_KEY, SM_CLIENT_CERT_FILE,
+# SM_CLIENT_CERT_PASSWORD, SM_HOST
+# Required tools: smctl, signtool (Windows SDK)
+
+$script:SigningReady = $null # $null = not checked, $true/$false = result
+
+function Find-SignTool {
+ $cmd = Get-Command signtool.exe -ErrorAction SilentlyContinue
+ if ($cmd) { return $cmd.Source }
+
+ $sdkGlobs = @(
+ "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe",
+ "$env:ProgramFiles\Windows Kits\10\bin\*\x64\signtool.exe"
+ )
+ foreach ($g in $sdkGlobs) {
+ $found = Get-ChildItem -Path $g -ErrorAction SilentlyContinue |
+ Sort-Object { [version]($_.Directory.Parent.Name) } -Descending |
+ Select-Object -First 1
+ if ($found) { return $found.FullName }
+ }
+ return $null
+}
+
+function Initialize-CodeSigning {
+ if ($null -ne $script:SigningReady) { return $script:SigningReady }
+
+ if (-not $env:SM_API_KEY) {
+ Write-Log "Code signing: SM_API_KEY not set, skipping."
+ $script:SigningReady = $false
+ return $false
+ }
+
+ $smctl = Get-Command smctl -ErrorAction SilentlyContinue
+ if (-not $smctl) {
+ Write-Err "SM_API_KEY is set but smctl not found in PATH. Signing disabled."
+ $script:SigningReady = $false
+ return $false
+ }
+
+ $script:SignToolPath = Find-SignTool
+ if (-not $script:SignToolPath) {
+ Write-Err "signtool.exe not found (need Windows SDK). Signing disabled."
+ $script:SigningReady = $false
+ return $false
+ }
+
+ Write-Log "Code signing: syncing certificates from DigiCert STM..."
+ & smctl windows certsync | Out-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "smctl windows certsync failed. Signing disabled."
+ $script:SigningReady = $false
+ return $false
+ }
+
+ Write-Log "Code signing: ready (signtool=$($script:SignToolPath))"
+ $script:SigningReady = $true
+ return $true
+}
+
+function Do-CodeSign {
+ param([string[]]$Files)
+
+ if (-not (Initialize-CodeSigning)) { return }
+
+ foreach ($f in $Files) {
+ if (-not (Test-Path $f)) {
+ Write-Err "Cannot sign, file not found: $f"
+ continue
+ }
+ $name = Split-Path $f -Leaf
+ Write-Log "Signing $name ..."
+ & $script:SignToolPath sign `
+ /tr http://timestamp.digicert.com /td sha256 `
+ /fd sha256 /a $f | Out-Host
+ if ($LASTEXITCODE -eq 0) {
+ Write-Log " Signed: $name"
+ } else {
+ Write-Err " Signing failed for $name (exit code $LASTEXITCODE)"
+ }
+ }
+}
function Show-Usage {
Write-Host @"
+
Usage:
- .\build.ps1 -h
- .\build.ps1 init
- .\build.ps1 clean
- .\build.ps1 release [--init] [-DName=Value ...]
- .\build.ps1 release [--init] [-DName=Value ...] --ninja [-j N]
+ .\build.ps1 -h Show this help
+ .\build.ps1 init Download & extract deps (from HTTP)
+ .\build.ps1 pack Pack deps from this machine into tar.gz
+ .\build.ps1 clean Remove build_* directories
+ .\build.ps1 [BuildType] Configure only (cmake)
+ .\build.ps1 [BuildType] --ninja Configure + compile (ninja)
+ .\build.ps1 [BuildType] --ninja -j 16 Compile with 16 jobs
+ .\build.ps1 [BuildType] --ninja --init Init deps, then build
+ .\build.ps1 release --ninja --target libseekdb Build embed DLL (example)
+ .\build.ps1 package Build release + MSI/ZIP installer
+
+BuildType:
+ debug Debug build (default)
+ release RelWithDebInfo build
+ relwithdebinfo Alias for release
-Supported compatibility build:
- Windows x64, RelWithDebInfo (-O2), Unity, seekdb production binary.
+Flags:
+ --ninja Configure + compile with Ninja
+ --init Run dependency init before building (like build.sh --init)
+ --target NAME Ninja target (default: observer); e.g. libseekdb
+ -DVAR=VALUE Extra CMake cache entries (repeatable); e.g. -DBUILD_EMBED_MODE=ON
+
+Environment variables (override dependency paths):
+ OB_VCPKG_DIR vcpkg install root (default: deps/3rd or C:/VcpkgInstalled)
+ OB_OPENSSL_DIR OpenSSL root (default: deps/3rd or C:/Program Files/OpenSSL-Win64)
+ OB_LLVM_DIR LLVM 18 root (default: deps/3rd or C:/Program Files/LLVM18)
-Bazel remains authoritative for modular dependencies, tests, architecture
-checks, and non-release options. Invoke it through .\bazel.py directly.
"@
}
-if ($Help) {
- Show-Usage
- exit 0
+if ($Jobs -eq 0) {
+ $cpuCount = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
+ if (-not $cpuCount -or $cpuCount -lt 1) { $cpuCount = 4 }
+ $totalMemGB = [math]::Floor((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB)
+ $memJobs = [math]::Max(1, [math]::Floor($totalMemGB / 3))
+ $Jobs = [math]::Min($cpuCount, $memJobs)
+ Write-Log "Auto jobs: $Jobs (cpus=$cpuCount, mem=${totalMemGB}GB, ~3GB/job)"
}
-$NativeArch = if ($env:PROCESSOR_ARCHITEW6432) {
- $env:PROCESSOR_ARCHITEW6432
-} else {
- $env:PROCESSOR_ARCHITECTURE
-}
-if ($NativeArch -notin "AMD64", "x86_64") {
- Write-Err "Only Windows x64 is supported; detected $NativeArch"
- exit 2
-}
+# -- init: download & extract dependencies (mirrors build.sh init) ---
+function Do-Init {
+ $depCreateScript = "$TOPDIR\deps\init\dep_create.ps1"
+ if (-not (Test-Path $depCreateScript)) {
+ Write-Err "dep_create.ps1 not found: $depCreateScript"
+ exit 1
+ }
-$DEPS_3RD = "$TOPDIR\deps\3rd"
-$TOOLS_DIR = "$DEPS_3RD\tools"
+ $sw = [System.Diagnostics.Stopwatch]::StartNew()
+ Write-Log "Running dep_create.ps1 ..."
+ & powershell -NoProfile -ExecutionPolicy Bypass -File $depCreateScript
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "dep_create.ps1 failed (exit code $LASTEXITCODE)"
+ exit $LASTEXITCODE
+ }
+ $sw.Stop()
+ $min = [math]::Floor($sw.Elapsed.TotalSeconds / 60)
+ $sec = $sw.Elapsed.Seconds
+ Write-Log "dep_create.ps1 completed in ${min}m${sec}s"
+
+ # Refresh path defaults now that deps/3rd is populated
+ $script:DepsInitialized = $true
+ $script:DefaultVcpkgDir = "$DEPS_3RD\vcpkg\x64-windows"
+ $script:DefaultOpenSSLDir = "$DEPS_3RD\openssl"
+ $script:DefaultLLVMDir = "$TOOLS_DIR\llvm18"
-function Add-DependencyToolsToPath {
- $ToolPaths = @(
+ $toolPaths = @(
"$TOOLS_DIR\cmake\bin",
"$TOOLS_DIR\ninja",
"$TOOLS_DIR\llvm18\bin",
"$TOOLS_DIR\win_flex_bison"
)
- foreach ($Path in $ToolPaths) {
- if ((Test-Path $Path) -and ($env:PATH -notlike "*$Path*")) {
- $env:PATH = "$Path;$env:PATH"
+ foreach ($tp in $toolPaths) {
+ if ((Test-Path $tp) -and ($env:PATH -notlike "*$tp*")) {
+ $env:PATH = "$tp;$env:PATH"
}
}
}
-function Do-Init {
- $Script = "$TOPDIR\deps\init\dep_create.ps1"
- if (-not (Test-Path $Script)) {
- throw "dependency initializer not found: $Script"
+# -- pack: create tar.gz dep archives from current machine -----------
+function Do-Pack {
+ $packScript = "$TOPDIR\deps\init\pack_win_deps.ps1"
+ if (-not (Test-Path $packScript)) {
+ Write-Err "pack_win_deps.ps1 not found: $packScript"
+ exit 1
+ }
+
+ $outDir = "$TOPDIR\win_deps_archives"
+ if (-not (Test-Path $outDir)) {
+ New-Item -ItemType Directory -Path $outDir | Out-Null
+ }
+
+ Write-Log "Running pack_win_deps.ps1 -> $outDir ..."
+ & powershell -NoProfile -ExecutionPolicy Bypass -File $packScript -OutputDir $outDir
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "pack_win_deps.ps1 failed (exit code $LASTEXITCODE)"
+ exit $LASTEXITCODE
}
- $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
- & powershell -NoProfile -ExecutionPolicy Bypass -File $Script
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- $Stopwatch.Stop()
- Write-Log "dependency initialization completed in $([int]$Stopwatch.Elapsed.TotalSeconds)s"
- Add-DependencyToolsToPath
}
+# -- clean -----------------------------------------------------------
function Do-Clean {
- $BuildDir = "$TOPDIR\build_release"
- if (Test-Path $BuildDir) {
- Remove-Item -Recurse -Force $BuildDir
- Write-Log "removed $BuildDir"
- } else {
- Write-Log "nothing to clean"
+ Write-Log "Cleaning build directories ..."
+ Get-ChildItem -Path $TOPDIR -Directory -Filter "build*" | ForEach-Object {
+ Write-Log " Removing $($_.Name)"
+ Remove-Item -Recurse -Force $_.FullName
}
+ Write-Log "Clean done."
}
-if ($Action.ToLower() -eq "init") {
- if ($Build -or $Init -or $ExtraCMakeArgs.Count -gt 0) {
- throw "init does not accept build options"
+# -- cmake configure ------------------------------------------------
+function Do-Build {
+ param(
+ [string]$BuildType,
+ [string[]]$ExtraCMakeArgs = @()
+ )
+
+ # Align directory names with build.sh: release -> build_release, debug -> build_debug
+ $folderName = switch ($BuildType) {
+ "RelWithDebInfo" { "release" }
+ "Debug" { "debug" }
+ default { $BuildType.ToLower() }
}
- Do-Init
- exit 0
+ $buildDir = "$TOPDIR\build_$folderName"
+ if (-not (Test-Path $buildDir)) {
+ New-Item -ItemType Directory -Path $buildDir | Out-Null
+ }
+
+ $cmakeArgs = @(
+ $TOPDIR,
+ "-G", "Ninja",
+ "-DCMAKE_EXPORT_COMPILE_COMMANDS=1",
+ "-DCMAKE_BUILD_TYPE=$BuildType",
+ "-DOB_USE_LLD=ON",
+ "-DOB_VCPKG_DIR=$DefaultVcpkgDir",
+ "-DOB_OPENSSL_DIR=$DefaultOpenSSLDir",
+ "-DOB_LLVM_DIR=$DefaultLLVMDir"
+ ) + $ExtraCMakeArgs + $ExtraCmake
+
+ Write-Log "CMake configure: build_$folderName"
+ Write-Log " Build type : $BuildType"
+ Write-Log " VcpkgDir : $DefaultVcpkgDir"
+ Write-Log " OpenSSLDir : $DefaultOpenSSLDir"
+ Write-Log " LLVMDir : $DefaultLLVMDir"
+ Write-Log ""
+
+ Push-Location $buildDir
+ try {
+ function Invoke-CMakeConfigure {
+ & cmake @cmakeArgs | Out-Host
+ return $LASTEXITCODE
+ }
+
+ $exit = Invoke-CMakeConfigure
+ if ($exit -ne 0) {
+ Write-Err "CMake configure failed (exit code $exit)"
+ exit $exit
+ }
+ Write-Log "CMake configure succeeded."
+
+ # Facts from CI: CMakeCache can say CMAKE_GENERATOR=Ninja while no *.ninja exists (incomplete/stale tree).
+ # Recover once by wiping cache + CMakeFiles and re-running cmake.
+ function Test-HasNinjaBuildFiles {
+ param([string]$Dir)
+ if (Test-Path (Join-Path $Dir "build.ninja")) { return $true }
+ try {
+ $nf = [System.IO.Directory]::GetFiles($Dir, "*.ninja", [System.IO.SearchOption]::TopDirectoryOnly)
+ return ($nf.Length -gt 0)
+ } catch {
+ return $false
+ }
+ }
+
+ if (-not (Test-HasNinjaBuildFiles -Dir $buildDir)) {
+ Write-Log "[build.ps1] No Ninja build files after configure; clearing CMakeCache + CMakeFiles and re-configuring once."
+ Remove-Item (Join-Path $buildDir "CMakeCache.txt") -Force -ErrorAction SilentlyContinue
+ Remove-Item (Join-Path $buildDir "CMakeFiles") -Recurse -Force -ErrorAction SilentlyContinue
+ $exit2 = Invoke-CMakeConfigure
+ if ($exit2 -ne 0) {
+ Write-Err "CMake re-configure failed (exit code $exit2)"
+ exit $exit2
+ }
+ Write-Log "CMake re-configure finished."
+ }
+
+ # Fail fast if Ninja was requested but the build tree still has no Ninja backend files.
+ if (-not (Test-HasNinjaBuildFiles -Dir $buildDir)) {
+ Write-Err "CMake did not create build.ninja (or any *.ninja) under $buildDir after configure/retry (expected -G Ninja)."
+ $cache = Join-Path $buildDir "CMakeCache.txt"
+ if (Test-Path $cache) {
+ Select-String -Path $cache -Pattern "^CMAKE_GENERATOR:" | ForEach-Object { Write-Err $_.Line }
+ }
+ Write-Err "Fix: delete $buildDir completely and re-run, or ensure Ninja is on PATH when cmake runs (e.g. deps\3rd\tools\ninja)."
+ exit 1
+ }
+
+ # Copy compile_commands.json to project root for IDE support
+ $ccJson = "$buildDir\compile_commands.json"
+ if (Test-Path $ccJson) {
+ Copy-Item $ccJson "$TOPDIR\compile_commands.json" -Force
+ Write-Log "compile_commands.json copied to project root."
+ }
+ }
+ finally {
+ Pop-Location
+ }
+
+ return $buildDir
}
-if ($Action.ToLower() -eq "clean") {
- if ($Build -or $Init -or $ExtraCMakeArgs.Count -gt 0) {
- throw "clean does not accept build options"
+
+# -- ninja build -----------------------------------------------------
+function Do-Ninja {
+ param(
+ [string]$BuildDir,
+ [string]$Target = "observer"
+ )
+
+ Write-Log "Building with Ninja (-j $Jobs) target=$Target in $BuildDir ..."
+ Push-Location $BuildDir
+ try {
+ & ninja -j $Jobs $Target | Out-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "Build failed (exit code $LASTEXITCODE)"
+ exit $LASTEXITCODE
+ }
+ Write-Log "Build succeeded!"
+
+ # Deterministic post-condition for libseekdb: link must emit a DLL somewhere under the build dir.
+ if ($Target -eq "libseekdb") {
+ $foundDll = $false
+ foreach ($leaf in @("seekdb.dll", "libseekdb.dll")) {
+ try {
+ $arr = [System.IO.Directory]::GetFiles($BuildDir, $leaf, [System.IO.SearchOption]::AllDirectories)
+ if ($arr -and $arr.Length -gt 0) {
+ $foundDll = $true
+ Write-Log "Found ${leaf} at $($arr[0])"
+ break
+ }
+ } catch {
+ # ignore enumeration errors; treat as not found
+ }
+ }
+ if (-not $foundDll) {
+ Write-Err "ninja libseekdb succeeded but no seekdb.dll / libseekdb.dll under $BuildDir — link step did not produce a DLL (check ninja/link output above)."
+ exit 1
+ }
+ }
+ }
+ finally {
+ Pop-Location
+ }
+}
+
+# -- build seekdb Configurator (.NET WPF wizard) ---------------------
+function Do-BuildConfigurator {
+ $projDir = "$TOPDIR\tools\windows\seekdbConfigurator"
+ $proj = "$projDir\seekdbConfigurator.csproj"
+ $pubDir = "$projDir\publish"
+
+ if (-not (Test-Path $proj)) {
+ Write-Err "Configurator project not found: $proj"
+ return $false
+ }
+
+ $dotnetCmd = Get-Command dotnet -ErrorAction SilentlyContinue
+ if (-not $dotnetCmd) {
+ Write-Err ".NET SDK not found. Install .NET 8 SDK to build the Configurator."
+ Write-Log " The MSI will be created without the Configurator wizard."
+ return $false
+ }
+
+ Write-Log "Building seekdb Configurator (self-contained, single-file) ..."
+ if (Test-Path $pubDir) { Remove-Item -Recurse -Force $pubDir }
+
+ & dotnet publish $proj `
+ -c Release `
+ -r win-x64 `
+ --self-contained `
+ -p:PublishSingleFile=true `
+ -p:IncludeNativeLibrariesForSelfExtract=true `
+ -o $pubDir | Out-Host
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "Configurator build failed (exit code $LASTEXITCODE)"
+ return $false
+ }
+
+ if (Test-Path "$pubDir\seekdbConfigurator.exe") {
+ Write-Log "Configurator built: $pubDir\seekdbConfigurator.exe"
+ Do-CodeSign "$pubDir\seekdbConfigurator.exe"
+ return $true
+ } else {
+ Write-Err "seekdbConfigurator.exe not found after publish."
+ return $false
+ }
+}
+
+# -- package: build release + create installer -----------------------
+function Do-Package {
+ # Build the Configurator first so it is available when cpack runs
+ $cfgOk = Do-BuildConfigurator
+ if (-not $cfgOk) {
+ Write-Log "Proceeding without Configurator in the MSI."
+ }
+
+ $buildDir = Do-Build -BuildType "RelWithDebInfo" -ExtraCMakeArgs @("-DOB_BUILD_PACKAGE=ON")
+ Do-Ninja -BuildDir $buildDir -Target observer
+
+ # Sign binaries before they are packaged into the MSI
+ $exesToSign = @(
+ "$buildDir\src\observer\seekdb.exe"
+ ) | Where-Object { Test-Path $_ }
+ if ($exesToSign) { Do-CodeSign $exesToSign }
+
+ Write-Log "Creating installer package in $buildDir ..."
+ Push-Location $buildDir
+ try {
+ $wixFound = Get-Command wix -ErrorAction SilentlyContinue
+ if ($wixFound) {
+ Write-Log "WiX v4 found, generating MSI..."
+ & cpack -G WIX -C RelWithDebInfo | Out-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Log "WiX MSI generation failed, falling back to ZIP..."
+ & cpack -G ZIP -C RelWithDebInfo | Out-Host
+ }
+ } else {
+ Write-Log "WiX not found, generating ZIP package..."
+ Write-Log " To generate MSI: dotnet tool install --global wix"
+ & cpack -G ZIP -C RelWithDebInfo | Out-Host
+ }
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "Package generation failed (exit code $LASTEXITCODE)"
+ exit $LASTEXITCODE
+ }
+ $packages = @(
+ Get-ChildItem -Path "$buildDir\seekdb-*.msi" -File -ErrorAction SilentlyContinue
+ Get-ChildItem -Path "$buildDir\seekdb-*.zip" -File -ErrorAction SilentlyContinue
+ )
+ if ($packages) {
+ # Sign MSI installers
+ $msiFiles = $packages | Where-Object { $_.Extension -eq ".msi" }
+ if ($msiFiles) { Do-CodeSign ($msiFiles | ForEach-Object { $_.FullName }) }
+
+ Write-Log "Package(s) created:"
+ foreach ($pkg in $packages) {
+ Write-Log " $($pkg.FullName)"
+ }
+ }
+ Write-Log "Package build succeeded!"
+ }
+ finally {
+ Pop-Location
}
- Do-Clean
- exit 0
}
-if ($Action.ToLower() -notin "release", "relwithdebinfo") {
- Write-Err "Unsupported build type: $Action (only release is maintained)"
+
+# -- Main ------------------------------------------------------------
+if ($h) {
Show-Usage
- exit 2
-}
-
-if ($Init) { Do-Init }
-Add-DependencyToolsToPath
-
-$CMake = Get-Command cmake -ErrorAction SilentlyContinue
-$Ninja = Get-Command ninja -ErrorAction SilentlyContinue
-if (-not $CMake) { throw "cmake not found; run with --init or install CMake 3.20+" }
-if (-not $Ninja) { throw "ninja not found; run with --init or install Ninja" }
-
-$DefaultVcpkgDir = if ($env:OB_VCPKG_DIR) { $env:OB_VCPKG_DIR } else { "$DEPS_3RD\vcpkg\x64-windows" }
-$DefaultOpenSSLDir = if ($env:OB_OPENSSL_DIR) { $env:OB_OPENSSL_DIR } else { "$DEPS_3RD\openssl" }
-$DefaultLLVMDir = if ($env:OB_LLVM_DIR) { $env:OB_LLVM_DIR } else { "$TOOLS_DIR\llvm18" }
-$BuildDir = "$TOPDIR\build_release"
-$CMakeArgs = @(
- "-S", $TOPDIR,
- "-B", $BuildDir,
- "-G", "Ninja",
- "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
- "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
- "-DOB_ENABLE_UNITY=ON",
- "-DOB_USE_LLD=ON",
- "-DOB_VCPKG_DIR=$DefaultVcpkgDir",
- "-DOB_OPENSSL_DIR=$DefaultOpenSSLDir",
- "-DOB_LLVM_DIR=$DefaultLLVMDir"
-) + $ExtraCMakeArgs
-
-Write-Log "configuring Windows x64 release in $BuildDir"
-& $CMake.Source @CMakeArgs
-if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
-
-if ($Build) {
- if ($Jobs -le 0) {
- $Jobs = (Get-CimInstance Win32_Processor |
- Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
- if (-not $Jobs -or $Jobs -lt 1) { $Jobs = 4 }
- }
- Write-Log "building seekdb with Ninja (-j $Jobs)"
- & $Ninja.Source -C $BuildDir -j $Jobs seekdb
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ exit 0
+}
+
+switch ($Action.ToLower()) {
+ "init" {
+ Do-Init
+ }
+ "pack" {
+ Do-Pack
+ }
+ "package" {
+ if ($Init) { Do-Init }
+ Do-Package
+ }
+ "clean" {
+ Do-Clean
+ }
+ { $_ -in "release", "relwithdebinfo" } {
+ if ($Init) { Do-Init }
+ $buildDir = Do-Build -BuildType "RelWithDebInfo"
+ if ($Ninja) { Do-Ninja -BuildDir $buildDir -Target $NinjaTarget }
+ }
+ { $_ -in "debug", "" } {
+ if ($Init) { Do-Init }
+ $buildDir = Do-Build -BuildType "Debug"
+ if ($Ninja) { Do-Ninja -BuildDir $buildDir -Target $NinjaTarget }
+ }
+ "-h" {
+ Show-Usage
+ }
+ default {
+ Write-Err "Unknown action: $Action"
+ Show-Usage
+ exit 1
+ }
}
diff --git a/build.sh b/build.sh
index 116a7cf265..f7d485c83b 100755
--- a/build.sh
+++ b/build.sh
@@ -7,9 +7,29 @@ readonly DEP_INIT_DIR="${TOPDIR}/deps/init"
readonly DEVTOOLS_DIR="${TOPDIR}/deps/3rd/usr/local/oceanbase/devtools"
readonly -a ALL_ARGS=("$@")
-function echo_log
-{
- echo "[build.sh] $*"
+# Get CPU cores; cmake path is resolved in do_build() (Linux may use host cmake before deps devtools exist)
+if [[ "$(uname -s)" == "Darwin" ]]; then
+ CPU_CORES=$(sysctl -n hw.ncpu)
+ KERNEL_RELEASE=""
+else
+ CPU_CORES=$(grep -c ^processor /proc/cpuinfo)
+ KERNEL_RELEASE=$(grep -Po 'release [0-9]{1}' /etc/issue 2>/dev/null)
+fi
+
+BUILD_ARGS=()
+MAKE_ARGS=(-j $CPU_CORES)
+NEED_MAKE=false
+NEED_INIT=false
+ANDROID_BUILD=false
+LLD_OPTION=ON
+STATIC_LINK_LGPL_DEPS_OPTION=ON
+ENABLE_BOLT_OPTION=ON
+WITH_COVERAGE=OFF
+
+echo "$0 ${ALL_ARGS[@]}"
+
+function echo_log() {
+ echo -e "[build.sh] $@"
}
function echo_err
@@ -184,6 +204,15 @@ function configure_release
-DANDROID_ABI=arm64-v8a
-DANDROID_PLATFORM=android-28
)
+ # cmake/Rust.cmake cross-compiles sql-nio with `cargo build --target
+ # aarch64-linux-android`; make sure that target exists. Run from rust/ so
+ # rust-toolchain.toml pins the toolchain (the default may be unset).
+ if command -v rustup >/dev/null 2>&1; then
+ (cd "${TOPDIR}/rust" && rustup target add aarch64-linux-android)
+ else
+ echo_err "rustup not found in PATH; cannot install the aarch64-linux-android Rust target (needed by sql-nio)"
+ exit 1
+ fi
fi
# Replace the former Bazel-backed build_release entry point in place. A
diff --git a/cmake/EnsureWindowsDiaGuids.cmake b/cmake/EnsureWindowsDiaGuids.cmake
new file mode 100644
index 0000000000..91397e9361
--- /dev/null
+++ b/cmake/EnsureWindowsDiaGuids.cmake
@@ -0,0 +1,66 @@
+# LLVM imports may reference BuildTools\DIA SDK\...\diaguids.lib; CI/home often only have Community/etc.
+# Copy one existing VS 2022 diaguids.lib there before linking (must run before add_subdirectory(src)).
+
+if(NOT WIN32)
+ return()
+endif()
+
+# LLVM/seekdb Windows CI is x64 -> amd64 DIA libs; ARM64 host uses arm64.
+if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(ARM64|aarch64)$")
+ set(_a "arm64")
+else()
+ set(_a "amd64")
+endif()
+
+set(_dst "C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/DIA SDK/lib/${_a}/diaguids.lib")
+if(EXISTS "${_dst}")
+ return()
+endif()
+
+set(_cand "")
+if(DEFINED ENV{VSINSTALLDIR})
+ file(TO_CMAKE_PATH "$ENV{VSINSTALLDIR}" _r)
+ string(REGEX REPLACE "/+$" "" _r "${_r}")
+ list(APPEND _cand "${_r}/DIA SDK/lib/${_a}/diaguids.lib")
+endif()
+
+set(_vw "C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe")
+if(EXISTS "${_vw}")
+ execute_process(
+ COMMAND "${_vw}" -latest -products * -utf8 -property installationPath
+ OUTPUT_VARIABLE _vp OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET RESULT_VARIABLE _vr
+ )
+ if(_vr EQUAL 0 AND _vp)
+ string(STRIP "${_vp}" _vp)
+ list(APPEND _cand "${_vp}/DIA SDK/lib/${_a}/diaguids.lib")
+ endif()
+endif()
+
+foreach(_root "C:/Program Files/Microsoft Visual Studio/2022" "C:/Program Files (x86)/Microsoft Visual Studio/2022")
+ if(EXISTS "${_root}")
+ file(GLOB _g "${_root}/*/DIA SDK/lib/${_a}/diaguids.lib")
+ list(APPEND _cand ${_g})
+ endif()
+endforeach()
+
+set(_src "")
+foreach(_i IN LISTS _cand)
+ if(EXISTS "${_i}")
+ set(_src "${_i}")
+ break()
+ endif()
+endforeach()
+
+if(NOT _src)
+ message(WARNING "EnsureWindowsDiaGuids: diaguids.lib (${_a}) not found. LLVM/lld link may fail.")
+ return()
+endif()
+
+get_filename_component(_dd "${_dst}" DIRECTORY)
+file(MAKE_DIRECTORY "${_dd}")
+execute_process(COMMAND "${CMAKE_COMMAND}" -E copy "${_src}" "${_dst}" RESULT_VARIABLE _ec)
+if(NOT _ec EQUAL 0)
+ message(WARNING "EnsureWindowsDiaGuids: copy failed (${_ec}); try elevated cmake or install DIA SDK.")
+else()
+ message(STATUS "EnsureWindowsDiaGuids: ${_src} -> ${_dst}")
+endif()
diff --git a/cmake/Env.cmake b/cmake/Env.cmake
index 3173c581f5..75413886a4 100644
--- a/cmake/Env.cmake
+++ b/cmake/Env.cmake
@@ -10,21 +10,30 @@ endif()
ob_define(DEBUG_PREFIX "-fdebug-prefix-map=${CMAKE_SOURCE_DIR}=.")
ob_define(FILE_PREFIX "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
ob_define(OB_LD_BIN ld)
+ob_define(ASAN_IGNORE_LIST "${CMAKE_SOURCE_DIR}/asan_ignore_list.txt")
+
ob_define(DEP_3RD_DIR "${CMAKE_SOURCE_DIR}/deps/3rd")
ob_define(DEVTOOLS_DIR "${CMAKE_SOURCE_DIR}/deps/3rd/usr/local/oceanbase/devtools")
ob_define(DEP_DIR "${CMAKE_SOURCE_DIR}/deps/3rd/usr/local/oceanbase/deps/devel")
+ob_define(BUILD_CDC_ONLY OFF)
# Deprecated no-op retained for compatibility with existing build invocations.
ob_define(BUILD_EMBED_MODE OFF)
if(BUILD_EMBED_MODE)
- message(STATUS "BUILD_EMBED_MODE is deprecated and has no effect")
+ add_definitions(-DOB_BUILD_EMBED_MODE)
endif()
ob_define(OB_USE_CLANG ON)
ob_define(OB_USE_LLD ON)
ob_define(OB_ERRSIM OFF)
ob_define(OB_SO_CACHE OFF)
ob_define(BUILD_NUMBER 1)
+ob_define(OB_GPERF_MODE OFF)
+ob_define(ENABLE_OBJ_LEAK_CHECK OFF)
+ob_define(ENABLE_FATAL_ERROR_HANG ON)
+ob_define(DETECT_RECURSION OFF)
+ob_define(ENABLE_COMPILE_DLL_MODE OFF)
ob_define(OB_CMAKE_RULES_CHECK ON)
+ob_define(OB_STATIC_LINK_LGPL_DEPS OFF)
ob_define(OB_BUILD_CCLS OFF)
ob_define(LTO_JOBS all)
ob_define(LTO_CACHE_DIR "${CMAKE_BINARY_DIR}/cache")
@@ -33,9 +42,15 @@ ob_define(NEED_PARSER_CACHE ON)
# get compiler from build.sh
ob_define(OB_CC "")
ob_define(OB_CXX "")
+ob_define(OB_BUILD_STANDALONE OFF)
ob_define(DEFAULT_LOG_LEVEL OB_LOG_LEVEL_ERROR)
ob_define(DEFAULT_LOG_FILE_SIZE_MB 256)
+# 'ENABLE_PERF_MODE' use for offline system insight performance test
+# PERF_MODE macro controls many special code path in system
+# we can open this to benchmark our system partial/layered
+ob_define(ENABLE_PERF_MODE OFF)
+
# begin of unity build config
ob_define(OB_MAX_UNITY_BATCH_SIZE 30)
# the global switch of unity build, default is 'ON'
@@ -49,6 +64,8 @@ ob_define(OB_ENABLE_MCMODEL OFF)
ob_define(USE_LTO_CACHE OFF)
+ob_define(ASAN_DISABLE_STACK ON)
+
# 开源模式默认支持系统租户使用向量索引
ob_define(OB_BUILD_SYS_VEC_IDX ON)
@@ -125,6 +142,17 @@ else()
set(CMAKE_CXX_FLAGS "-std=gnu++20")
endif()
+
+# Before the first project(), WIN32 may be unset, so the elseif(WIN32) block below can skip
+# CMAKE_MAP_IMPORTED_CONFIG_*. That breaks Windows RelWithDebInfo with FindPython3 (Python3::Module
+# has no IMPORTED_IMPLIB for that config). CMAKE_HOST_WIN32 is set when cmake runs on Windows; skip
+# for Android NDK cross-builds (OB_ANDROID) so we do not force host mapping onto the NDK tree.
+if(CMAKE_HOST_WIN32 AND NOT OB_ANDROID)
+ set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release CACHE STRING "imported: map Debug -> Release" FORCE)
+ set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release CACHE STRING "imported: map RelWithDebInfo -> Release" FORCE)
+ set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL Release CACHE STRING "imported: map MinSizeRel -> Release" FORCE)
+endif()
+
if(OB_DISABLE_PIE)
message(STATUS "build without pie")
set(PIE_OPT "-no-pie")
@@ -135,10 +163,25 @@ endif()
set(ob_close_deps_static_name "")
+set(OB_BUILD_CLOSE_MODULES OFF)
+
+if(OB_BUILD_STANDALONE)
+ add_definitions(-DOB_BUILD_STANDALONE)
+endif()
+
+if (OB_USE_TEST_PUBKEY)
+ add_definitions(-DOB_USE_TEST_PUBKEY)
+endif()
+
if (OB_BUILD_SYS_VEC_IDX)
add_definitions(-DOB_BUILD_SYS_VEC_IDX)
endif()
+# should not use initial-exec for tls-model if building OBCDC.
+if(BUILD_CDC_ONLY)
+ add_definitions(-DOB_BUILD_CDC_DISABLE_VSAG)
+endif()
+
# Find objcopy - on macOS it may be installed via Homebrew or available as llvm-objcopy
set(OB_CLANG_BIN "clang-17")
set(OB_CLANGXX_BIN "clang++-17")
@@ -157,12 +200,15 @@ if(OB_ANDROID)
# and Env.cmake runs before project() which would set ANDROID.
set(OB_CLANG_BIN "clang")
set(OB_CLANGXX_BIN "clang++")
- # NDK toolchain bin dir (derive from ANDROID_NDK_HOME or
- # CMAKE_TOOLCHAIN_FILE). Discover the NDK host tag instead of assuming the
- # cross-compile is launched from macOS; Linux hosts use linux-x86_64.
+ # NDK toolchain bin dir (derive from ANDROID_NDK_HOME or CMAKE_TOOLCHAIN_FILE).
+ # GLOB prebuilt/*/bin so Linux hosts (e.g. CI) resolve linux-x86_64, macOS resolves darwin-x86_64.
if(DEFINED ENV{ANDROID_NDK_HOME})
- file(GLOB _NDK_TOOLCHAIN_BIN
- "$ENV{ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/*/bin")
+ file(GLOB _NDK_TOOLCHAIN_BIN "$ENV{ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/*/bin")
+ list(LENGTH _NDK_TOOLCHAIN_BIN _ndk_bin_n)
+ if(_ndk_bin_n LESS 1)
+ message(FATAL_ERROR "ANDROID_NDK_HOME: no toolchains/llvm/prebuilt/*/bin under $ENV{ANDROID_NDK_HOME}")
+ endif()
+ list(GET _NDK_TOOLCHAIN_BIN 0 _NDK_TOOLCHAIN_BIN)
else()
# Derive from toolchain file path: .../build/cmake/android.toolchain.cmake -> .../toolchains/llvm/prebuilt/*/bin
get_filename_component(_NDK_ROOT "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY)
@@ -268,9 +314,14 @@ endif()
ob_define(OB_USE_CCACHE OFF)
if (OB_USE_CCACHE)
+ # Prefer devtools (from deps); dep_create may wipe deps/3rd before a symlink is recreated,
+ # and Android deps do not ship obdevtools-ccache — fall back to ccache on PATH.
find_program(OB_CCACHE ccache PATHS "${DEVTOOLS_DIR}/bin" NO_DEFAULT_PATH)
if (NOT OB_CCACHE)
- message(FATAL_ERROR "cannot find ccache.")
+ find_program(OB_CCACHE ccache)
+ endif()
+ if (NOT OB_CCACHE)
+ message(FATAL_ERROR "cannot find ccache. Install ccache (e.g. apt install ccache) or place it under ${DEVTOOLS_DIR}/bin.")
else()
set(CMAKE_C_COMPILER_LAUNCHER ${OB_CCACHE})
set(CMAKE_CXX_COMPILER_LAUNCHER ${OB_CCACHE})
@@ -305,6 +356,14 @@ if (OB_USE_CLANG)
set(_CMAKE_TOOLCHAIN_PREFIX llvm-)
set(_CMAKE_TOOLCHAIN_LOCATION "${CMAKE_TOOLCHAIN_PATH}/bin")
+ if (OB_USE_ASAN)
+ if (ASAN_DISABLE_STACK)
+ ob_define(CMAKE_ASAN_FLAG "-mllvm -asan-stack=0 -fsanitize=address -fno-optimize-sibling-calls -fsanitize-blacklist=${ASAN_IGNORE_LIST}")
+ else()
+ ob_define(CMAKE_ASAN_FLAG "-fstack-protector-strong -fsanitize=address -fno-optimize-sibling-calls -fsanitize-blacklist=${ASAN_IGNORE_LIST}")
+ endif()
+ endif()
+
if (OB_USE_LLD)
if(OB_ANDROID)
# Android: OB_LD_BIN already set in platform block above
@@ -330,33 +389,33 @@ if (OB_USE_CLANG)
if(OB_ANDROID)
# Android NDK: no --gcc-toolchain, no macOS frameworks
# -D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION: Boost headers use std::unary_function removed in C++17
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 -D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION")
- set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG} -D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION")
+ set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
set(CMAKE_CXX_LINK_FLAGS "${LD_OPT} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT}")
set(CMAKE_SHARED_LINKER_FLAGS "${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT}")
set(CMAKE_EXE_LINKER_FLAGS "${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${CMAKE_COVERAGE_EXE_LINKER_OPTIONS}")
elseif(APPLE)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
- set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
+ set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
set(CMAKE_CXX_LINK_FLAGS "${LD_OPT} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT}")
set(CMAKE_SHARED_LINKER_FLAGS "${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${COMPACT_UNWIND_FLAG}")
set(CMAKE_EXE_LINKER_FLAGS "${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${CMAKE_COVERAGE_EXE_LINKER_OPTIONS} ${COMPACT_UNWIND_FLAG}")
elseif(WIN32)
set(OB_OBJCOPY_BIN "llvm-objcopy")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} ${REORDER_COMP_OPT}")
- set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} ${REORDER_COMP_OPT}")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} ${REORDER_COMP_OPT} ${CMAKE_ASAN_FLAG}")
+ set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} ${REORDER_COMP_OPT} ${CMAKE_ASAN_FLAG}")
set(CMAKE_CXX_LINK_FLAGS "${LD_OPT} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT}")
set(CMAKE_SHARED_LINKER_FLAGS "/INCREMENTAL:NO ${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${COMPACT_UNWIND_FLAG}")
set(CMAKE_EXE_LINKER_FLAGS "/INCREMENTAL:NO ${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${CMAKE_COVERAGE_EXE_LINKER_OPTIONS} ${COMPACT_UNWIND_FLAG}")
elseif(OB_ANDROID)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
- set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
+ set(CMAKE_C_FLAGS "${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
set(CMAKE_CXX_LINK_FLAGS "${LD_OPT} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT}")
set(CMAKE_SHARED_LINKER_FLAGS "${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT}")
set(CMAKE_EXE_LINKER_FLAGS "${LD_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${CMAKE_COVERAGE_EXE_LINKER_OPTIONS}")
else()
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --gcc-toolchain=${GCC9} -gdwarf-4 ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
- set(CMAKE_C_FLAGS "--gcc-toolchain=${GCC9} -gdwarf-4 ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --gcc-toolchain=${GCC9} -gdwarf-4 ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
+ set(CMAKE_C_FLAGS "--gcc-toolchain=${GCC9} -gdwarf-4 ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT} ${THIN_LTO_OPT} -fcolor-diagnostics ${REORDER_COMP_OPT} -fmax-type-align=8 ${CMAKE_ASAN_FLAG}")
set(CMAKE_CXX_LINK_FLAGS "${LD_OPT} --gcc-toolchain=${GCC9} ${DEBUG_PREFIX} ${FILE_PREFIX} ${AUTO_FDO_OPT}")
set(CMAKE_SHARED_LINKER_FLAGS "${LD_OPT} -Wl,-z,noexecstack ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT}")
set(CMAKE_EXE_LINKER_FLAGS "${LD_OPT} -Wl,-z,noexecstack ${PIE_OPT} ${THIN_LTO_CONCURRENCY_LINK} ${REORDER_LINK_OPT} ${CMAKE_COVERAGE_EXE_LINKER_OPTIONS}")
diff --git a/cmake/Rust.cmake b/cmake/Rust.cmake
index 3d1118b9d1..93028fc36c 100644
--- a/cmake/Rust.cmake
+++ b/cmake/Rust.cmake
@@ -50,20 +50,6 @@ endif()
# Keep all cargo output inside the CMake build tree (isolated per build dir).
set(RUST_TARGET_DIR "${CMAKE_BINARY_DIR}/rust-target")
-# Cargo's staticlib artifact name is platform-specific: libsql_nio.a on
-# Unix/MSYS, sql_nio.lib with the MSVC toolchain.
-if(WIN32)
- set(RUST_STATICLIB "${RUST_TARGET_DIR}/${_cargo_out_subdir}/sql_nio.lib")
-else()
- set(RUST_STATICLIB "${RUST_TARGET_DIR}/${_cargo_out_subdir}/libsql_nio.a")
-endif()
-
-# Sources whose change should retrigger a rebuild of the staticlib.
-file(GLOB_RECURSE _rust_sources CONFIGURE_DEPENDS "${RUST_CRATE_DIR}/src/*.rs")
-list(APPEND _rust_sources
- "${RUST_WORKSPACE_DIR}/Cargo.toml"
- "${RUST_WORKSPACE_DIR}/rust-toolchain.toml"
- "${RUST_CRATE_DIR}/Cargo.toml")
# CC/AR: cargo inherits CMake's PATH but not its compiler variables, and
# `ring` (rustls's crypto backend) compiles C through the `cc` crate. Pin it
@@ -89,12 +75,99 @@ if(APPLE)
endif()
endif()
+# Android NDK: cargo must cross-compile for the Android target triple instead
+# of the host. Without --target, cargo builds sql-nio for the host (macOS) and
+# ring's cc crate then drives the NDK clang with Apple target flags, which
+# fails on Apple-only headers (TargetConditionals.h). Wire the per-target
+# CC/AR/linker env at the NDK clang wrappers so ring's C compiles for Android.
+set(_rust_cargo_target_args "")
+set(_rust_target_subdir "")
+if(OB_ANDROID)
+ # ABI -> Rust target triple (seekdb's build.sh pins arm64-v8a; map the rest).
+ if(ANDROID_ABI STREQUAL "arm64-v8a")
+ set(_rust_android_target "aarch64-linux-android")
+ set(_rust_android_clang_prefix "aarch64-linux-android")
+ elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
+ # Note the NDK clang wrapper for 32-bit ARM is armv7a-, the ar wrapper armv7-.
+ set(_rust_android_target "armv7-linux-androideabi")
+ set(_rust_android_clang_prefix "armv7a-linux-androideabi")
+ elseif(ANDROID_ABI STREQUAL "x86")
+ set(_rust_android_target "i686-linux-android")
+ set(_rust_android_clang_prefix "i686-linux-android")
+ elseif(ANDROID_ABI STREQUAL "x86_64")
+ set(_rust_android_target "x86_64-linux-android")
+ set(_rust_android_clang_prefix "x86_64-linux-android")
+ else()
+ message(FATAL_ERROR "[rust] unsupported ANDROID_ABI '${ANDROID_ABI}'")
+ endif()
+
+ # NDK toolchain bin dir: prefer the toolchain's own var, else CMAKE_ANDROID_NDK,
+ # else derive the NDK root from CMAKE_TOOLCHAIN_FILE (same walk as src/include/CMakeLists.txt).
+ if(ANDROID_TOOLCHAIN_ROOT)
+ set(_rust_android_bin "${ANDROID_TOOLCHAIN_ROOT}/bin")
+ else()
+ set(_rust_ndk "")
+ if(CMAKE_ANDROID_NDK)
+ set(_rust_ndk "${CMAKE_ANDROID_NDK}")
+ elseif(CMAKE_TOOLCHAIN_FILE)
+ get_filename_component(_rust_ndk "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY)
+ get_filename_component(_rust_ndk "${_rust_ndk}" DIRECTORY)
+ get_filename_component(_rust_ndk "${_rust_ndk}" DIRECTORY)
+ endif()
+ if(_rust_ndk)
+ file(GLOB _rust_ndk_bin_dirs "${_rust_ndk}/toolchains/llvm/prebuilt/*/bin")
+ if(_rust_ndk_bin_dirs)
+ list(GET _rust_ndk_bin_dirs 0 _rust_android_bin)
+ endif()
+ endif()
+ endif()
+ if(NOT _rust_android_bin)
+ message(FATAL_ERROR "[rust] cannot locate the NDK toolchain bin dir "
+ "(set ANDROID_TOOLCHAIN_ROOT or configure with the NDK toolchain file)")
+ endif()
+
+ # ANDROID_PLATFORM is either android-28 or 28; the NDK clang wrapper is
+ # suffixed with the API level (aarch64-linux-android28-clang).
+ string(REGEX REPLACE "^android-" "" _rust_android_api "${ANDROID_PLATFORM}")
+ if(NOT _rust_android_api MATCHES "^[0-9]+$")
+ set(_rust_android_api "28")
+ endif()
+
+ # cargo/cc-rs per-target env names: triple uppercased with '-' -> '_'
+ # (e.g. aarch64_linux_android).
+ string(TOUPPER "${_rust_android_target}" _rust_android_env)
+ string(REPLACE "-" "_" _rust_android_env "${_rust_android_env}")
+
+ list(APPEND _rust_build_env
+ "CC_${_rust_android_env}=${_rust_android_bin}/${_rust_android_clang_prefix}${_rust_android_api}-clang"
+ "AR_${_rust_android_env}=${_rust_android_bin}/llvm-ar"
+ "CARGO_TARGET_${_rust_android_env}_LINKER=${_rust_android_bin}/${_rust_android_clang_prefix}${_rust_android_api}-clang++")
+ set(_rust_cargo_target_args "--target" "${_rust_android_target}")
+ set(_rust_target_subdir "${_rust_android_target}/")
+endif()
+
+# Cargo's staticlib artifact name is platform-specific: libsql_nio.a on
+# Unix/MSYS, sql_nio.lib with the MSVC toolchain; with --target the artifact
+# nests under /.
+if(WIN32)
+ set(RUST_STATICLIB "${RUST_TARGET_DIR}/${_rust_target_subdir}${_cargo_out_subdir}/sql_nio.lib")
+else()
+ set(RUST_STATICLIB "${RUST_TARGET_DIR}/${_rust_target_subdir}${_cargo_out_subdir}/libsql_nio.a")
+endif()
+
+# Sources whose change should retrigger a rebuild of the staticlib.
+file(GLOB_RECURSE _rust_sources CONFIGURE_DEPENDS "${RUST_CRATE_DIR}/src/*.rs")
+list(APPEND _rust_sources
+ "${RUST_WORKSPACE_DIR}/Cargo.toml"
+ "${RUST_WORKSPACE_DIR}/rust-toolchain.toml"
+ "${RUST_CRATE_DIR}/Cargo.toml")
+
add_custom_command(
OUTPUT "${RUST_STATICLIB}"
COMMAND "${CMAKE_COMMAND}" -E env ${_rust_build_env}
"${CARGO}" build ${_cargo_profile_flag}
--manifest-path "${RUST_WORKSPACE_DIR}/Cargo.toml"
- --package sql-nio
+ --package sql-nio ${_rust_cargo_target_args}
WORKING_DIRECTORY "${RUST_WORKSPACE_DIR}"
DEPENDS ${_rust_sources}
COMMENT "[rust] cargo build sql-nio (${_cargo_out_subdir})"
@@ -111,7 +184,9 @@ if(WIN32)
else()
find_package(Threads REQUIRED)
set(_rust_syslibs Threads::Threads ${CMAKE_DL_LIBS} m)
- if(NOT APPLE)
+ if(NOT APPLE AND NOT ANDROID)
+ # glibc < 2.17 needed librt for clock_*; Android's bionic merged rt into
+ # libc and ships no librt.so, so -lrt would fail the final link.
list(APPEND _rust_syslibs rt)
endif()
endif()
diff --git a/deps/init/dep_create.ps1 b/deps/init/dep_create.ps1
index 118b828343..96095662eb 100644
--- a/deps/init/dep_create.ps1
+++ b/deps/init/dep_create.ps1
@@ -9,7 +9,7 @@
After extraction the layout is:
deps/3rd/vcpkg/x64-windows/ (vcpkg installed packages)
deps/3rd/openssl/ (OpenSSL)
- deps/3rd/vsag/ (vsag vector search library)
+ deps/3rd/vsag/ (vsag vector search library)
deps/3rd/tools/cmake/ (CMake)
deps/3rd/tools/ninja/ (Ninja)
deps/3rd/tools/llvm18/ (LLVM 18)
@@ -123,20 +123,16 @@ foreach ($sect in $sections.Keys) {
Write-Log " cached"
} else {
Write-Log " downloading from $url ..."
+ # Schannel/CI: use --ssl-no-revoke (avoids curl 35 / revocation offline); retries for flaky links.
$tmpPath = "$pkgPath.tmp"
- try {
- & curl.exe -L -f -s --retry 3 --retry-delay 2 -o $tmpPath $url
- if ($LASTEXITCODE -ne 0) {
- throw "curl exit code $LASTEXITCODE"
- }
- Move-Item -Force $tmpPath $pkgPath
- }
- catch {
- if (Test-Path $tmpPath) { Remove-Item -Force $tmpPath }
- Write-Err "Failed to download: $url"
- Write-Err "$_"
+ if (Test-Path $tmpPath) { Remove-Item -Force $tmpPath -ErrorAction SilentlyContinue }
+ & curl.exe -L -f -sS --connect-timeout 120 --ssl-no-revoke --retry 3 --retry-delay 2 -o $tmpPath $url
+ if ($LASTEXITCODE -ne 0) {
+ if (Test-Path $tmpPath) { Remove-Item -Force $tmpPath -ErrorAction SilentlyContinue }
+ Write-Err "Failed to download: $url (curl exit $LASTEXITCODE)"
exit 4
}
+ Move-Item -Force $tmpPath $pkgPath
}
# -- Extract -------------------------------------------------
diff --git a/deps/init/dep_create.sh b/deps/init/dep_create.sh
index ac91f1d1de..1c79bccbe7 100644
--- a/deps/init/dep_create.sh
+++ b/deps/init/dep_create.sh
@@ -72,6 +72,17 @@ function echo_err() {
echo -e "[dep_create.sh][ERROR] $@" 1>&2
}
+# GNU tar: archives built on macOS may contain PAX extended headers (e.g. LIBARCHIVE.xattr.com.apple.provenance);
+# extracting on Linux otherwise prints harmless "Ignoring unknown extended header keyword" noise.
+function extract_tar_gz_strip1() {
+ local dir="$1" archive="$2"
+ if tar --version 2>/dev/null | head -n1 | grep -q 'GNU tar'; then
+ (cd "$dir" && tar --warning=no-unknown-keyword -xzf "$archive" --strip-components=1)
+ else
+ (cd "$dir" && tar -xzf "$archive" --strip-components=1)
+ fi
+}
+
function get_os_release() {
if [[ "${ANDROID_BUILD}" == "true" ]]; then
OS_RELEASE="android"
@@ -461,7 +472,7 @@ do
fi
echo_log "unpack package <${pkg}>... \c"
if [[ "${IS_TAR_PLATFORM}" == "true" ]]; then
- (cd ${TARGET_DIR_3RD} && tar -xzf "${TARGET_DIR_3RD}/pkg/${pkg}" --strip-components=1)
+ extract_tar_gz_strip1 "${TARGET_DIR_3RD}" "${TARGET_DIR_3RD}/pkg/${pkg}"
elif [[ "$ID" = "arch" || "$ID" = "garuda" ]]; then
(cd ${TARGET_DIR_3RD} && rpmextract.sh "${TARGET_DIR_3RD}/pkg/${pkg}")
else
diff --git a/deps/init/oceanbase.al8.aarch64.deps b/deps/init/oceanbase.al8.aarch64.deps
index 97edde30c9..4afb2306e5 100644
--- a/deps/init/oceanbase.al8.aarch64.deps
+++ b/deps/init/oceanbase.al8.aarch64.deps
@@ -38,7 +38,7 @@ obdevtools-flex-2.5.35-42024092621.al8.aarch64.rpm
obdevtools-gcc-12.3.0-32024122017.al8.aarch64.rpm
obdevtools-llvm-17.0.6-202026032415.al8.aarch64.rpm
#RANGE_IF_BUSINESS
-ob-sanity-1.0.0-182026021018.al8.aarch64.rpm
+#ob-sanity-1.0.0-182026021018.al8.aarch64.rpm
#RANGE_END
[tools-deps]
diff --git a/deps/init/oceanbase.al8.x86_64.deps b/deps/init/oceanbase.al8.x86_64.deps
index 291edc48e5..b9f12fb39a 100644
--- a/deps/init/oceanbase.al8.x86_64.deps
+++ b/deps/init/oceanbase.al8.x86_64.deps
@@ -39,7 +39,7 @@ obdevtools-flex-2.5.35-42024092621.al8.x86_64.rpm
obdevtools-gcc-12.3.0-32024122017.al8.x86_64.rpm
obdevtools-llvm-17.0.6-202026032415.al8.x86_64.rpm
#RANGE_IF_BUSINESS
-ob-sanity-1.0.0-182026021018.al8.x86_64.rpm
+#ob-sanity-1.0.0-182026021018.al8.x86_64.rpm
#RANGE_END
[tools-deps]
diff --git a/deps/init/oceanbase.el7.aarch64.deps b/deps/init/oceanbase.el7.aarch64.deps
index 653bcc48df..78069872ea 100644
--- a/deps/init/oceanbase.el7.aarch64.deps
+++ b/deps/init/oceanbase.el7.aarch64.deps
@@ -50,24 +50,24 @@ obdevtools-flex-2.5.35-12022100417.el7.aarch64.rpm
obdevtools-gcc-12.3.0-32024122017.el7.aarch64.rpm
obdevtools-llvm-17.0.6-202026032415.el7.aarch64.rpm
#RANGE_IF_BUSINESS
-ob-sanity-1.0.0-182026021018.el7.aarch64.rpm
+#ob-sanity-1.0.0-182026021018.el7.aarch64.rpm
#RANGE_END
[tools-deps]
#RANGE_IF_BUSINESS
-obshell-4.5.0.0-12026050816.el7.aarch64.rpm target=obshell
+#obshell-4.5.0.0-12026050816.el7.aarch64.rpm target=obshell
#RANGE_ELSE
-#obshell-4.4.1.1-32026031914.el7.aarch64.rpm target=community
+obshell-4.4.1.1-32026031914.el7.aarch64.rpm target=community
##obshell-4.5.0.0-12026050816.el7.aarch64.rpm target=community
#RANGE_END
[test-utils]
#RANGE_IF_BUSINESS
-ob-deploy-4.1.x-190.el7.aarch64.rpm target=obdeploy
-obclient-2.2.3-20230515141610.el7.aarch64.rpm
-libobclient-2.2.3-20230512140006.el7.aarch64.rpm
+#ob-deploy-4.1.x-190.el7.aarch64.rpm target=obdeploy
+#obclient-2.2.3-20230515141610.el7.aarch64.rpm
+#libobclient-2.2.3-20230512140006.el7.aarch64.rpm
#RANGE_ELSE
-#ob-deploy-4.1.0-2.el7.aarch64.rpm target=community
-#obclient-2.2.2-1.el7.aarch64.rpm target=community
-#libobclient-2.2.2-3.el7.aarch64.rpm target=community
+ob-deploy-4.1.0-2.el7.aarch64.rpm target=community
+obclient-2.2.2-1.el7.aarch64.rpm target=community
+libobclient-2.2.2-3.el7.aarch64.rpm target=community
#RANGE_END
diff --git a/deps/init/oceanbase.el7.x86_64.deps b/deps/init/oceanbase.el7.x86_64.deps
index 4b95984bb0..25ed456c4d 100644
--- a/deps/init/oceanbase.el7.x86_64.deps
+++ b/deps/init/oceanbase.el7.x86_64.deps
@@ -58,24 +58,24 @@ obdevtools-flex-2.5.35-12022100417.el7.x86_64.rpm
obdevtools-gcc-12.3.0-32024122017.el7.x86_64.rpm
obdevtools-llvm-17.0.6-202026032415.el7.x86_64.rpm
#RANGE_IF_BUSINESS
-ob-sanity-1.0.0-182026021018.el7.x86_64.rpm
+#ob-sanity-1.0.0-182026021018.el7.x86_64.rpm
#RANGE_END
[tools-deps]
#RANGE_IF_BUSINESS
-obshell-4.5.0.0-12026050816.el7.x86_64.rpm target=obshell
+#obshell-4.5.0.0-12026050816.el7.x86_64.rpm target=obshell
#RANGE_ELSE
-#obshell-4.4.1.1-32026031914.el7.x86_64.rpm target=community
+obshell-4.4.1.1-32026031914.el7.x86_64.rpm target=community
##obshell-4.5.0.0-12026050816.el7.x86_64.rpm target=community
#RANGE_END
[test-utils]
#RANGE_IF_BUSINESS
-ob-deploy-4.1.x-190.el7.x86_64.rpm target=obdeploy
-obclient-2.2.3-20230515141610.el7.x86_64.rpm
-libobclient-2.2.3-20230512140006.el7.x86_64.rpm
+#ob-deploy-4.1.x-190.el7.x86_64.rpm target=obdeploy
+#obclient-2.2.3-20230515141610.el7.x86_64.rpm
+#libobclient-2.2.3-20230512140006.el7.x86_64.rpm
#RANGE_ELSE
-#ob-deploy-4.1.0-2.el7.x86_64.rpm target=community
-#obclient-2.2.2-1.el7.x86_64.rpm target=community
-#libobclient-2.2.2-3.el7.x86_64.rpm target=community
+ob-deploy-4.1.0-2.el7.x86_64.rpm target=community
+obclient-2.2.2-1.el7.x86_64.rpm target=community
+libobclient-2.2.2-3.el7.x86_64.rpm target=community
#RANGE_END
diff --git a/deps/init/oceanbase.el9.x86_64.deps b/deps/init/oceanbase.el9.x86_64.deps
index 920668e571..86eece9b41 100644
--- a/deps/init/oceanbase.el9.x86_64.deps
+++ b/deps/init/oceanbase.el9.x86_64.deps
@@ -60,19 +60,19 @@ obdevtools-llvm-17.0.6-202026032415.el8.x86_64.rpm
[tools-deps]
#RANGE_IF_BUSINESS
-obshell-4.5.0.0-12026050816.el8.x86_64.rpm target=obshell
+#obshell-4.5.0.0-12026050816.el8.x86_64.rpm target=obshell
#RANGE_ELSE
-#obshell-4.4.1.1-32026031914.el8.x86_64.rpm target=community
+obshell-4.4.1.1-32026031914.el8.x86_64.rpm target=community
##obshell-4.5.0.0-12026050816.el8.x86_64.rpm target=community
#RANGE_END
[test-utils]
#RANGE_IF_BUSINESS
-ob-deploy-4.1.x-190.el8.x86_64.rpm target=obdeploy
-obclient-2.2.3-1.el8.x86_64.rpm
-libobclient-2.2.3-32024020117.el8.x86_64.rpm
+#ob-deploy-4.1.x-190.el8.x86_64.rpm target=obdeploy
+#obclient-2.2.3-1.el8.x86_64.rpm
+#libobclient-2.2.3-32024020117.el8.x86_64.rpm
#RANGE_ELSE
-#ob-deploy-4.1.0-2.el8.x86_64.rpm target=community
-#obclient-2.2.2-1.el8.x86_64.rpm target=community
-#libobclient-2.2.2-3.el8.x86_64.rpm target=community
+ob-deploy-4.1.0-2.el8.x86_64.rpm target=community
+obclient-2.2.2-1.el8.x86_64.rpm target=community
+libobclient-2.2.2-3.el8.x86_64.rpm target=community
#RANGE_END
diff --git a/deps/oblib/src/grpc/CMakeLists.txt b/deps/oblib/src/grpc/CMakeLists.txt
new file mode 100644
index 0000000000..919c898e5d
--- /dev/null
+++ b/deps/oblib/src/grpc/CMakeLists.txt
@@ -0,0 +1,179 @@
+set(PROTO_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+set(PROTO_GEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+
+set(PROTO_NAMES
+ example
+ storageservice
+ logservice
+ serverservice
+)
+
+# On Linux: grpc libs are in lib/grpc/, abseil/protobuf libs are in lib64/grpc/
+# On macOS: all libs are in lib/grpc/
+if(APPLE OR OB_ANDROID)
+ set(GRPC_LIB_DIR "lib")
+ set(ABSL_LIB_DIR "lib")
+else()
+ set(GRPC_LIB_DIR "lib")
+ set(ABSL_LIB_DIR "lib64")
+endif()
+
+if(WIN32)
+ list(APPEND CMAKE_PREFIX_PATH "${OB_VCPKG_DIR}")
+ # NOTE: CMAKE_MAP_IMPORTED_CONFIG_ (DEBUG/RelWithDebInfo/MinSizeRel
+ # -> Release) is set globally in cmake/Env.cmake. It forces imported
+ # targets from find_package(... CONFIG) below (and any transitive deps
+ # such as abseil/protobuf/openssl from gRPC) to resolve to their Release
+ # .lib variants, matching our Release MSVC runtime in all build types.
+ find_package(gRPC CONFIG REQUIRED)
+ find_package(protobuf CONFIG REQUIRED)
+ set(_PROTOC $)
+ set(_GRPC_CPP_PLUGIN $)
+else()
+ set(DEVEL_PATH "${DEP_3RD_DIR}/usr/local/oceanbase/deps/devel")
+ option(protobuf_MODULE_COMPATIBLE TRUE)
+ set(_PROTOC "${DEVEL_PATH}/bin/protoc")
+ set(_GRPC_CPP_PLUGIN "${DEVEL_PATH}/bin/grpc_cpp_plugin")
+ set(_PROTOC_LD_PATH "${DEVTOOLS_DIR}/lib64")
+ include_directories(
+ ${DEP_3RD_DIR}/usr/include
+ ${DEVEL_PATH}/include
+ )
+endif()
+
+# Generate .pb.cc/.pb.h and .grpc.pb.cc/.grpc.pb.h from .proto files at build time
+set(GENERATED_SRCS)
+foreach(PROTO_NAME ${PROTO_NAMES})
+ set(PROTO_FILE "${PROTO_SRC_DIR}/${PROTO_NAME}.proto")
+ set(PB_CC "${PROTO_GEN_DIR}/${PROTO_NAME}.pb.cc")
+ set(PB_H "${PROTO_GEN_DIR}/${PROTO_NAME}.pb.h")
+ set(GRPC_CC "${PROTO_GEN_DIR}/${PROTO_NAME}.grpc.pb.cc")
+ set(GRPC_H "${PROTO_GEN_DIR}/${PROTO_NAME}.grpc.pb.h")
+
+ # Linux/macOS: protoc may need LD_LIBRARY_PATH for bundled libprotobuf.so.
+ # Windows (vcpkg): invoke protoc/grpc_cpp_plugin directly; LD_LIBRARY_PATH is meaningless.
+ if(WIN32)
+ add_custom_command(
+ OUTPUT ${PB_CC} ${PB_H}
+ COMMAND ${_PROTOC}
+ --cpp_out=${PROTO_GEN_DIR}
+ -I${PROTO_SRC_DIR}
+ ${PROTO_FILE}
+ DEPENDS ${PROTO_FILE}
+ COMMENT "Generating protobuf C++ from ${PROTO_NAME}.proto"
+ )
+
+ add_custom_command(
+ OUTPUT ${GRPC_CC} ${GRPC_H}
+ COMMAND ${_PROTOC}
+ --grpc_out=${PROTO_GEN_DIR}
+ --plugin=protoc-gen-grpc=${_GRPC_CPP_PLUGIN}
+ -I${PROTO_SRC_DIR}
+ ${PROTO_FILE}
+ DEPENDS ${PROTO_FILE}
+ COMMENT "Generating gRPC C++ from ${PROTO_NAME}.proto"
+ )
+ else()
+ add_custom_command(
+ OUTPUT ${PB_CC} ${PB_H}
+ COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${_PROTOC_LD_PATH}:$ENV{LD_LIBRARY_PATH}"
+ ${_PROTOC}
+ --cpp_out=${PROTO_GEN_DIR}
+ -I${PROTO_SRC_DIR}
+ ${PROTO_FILE}
+ DEPENDS ${PROTO_FILE}
+ COMMENT "Generating protobuf C++ from ${PROTO_NAME}.proto"
+ )
+
+ add_custom_command(
+ OUTPUT ${GRPC_CC} ${GRPC_H}
+ COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${_PROTOC_LD_PATH}:$ENV{LD_LIBRARY_PATH}"
+ ${_PROTOC}
+ --grpc_out=${PROTO_GEN_DIR}
+ --plugin=protoc-gen-grpc=${_GRPC_CPP_PLUGIN}
+ -I${PROTO_SRC_DIR}
+ ${PROTO_FILE}
+ DEPENDS ${PROTO_FILE}
+ COMMENT "Generating gRPC C++ from ${PROTO_NAME}.proto"
+ )
+ endif()
+
+ list(APPEND GENERATED_SRCS ${PB_CC} ${GRPC_CC})
+endforeach()
+
+add_library(oblib_grpc STATIC
+ ${GENERATED_SRCS}
+ ob_grpc_context.cpp
+ ob_grpc_server.cpp
+)
+
+target_include_directories(oblib_grpc
+ PUBLIC ${PROTO_GEN_DIR}
+)
+
+if(WIN32)
+ target_link_libraries(oblib_grpc
+ PUBLIC oblib_base
+ gRPC::grpc++
+ gRPC::grpc++_reflection
+ protobuf::libprotobuf
+ )
+else()
+ set(GRPC_STATIC_LIB_DIR "${DEVEL_PATH}/${GRPC_LIB_DIR}/grpc")
+ set(ABSL_STATIC_LIB_DIR "${DEVEL_PATH}/${ABSL_LIB_DIR}/grpc")
+
+ set(GRPC_CORE_LIB_NAMES
+ grpc++ grpc grpc_unsecure grpc++_reflection grpc++_error_details
+ grpc_plugin_support grpcpp_channelz gpr
+ address_sorting re2 upb
+ )
+
+ set(ABSL_LIB_NAMES
+ absl_base absl_int128 absl_throw_delegate absl_raw_logging_internal
+ absl_log_severity absl_spinlock_wait absl_malloc_internal
+ absl_debugging_internal absl_demangle_internal absl_stacktrace
+ absl_symbolize absl_examine_stack absl_failure_signal_handler
+ absl_strerror
+ absl_strings absl_strings_internal absl_str_format_internal
+ absl_cord absl_cord_internal absl_cordz_info absl_cordz_handle
+ absl_cordz_functions absl_cordz_sample_token
+ absl_hash absl_low_level_hash absl_city
+ absl_raw_hash_set absl_hashtablez_sampler
+ absl_status absl_statusor
+ absl_bad_any_cast_impl absl_bad_optional_access absl_bad_variant_access
+ absl_synchronization absl_graphcycles_internal absl_time absl_time_zone
+ absl_civil_time absl_exponential_biased absl_periodic_sampler
+ absl_random_distributions absl_random_seed_sequences
+ absl_random_seed_gen_exception absl_random_internal_pool_urbg
+ absl_random_internal_randen absl_random_internal_randen_hwaes
+ absl_random_internal_randen_hwaes_impl absl_random_internal_randen_slow
+ absl_random_internal_seed_material absl_random_internal_platform
+ absl_random_internal_distribution_test_util
+ absl_flags absl_flags_commandlineflag absl_flags_commandlineflag_internal
+ absl_flags_config absl_flags_internal absl_flags_marshalling
+ absl_flags_parse absl_flags_private_handle_accessor
+ absl_flags_program_name absl_flags_reflection
+ absl_flags_usage absl_flags_usage_internal
+ absl_leak_check absl_leak_check_disable absl_scoped_set_env
+ )
+
+ set(_GRPC_STATIC_LIBS)
+ foreach(_name ${GRPC_CORE_LIB_NAMES})
+ list(APPEND _GRPC_STATIC_LIBS "${GRPC_STATIC_LIB_DIR}/lib${_name}.a")
+ endforeach()
+ foreach(_name ${ABSL_LIB_NAMES})
+ list(APPEND _GRPC_STATIC_LIBS "${ABSL_STATIC_LIB_DIR}/lib${_name}.a")
+ endforeach()
+
+ target_link_libraries(oblib_grpc
+ PUBLIC oblib_base
+ ${_GRPC_STATIC_LIBS}
+ ${ABSL_STATIC_LIB_DIR}/libcares.a
+ ${ABSL_STATIC_LIB_DIR}/libprotobuf.a
+ )
+endif()
+
+# macOS requires libresolv for c-ares DNS resolution
+if(APPLE)
+ target_link_libraries(oblib_grpc PUBLIC resolv)
+endif()
diff --git a/docs/developer-guide/en/android.md b/docs/developer-guide/en/android.md
index dc7ecd5129..56f68ff95c 100644
--- a/docs/developer-guide/en/android.md
+++ b/docs/developer-guide/en/android.md
@@ -5,7 +5,7 @@ Cross-compile seekdb for Android arm64-v8a on macOS using the NDK toolchain, the
## Prerequisites
- macOS host (this guide is written for macOS)
-- Android NDK 27.x installed (default: `~/Library/Android/sdk/ndk/27.3.13750724`)
+- Android NDK installed (**27.x is recommended** to match pre-built dependencies; other major versions are untested). Default path example: `~/Library/Android/sdk/ndk/27.3.13750724`
- Android emulator running arm64-v8a (API 28+), or a physical device
- Dependencies built via [ob-deps](https://github.com/oceanbase/ob-deps/tree/android_arm64-v8a) `ndk/build_all.sh`
- `adb` available on PATH
@@ -31,6 +31,7 @@ This runs `deps/init/dep_create.sh` in Android mode, which downloads and extract
pre-built NDK dependency tarballs into `deps/3rd/`.
### 2. Configure and build
+
To build only the observer binary:
```bash
@@ -38,6 +39,26 @@ cd build_android_release
make seekdb -j$(nproc)
```
+### Build libseekdb (FFI shared library)
+
+In the same Android build directory, build the C API shared library (CMake target `libseekdb`, output `libseekdb.so`):
+
+```bash
+cd build_android_release
+make libseekdb -j$(nproc)
+```
+
+The artifact is usually `build_android_release/src/include/libseekdb.so` (relative to the repo root). The public header is `src/include/seekdb.h` in the source tree.
+
+To reduce size, strip ELF with the NDK `llvm-strip` (not the host `strip`). On macOS or Linux hosts the toolchain lives under `toolchains/llvm/prebuilt//bin/`, for example:
+
+```bash
+NDK_STRIP=$(echo "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/llvm-strip)
+$NDK_STRIP -o /tmp/libseekdb.stripped build_android_release/src/include/libseekdb.so
+```
+
+You can also pack `seekdb.h` and `libseekdb.so` into **`libseekdb-android-arm64-v8a.zip`** with [`package/libseekdb/libseekdb-build.sh`](../../../package/libseekdb/libseekdb-build.sh) (**arm64-v8a only**). From `package/libseekdb/` run `./libseekdb-build.sh --android` (builds if needed), or `./libseekdb-build.sh ` to pack an existing tree. On macOS, a tree that only contains the NDK-built `libseekdb.so` still gets that naming (not `darwin-*`).
+
### 3. Build unit tests (optional)
A combined `all_tests` binary includes all unit tests in a single executable:
@@ -50,8 +71,9 @@ make all_tests
## Deploy to Emulator
### Strip debug symbols
+
```bash
-NDK_STRIP=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-strip
+NDK_STRIP=$(echo "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/llvm-strip)
$NDK_STRIP -o /tmp/seekdb build_android_release/src/observer/seekdb
```
diff --git a/docs/developer-guide/zh/android.md b/docs/developer-guide/zh/android.md
index d07e1d61c6..34b4e804e9 100644
--- a/docs/developer-guide/zh/android.md
+++ b/docs/developer-guide/zh/android.md
@@ -5,7 +5,7 @@
## 前置条件
- macOS 主机(本文档基于 macOS 环境编写)
-- 已安装 Android NDK 27.x(默认路径:`~/Library/Android/sdk/ndk/27.3.13750724`)
+- 已安装 Android NDK(**推荐 27.x**,与预构建依赖一致;其它主版本需自行验证。默认路径示例:`~/Library/Android/sdk/ndk/27.3.13750724`)
- 运行 arm64-v8a(API 28+)的 Android 模拟器,或物理设备
- 通过 [ob-deps](https://github.com/oceanbase/ob-deps/tree/android_arm64-v8a) 的 `ndk/build_all.sh` 构建依赖
- 已安装 `adb` 并加入 PATH
@@ -38,6 +38,26 @@ cd build_android_release
make seekdb -j$(nproc)
```
+### 构建 libseekdb(FFI 共享库)
+
+在相同 Android 构建目录下编译 C API 共享库(CMake 目标名 `libseekdb`,产物为 `libseekdb.so`):
+
+```bash
+cd build_android_release
+make libseekdb -j$(nproc)
+```
+
+产物路径一般为仓库根目录下的 `build_android_release/src/include/libseekdb.so`,头文件为源码树中的 `src/include/seekdb.h`。
+
+若需缩小体积,请使用 NDK 自带的 `llvm-strip` 处理 ELF(不要用 macOS 自带的 `strip`)。在 macOS / Linux 主机上,工具链位于 `toolchains/llvm/prebuilt/<宿主>/bin/`,例如:
+
+```bash
+NDK_STRIP=$(echo "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/llvm-strip)
+$NDK_STRIP -o /tmp/libseekdb.stripped build_android_release/src/include/libseekdb.so
+```
+
+也可在仓库内使用 [`package/libseekdb/libseekdb-build.sh`](../../../package/libseekdb/libseekdb-build.sh) 打包 `seekdb.h` 与 `libseekdb.so` 为 **`libseekdb-android-arm64-v8a.zip`**(仅支持 **arm64-v8a**)。在 `package/libseekdb/` 下执行 `./libseekdb-build.sh --android`(会先按需构建),或 `./libseekdb-build.sh ` 仅打包已有产物;在 macOS 上仅含 NDK 产出的 `libseekdb.so` 时也会使用该命名,避免误用 `darwin-*`。
+
### 3. 构建单元测试(可选)
`all_tests` 会将所有单元测试合并到一个可执行文件中:
@@ -52,7 +72,7 @@ make all_tests
### 移除调试符号
```bash
-NDK_STRIP=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-strip
+NDK_STRIP=$(echo "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/llvm-strip)
$NDK_STRIP -o /tmp/seekdb build_android_release/src/observer/seekdb
```
diff --git a/package/libseekdb/README.md b/package/libseekdb/README.md
new file mode 100644
index 0000000000..03ec0aed8f
--- /dev/null
+++ b/package/libseekdb/README.md
@@ -0,0 +1,143 @@
+# libseekdb package
+
+Portable C library build of libseekdb for Linux (x64/arm64), macOS (arm64), and Windows (x64). Output is a zip containing `seekdb.h` and `libseekdb.so` (Linux), `libseekdb.dylib` (macOS), or `seekdb.dll` / `seekdb.lib` (Windows), suitable for standalone use.
+
+## Build
+
+```bash
+./libseekdb-build.sh
+```
+
+On Windows (after configuring and building target `libseekdb`, e.g. `.\build.ps1 release --ninja --target libseekdb -DBUILD_EMBED_MODE=ON`):
+
+```powershell
+cd package\libseekdb
+.\libseekdb-build.ps1
+```
+
+Output: `libseekdb--.zip` is created in this directory. Arch is `x64` (for x86_64) or `arm64`, e.g. `libseekdb-linux-x64.zip`, `libseekdb-linux-arm64.zip`, `libseekdb-darwin-arm64.zip`, `libseekdb-windows-x64.zip`.
+
+### Reference build environments (CI)
+
+The supported systems and environments are defined by the GitHub Actions workflow [`.github/workflows/build-libseekdb.yml`](../../.github/workflows/build-libseekdb.yml). The workflow builds on push/PR and optionally uploads zips to S3 when **DESTINATION_TARGET_PATH** (e.g. `s3://bucket/libseekdb/`) or **AWS_S3_BUCKET** and AWS credentials are configured.
+
+| Platform | Zip name | Runner / container | Deps profile |
+| ----------- | -------------------------- | ---------------------------------------------------- | ------------------------- |
+| Linux x64 | libseekdb-linux-x64.zip | ubuntu-22.04 + quay.io/pypa/manylinux2014_x86_64 | oceanbase.el7.x86_64.deps |
+| Linux arm64 | libseekdb-linux-arm64.zip | ubuntu-22.04-arm + quay.io/pypa/manylinux2014_aarch64 | oceanbase.el7.aarch64.deps |
+| macOS arm64 | libseekdb-darwin-arm64.zip | macos-15 (native) | oceanbase.macos.arm64.deps |
+| Windows x64 | libseekdb-windows-x64.zip | windows-2022 (native) | oceanbase.windows.x86_64.deps |
+
+Use these systems and deps as the standard when building or consuming libseekdb.
+
+### Linux glibc compatibility
+
+CI Linux builds use **pypa/manylinux2014** (CentOS 7–based), which ships **glibc 2.17**. The prebuilt `libseekdb.so` therefore requires **GLIBC_2.17** or newer on the target system, **including CentOS 7**.
+
+- **Supported (glibc ≥ 2.17)**: CentOS 7 / RHEL 7, CentOS 8 / RHEL 8, AlmaLinux 7/8, Rocky Linux 8/9, Ubuntu 18.04+, Debian 10+, Fedora 25+, and most distros from about 2014 onward. This covers current and legacy Linux environments, including CentOS 7.
+- **Not supported (glibc < 2.17)**: CentOS 6 (2.12) and older distros. For these, build libseekdb locally on the target system.
+
+To check your system: `ldd --version` or `getconf GNU_LIBC_VERSION`.
+
+### Linux (Node.js / static TLS)
+
+On Linux, loading `libseekdb.so` as a dependency of `seekdb.node` after Node has started can fail with:
+
+`cannot allocate memory in static TLS block`
+
+The library is whole-archive linked and needs a large static TLS reservation. Preload it when launching Node:
+
+```bash
+export LD_PRELOAD="/path/to/libseekdb.so${LD_PRELOAD:+:$LD_PRELOAD}"
+node your-app.js
+```
+
+`test-packed-artifact-smoke.sh` sets this automatically on Linux.
+
+### macOS compatibility
+
+CI macOS builds use **macOS 15** runners and set **CMAKE_OSX_DEPLOYMENT_TARGET=11.0**, so the prebuilt `libseekdb.dylib` runs on **macOS 11 (Big Sur) and later** (12, 13, 14, 15). Setting the deployment target to 11.0 allows use on most current and recent macOS versions.
+
+### CI validation (packed zip smoke)
+
+After `libseekdb-build.sh`, CI runs `test-packed-artifact-smoke.sh` on the zip. It:
+
+1. Unpacks the zip into a temp directory (`libseekdb.dylib` + `libs/` on macOS).
+2. Ad-hoc signs dylibs (macOS).
+3. Builds `seekdb.node` **into that directory** with `@loader_path` (`smoke-loader/binding.gyp`).
+4. Runs `smoke-loader/smoke-vsag.js` (VECTOR INDEX `lib=vsag` + `DBMS_HYBRID_SEARCH.SEARCH`).
+
+This exercises the standalone embed layout (`seekdb.node` + `@loader_path/libseekdb` + `libs/`). The old smoke used `nodejs_napi` linked to `build_release` via `@rpath`, which could pass while the packaged zip failed.
+
+On Windows the same smoke runs as `test-packed-artifact-smoke.ps1 -Zip ` (`smoke-loader/binding.gyp` has an `OS=='win'` branch that links `seekdb.lib` from the zip and builds `seekdb.node` next to `seekdb.dll`). It additionally prepends the extract directory and `libs/` to `PATH` because `seekdb.dll` loads its runtime DLLs from `libs/` (Windows DLL search does not recurse into subdirectories), and guards the vsag run with a wall-clock timeout + exit probe + `taskkill /T` fallback (vsag smoke has a stall history on Windows runners). The Windows smoke step runs as a **hard gate** in CI, same as linux/macos. The link-time import library is redirected to `seekdb_import.lib` in `binding.gyp` (`VCLinkerTool.ImportLibrary`) so the import lib MSVC emits while building `seekdb.node` does not collide with the `seekdb.lib` shipped in the zip (LNK1149).
+
+Locally:
+
+```bash
+cd package/libseekdb
+./test-packed-artifact-smoke.sh libseekdb-darwin-arm64.zip
+```
+
+PowerShell (Windows):
+
+```powershell
+cd package\libseekdb
+.\test-packed-artifact-smoke.ps1 -Zip libseekdb-windows-x64.zip
+```
+
+### macOS CI vs local dev builds (likely causes of divergent zips)
+
+| Factor | GitHub Actions (macos-15) | Typical local Mac |
+| ------ | ------------------------- | ----------------- |
+| Runner / OS | `macos-15`, `CMAKE_OSX_DEPLOYMENT_TARGET=11.0` | Host SDK, often no deployment target |
+| Dependencies | `install-macos-brew-deps.sh` (thrift **0.22** via formula or `brew extract`) | `brew install thrift` may pull 0.23+ |
+| Compile | `-DOB_USE_CCACHE=ON`, cached `deps/3rd` | Local ccache / incremental `build.sh` |
+| Pack input | Bundled dylib from CI `build_release` | Bundled from local `build_release` |
+| Codesign | Optional Developer ID + entitlements (repo secrets) | Often ad-hoc only |
+
+If smoke fails only on the CI zip, compare the main library and `libs/` sha256 between local and CI artifacts for the same commit.
+
+**ccache:** macOS/Linux CI ccache keys include `COMMIT_SHA` so object files from other commits are not restored into the same cache slot.
+
+## Package contents and standalone distribution
+
+Zip layout:
+
+```
+seekdb.h # C API header
+libseekdb.dylib # Main library (macOS) or libseekdb.so (Linux)
+seekdb.dll # Main library (Windows)
+seekdb.lib # Import library for MSVC-style linking (Windows)
+libs/ # Runtime dependencies (macOS: dylibbundler; Windows: vcpkg/OpenSSL/etc.)
+ *.dylib / *.dll
+```
+
+- **Standalone distribution**: After extraction, the package can be used by other projects without this repo or the build environment.
+- **macOS**: The main library and its dependencies use relative paths (`@loader_path/libs`). Unzip to any directory and keep the main library and `libs/` at the same level so they load correctly.
+- **Windows**: `libseekdb-build.ps1` runs `cmake/BundleRuntimeDllsWindows.cmake` to copy third-party DLLs into `libs/` (same dependency set as CI binding tests). Consumers should prepend the extract directory and `libs/` to `PATH`, or load `seekdb.dll` from a directory that includes `libs/` on the DLL search path.
+- **Linux**: Usually has no extra dependencies or relies on system libraries; unzip and use as-is.
+
+### How to use (standalone)
+
+1. Unzip to a target directory, e.g. `/opt/seekdb-sdk/`:
+ ```
+ /opt/seekdb-sdk/
+ seekdb.h
+ libseekdb.dylib
+ libs/
+ libfoo.dylib
+ ...
+ ```
+
+2. Point your build at the header and library, e.g.:
+ ```bash
+ gcc -I/opt/seekdb-sdk -L/opt/seekdb-sdk -lseekdb ...
+ ```
+ At runtime on macOS, the main library loads dependencies from `libs/` in the same directory; you do not need to set `DYLD_LIBRARY_PATH`.
+
+3. If the main library and the executable are in different directories (e.g. executable in `bin/`, library in `lib/`), ensure `libs/` exists next to the main library, or put the dependencies where the system can find them and set `DYLD_LIBRARY_PATH` (not recommended; prefer keeping the zip layout).
+
+### Notes
+
+- **OS and architecture**: The zip name reflects the build OS and CPU: `darwin-arm64`, `linux-x64`, `linux-arm64`, `windows-x64` (x64 = x86_64). Use the matching zip for the target environment. On Linux, the prebuilt .so requires glibc ≥ 2.17 (see [Linux glibc compatibility](#linux-glibc-compatibility)), including CentOS 7; on macOS, the prebuilt dylib is built on **macOS 15** with **minimum deployment target 11.0**, so it runs on **macOS 11 (Big Sur) and later** (12, 13, 14, 15). On Windows, CI builds on **windows-2022** with the MSVC-compatible Clang toolchain from deps (`deps/init/oceanbase.windows.x86_64.deps`).
+- **Rebuilding**: After changing loader path or dependencies, run `libseekdb-build.sh` again to produce a new zip.
diff --git a/package/libseekdb/homebrew-local/Formula/thrift@0.22.rb b/package/libseekdb/homebrew-local/Formula/thrift@0.22.rb
new file mode 100644
index 0000000000..426a572e75
--- /dev/null
+++ b/package/libseekdb/homebrew-local/Formula/thrift@0.22.rb
@@ -0,0 +1,89 @@
+class ThriftAT022 < Formula
+ desc "Framework for scalable cross-language services development"
+ homepage "https://thrift.apache.org/"
+ license "Apache-2.0"
+
+ stable do
+ url "https://www.apache.org/dyn/closer.lua?path=thrift/0.22.0/thrift-0.22.0.tar.gz"
+ mirror "https://archive.apache.org/dist/thrift/0.22.0/thrift-0.22.0.tar.gz"
+ sha256 "794a0e455787960d9f27ab92c38e34da27e8deeda7a5db0e59dc64a00df8a1e5"
+
+ # Fix -flat_namespace being used on Big Sur and later.
+ patch do
+ url "https://raw.githubusercontent.com/Homebrew/formula-patches/03cf8088210822aa2c1ab544ed58ea04c897d9c4/libtool/configure-big_sur.diff"
+ sha256 "35acd6aebc19843f1a2b3a63e880baceb0f5278ab1ace661e57a502d9d78c93c"
+ end
+ end
+
+ head do
+ url "https://github.com/apache/thrift.git", branch: "master"
+
+ depends_on "autoconf" => :build
+ depends_on "automake" => :build
+ depends_on "libtool" => :build
+ depends_on "pkgconf" => :build
+ end
+
+ depends_on "bison" => :build
+ depends_on "boost" => [:build, :test]
+ depends_on "openssl@3"
+ uses_from_macos "zlib"
+
+ def install
+ system "./bootstrap.sh" unless build.stable?
+
+ args = %W[
+ --disable-debug
+ --disable-tests
+ --prefix=#{prefix}
+ --libdir=#{lib}
+ --with-openssl=#{Formula["openssl@3"].opt_prefix}
+ --without-java
+ --without-kotlin
+ --without-python
+ --without-py3
+ --without-ruby
+ --without-haxe
+ --without-netstd
+ --without-perl
+ --without-php
+ --without-php_extension
+ --without-dart
+ --without-erlang
+ --without-go
+ --without-d
+ --without-nodejs
+ --without-nodets
+ --without-lua
+ --without-rs
+ --without-swift
+ ]
+
+ ENV.cxx11 if ENV.compiler == :clang
+
+ # Don't install extensions to /usr:
+ ENV["PY_PREFIX"] = prefix
+ ENV["PHP_PREFIX"] = prefix
+ ENV["JAVA_PREFIX"] = buildpath
+
+ system "./configure", *args
+ ENV.deparallelize
+ system "make"
+ system "make", "install"
+ end
+
+ test do
+ (testpath/"test.thrift").write <<~THRIFT
+ service MultiplicationService {
+ i32 multiply(1:i32 x, 2:i32 y),
+ }
+ THRIFT
+
+ system bin/"thrift", "-r", "--gen", "cpp", "test.thrift"
+
+ system ENV.cxx, "-std=c++11", "gen-cpp/MultiplicationService.cpp",
+ "gen-cpp/MultiplicationService_server.skeleton.cpp",
+ "-I#{include}/include",
+ "-L#{lib}", "-lthrift"
+ end
+end
diff --git a/package/libseekdb/homebrew-local/README.md b/package/libseekdb/homebrew-local/README.md
new file mode 100644
index 0000000000..7e591e9915
--- /dev/null
+++ b/package/libseekdb/homebrew-local/README.md
@@ -0,0 +1,5 @@
+# seekdb/local Homebrew tap
+
+Vendored `thrift@0.22` formula for macOS libseekdb CI. GitHub Actions `macos-14` runners often lack `thrift@0.22` in Homebrew core (only unversioned `thrift` 0.23+), which caused darwin zip drift vs local dev machines.
+
+Installed by `install-macos-brew-deps.sh`: `brew tap-new seekdb/local --no-git`, copy this Formula into the tap, then `brew install seekdb/local/thrift@0.22`.
diff --git a/package/libseekdb/install-macos-brew-deps.sh b/package/libseekdb/install-macos-brew-deps.sh
new file mode 100755
index 0000000000..d487525d32
--- /dev/null
+++ b/package/libseekdb/install-macos-brew-deps.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# Install Homebrew deps for macOS libseekdb CI with pinned versions where drift broke darwin packs.
+# CI was bundling thrift 0.23 while many dev machines use thrift@0.22.
+set -euo pipefail
+
+export HOMEBREW_NO_ENV_HINTS=1
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+THRIFT_FORMULA="$SCRIPT_DIR/homebrew-local/Formula/thrift@0.22.rb"
+
+echo "[brew] installing build tools (thrift 0.22 pinned separately)"
+brew update
+brew install cmake dylibbundler googletest ccache pybind11 utf8proc re2 brotli bzip2
+
+install_thrift_0_22() {
+ if brew list thrift@0.22 &>/dev/null; then
+ brew link --force --overwrite thrift@0.22
+ return 0
+ fi
+
+ if brew list thrift &>/dev/null; then
+ echo "[brew] unlinking unversioned thrift (0.23+) before pin"
+ brew unlink thrift || true
+ fi
+
+ set +e
+ brew install thrift@0.22
+ core_rc=$?
+ set -e
+ if [[ "$core_rc" -eq 0 ]]; then
+ brew link --force --overwrite thrift@0.22
+ return 0
+ fi
+
+ echo "[brew] core thrift@0.22 unavailable; installing via seekdb/local tap (tap-new)"
+ if [[ ! -f "$THRIFT_FORMULA" ]]; then
+ echo "[brew] error: missing $THRIFT_FORMULA" >&2
+ exit 1
+ fi
+
+ brew install bison boost openssl@3
+ export PATH="/opt/homebrew/opt/bison/bin:${PATH:-}"
+
+ brew untap seekdb/local 2>/dev/null || true
+ brew tap-new seekdb/local --no-git
+ local tap_formula
+ tap_formula="$(brew --repository seekdb/local)/Formula/thrift@0.22.rb"
+ mkdir -p "$(dirname "$tap_formula")"
+ cp "$THRIFT_FORMULA" "$tap_formula"
+
+ brew install seekdb/local/thrift@0.22
+ brew link --force --overwrite thrift@0.22
+}
+
+install_thrift_0_22
+
+echo "[brew] macOS libseekdb dependency versions:"
+brew list --versions cmake dylibbundler re2 brotli utf8proc thrift@0.22 2>/dev/null || true
+
+if command -v thrift &>/dev/null; then
+ echo "[brew] active thrift: $(thrift -version 2>&1 || true)"
+else
+ echo "[brew] error: thrift not on PATH after install" >&2
+ exit 1
+fi
diff --git a/package/libseekdb/libseekdb-build.ps1 b/package/libseekdb/libseekdb-build.ps1
new file mode 100644
index 0000000000..2f9062d85e
--- /dev/null
+++ b/package/libseekdb/libseekdb-build.ps1
@@ -0,0 +1,202 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Pack libseekdb for Windows into libseekdb-windows-x64.zip (seekdb.h, seekdb.dll, seekdb.lib, libs/*.dll).
+
+ Runtime DLLs are collected with cmake/BundleRuntimeDllsWindows.cmake (same as POST_BUILD on
+ libseekdb and the binding-test PATH layout). macOS packs deps under libs/ via dylibbundler;
+ Windows zip must match that layout for standalone embed consumers.
+
+.EXAMPLE
+ cd package\libseekdb
+ .\libseekdb-build.ps1
+ .\libseekdb-build.ps1 -IncludeDir C:\path\to\build_release\src\include
+#>
+param(
+ [string]$IncludeDir = ""
+)
+
+$ErrorActionPreference = "Stop"
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$TopDir = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path
+. (Join-Path $TopDir "unittest\include\seekdb-windows-dll-resolve.ps1")
+
+function Get-CMakeExecutable {
+ param([Parameter(Mandatory = $true)][string]$RepoRoot)
+ $bundled = Join-Path $RepoRoot "deps\3rd\tools\cmake\bin\cmake.exe"
+ if (Test-Path -LiteralPath $bundled) { return $bundled }
+ $cmd = Get-Command cmake -ErrorAction SilentlyContinue
+ if ($cmd) { return $cmd.Source }
+ throw "cmake not found (expected deps\3rd\tools\cmake\bin\cmake.exe after build init)"
+}
+
+function Get-SeekDbWindowsRuntimeSearchDirs {
+ param(
+ [Parameter(Mandatory = $true)][string]$RepoRoot,
+ [Parameter(Mandatory = $true)][string]$DllDir
+ )
+ $dirs = [System.Collections.Generic.List[string]]::new()
+ $seen = @{}
+
+ function Add-Dir([string]$path) {
+ if (-not $path -or -not (Test-Path -LiteralPath $path)) { return }
+ $key = (Resolve-Path -LiteralPath $path).Path.ToLowerInvariant()
+ if ($seen.ContainsKey($key)) { return }
+ $seen[$key] = $true
+ $null = $dirs.Add($key)
+ }
+
+ # POST_BUILD may already have copied DLLs next to seekdb.dll.
+ Add-Dir $DllDir
+
+ $depsDone = Test-Path (Join-Path $RepoRoot "deps\3rd\DONE")
+ $vcpkgRoot = if ($env:OB_VCPKG_DIR -and $env:OB_VCPKG_DIR.Trim().Length -gt 0) {
+ $env:OB_VCPKG_DIR.TrimEnd('\', '/')
+ } elseif ($depsDone) {
+ Join-Path $RepoRoot "deps\3rd\vcpkg\x64-windows"
+ } else {
+ "C:/VcpkgInstalled/x64-windows"
+ }
+ Add-Dir (Join-Path $vcpkgRoot "bin")
+
+ $opensslRoot = if ($env:OB_OPENSSL_DIR -and $env:OB_OPENSSL_DIR.Trim().Length -gt 0) {
+ $env:OB_OPENSSL_DIR.TrimEnd('\', '/')
+ } elseif ($depsDone) {
+ Join-Path $RepoRoot "deps\3rd\openssl"
+ } else {
+ "C:/Program Files/OpenSSL-Win64"
+ }
+ Add-Dir (Join-Path $opensslRoot "bin")
+
+ $vsagRoot = if ($env:OB_VSAG_DIR -and $env:OB_VSAG_DIR.Trim().Length -gt 0) {
+ $env:OB_VSAG_DIR.TrimEnd('\', '/')
+ } elseif ($depsDone) {
+ Join-Path $RepoRoot "deps\3rd\vsag"
+ } else {
+ ""
+ }
+ if ($vsagRoot) { Add-Dir (Join-Path $vsagRoot "bin") }
+
+ return @($dirs)
+}
+
+function Invoke-BundleRuntimeDllsForPack {
+ param(
+ [Parameter(Mandatory = $true)][string]$CmakeExe,
+ [Parameter(Mandatory = $true)][string]$BundleScript,
+ [Parameter(Mandatory = $true)][string]$DllPath,
+ [Parameter(Mandatory = $true)][string]$OutLibsDir,
+ [Parameter(Mandatory = $true)][string[]]$SearchDirs
+ )
+ New-Item -ItemType Directory -Path $OutLibsDir -Force | Out-Null
+ $searchJoined = ($SearchDirs -join ';')
+ & $CmakeExe `
+ "-DEXE=$DllPath" `
+ "-DOUT_DIR=$OutLibsDir" `
+ "-DSEARCH_DIRS=$searchJoined" `
+ -P $BundleScript
+ if ($LASTEXITCODE -ne 0) {
+ throw "BundleRuntimeDllsWindows.cmake failed with exit code $LASTEXITCODE"
+ }
+}
+
+function Copy-ColocatedRuntimeDlls {
+ param(
+ [Parameter(Mandatory = $true)][string]$DllDir,
+ [Parameter(Mandatory = $true)][string]$OutLibsDir
+ )
+ New-Item -ItemType Directory -Path $OutLibsDir -Force | Out-Null
+ foreach ($item in Get-ChildItem -LiteralPath $DllDir -Filter "*.dll" -File -ErrorAction SilentlyContinue) {
+ if ($item.Name -ieq "seekdb.dll" -or $item.Name -ieq "libseekdb.dll") { continue }
+ $dest = Join-Path $OutLibsDir $item.Name
+ if (-not (Test-Path -LiteralPath $dest)) {
+ Copy-Item -LiteralPath $item.FullName -Destination $dest
+ }
+ }
+}
+
+$BuildDirName = Get-SeekDbWindowsBuildDirNameFromEnv
+
+$WorkDir = if ($IncludeDir) {
+ (Resolve-Path $IncludeDir).Path
+} else {
+ Join-Path $TopDir "build_$BuildDirName\src\include"
+}
+
+$BuildRoot = Join-Path $TopDir "build_$BuildDirName"
+$Dll = $null
+if ($IncludeDir) {
+ foreach ($name in @("seekdb.dll", "libseekdb.dll")) {
+ $p = Join-Path $WorkDir $name
+ if (Test-Path -LiteralPath $p) { $Dll = $p; break }
+ }
+} else {
+ $resolved = Find-SeekDbWindowsDll -RepoRoot $TopDir -BuildDirName $BuildDirName
+ if ($resolved) { $Dll = $resolved.DllPath }
+}
+
+if (-not $Dll -or -not (Test-Path -LiteralPath $Dll)) {
+ if (-not $IncludeDir) {
+ Write-SeekDbWindowsDllDiagnostics -RepoRoot $TopDir -BuildDirName $BuildDirName
+ }
+ $hint = if ($IncludeDir) { $WorkDir } else { $BuildRoot }
+ Write-Error "seekdb.dll not found under $hint (build libseekdb first: .\build.ps1 release --ninja --target libseekdb -DBUILD_EMBED_MODE=ON)"
+}
+
+$DllDir = Split-Path -Parent $Dll
+$Lib = $null
+foreach ($ln in @("seekdb.lib", "libseekdb.lib")) {
+ $c = Join-Path $DllDir $ln
+ if (Test-Path -LiteralPath $c) { $Lib = $c; break }
+}
+
+$Header = Join-Path $TopDir "src\include\seekdb.h"
+if (-not (Test-Path $Header)) {
+ Write-Error "seekdb.h not found: $Header"
+}
+
+$ZipName = "libseekdb-windows-x64.zip"
+$OutZip = Join-Path $ScriptDir $ZipName
+$Staging = Join-Path $env:TEMP ("libseekdb-pack-" + [guid]::NewGuid().ToString())
+$LibsStaging = Join-Path $Staging "libs"
+New-Item -ItemType Directory -Path $Staging -Force | Out-Null
+
+try {
+ $cmakeExe = Get-CMakeExecutable -RepoRoot $TopDir
+ $bundleScript = Join-Path $TopDir "cmake\BundleRuntimeDllsWindows.cmake"
+ if (-not (Test-Path -LiteralPath $bundleScript)) {
+ Write-Error "Missing $bundleScript"
+ }
+
+ $searchDirs = Get-SeekDbWindowsRuntimeSearchDirs -RepoRoot $TopDir -DllDir $DllDir
+ Write-Host "[libseekdb-build.ps1] Bundling runtime DLLs into libs/ (search: $($searchDirs.Count) dirs)"
+ Invoke-BundleRuntimeDllsForPack `
+ -CmakeExe $cmakeExe `
+ -BundleScript $bundleScript `
+ -DllPath $Dll `
+ -OutLibsDir $LibsStaging `
+ -SearchDirs $searchDirs
+
+ # Include DLLs already colocated by libseekdb POST_BUILD (may overlap).
+ Copy-ColocatedRuntimeDlls -DllDir $DllDir -OutLibsDir $LibsStaging
+
+ $libCount = @(Get-ChildItem -LiteralPath $LibsStaging -Filter "*.dll" -File -ErrorAction SilentlyContinue).Count
+ if ($libCount -lt 1) {
+ throw "No runtime DLLs under $LibsStaging — cannot produce a standalone Windows zip. Check vcpkg/OpenSSL paths and BundleRuntimeDllsWindows.cmake output."
+ }
+ Write-Host "[libseekdb-build.ps1] libs/ contains $libCount runtime DLL(s)"
+
+ Copy-Item $Header (Join-Path $Staging "seekdb.h")
+ Copy-Item $Dll (Join-Path $Staging "seekdb.dll")
+ if ($Lib -and (Test-Path -LiteralPath $Lib)) {
+ Copy-Item $Lib (Join-Path $Staging "seekdb.lib")
+ } else {
+ Write-Host "[libseekdb-build.ps1][WARN] seekdb.lib not found; zip will contain DLL + header only." -ForegroundColor Yellow
+ }
+
+ if (Test-Path $OutZip) { Remove-Item -Force $OutZip }
+ Compress-Archive -Path (Join-Path $Staging "*") -DestinationPath $OutZip
+ Write-Host "[libseekdb-build.ps1] Created $OutZip"
+} finally {
+ Remove-Item -Recurse -Force $Staging -ErrorAction SilentlyContinue
+}
diff --git a/package/libseekdb/libseekdb-build.sh b/package/libseekdb/libseekdb-build.sh
new file mode 100755
index 0000000000..57dfa6db55
--- /dev/null
+++ b/package/libseekdb/libseekdb-build.sh
@@ -0,0 +1,280 @@
+#!/usr/bin/env bash
+# Build libseekdb, on macOS bundle deps to libs/, then pack lib + libs/ + seekdb.h into a .zip
+# Zip is written to this script's directory (package/libseekdb/).
+#
+# Invokes build.sh with -DBUILD_EMBED_MODE=ON and --make libseekdb (same as CI), not a full make.
+# Windows (seekdb.dll): build with .\build.ps1 release --ninja --target libseekdb -DBUILD_EMBED_MODE=ON,
+# then run libseekdb-build.ps1 in this directory (see README.md).
+#
+# Usage:
+# cd package/libseekdb && ./libseekdb-build.sh
+# BUILD_TYPE=debug ./libseekdb-build.sh
+# ./libseekdb-build.sh /path/to/dir-with-libseekdb # pack from existing dir (no build)
+#
+# Android (NDK, arm64-v8a only): zip is libseekdb-android-arm64-v8a.zip — not host uname (e.g. darwin-*).
+# ./libseekdb-build.sh --android # use build_android_, build if needed
+# ./libseekdb-build.sh /path/to/build_android_*/src/include # pack only
+
+set -e
+
+# --- Parse flags (no LIBSEEKDB_* env vars) ---
+android_build=false
+remaining=()
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --android) android_build=true; shift ;;
+ *) remaining+=("$1"); shift ;;
+ esac
+done
+set -- "${remaining[@]}"
+
+# --- Paths and config ---
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TOP_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+BUILD_TYPE="${BUILD_TYPE:-release}"
+BUILD_DIR="$TOP_DIR/build_${BUILD_TYPE}"
+WORK_DIR=""
+ANDROID_PACK=false
+UNAME_S="$(uname -s)"
+UNAME_M="$(uname -m)"
+
+# --- Helpers ---
+die() { echo "error: $*" >&2; exit 1; }
+
+# CMake flags aligned with .github/workflows/build-libseekdb.yml (embed + libseekdb only).
+build_libseekdb_embed_args() {
+ BUILD_EMBED_ARGS=(-DBUILD_EMBED_MODE=ON)
+ if command -v python3 &>/dev/null; then
+ PYVER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)"
+ [[ -n "$PYVER" ]] && BUILD_EMBED_ARGS+=(-DPYTHON_VERSION="$PYVER")
+ fi
+ if [[ "$UNAME_S" == "Darwin" ]]; then
+ BUILD_EMBED_ARGS+=(-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0)
+ if [[ -n "${ARCH:-}" ]]; then
+ case "$ARCH" in
+ arm64|aarch64) BUILD_EMBED_ARGS+=(-DCMAKE_OSX_ARCHITECTURES=arm64) ;;
+ x86_64|amd64) BUILD_EMBED_ARGS+=(-DCMAKE_OSX_ARCHITECTURES=x86_64) ;;
+ esac
+ fi
+ fi
+}
+
+# Build only libseekdb (not the full tree / unittest).
+run_build_libseekdb() {
+ local need_init="$1"
+ build_libseekdb_embed_args
+ local -a args=("$BUILD_TYPE")
+ [[ "$need_init" == true ]] && args+=(--init)
+ [[ "$ANDROID_PACK" == true ]] && args+=(--android)
+ args+=("${BUILD_EMBED_ARGS[@]}" --make libseekdb)
+ echo "[BUILD] ./build.sh ${args[*]}"
+ (cd "$TOP_DIR" && ./build.sh "${args[@]}") || return 1
+}
+
+# List dependency paths from a dylib (one per line, trimmed). Skips first line (the dylib itself).
+get_dylib_deps() {
+ local dylib="$1"
+ otool -L "$dylib" | tail -n +2 | while IFS= read -r line; do
+ dep="${line%% (*}"
+ printf '%s\n' "$(printf '%s' "$dep" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
+ done
+}
+
+# Change a dependency reference in dylib: from_ref -> to_ref (exact match).
+fix_dylib_ref() {
+ local dylib="$1" from_ref="$2" to_ref="$3"
+ install_name_tool -change "$from_ref" "$to_ref" "$dylib"
+}
+
+# Remove all LC_RPATH entries from dylib that look like absolute paths.
+strip_rpaths() {
+ local dylib="$1"
+ while IFS= read -r rpath; do
+ [[ -n "$rpath" ]] || continue
+ echo " delete_rpath: $rpath"
+ install_name_tool -delete_rpath "$rpath" "$dylib"
+ done < <(otool -l "$dylib" | awk '/[[:space:]]path[[:space:]]+\// { print $2 }')
+}
+
+# --- 1) Resolve WORK_DIR ---
+if [[ -n "${1:-}" ]]; then
+ WORK_DIR="$(cd "$1" && pwd)"
+ echo "[BUILD] Using directory: $WORK_DIR (pack from existing tree; no build)"
+ if [[ "$android_build" == true ]]; then
+ ANDROID_PACK=true
+ if [[ "$WORK_DIR" == *"/src/include" ]]; then
+ BUILD_DIR="$(cd "$WORK_DIR/../.." && pwd)"
+ fi
+ fi
+elif [[ "$android_build" == true ]]; then
+ ANDROID_PACK=true
+ BUILD_DIR="$TOP_DIR/build_android_${BUILD_TYPE}"
+ WORK_DIR="$BUILD_DIR/src/include"
+ echo "[BUILD] Android: BUILD_DIR=$BUILD_DIR"
+else
+ WORK_DIR="$BUILD_DIR/src/include"
+fi
+
+if [[ -z "${1:-}" ]]; then
+ # --- 2) Build libseekdb if not present (main lib is always next to libs/, not inside) ---
+ if [[ ! -f "$WORK_DIR/libseekdb.dylib" && ! -f "$WORK_DIR/libseekdb.so" ]]; then
+ echo "[BUILD] Building libseekdb (BUILD_TYPE=$BUILD_TYPE)..."
+ if [[ ! -d "$BUILD_DIR" ]]; then
+ run_build_libseekdb true || exit 1
+ else
+ run_build_libseekdb false || exit 1
+ fi
+ fi
+
+ # --- 3) macOS: bundle deps to libs/, fix @loader_path ---
+ # Layout: libseekdb.dylib at root, deps in libs/. When main is loaded from root,
+ # @loader_path = root, so main's deps must be @loader_path/libs/xxx. When a dep in libs/
+ # is loaded, @loader_path = libs/, so dep refs must be @loader_path/xxx (not libs/xxx).
+ if [[ "$UNAME_S" == "Darwin" && -f "$WORK_DIR/libseekdb.dylib" ]]; then
+ # If the dylib was already bundled (deps point to @loader_path/libs/), dylibbundler
+ # would prompt for paths when libs/ is empty. Rebuild to get a clean dylib with absolute deps.
+ if otool -L "$WORK_DIR/libseekdb.dylib" | grep -q '@loader_path/libs/'; then
+ echo "[BUILD] libseekdb.dylib was already bundled; rebuilding to get clean dylib for this run..."
+ rm -f "$WORK_DIR/libseekdb.dylib"
+ run_build_libseekdb false || exit 1
+ fi
+
+ # Save pristine dylib so we can restore after zip (keeps build dir clean; next run won't rebuild)
+ cp "$WORK_DIR/libseekdb.dylib" "$WORK_DIR/libseekdb.dylib.orig"
+ SAVED_PRISTINE_DYLIB="$WORK_DIR/libseekdb.dylib.orig"
+
+ echo "[BUILD] Bundling libseekdb.dylib for macOS..."
+ cd "$WORK_DIR"
+ DYLIB_NAME="libseekdb.dylib"
+
+ strip_rpaths "$DYLIB_NAME"
+
+ if ! command -v dylibbundler &>/dev/null; then
+ die "dylibbundler not found. Install with: brew install dylibbundler"
+ fi
+ rm -rf libs && mkdir -p libs
+ dylibbundler -x "$DYLIB_NAME" -cd -b -p '@loader_path/libs'
+
+ install_name_tool -id "@loader_path/$DYLIB_NAME" "$DYLIB_NAME"
+ while IFS= read -r dep; do
+ [[ -n "$dep" ]] || continue
+ if [[ "$dep" == @loader_path/* ]]; then
+ depname="${dep#@loader_path/}"
+ [[ -f "libs/$depname" ]] && fix_dylib_ref "$DYLIB_NAME" "$dep" "@loader_path/libs/$depname"
+ fi
+ done < <(get_dylib_deps "$DYLIB_NAME")
+
+ for d in libs/*.dylib; do
+ [[ -f "$d" ]] || continue
+ name="$(basename "$d")"
+ while IFS= read -r dep; do
+ [[ -n "$dep" ]] || continue
+ if [[ "$dep" == @loader_path/libs/* ]]; then
+ fix_dylib_ref "$d" "$dep" "@loader_path/${dep#@loader_path/libs/}"
+ elif [[ "$dep" == @rpath/libs/* ]]; then
+ fix_dylib_ref "$d" "$dep" "@loader_path/${dep#@rpath/libs/}"
+ fi
+ done < <(get_dylib_deps "$d")
+ install_name_tool -id "@loader_path/$name" "$d"
+ done
+
+ echo "[BUILD] Bundle done: $DYLIB_NAME at root, deps in $WORK_DIR/libs"
+
+ # Sign dylibs. Ad-hoc when CODESIGN_IDENTITY unset;
+ # when set, use Developer ID and optionally CODESIGN_ENTITLEMENTS + --options runtime.
+ if command -v codesign &>/dev/null; then
+ SIGN_ID="${CODESIGN_IDENTITY:--}"
+ ENTITLEMENTS_ARG=()
+ [[ -n "${CODESIGN_ENTITLEMENTS:-}" && -f "${CODESIGN_ENTITLEMENTS}" ]] && ENTITLEMENTS_ARG=(--entitlements "$CODESIGN_ENTITLEMENTS")
+ RUNTIME_ARG=()
+ [[ -n "${CODESIGN_IDENTITY:-}" && "$SIGN_ID" != "-" ]] && RUNTIME_ARG=(--options runtime)
+ echo "[BUILD] Signing dylibs for macOS (identity: ${SIGN_ID})..."
+ for d in libs/*.dylib; do
+ [[ -f "$d" ]] || continue
+ codesign "${RUNTIME_ARG[@]}" "${ENTITLEMENTS_ARG[@]}" -f --sign "$SIGN_ID" "$d" || die "codesign failed: $d"
+ done
+ codesign "${RUNTIME_ARG[@]}" "${ENTITLEMENTS_ARG[@]}" -f --sign "$SIGN_ID" "$DYLIB_NAME" || die "codesign failed: $DYLIB_NAME"
+ echo "[BUILD] Signing done."
+ else
+ echo "[BUILD] WARNING: codesign not found; dylibs are unsigned (may fail to load on macOS)."
+ fi
+ cd - >/dev/null
+ fi
+fi
+
+# --- 4) Resolve main library and deps dir (main and libs/ are siblings) ---
+MAIN_LIB=""
+DEPS_DIR="$WORK_DIR/libs"
+if [[ -f "$WORK_DIR/libseekdb.dylib" ]]; then
+ MAIN_LIB="$WORK_DIR/libseekdb.dylib"
+elif [[ -f "$WORK_DIR/libseekdb.so" ]]; then
+ MAIN_LIB="$WORK_DIR/libseekdb.so"
+else
+ die "libseekdb.dylib or libseekdb.so not found in $WORK_DIR"
+fi
+
+HEADER="$TOP_DIR/src/include/seekdb.h"
+[[ -f "$HEADER" ]] || die "seekdb.h not found: $HEADER"
+
+# --- 5) OS / Arch for zip name ---
+# Android (arm64-v8a only): fixed zip name; not host uname. Detect ELF .so on Mac without --android.
+ZIP_USE_ANDROID_PREFIX="$ANDROID_PACK"
+if [[ "$ZIP_USE_ANDROID_PREFIX" != true && "$UNAME_S" == "Darwin" && -f "$WORK_DIR/libseekdb.so" && ! -f "$WORK_DIR/libseekdb.dylib" ]]; then
+ if command -v file >/dev/null 2>&1; then
+ _so_info="$(file -b "$WORK_DIR/libseekdb.so" 2>/dev/null || true)"
+ if echo "$_so_info" | grep -q 'ELF.*shared object'; then
+ ZIP_USE_ANDROID_PREFIX=true
+ echo "[BUILD] libseekdb.so is ELF (NDK): zip libseekdb-android-arm64-v8a.zip (not darwin-*)"
+ fi
+ fi
+fi
+
+if [[ "$ZIP_USE_ANDROID_PREFIX" == true ]]; then
+ ZIP_NAME="libseekdb-android-arm64-v8a.zip"
+ echo "[BUILD] Android artifact zip: $ZIP_NAME"
+else
+ case "$UNAME_S" in
+ Darwin) OS="darwin" ;;
+ Linux) OS="linux" ;;
+ *) die "unsupported OS: $UNAME_S" ;;
+ esac
+ if [[ -n "${ARCH:-}" ]]; then
+ echo "[BUILD] Using ARCH from environment: $ARCH"
+ else
+ case "$UNAME_M" in
+ arm64|aarch64) ARCH="arm64" ;;
+ x86_64|amd64) ARCH="x86_64" ;;
+ *) die "unsupported arch: $UNAME_M" ;;
+ esac
+ fi
+ ARCH_SUFFIX="${ARCH}"
+ [[ "$ARCH" == "x86_64" ]] && ARCH_SUFFIX="x64"
+ ZIP_NAME="libseekdb-${OS}-${ARCH_SUFFIX}.zip"
+fi
+MAIN_LIB_NAME="$(basename "$MAIN_LIB")"
+
+# --- 6) Assemble and zip ---
+OUTPUT_ZIP="$SCRIPT_DIR/$ZIP_NAME"
+PACK_DIR="$(mktemp -d)"
+trap "rm -rf '$PACK_DIR'" EXIT
+
+cp "$HEADER" "$PACK_DIR/seekdb.h"
+cp "$MAIN_LIB" "$PACK_DIR/$MAIN_LIB_NAME"
+if [[ -d "$DEPS_DIR" ]]; then
+ mkdir -p "$PACK_DIR/libs"
+ for f in "$DEPS_DIR"/*; do
+ [[ -f "$f" ]] || continue
+ cp "$f" "$PACK_DIR/libs/"
+ done
+fi
+
+(cd "$PACK_DIR" && zip -r "$OUTPUT_ZIP" . -x "*.DS_Store")
+echo "[BUILD] Created: $OUTPUT_ZIP"
+
+# Restore build dir to pristine dylib on macOS so next run does not trigger rebuild
+if [[ -n "${SAVED_PRISTINE_DYLIB:-}" && -f "${SAVED_PRISTINE_DYLIB}" ]]; then
+ cp "$SAVED_PRISTINE_DYLIB" "$WORK_DIR/libseekdb.dylib"
+ rm -rf "$WORK_DIR/libs"
+ rm -f "$SAVED_PRISTINE_DYLIB"
+ echo "[BUILD] Restored build dir to pristine dylib (cleanup for next run)."
+fi
diff --git a/package/libseekdb/osx_import_codesign_certificate.sh b/package/libseekdb/osx_import_codesign_certificate.sh
new file mode 100755
index 0000000000..14133cd9d3
--- /dev/null
+++ b/package/libseekdb/osx_import_codesign_certificate.sh
@@ -0,0 +1,20 @@
+# Import Apple code signing certificate for CI.
+# Required env: BUILD_CERTIFICATE_BASE64, P12_PASSWORD, KEYCHAIN_PASSWORD
+# When BUILD_CERTIFICATE_BASE64 is empty, skip import; libseekdb-build.sh will use ad-hoc signing.
+set -e
+export CERTIFICATE_PATH=${CERTIFICATE_PATH:-$RUNNER_TEMP/build_certificate.p12}
+export KEYCHAIN_PATH=${KEYCHAIN_PATH:-$RUNNER_TEMP/app-signing.keychain-db}
+
+if [[ -z "${BUILD_CERTIFICATE_BASE64:-}" ]]; then
+ echo "[BUILD] No certificate configured; skipping import (ad-hoc signing will be used)."
+ exit 0
+fi
+
+echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH"
+
+security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
+security list-keychain -d user -s "$KEYCHAIN_PATH"
diff --git a/package/libseekdb/smoke-loader/.gitignore b/package/libseekdb/smoke-loader/.gitignore
new file mode 100644
index 0000000000..b38db2f296
--- /dev/null
+++ b/package/libseekdb/smoke-loader/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+build/
diff --git a/package/libseekdb/smoke-loader/binding.gyp b/package/libseekdb/smoke-loader/binding.gyp
new file mode 100644
index 0000000000..b3e20c19c3
--- /dev/null
+++ b/package/libseekdb/smoke-loader/binding.gyp
@@ -0,0 +1,68 @@
+{
+ "variables": {
+ "pack_dir%": ""
+ },
+ "targets": [
+ {
+ "target_name": "seekdb",
+ "sources": [
+ "../../../unittest/include/nodejs_napi/seekdb.cpp"
+ ],
+ "include_dirs": [
+ "/seekdb.dll + /libs/ + /seekdb.node
+
+ Mirrors package/libseekdb/test-packed-artifact-smoke.sh (linux/macos) on Windows:
+ - unzip the artifact to a temp dir
+ - prepend and /libs to PATH (seekdb.dll imports runtime DLLs from libs/;
+ Windows DLL search does not recurse into subdirectories, so libs/ must be on PATH)
+ - build seekdb.node into the unpack tree via smoke-loader (node-gyp rebuild --pack_dir=)
+ - run node smoke-vsag.js with wall-clock timeout + binding exit probe + taskkill /T fallback
+ (vsag smoke has a stall history on Windows runners; the process guard is mandatory)
+ - exit code 0/1
+
+ Usage:
+ .\test-packed-artifact-smoke.ps1 -Zip package\libseekdb\libseekdb-windows-x64.zip
+#>
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$Zip
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$TopDir = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path
+$LoaderDir = Join-Path $ScriptDir "smoke-loader"
+$Utils = Join-Path $TopDir "unittest\include\windows-process-utils.ps1"
+if (-not (Test-Path -LiteralPath $Utils)) {
+ throw "Missing shared module: $Utils"
+}
+. $Utils
+
+function Write-SmokeLog {
+ param([string]$Message)
+ Write-Host "[smoke] $Message"
+ try { [Console]::Out.Flush() } catch {}
+}
+
+if (-not (Test-Path -LiteralPath $Zip)) {
+ Write-Host "::error::zip not found: $Zip"
+ exit 1
+}
+if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
+ Write-Host "::error::node not found (install Node.js 18+)"
+ exit 1
+}
+
+$UnpackDir = Join-Path $env:TEMP ("libseekdb-smoke-" + [guid]::NewGuid().ToString())
+try {
+ Write-SmokeLog "unpacking $Zip -> $UnpackDir"
+ Expand-Archive -LiteralPath $Zip -DestinationPath $UnpackDir -Force
+
+ $MainDll = Join-Path $UnpackDir "seekdb.dll"
+ if (-not (Test-Path -LiteralPath $MainDll)) {
+ Write-Host "::error::seekdb.dll not found in zip"
+ Get-ChildItem -LiteralPath $UnpackDir | Out-Host
+ exit 1
+ }
+
+ $LibsDir = Join-Path $UnpackDir "libs"
+ if (Test-Path -LiteralPath $LibsDir) {
+ $dllCount = @(Get-ChildItem -LiteralPath $LibsDir -Filter *.dll -File).Count
+ Write-SmokeLog "layout: seekdb.dll + libs/ ($dllCount runtime DLLs)"
+ }
+ else {
+ Write-SmokeLog "layout: seekdb.dll (no libs/)"
+ }
+
+ # seekdb.dll imports transitive runtime DLLs from libs/ (vcpkg/OpenSSL); Windows DLL search does
+ # not recurse into subdirectories — both the unpack root and libs/ must be on PATH.
+ $env:PATH = (($UnpackDir + ";" + $LibsDir + ";" + $env:PATH) -replace ";;", ";")
+
+ # --- Build seekdb.node INTO the unpack tree (@loader_path equivalent) ---
+ if (-not (Test-Path (Join-Path $LoaderDir "node_modules"))) {
+ Write-SmokeLog "npm install (smoke-loader, no lifecycle build)"
+ $p = Start-NpmProcess -NpmArguments @('install', '--ignore-scripts', '--no-audit', '--no-fund') -WorkingDirectory $LoaderDir
+ if ($null -eq $p) { throw "Start-NpmProcess returned null" }
+ $npmR = Wait-ProcessWithDeadline -Process $p -TimeoutMs 2400000 -Label "npm install (smoke-loader)" -HeartbeatSec 120
+ if ($null -eq $npmR -or $npmR['TimedOut']) {
+ Write-Host "::error::npm install (smoke-loader) timed out or failed"
+ exit 1
+ }
+ if ($npmR['ExitCode'] -ne 0) {
+ Write-Host "::error::npm install (smoke-loader) failed (exit $($npmR['ExitCode']))"
+ exit 1
+ }
+ }
+
+ Write-SmokeLog "building seekdb.node into unpack dir (pack_dir=$UnpackDir)"
+ $p = Start-NpmProcess -NpmArguments @('exec', '--yes', '--', 'node-gyp', 'rebuild', "--pack_dir=$UnpackDir") -WorkingDirectory $LoaderDir
+ if ($null -eq $p) { throw "Start-NpmProcess returned null" }
+ $gypR = Wait-ProcessWithDeadline -Process $p -TimeoutMs 1800000 -Label "node-gyp rebuild (smoke-loader)" -HeartbeatSec 120
+ if ($null -eq $gypR -or $gypR['TimedOut'] -or $gypR['ExitCode'] -ne 0) {
+ Write-Host "::error::node-gyp rebuild failed (pack_dir=$UnpackDir)"
+ exit 1
+ }
+
+ $NodeOut = Join-Path $UnpackDir "seekdb.node"
+ if (-not (Test-Path -LiteralPath $NodeOut)) {
+ Write-Host "::error::seekdb.node not produced in $UnpackDir"
+ exit 1
+ }
+
+ # --- vsag + hybrid search (embedded N-API path) ---
+ $DbDir = Join-Path $UnpackDir "smoke-seekdb.db"
+ if (Test-Path -LiteralPath $DbDir) { Remove-Item -Recurse -Force $DbDir }
+ Write-SmokeLog "vsag + hybrid search (embedded N-API path)"
+ try {
+ Push-Location $UnpackDir
+ try {
+ Invoke-ExternalTestWithBindingExitProbe `
+ -FilePath (Get-Command node).Source `
+ -ArgumentList @((Join-Path $LoaderDir "smoke-vsag.js"), $DbDir) `
+ -Description 'smoke-vsag.js'
+ }
+ finally {
+ Pop-Location
+ }
+ }
+ catch {
+ Write-Host "::error::smoke-vsag.js failed: $($_.Exception.Message)"
+ exit 1
+ }
+
+ Write-SmokeLog "passed (packed zip load path + vsag)"
+}
+finally {
+ if (Test-Path -LiteralPath $UnpackDir) {
+ Remove-Item -Recurse -Force $UnpackDir -ErrorAction SilentlyContinue
+ }
+}
\ No newline at end of file
diff --git a/package/libseekdb/test-packed-artifact-smoke.sh b/package/libseekdb/test-packed-artifact-smoke.sh
new file mode 100755
index 0000000000..eddf7bbcca
--- /dev/null
+++ b/package/libseekdb/test-packed-artifact-smoke.sh
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+# Smoke-test a packed libseekdb zip using the embedded Node.js load layout:
+# /libseekdb.dylib + /libs/ + /seekdb.node (@loader_path)
+#
+# The old flow used nodejs_napi linked to build_release (@rpath), which did NOT
+# exercise the packaged zip layout and could pass while standalone embed failed.
+#
+# Usage:
+# ./test-packed-artifact-smoke.sh package/libseekdb/libseekdb-darwin-arm64.zip
+
+set -euo pipefail
+
+ZIP="${1:?usage: $0 }"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TOP_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+LOADER_DIR="$SCRIPT_DIR/smoke-loader"
+NAPI_DIR="$TOP_DIR/unittest/include/nodejs_napi"
+# shellcheck source=../../unittest/include/binding-exit-probe.sh
+source "$TOP_DIR/unittest/include/binding-exit-probe.sh"
+
+if [[ ! -f "$ZIP" ]]; then
+ echo "error: zip not found: $ZIP" >&2
+ exit 1
+fi
+
+if ! command -v node >/dev/null 2>&1; then
+ echo "error: node not found (install Node.js 18+)" >&2
+ exit 1
+fi
+
+UNPACK_DIR="$(mktemp -d)"
+trap 'rm -rf "$UNPACK_DIR"' EXIT
+
+echo "[smoke] unpacking $ZIP -> $UNPACK_DIR"
+unzip -q "$ZIP" -d "$UNPACK_DIR"
+
+case "$(uname -s)" in
+ Darwin) MAIN_NAME="libseekdb.dylib" ;;
+ Linux) MAIN_NAME="libseekdb.so" ;;
+ *)
+ echo "error: unsupported host OS: $(uname -s)" >&2
+ exit 1
+ ;;
+esac
+
+if [[ ! -f "$UNPACK_DIR/$MAIN_NAME" ]]; then
+ echo "error: $MAIN_NAME not found in zip" >&2
+ ls -la "$UNPACK_DIR" >&2
+ exit 1
+fi
+
+if [[ -d "$UNPACK_DIR/libs" ]]; then
+ echo "[smoke] layout: $MAIN_NAME + libs/ ($(find "$UNPACK_DIR/libs" -name '*.dylib' -o -name '*.so' | wc -l | tr -d ' ') deps)"
+else
+ echo "[smoke] layout: $MAIN_NAME (no libs/)"
+fi
+
+# --- Build seekdb.node INTO the unpack tree (@loader_path) ---
+if [[ ! -d "$LOADER_DIR/node_modules" ]]; then
+ echo "[smoke] npm install (smoke-loader, no lifecycle build)"
+ (cd "$LOADER_DIR" && npm install --ignore-scripts)
+fi
+
+echo "[smoke] building seekdb.node into unpack dir (pack_dir=$UNPACK_DIR)"
+(
+ cd "$LOADER_DIR"
+ npx node-gyp rebuild --pack_dir="$UNPACK_DIR"
+)
+
+if [[ ! -f "$UNPACK_DIR/seekdb.node" ]]; then
+ echo "error: seekdb.node not produced in $UNPACK_DIR" >&2
+ exit 1
+fi
+
+echo "[smoke] seekdb.node install_name / rpath:"
+if [[ "$(uname -s)" == "Darwin" ]]; then
+ otool -L "$UNPACK_DIR/seekdb.node" | head -5
+else
+ readelf -d "$UNPACK_DIR/seekdb.node" 2>/dev/null | grep -E 'RPATH|RUNPATH|NEEDED' | head -8 || true
+ if ! readelf -d "$UNPACK_DIR/seekdb.node" 2>/dev/null | grep -qE 'RUNPATH|RPATH'; then
+ echo "error: seekdb.node missing RUNPATH/RPATH; cannot load libseekdb.so from unpack dir" >&2
+ exit 1
+ fi
+fi
+
+# Ad-hoc sign dylibs (macOS only)
+if [[ "$(uname -s)" == Darwin ]] && command -v codesign >/dev/null 2>&1; then
+ echo "[smoke] codesign (ad-hoc) main + libs/"
+ codesign --force --sign - "$UNPACK_DIR/$MAIN_NAME"
+ if [[ -d "$UNPACK_DIR/libs" ]]; then
+ for d in "$UNPACK_DIR/libs"/*; do
+ [[ -f "$d" ]] || continue
+ codesign --force --sign - "$d"
+ done
+ fi
+ codesign --force --sign - "$UNPACK_DIR/seekdb.node"
+fi
+
+DB_DIR="$UNPACK_DIR/smoke-seekdb.db"
+rm -rf "$DB_DIR"
+
+echo "[smoke] vsag + hybrid search (embedded N-API path)"
+(
+ cd "$UNPACK_DIR"
+ if [[ "$(uname -s)" == "Linux" ]]; then
+ # libseekdb.so is whole-archive linked; loading it as seekdb.node's DT_NEEDED
+ # after Node starts can exceed glibc's static TLS block. Preload at startup.
+ export LD_PRELOAD="$UNPACK_DIR/$MAIN_NAME${LD_PRELOAD:+:$LD_PRELOAD}"
+ fi
+ run_node_with_binding_exit_probe "$BINDING_TEST_TIMEOUT_MS" "$BINDING_EXIT_PROBE_GRACE_MS" -- \
+ "$LOADER_DIR/smoke-vsag.js" "$DB_DIR"
+)
+
+# Optional: full nodejs_napi suite (can SIGSEGV on some macOS builds at exit; not required for pack gate)
+if [[ "${SMOKE_FULL_NAPI:-0}" == "1" ]]; then
+ if [[ ! -d "$NAPI_DIR/node_modules" ]]; then
+ echo "[smoke] npm install (nodejs_napi test.js)"
+ (cd "$NAPI_DIR" && npm install)
+ fi
+ echo "[smoke] running full nodejs_napi test.js (SMOKE_FULL_NAPI=1)"
+ export SEEKDB_NODE_NAPI_SKIP_HEAVY=0
+ (
+ cd "$UNPACK_DIR"
+ if [[ "$(uname -s)" == "Linux" ]]; then
+ export LD_PRELOAD="$UNPACK_DIR/$MAIN_NAME${LD_PRELOAD:+:$LD_PRELOAD}"
+ fi
+ run_node_with_binding_exit_probe "$BINDING_TEST_TIMEOUT_MS" "$BINDING_EXIT_PROBE_GRACE_MS" -- \
+ "$NAPI_DIR/test.js" "$DB_DIR" "test"
+ )
+fi
+
+echo "[smoke] passed (packed zip load path + vsag)"
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
new file mode 100644
index 0000000000..ef593f40a9
--- /dev/null
+++ b/rust/Cargo.lock
@@ -0,0 +1,616 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "asn1-rs"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "cc"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+
+[[package]]
+name = "der-parser"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "oid-registry"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
+dependencies = [
+ "asn1-rs",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "sql-nio"
+version = "0.1.0"
+dependencies = [
+ "flate2",
+ "libc",
+ "mio",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "socket2",
+ "windows-sys 0.61.2",
+ "x509-parser",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "x509-parser"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
diff --git a/rust/sql-nio/src/cert.rs b/rust/sql-nio/src/cert.rs
index f5be08e14b..0466ad5b00 100644
--- a/rust/sql-nio/src/cert.rs
+++ b/rust/sql-nio/src/cert.rs
@@ -31,7 +31,7 @@ pub(crate) struct PeerCertificateInfo {
impl PeerCertificateInfo {
pub(crate) fn parse(der: &[u8]) -> Self {
match parse_x509_certificate(der) {
- Ok((remaining, cert)) if remaining.is_empty() => {
+ Ok(([], cert)) => {
let issuer = format_name(cert.issuer());
let subject = format_name(cert.subject());
let common_name = cert
diff --git a/rust/sql-nio/src/tls.rs b/rust/sql-nio/src/tls.rs
index 640c6d4af4..c39587257a 100644
--- a/rust/sql-nio/src/tls.rs
+++ b/rust/sql-nio/src/tls.rs
@@ -77,23 +77,6 @@ pub(crate) fn tls_cipher_name(suite: rustls::CipherSuite) -> Option<&'static [u8
})
}
-#[cfg(test)]
-mod tests {
- use super::tls_cipher_name;
-
- #[test]
- fn exposes_sql_cipher_names() {
- assert_eq!(
- tls_cipher_name(rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
- Some(&b"TLS_AES_256_GCM_SHA384"[..])
- );
- assert_eq!(
- tls_cipher_name(rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- Some(&b"ECDHE-RSA-AES128-GCM-SHA256"[..])
- );
- }
-}
-
pub(crate) fn flush_tls_locked(conn: &Arc, g: &mut ConnInner) -> bool {
let mut fatal = false;
{
@@ -338,3 +321,20 @@ pub(crate) fn process_tls_packets(g: &mut ConnInner) -> std::io::Result {
}
Ok(peer_closed)
}
+
+#[cfg(test)]
+mod tests {
+ use super::tls_cipher_name;
+
+ #[test]
+ fn exposes_sql_cipher_names() {
+ assert_eq!(
+ tls_cipher_name(rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
+ Some(&b"TLS_AES_256_GCM_SHA384"[..])
+ );
+ assert_eq!(
+ tls_cipher_name(rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
+ Some(&b"ECDHE-RSA-AES128-GCM-SHA256"[..])
+ );
+ }
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2c25541677..975cf97118 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -23,7 +23,21 @@ target_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/data_plane/api
${CMAKE_CURRENT_SOURCE_DIR}/objit/include)
+if(WIN32)
+ # Windows resolves protobuf/gRPC from vcpkg (protobuf 33.x). The checked-in
+ # standbyservice.pb.* were generated by protoc 3.19 and reference headers
+ # (e.g. generated_message_table_driven.h) that no longer exist in vcpkg, so
+ # src/oblib/grpc regenerates them at build time into
+ # ${CMAKE_BINARY_DIR}/src/oblib/grpc. Putting that build dir first in the
+ # include search order lets the regenerated headers shadow the stale
+ # checked-in ones for every target (source files include them as
+ # "grpc/standbyservice.grpc.pb.h" resolved against the src/oblib root).
+ target_include_directories(ob_base_without_pass INTERFACE BEFORE
+ ${CMAKE_BINARY_DIR}/src/oblib)
+endif()
+
target_compile_features(ob_base_without_pass INTERFACE cxx_std_11)
+# RELRO + linker script are ELF-only; Windows (lld-link) and macOS must not see -Wl,-T / -Wl,-z.
target_link_libraries(ob_base_without_pass INTERFACE oblib_base_without_pass
${OB_RELRO_FLAG}
$<$:-Wl,-T,${CMAKE_SOURCE_DIR}/rpm/ld.lds>)
@@ -58,3 +72,4 @@ add_subdirectory(rootserver)
add_subdirectory(logservice)
add_subdirectory(storage)
add_subdirectory(observer)
+add_subdirectory(include)
diff --git a/src/include/CMakeLists.txt b/src/include/CMakeLists.txt
new file mode 100644
index 0000000000..4dd9773269
--- /dev/null
+++ b/src/include/CMakeLists.txt
@@ -0,0 +1,201 @@
+# C API for SeekDB
+# Provides C API for multi-language bindings (Node.js N-API, FFI, Rust, Go, etc.)
+
+# Set source files
+set(FFI_SOURCES
+ seekdb.cpp
+)
+
+# Create object library for FFI
+ob_set_subtarget(seekdb_object_list common
+ ${FFI_SOURCES}
+)
+
+ob_add_new_object_target(seekdb_objects seekdb_object_list)
+
+target_include_directories(seekdb_objects
+ PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/src/libtable/src
+)
+
+# Link against oceanbase for object compilation (headers/symbols)
+target_link_libraries(seekdb_objects PUBLIC oceanbase)
+
+# Create self-contained shared library for FFI (target name: libseekdb to avoid
+# conflict with the seekdb custom target in src/observer/CMakeLists.txt)
+# This statically links liboceanbase_static.a into libseekdb.so
+# so users only need this single .so file
+add_library(libseekdb
+ SHARED
+ ${FFI_SOURCES}
+)
+
+target_include_directories(libseekdb
+ PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/src/libtable/src
+)
+
+# Propagate oblib/src and src/ include paths for seekdb.cpp (same as seekdb_objects).
+target_link_libraries(libseekdb PRIVATE ob_base)
+
+# Link oceanbase_static into the shared library
+# Use --whole-archive (Linux/Android) / -force_load (macOS) to include all symbols from static library
+# On Linux desktop: -static-libstdc++ and -static-libgcc to avoid runtime dependency on system C++ libs
+# Android NDK: use libc++ from sysroot; no static-libstdc++/libgcc (same idea as oceanbase target)
+# (macOS/clang does not support -static-libgcc/-static-libstdc++)
+# Use version script (Linux/Android) / exported_symbols_list (macOS) to hide internal symbols
+if(APPLE)
+ # macOS: -exported_symbols_list does NOT support wildcards; generate list from seekdb.h
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/seekdb.h" SEEKDB_HEADER_CONTENT)
+ string(REGEX MATCHALL "seekdb_[a-zA-Z0-9_]+" SEEKDB_SYMBOLS "${SEEKDB_HEADER_CONTENT}")
+ list(REMOVE_DUPLICATES SEEKDB_SYMBOLS)
+ list(FILTER SEEKDB_SYMBOLS EXCLUDE REGEX "seekdb_cell_callback_t")
+ # Exclude false positives from comments (e.g. "seekdb_stmt_*" yields "seekdb_stmt_")
+ list(FILTER SEEKDB_SYMBOLS EXCLUDE REGEX "_$")
+ list(SORT SEEKDB_SYMBOLS)
+ set(SEEKDB_EXPORT_CONTENT "# Generated from seekdb.h - do not edit\n")
+ foreach(SYM ${SEEKDB_SYMBOLS})
+ # macOS C symbols have leading underscore in the symbol table
+ string(APPEND SEEKDB_EXPORT_CONTENT "_${SYM}\n")
+ endforeach()
+ file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/seekdb_export.txt" "${SEEKDB_EXPORT_CONTENT}")
+ set(SEEKDB_EXPORT_FILE "${CMAKE_CURRENT_BINARY_DIR}/seekdb_export.txt")
+
+ # macOS: use -force_load only for liboceanbase_static.a (avoid -all_load duplicate symbols).
+ target_link_options(libseekdb PRIVATE "LINKER:-force_load,$")
+ target_link_libraries(libseekdb
+ PRIVATE
+ oceanbase_static
+ sql_nio
+ -Wl,-exported_symbols_list,${SEEKDB_EXPORT_FILE}
+ )
+elseif(ANDROID)
+ # NDK: libc++ from sysroot; do not use -static-libstdc++ / -static-libgcc (Linux desktop only).
+ target_link_libraries(libseekdb
+ PRIVATE
+ -Wl,--whole-archive
+ oceanbase_static
+ -Wl,--no-whole-archive
+ sql_nio
+ -Wl,--allow-multiple-definition
+ -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/seekdb.version
+ )
+elseif(WIN32)
+ # Windows (Clang + lld): export C API from seekdb.cpp; pull full static archive like Linux --whole-archive.
+ set_target_properties(libseekdb PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
+ target_link_options(libseekdb PRIVATE /FORCE:MULTIPLE
+ "/WHOLEARCHIVE:$")
+ # oceanbase_static is EXCLUDE_FROM_ALL and only referenced via LINK_OPTIONS; some generators do not
+ # infer a build dependency, so ninja may link libseekdb before oceanbase_static.lib exists.
+ add_dependencies(libseekdb oceanbase_static zstd_1_3_8_objs)
+ get_filename_component(_seekdb_clang_bin_dir ${CMAKE_CXX_COMPILER} DIRECTORY)
+ get_filename_component(_seekdb_llvm_root ${_seekdb_clang_bin_dir} DIRECTORY)
+ file(GLOB _seekdb_clang_rt "${_seekdb_llvm_root}/lib/clang/*/lib/windows/clang_rt.builtins-x86_64.lib")
+ if(_seekdb_clang_rt)
+ list(GET _seekdb_clang_rt 0 _seekdb_clang_rt_lib)
+ target_link_libraries(libseekdb PRIVATE ${_seekdb_clang_rt_lib})
+ endif()
+ # oceanbase_static.lib only carries its own members; CMake does not merge PUBLIC static deps into it
+ # on MSVC. Link the same concrete libs as oceanbase_static PUBLIC (cf. observer_without_bolt), or
+ # symbols such as ObSqlString (~destructor in oblib) stay unresolved.
+ target_link_libraries(libseekdb PRIVATE
+ ob_sql_static
+ ob_storage_static
+ ob_share_static
+ oblib
+ ob_malloc
+ sql_nio
+ synchronization
+ ${CMAKE_BINARY_DIR}/src/oblib/lib/compress/zstd_1_3_8/zstd_1_3_8_objs.lib
+ )
+ # oceanbase_static PUBLIC links Hyperscan (see src/observer/CMakeLists.txt); DLL link does not inherit it.
+ if(HYPERSCAN_LIB)
+ target_link_libraries(libseekdb PRIVATE ${HYPERSCAN_LIB})
+ endif()
+ # Bundle third-party runtime DLLs next to seekdb.dll (mirrors seekdb.exe POST_BUILD in
+ # src/observer/CMakeLists.txt). Binding tests load seekdb.dll from src/include via ctypes;
+ # without colocated DLLs, Python 3.8+ LoadLibrary search does not use PATH for dependencies.
+ add_custom_command(TARGET libseekdb POST_BUILD
+ COMMAND ${CMAKE_COMMAND}
+ "-DEXE=$"
+ "-DOUT_DIR=$"
+ "-DSEARCH_DIRS=${OB_VCPKG_DIR}/bin;${OB_VSAG_DIR}/bin;${OB_OPENSSL_DIR}/bin"
+ -P "${CMAKE_SOURCE_DIR}/cmake/BundleRuntimeDllsWindows.cmake"
+ COMMENT "Bundling runtime DLLs next to seekdb.dll"
+ VERBATIM)
+else()
+ target_link_libraries(libseekdb
+ PRIVATE
+ -Wl,--whole-archive
+ oceanbase_static
+ -Wl,--no-whole-archive
+ sql_nio
+ -static-libstdc++
+ -static-libgcc
+ -Wl,--allow-multiple-definition
+ -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/seekdb.version
+ )
+endif()
+
+# Output file name remains libseekdb.so / libseekdb.dylib
+set_target_properties(libseekdb PROPERTIES
+ OUTPUT_NAME "seekdb"
+)
+
+# Strip debug symbols in release builds to reduce library size
+if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT CMAKE_BUILD_TYPE STREQUAL "debug")
+ # Android on macOS host: CMAKE_STRIP still points at host strip (Mach-O only); use NDK llvm-strip for ELF.
+ # NDK path: CMAKE_ANDROID_NDK from android.toolchain.cmake, else same derivation as cmake/Env.cmake from CMAKE_TOOLCHAIN_FILE.
+ # Do not rely on ENV{ANDROID_NDK_HOME}: build.sh does not export it, so CMake often has no such env.
+ if(ANDROID)
+ set(_seekdb_ndk "")
+ if(CMAKE_ANDROID_NDK)
+ set(_seekdb_ndk "${CMAKE_ANDROID_NDK}")
+ elseif(CMAKE_TOOLCHAIN_FILE)
+ # CMAKE_TOOLCHAIN_FILE is .../ndk//build/cmake/android.toolchain.cmake; we need the NDK root for toolchains/.
+ # Each DIRECTORY step goes up one level: cmake -> build -> NDK root (three times; same as cmake/Env.cmake).
+ get_filename_component(_seekdb_ndk "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY)
+ get_filename_component(_seekdb_ndk "${_seekdb_ndk}" DIRECTORY)
+ get_filename_component(_seekdb_ndk "${_seekdb_ndk}" DIRECTORY)
+ endif()
+ set(_seekdb_llvm_strip "")
+ if(_seekdb_ndk)
+ file(GLOB _seekdb_llvm_strip "${_seekdb_ndk}/toolchains/llvm/prebuilt/*/bin/llvm-strip")
+ if(_seekdb_llvm_strip)
+ list(GET _seekdb_llvm_strip 0 _seekdb_llvm_strip)
+ endif()
+ endif()
+ if(_seekdb_llvm_strip)
+ add_custom_command(TARGET libseekdb POST_BUILD
+ COMMAND "${_seekdb_llvm_strip}" $
+ COMMENT "Stripping debug symbols from libseekdb (Android ELF via NDK llvm-strip)"
+ )
+ else()
+ message(WARNING "Android libseekdb: llvm-strip not found under NDK; skipping strip")
+ endif()
+ elseif(APPLE)
+ # macOS: -S strip __DWARF (debug), -x strip local symbol table; full strip fails due to indirect symbol table
+ add_custom_command(TARGET libseekdb POST_BUILD
+ COMMAND ${CMAKE_STRIP} -Sx $
+ COMMENT "Stripping debug and local symbols from libseekdb (macOS)"
+ )
+ elseif(UNIX)
+ add_custom_command(TARGET libseekdb POST_BUILD
+ COMMAND ${CMAKE_STRIP} $
+ COMMENT "Stripping debug symbols from libseekdb to reduce size"
+ )
+ endif()
+endif()
+
+# Install the shared library and header
+install(TARGETS libseekdb
+ LIBRARY DESTINATION lib
+ ARCHIVE DESTINATION lib
+ RUNTIME DESTINATION bin
+)
+
+install(FILES seekdb.h
+ DESTINATION include
+)
diff --git a/src/include/seekdb.cpp b/src/include/seekdb.cpp
new file mode 100644
index 0000000000..9b21aef9c3
--- /dev/null
+++ b/src/include/seekdb.cpp
@@ -0,0 +1,5476 @@
+/*
+ * Copyright (c) 2025 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define USING_LOG_PREFIX CLIENT
+#include "seekdb.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include "lib/utility/ob_common_utility.h" // For set_stackattr()
+#include "lib/time/ob_time_utility.h" // For ObTimeUtility::current_time
+#include "common/object/ob_object.h" // For ObObj / ObLobLocatorV2 (out-row LOB read)
+#include "data_plane/lob/ob_lob_read.h" // For data_plane::read_lob_to_buffer
+#include "common/mysqlclient/ob_mysql_proxy.h"
+#include "common/mysqlclient/ob_mysql_result.h"
+#include "lib/string/ob_string.h"
+#include "lib/allocator/ob_malloc.h"
+#include "lib/alloc/alloc_func.h"
+#include "lib/resource/ob_resource_mgr.h"
+#include "observer/ob_inner_sql_connection.h"
+#include "observer/ob_inner_sql_result.h"
+#include "observer/ob_server.h"
+#include "observer/ob_server_options.h"
+#include "share/ob_server_struct.h"
+#include "lib/file/file_directory_utils.h"
+#include "lib/utility/utility.h"
+#include "lib/oblog/ob_warning_buffer.h"
+#include "lib/oblog/ob_log.h"
+#include "lib/string/ob_sql_string.h"
+#include "sql/session/ob_sql_session_info.h"
+#include "share/schema/ob_schema_getter_guard.h"
+#include "share/schema/ob_multi_version_schema_service.h"
+#include "share/schema/ob_server_schema_service.h"
+#include "lib/container/ob_array.h"
+#include "sql/parser/ob_parser.h"
+#include "sql/parser/parse_node.h"
+#include "share/schema/ob_priv_type.h"
+#include "share/ob_define.h"
+#include "sql/session/ob_system_variable.h"
+#include "share/system_variable/ob_sys_var_class_type.h"
+#include "share/ob_errno.h" // For ob_strerror
+#include "sql/ob_sql_utils.h" // For ObSQLUtils::update_session_last_schema_version (DDL visibility)
+#include "lib/ob_define.h" // For OB_SYS_USER_ID
+#include "lib/profile/ob_trace_id.h" // For ObCurTraceId
+#include "lib/worker.h" // For lib::Worker
+#include "lib/thread/threads.h" // For global_thread_stack_size, THREAD_STACK_RESERVED_SIZE
+#include "lib/utility/alloc_assist.h" // For ACHUNK_PRESERVE_SIZE
+#include "lib/utility/ob_smart_call.h" // For CALL_WITH_NEW_STACK
+#ifdef _WIN32
+#include
+#include
+#include
+#include
+#include
+#include
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+#else
+#include
+#include
+#ifdef __APPLE__
+#include // statfs on macOS
+#else
+#include
+#endif
+#include // For mmap/munmap
+#endif
+#include
+#include
+#include
+#ifndef _WIN32
+#include // sigaction, SIGBUS (not used on Windows)
+#endif
+
+#ifdef _WIN32
+#define SEEKDB_CHDIR(p) ::_chdir(p)
+#define SEEKDB_GETCWD(buf, sz) ::_getcwd((buf), static_cast(sz))
+#define SEEKDB_GETPID() (static_cast(::_getpid()))
+#define SEEKDB_UNLINK(p) ::_unlink(p)
+#else
+#define SEEKDB_CHDIR(p) ::chdir(p)
+#define SEEKDB_GETCWD(buf, sz) ::getcwd((buf), (sz))
+#define SEEKDB_GETPID() (static_cast(::getpid()))
+#define SEEKDB_UNLINK(p) ::unlink(p)
+#endif
+
+static void *seekdb_mmap_anonymous_stack(size_t stack_size)
+{
+#ifdef _WIN32
+ return ::VirtualAlloc(nullptr, stack_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
+#else
+ void *const p =
+ ::mmap(nullptr, stack_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ return p;
+#endif
+}
+
+static void seekdb_munmap_anonymous_stack(void *addr, size_t stack_size)
+{
+#ifdef _WIN32
+ (void)stack_size;
+ if (addr != nullptr) {
+ ::VirtualFree(addr, 0, MEM_RELEASE);
+ }
+#else
+ (void)::munmap(addr, stack_size);
+#endif
+}
+
+using namespace oceanbase::common;
+using namespace oceanbase::sqlclient;
+using namespace oceanbase::observer;
+using namespace oceanbase::sql;
+using namespace oceanbase::share;
+namespace share = oceanbase::share;
+using namespace oceanbase::lib;
+
+// RAII: redirect stdout/stderr to /dev/null for the scope so LOG_STDOUT (e.g. "successfully init log writer") does not print.
+struct SuppressLogStdoutScope {
+ int saved_stdout = -1;
+ int saved_stderr = -1;
+ SuppressLogStdoutScope() {
+#ifdef _WIN32
+ saved_stdout = ::_dup(::_fileno(stdout));
+ saved_stderr = ::_dup(::_fileno(stderr));
+ const int fd = ::_open("NUL", _O_WRONLY);
+ if (fd >= 0) {
+ ::_dup2(fd, ::_fileno(stdout));
+ ::_dup2(fd, ::_fileno(stderr));
+ ::_close(fd);
+ }
+#else
+ saved_stdout = dup(STDOUT_FILENO);
+ saved_stderr = dup(STDERR_FILENO);
+ int fd = open("/dev/null", O_WRONLY);
+ if (fd >= 0) {
+ dup2(fd, STDOUT_FILENO);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ }
+#endif
+ }
+ ~SuppressLogStdoutScope() {
+#ifdef _WIN32
+ if (saved_stderr >= 0) {
+ ::_dup2(saved_stderr, ::_fileno(stderr));
+ ::_close(saved_stderr);
+ }
+ if (saved_stdout >= 0) {
+ ::_dup2(saved_stdout, ::_fileno(stdout));
+ ::_close(saved_stdout);
+ }
+#else
+ if (saved_stderr >= 0) {
+ dup2(saved_stderr, STDERR_FILENO);
+ close(saved_stderr);
+ }
+ if (saved_stdout >= 0) {
+ dup2(saved_stdout, STDOUT_FILENO);
+ close(saved_stdout);
+ }
+#endif
+ }
+};
+
+// Thread-local error storage for improved error handling
+thread_local static std::string g_thread_last_error;
+thread_local static int g_thread_last_error_code = SEEKDB_SUCCESS;
+
+// Forward declaration for embedded connection
+namespace oceanbase {
+namespace observer {
+class ObInnerSQLConnection;
+} // namespace observer
+} // namespace oceanbase
+
+// Forward declarations for internal structures
+struct SeekdbResultSet;
+struct SeekdbRowData;
+struct SeekdbConnection;
+
+// Use OBSERVER macro directly like Python embed does
+// OBSERVER is defined in observer/ob_server.h as ObServer::get_instance()
+
+// Store last affected rows per connection for seekdb_affected_rows().
+// Must not use thread_local: Go CGO can call seekdb_query and seekdb_affected_rows on different OS threads.
+
+// Internal structures - define SeekdbRowData first, then SeekdbResultSet
+// so that SeekdbResultSet can properly delete SeekdbRowData in destructor
+struct SeekdbRowData {
+ SeekdbResultSet* result_set;
+ int64_t row_index;
+ bool freed; // Flag to detect double free
+
+ SeekdbRowData(SeekdbResultSet* rs, int64_t idx)
+ : result_set(rs), row_index(idx), freed(false) {}
+};
+
+// Structure to store field strings for lifetime management
+struct SeekdbFieldStrings {
+ std::string col_name;
+ std::string org_col_name;
+ std::string table_name;
+ std::string org_table_name;
+ std::string db_name;
+};
+
+struct SeekdbResultSet {
+ std::vector> rows;
+ std::vector> row_nulls; // row_nulls[r][c] true iff cell (r,c) is SQL NULL (distinct from empty string)
+ std::vector column_names;
+ int64_t current_row;
+ int64_t row_count;
+ int32_t column_count;
+ std::vector current_lengths; // For mysql_fetch_lengths() compatibility
+ SeekdbRowData* current_row_data; // Current row data for fetch_lengths
+ std::vector fields; // For mysql_fetch_fields() compatibility
+ std::vector field_strings; // Store strings for field lifetime management
+ unsigned int current_field; // Current field position for seekdb_fetch_field()
+ bool use_result_mode; // true for use_result (streaming), false for store_result (buffered)
+ struct SeekdbConnection* owner_conn; // Connection that owns this result set (for cleanup)
+ bool freed; // Flag to detect double free
+
+ SeekdbResultSet() : current_row(-1), row_count(0), column_count(0), current_row_data(nullptr),
+ current_field(0), use_result_mode(false), owner_conn(nullptr), freed(false) {}
+
+ ~SeekdbResultSet() {
+ if (current_row_data) {
+ // Check if already freed to prevent double free
+ if (!current_row_data->freed) {
+ delete current_row_data;
+ }
+ current_row_data = nullptr;
+ }
+ }
+};
+
+struct SeekdbConnection {
+ ObISQLConnectionGuard embed_conn_guard; // Owns embedded connection lifetime
+ ObInnerSQLConnection* embed_conn; // Embedded connection
+ ObSQLSessionInfo* embed_session; // Session for transaction management
+ ObCommonSqlProxy::ReadResult* embed_result; // Query result
+ SeekdbResultSet* last_result_set; // Last result set for mysql_store_result() compatibility
+ SeekdbResultSet* use_result_set; // Result set for mysql_use_result() (streaming mode)
+ std::vector result_sets; // Multiple result sets queue
+ int current_result_index; // Current result set index for multiple results
+ std::string last_error;
+ bool initialized;
+ unsigned long long last_affected_rows;
+
+ SeekdbConnection() : embed_conn(nullptr), embed_session(nullptr),
+ embed_result(nullptr), last_result_set(nullptr),
+ use_result_set(nullptr), current_result_index(-1), initialized(false),
+ last_affected_rows(0) {}
+ ~SeekdbConnection() {
+ // Cleanup last result set (only if still owned by connection)
+ // If it was transferred to user via seekdb_store_result(), it's nullptr
+ if (last_result_set) {
+ // Check if already freed to prevent double free
+ if (!last_result_set->freed) {
+ delete last_result_set;
+ }
+ last_result_set = nullptr;
+ }
+ // Cleanup use result set
+ if (use_result_set) {
+ // Check if already freed to prevent double free
+ if (!use_result_set->freed) {
+ delete use_result_set;
+ }
+ use_result_set = nullptr;
+ }
+ // Cleanup multiple result sets
+ for (auto* rs : result_sets) {
+ if (rs) {
+ // Check if already freed to prevent double free
+ if (!rs->freed) {
+ delete rs;
+ }
+ }
+ }
+ result_sets.clear();
+ // Cleanup embedded result
+ if (embed_result) {
+ embed_result->close();
+ embed_result->~ReadResult();
+ ob_free(embed_result);
+ embed_result = nullptr;
+ }
+ // Release connection (guard owns lifetime; reset releases via free_self)
+ if (embed_conn) {
+ embed_conn_guard.reset();
+ embed_conn = nullptr;
+ }
+ // Release session (like Python embed does)
+ if (embed_session) {
+ GCTX.session_mgr_->revert_session(embed_session);
+ embed_session = nullptr;
+ }
+ }
+};
+
+struct SeekdbStmtData {
+ SeekdbConnection* conn;
+ std::string sql;
+ std::vector param_binds;
+ std::vector result_binds;
+ uint64_t ps_stmt_id; // Prepared statement ID from OceanBase
+ unsigned long param_count;
+ unsigned long result_count;
+ SeekdbResultSet* result_set;
+ std::string last_error;
+ std::vector param_column_types; // Column types for parameters (from table schema)
+ bool prepared;
+ bool executed;
+
+ SeekdbStmtData(SeekdbConnection* c)
+ : conn(c), ps_stmt_id(0), param_count(0), result_count(0),
+ result_set(nullptr), prepared(false), executed(false) {}
+
+ ~SeekdbStmtData() {
+ if (result_set) {
+ delete result_set;
+ result_set = nullptr;
+ }
+ }
+};
+
+// VARBINARY(512) length for _id column; semantics from bind type (SEEKDB_TYPE_VARBINARY_ID), no SQL parsing
+static const unsigned int VARBINARY_ID_LENGTH = 512;
+
+// VECTOR read: convert raw float32 binary to JSON string "[v1, v2, ...]" without precision rounding.
+// Directly formats each float (e.g. 1.1, 2.2, 3.3) so result is "[1.1, 2.2, 3.3]".
+static bool vector_binary_to_json(const char* ptr, int64_t len, std::string& out) {
+ if (!ptr || len <= 0 || (len % sizeof(float)) != 0) {
+ out.clear();
+ return false;
+ }
+ const int64_t n = len / static_cast(sizeof(float));
+ const float* f = reinterpret_cast(ptr);
+ std::string buf;
+ buf.reserve(static_cast(n * 16));
+ buf.push_back('[');
+ char num[64];
+ for (int64_t i = 0; i < n; ++i) {
+ int wr = snprintf(num, sizeof(num), "%g", f[i]);
+ if (wr <= 0 || wr >= static_cast(sizeof(num))) {
+ out.clear();
+ return false;
+ }
+ if (i > 0) buf.append(", ");
+ buf.append(num, static_cast(wr));
+ }
+ buf.push_back(']');
+ out = std::move(buf);
+ return true;
+}
+
+static std::string seekdb_escape_sql_single_quoted(const std::string& str_val, bool escape_backslash) {
+ std::string escaped;
+ escaped.reserve(str_val.size() + 16);
+ for (char c : str_val) {
+ if (escape_backslash && c == '\\') {
+ escaped += "\\\\";
+ } else if (c == '\'') {
+ escaped += "''";
+ } else {
+ escaped += c;
+ }
+ }
+ return escaped;
+}
+
+static bool seekdb_is_json_bind_string(const std::string& str_val) {
+ return str_val.size() >= 2 && str_val.front() == '{' && str_val.find('"') != std::string::npos;
+}
+
+static bool seekdb_is_sparse_vector_bind_string(const std::string& str_val) {
+ return str_val.size() >= 2 && str_val.front() == '{' && str_val.find('"') == std::string::npos &&
+ str_val.find(':') != std::string::npos;
+}
+
+static std::string seekdb_format_string_bind_param(
+ SeekdbConnection* conn,
+ const SeekdbStmtData* stmt_data,
+ size_t param_idx,
+ const std::string& str_val) {
+ if (!str_val.empty() && str_val.front() == '[') {
+ return str_val;
+ }
+ if (seekdb_is_sparse_vector_bind_string(str_val)) {
+ return "'" + seekdb_escape_sql_single_quoted(str_val, false) + "'";
+ }
+ size_t col_idx = param_idx;
+ if (stmt_data != nullptr && !stmt_data->param_column_types.empty()) {
+ col_idx = param_idx % stmt_data->param_column_types.size();
+ }
+ const bool json_column =
+ seekdb_is_json_bind_string(str_val) ||
+ (stmt_data != nullptr && col_idx < stmt_data->param_column_types.size() &&
+ ob_is_json_tc(stmt_data->param_column_types[col_idx]));
+ if (json_column) {
+ return "'" + seekdb_escape_sql_single_quoted(str_val, true) + "'";
+ }
+ const size_t buf_len = str_val.length() * 2 + 1;
+ std::vector escaped_buf(buf_len);
+ const unsigned long escaped_length = seekdb_real_escape_string(
+ static_cast(conn),
+ escaped_buf.data(),
+ static_cast(escaped_buf.size()),
+ str_val.c_str(),
+ static_cast(str_val.length()));
+ if (escaped_length != static_cast(-1)) {
+ return "'" + std::string(escaped_buf.data(), escaped_length) + "'";
+ }
+ return "'" + seekdb_escape_sql_single_quoted(str_val, false) + "'";
+}
+
+static int seekdb_read_outrow_lob(SeekdbConnection* conn,
+ const oceanbase::common::ObObj& obj,
+ oceanbase::common::ObArenaAllocator& allocator,
+ oceanbase::common::ObString& out_str) {
+ using namespace oceanbase;
+ using namespace oceanbase::common;
+ if (OB_ISNULL(conn) || !obj.is_lob_storage()) {
+ return OB_INVALID_ARGUMENT;
+ }
+ const char* payload = obj.get_string_ptr();
+ const uint32_t payload_len = static_cast(obj.get_data_length());
+ if (OB_ISNULL(payload) || payload_len < static_cast(sizeof(ObLobCommon))) {
+ return OB_INVALID_ARGUMENT;
+ }
+ // The embedded SQL result path (datum2obj) copies the cell
+ // payload but does NOT set the ObObj LOB header flag, so obj.has_lob_header()
+ // is false even though the payload is the full LOB locator binary. The locator
+ // must therefore be constructed with has_lob_header=true.
+ ObLobLocatorV2 locator(const_cast(payload), payload_len, true);
+ if (locator.has_inrow_data()) {
+ return locator.get_inrow_data(out_str);
+ }
+ // Out-row LOB: the read snapshot is embedded in the locator, so a null tx_desc is
+ // sufficient for committed reads in embedded single-connection mode.
+ const int64_t timeout_ts = ObTimeUtility::current_time() + 10LL * 1000LL * 1000LL; // 10s in us
+ // read_lob_to_buffer writes into a caller-supplied buffer: allocate capacity for the
+ // full LOB byte size first, otherwise ObLobQueryDataHandler::write_data_to_buffer
+ // fails with OB_ERR_INTERVAL_INVALID on an empty ObString (buffer_size_ == 0).
+ int64_t byte_size = 0;
+ if (locator.get_lob_data_byte_len(byte_size) != OB_SUCCESS || byte_size <= 0) {
+ return OB_INVALID_ARGUMENT;
+ }
+ char* buf = static_cast(allocator.alloc(static_cast(byte_size)));
+ if (OB_ISNULL(buf)) {
+ return OB_ALLOCATE_MEMORY_FAILED;
+ }
+ out_str.assign_buffer(buf, static_cast(byte_size));
+ return data_plane::read_lob_to_buffer(allocator, locator, timeout_ts, nullptr, out_str);
+}
+
+static void seekdb_materialize_string_cell(
+ SeekdbConnection* conn,
+ oceanbase::common::ObObj& obj,
+ oceanbase::common::ObObjType obj_type,
+ oceanbase::common::ObArenaAllocator& lob_allocator,
+ std::string& cell_out) {
+ using namespace oceanbase::common;
+ cell_out.clear();
+ if (obj.is_null()) {
+ return;
+ }
+ if (ob_is_text_tc(obj_type)) {
+ // A text cell's payload is either the raw document (in-row data or a
+ // full-data materialized read) or a LOB locator binary (out-row reads
+ // build a MEM persist locator). datum2obj does not set
+ // the ObObj LOB header flag, so get_string() would return the locator
+ // bytes (leading NUL) and the Napi C-string layer would truncate the
+ // value to "". Detect the locator by content and materialize the
+ // document through the LOB manager instead.
+ const char* payload = obj.get_string_ptr();
+ const uint32_t payload_len = static_cast(obj.get_data_length());
+ if (payload != nullptr && payload_len >= static_cast(sizeof(ObLobCommon))) {
+ ObLobLocatorV2 locator(const_cast(payload), payload_len, true);
+ if (locator.is_valid(false)) {
+ ObString lob_str;
+ if (locator.has_inrow_data()) {
+ if (OB_SUCCESS == locator.get_inrow_data(lob_str) && lob_str.ptr() != nullptr &&
+ lob_str.length() > 0) {
+ cell_out.assign(lob_str.ptr(), lob_str.length());
+ return;
+ }
+ } else if (conn != nullptr &&
+ OB_SUCCESS == seekdb_read_outrow_lob(conn, obj, lob_allocator, lob_str) &&
+ lob_str.ptr() != nullptr && lob_str.length() > 0) {
+ cell_out.assign(lob_str.ptr(), lob_str.length());
+ return;
+ }
+ // Locator recognized but materialization failed; fall through to
+ // the plain-string path so in-row payloads are still served.
+ LOG_WARN_RET(OB_ERR_UNEXPECTED, "seekdb: LOB locator materialization failed",
+ K(payload_len));
+ }
+ }
+ }
+ if (!obj.has_lob_header()) {
+ // Plain string and legacy in-row text (no LOB header) materialize directly.
+ // A cell with a LOB header must never go through get_string(): the payload is
+ // the LOB locator binary (leading NUL), not the document text.
+ ObString str_val;
+ const int get_ret = obj.get_string(str_val);
+ if (OB_SUCCESS == get_ret && str_val.ptr() != nullptr && str_val.length() > 0) {
+ cell_out.assign(str_val.ptr(), str_val.length());
+ return;
+ }
+ }
+ if (ob_is_text_tc(obj_type) || ob_is_string_type(obj_type)) {
+ ObString lob_str;
+ if (OB_SUCCESS == obj.read_lob_data(lob_allocator, lob_str) && lob_str.ptr() != nullptr &&
+ lob_str.length() > 0) {
+ cell_out.assign(lob_str.ptr(), lob_str.length());
+ return;
+ }
+ // Fallback: out-row LOB payloads cannot be materialized by ObObj::read_lob_data
+ // (embedded build ships a NOT_SUPPORTED stub for ob_obj_read_lob_data). Read the
+ // payload through the LOB manager instead so large documents survive the roundtrip.
+ if (conn != nullptr &&
+ OB_SUCCESS == seekdb_read_outrow_lob(conn, obj, lob_allocator, lob_str) &&
+ lob_str.ptr() != nullptr && lob_str.length() > 0) {
+ cell_out.assign(lob_str.ptr(), lob_str.length());
+ return;
+ }
+ }
+ char buf[4096];
+ int64_t pos = 0;
+ if (OB_SUCCESS == obj.print_sql_literal(buf, sizeof(buf), pos) && pos > 0) {
+ std::string sql_literal(buf, static_cast(pos));
+ if (sql_literal.length() >= 2 && sql_literal.front() == '\'' && sql_literal.back() == '\'') {
+ sql_literal = sql_literal.substr(1, sql_literal.length() - 2);
+ for (size_t q = 0; (q = sql_literal.find("''", q)) != std::string::npos; q += 1) {
+ sql_literal.replace(q, 2, "'");
+ }
+ }
+ cell_out = std::move(sql_literal);
+ return;
+ }
+ std::vector large_buf(2 * 1024 * 1024, 0);
+ pos = 0;
+ if (OB_SUCCESS == obj.print_sql_literal(large_buf.data(), large_buf.size(), pos) && pos > 0) {
+ std::string sql_literal(large_buf.data(), static_cast(pos));
+ if (sql_literal.length() >= 2 && sql_literal.front() == '\'' && sql_literal.back() == '\'') {
+ sql_literal = sql_literal.substr(1, sql_literal.length() - 2);
+ for (size_t q = 0; (q = sql_literal.find("''", q)) != std::string::npos; q += 1) {
+ sql_literal.replace(q, 2, "'");
+ }
+ }
+ cell_out = std::move(sql_literal);
+ }
+}
+
+// Global state
+static std::mutex g_init_mutex;
+static bool g_initialized = false;
+static bool g_embedded_opened = false;
+static ObSqlString g_embedded_pid_file;
+static bool g_embedded_pid_locked = false;
+static char g_embedded_work_dir[PATH_MAX];
+static char g_embedded_base_dir[PATH_MAX] = {0}; // Absolute path: opened db path for same-path reuse
+static bool g_closing = false; // Flag to indicate we're in closing process
+#ifndef _WIN32
+static struct sigaction g_old_segv_handler; // Store original SIGSEGV handler
+static struct sigaction g_old_sigabrt_handler; // Store original SIGABRT handler
+static struct sigaction g_old_sigbus_handler; // Store original SIGBUS handler
+#endif
+static bool g_segv_handler_installed = false;
+static bool g_sigabrt_handler_installed = false;
+static bool g_sigbus_handler_installed = false;
+
+// Set when embedded DB was ever successfully opened; never cleared.
+// Used by the signal handler so we can recognize cleanup segfaults even when
+// g_embedded_opened has already been set to false by seekdb_close() or during destructors.
+static bool g_embedded_ever_opened = false;
+
+#ifndef _WIN32
+// Signal handler for SIGSEGV during cleanup
+// This allows graceful handling of segfaults during static destructors
+// Must be defined before seekdb_library_init() which uses it
+static void segv_handler_during_close(int sig, siginfo_t* info, void* context) {
+ // If we're in the closing process or database was (or had been) opened, treat as cleanup segfault
+ // This is expected during OceanBase static destructor cleanup at program exit
+ if (g_closing || g_embedded_opened || g_embedded_ever_opened) {
+ // Exit gracefully with success code since cleanup segfault is expected
+ _exit(0);
+ }
+
+ // If not in closing process and database not opened, restore original handler
+ if (g_segv_handler_installed) {
+ sigaction(SIGSEGV, &g_old_segv_handler, nullptr);
+ g_segv_handler_installed = false;
+ raise(SIGSEGV);
+ }
+}
+
+// Signal handler for SIGABRT during cleanup
+// ob_abort() triggers SIGABRT during static destructors (e.g. Node.js N-API on macOS).
+// Catch it and exit gracefully when we're in the embedded DB cleanup path.
+static void sigabrt_handler_during_close(int sig, siginfo_t* info, void* context) {
+ if (g_closing || g_embedded_opened || g_embedded_ever_opened) {
+ _exit(0);
+ }
+ if (g_sigabrt_handler_installed) {
+ sigaction(SIGABRT, &g_old_sigabrt_handler, nullptr);
+ g_sigabrt_handler_installed = false;
+ raise(SIGABRT);
+ }
+}
+
+// Signal handler for SIGBUS during cleanup
+// SIGBUS can occur during static destructors (e.g. Rust FFI on macOS).
+// Catch it and exit gracefully when we're in the embedded DB cleanup path.
+static void sigbus_handler_during_close(int sig, siginfo_t* info, void* context) {
+ if (g_closing || g_embedded_opened || g_embedded_ever_opened) {
+ _exit(0);
+ }
+ if (g_sigbus_handler_installed) {
+ sigaction(SIGBUS, &g_old_sigbus_handler, nullptr);
+ g_sigbus_handler_installed = false;
+ raise(SIGBUS);
+ }
+}
+#endif // !_WIN32
+
+// Use OBSERVER macro directly like Python embed does
+// No need to cache since ObServer::get_instance() is a singleton
+
+// =============================================================================
+// Absolute path handling: same-process reuse when multiple clients use same path
+// =============================================================================
+// - to_absolute_path: normalize db_dir to absolute for opts and for comparison
+// - same_embedded_path: compare two paths (e.g. requested vs g_embedded_base_dir)
+// - g_embedded_base_dir: stored absolute path of opened db; reuse open if same path
+
+static bool is_path_absolute(const char* p)
+{
+ if (p == nullptr || p[0] == '\0') {
+ return false;
+ }
+ if (p[0] == '/') {
+ return true;
+ }
+#ifdef _WIN32
+ // "C:\..." or "C:/..."
+ if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) && p[1] == ':') {
+ return true;
+ }
+ // UNC "\\server\share\..."
+ if (p[0] == '\\' && p[1] == '\\') {
+ return true;
+ }
+#endif
+ return false;
+}
+
+static int to_absolute_path(const char* cwd, ObSqlString& dir) {
+ int ret = OB_SUCCESS;
+ if (!dir.empty() && dir.ptr()[0] != '\0' && !is_path_absolute(dir.ptr())) {
+ char abs_path[OB_MAX_FILE_NAME_LENGTH] = {0};
+ if (snprintf(abs_path, sizeof(abs_path), "%s/%s", cwd, dir.ptr()) >= static_cast(sizeof(abs_path))) {
+ ret = OB_SIZE_OVERFLOW;
+ } else if (OB_FAIL(dir.assign(abs_path))) {
+ // Error
+ }
+ }
+ return ret;
+}
+
+// Normalize path for comparison: strip trailing slash (except for "/")
+static void path_normalize_for_cmp(const char* path_in, char* out, size_t out_size) {
+ if (!path_in || out_size == 0) return;
+ size_t len = strlen(path_in);
+ while (len > 1 && path_in[len - 1] == '/') len--;
+ size_t n = len < out_size - 1 ? len : out_size - 1;
+ memcpy(out, path_in, n);
+ out[n] = '\0';
+}
+
+static bool same_embedded_path(const char* a, const char* b) {
+ char na[PATH_MAX], nb[PATH_MAX];
+ path_normalize_for_cmp(a, na, sizeof(na));
+ path_normalize_for_cmp(b, nb, sizeof(nb));
+ return strcmp(na, nb) == 0;
+}
+
+static int read_pid_from_file(const char* pidfile, long& pid_out) {
+#ifdef _WIN32
+ const int fd = ::_open(pidfile, _O_RDONLY);
+ if (fd < 0) return -1;
+ char buf[64];
+ const int n = ::_read(fd, buf, sizeof(buf) - 1);
+ ::_close(fd);
+ if (n <= 0) return -1;
+#else
+ int fd = open(pidfile, O_RDONLY);
+ if (fd < 0) return -1;
+ char buf[64];
+ ssize_t n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0) return -1;
+#endif
+ buf[n] = '\0';
+ char* end = nullptr;
+ long pid = strtol(buf, &end, 10);
+ if (end == buf || pid <= 0) return -1;
+ pid_out = pid;
+ return 0;
+}
+
+// Helper function to convert ObString to std::string
+static std::string obstring_to_string(const ObString& str) {
+ return std::string(str.ptr(), str.length());
+}
+
+// Helper function to set error message
+static void set_error(SeekdbConnection* conn, const char* msg) {
+ if (conn) {
+ conn->last_error = msg ? msg : "Unknown error";
+ }
+ // Also update thread-local error
+ g_thread_last_error = msg ? msg : "Unknown error";
+}
+
+// Helper function to set error code and message
+static void set_error_code(int code, const char* msg) {
+ g_thread_last_error_code = code;
+ g_thread_last_error = msg ? msg : "Unknown error";
+}
+
+extern "C" {
+
+// Constructor function to initialize global_thread_stack_size when library is loaded
+// This is critical for Node.js worker processes where the library is loaded via dlopen
+// before seekdb_open() is called. Without this, thread creation may fail with
+// "pthread_create: Invalid argument" because global_thread_stack_size is not set.
+//
+// CRITICAL: Since libseekdb.so depends on liboceanbase.so, liboceanbase.so will be
+// loaded first when libseekdb.so is loaded. If liboceanbase.so's static initializers
+// create threads, they will execute before this constructor. However, liboceanbase.so
+// typically doesn't create threads during static initialization - it only creates
+// threads when observer.init() or similar functions are called.
+//
+// The real issue is that when koffi loads the library, the library's static initializers
+// may trigger code paths that eventually try to create threads (e.g., through singleton
+// initialization). By setting global_thread_stack_size here, we ensure it's set before
+// any such code paths execute.
+//
+// Note: We use a lower priority (200) to ensure this runs after liboceanbase.so's
+// constructors, but before any code that might create threads.
+__attribute__((constructor(200)))
+static void seekdb_library_init() {
+ // Set global_thread_stack_size to a safe default when library is loaded
+ // This ensures threads can be created even if seekdb_open() hasn't been called yet
+ // Use a larger default (2MB) to ensure it works in all scenarios
+ const int64_t default_stack_size = (1LL << 21); // 2MB
+ int64_t calculated_size = default_stack_size - THREAD_STACK_RESERVED_SIZE - ACHUNK_PRESERVE_SIZE;
+
+ // Ensure stack size is at least 1MB for better compatibility
+ // This is larger than the typical minimum (512KB) to handle edge cases
+ if (calculated_size < (1L << 20)) { // 1MB minimum
+ calculated_size = (1L << 20);
+ }
+
+ // Only set if not already set (to avoid overwriting a value set by liboceanbase.so)
+ if (global_thread_stack_size <= 0 || global_thread_stack_size < (512L << 10)) {
+ global_thread_stack_size = calculated_size;
+ }
+
+#ifndef _WIN32
+ // Install global SIGSEGV handler to catch segfaults during static destructors
+ // This allows graceful handling of OceanBase static destructor issues at program exit
+ struct sigaction sa;
+ sa.sa_sigaction = segv_handler_during_close;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_SIGINFO;
+
+ if (sigaction(SIGSEGV, &sa, &g_old_segv_handler) == 0) {
+ g_segv_handler_installed = true;
+ }
+ sa.sa_sigaction = sigabrt_handler_during_close;
+ if (sigaction(SIGABRT, &sa, &g_old_sigabrt_handler) == 0) {
+ g_sigabrt_handler_installed = true;
+ }
+ sa.sa_sigaction = sigbus_handler_during_close;
+ if (sigaction(SIGBUS, &sa, &g_old_sigbus_handler) == 0) {
+ g_sigbus_handler_installed = true;
+ }
+#endif
+}
+
+// Internal implementation of seekdb_open, called on a dedicated stack
+// Matches Python embed's do_open_() behavior:
+// - If port > 0: embed_mode = false (server mode)
+// - If port <= 0: embed_mode = true (embedded mode)
+static int do_seekdb_open_inner(const char* db_dir, int port) {
+ if (g_embedded_opened) {
+ // Absolute path: same-process reuse if requested path equals g_embedded_base_dir.
+ bool same_path = false;
+ if (g_embedded_base_dir[0] != '\0') {
+ char cwd_buf[PATH_MAX];
+ if (SEEKDB_GETCWD(cwd_buf, sizeof(cwd_buf)) != nullptr) {
+ ObSqlString req_abs;
+ if (req_abs.assign(db_dir) == OB_SUCCESS && to_absolute_path(cwd_buf, req_abs) == OB_SUCCESS &&
+ same_embedded_path(req_abs.ptr(), g_embedded_base_dir)) {
+ same_path = true;
+ }
+ }
+ }
+ if (same_path) {
+ return SEEKDB_SUCCESS;
+ }
+ // Different path: close current embedded DB, then reopen the new path below
+ if (g_embedded_pid_locked) {
+ char pid_path[PATH_MAX];
+ snprintf(pid_path, sizeof(pid_path), "%s/run/seekdb.pid", g_embedded_base_dir);
+ SEEKDB_UNLINK(pid_path);
+ g_embedded_pid_locked = false;
+ }
+ g_embedded_opened = false;
+ g_embedded_base_dir[0] = '\0';
+ if (GCTX.is_inited()) {
+ OBSERVER.destroy();
+ ob_usleep(100 * 1000); // 100ms
+ }
+ }
+
+ // Get observer instance
+ // CRITICAL: We need to call observer.destroy() to clean up any previous state,
+ // but this sets stop_ = true. However, observer.start() checks stop_ status
+ // during startup (line 1144, 1184), and only resets it after successful startup (line 1099-1101).
+ // The issue is that if stop_ is true, observer.start() may fail or exit early.
+ //
+ // Solution: Call observer.destroy() to clean up, but we need to ensure that
+ // observer.start() can handle stop_ = true initially. Looking at the code,
+ // observer.start() resets stop_ after successful startup, so if we can get
+ // through the startup process, stop_ will be reset. However, the checks at
+ // line 1144 and 1184 may cause early exit if stop_ is true.
+ //
+ // Actually, from the code, observer.start() at line 1144 checks stop_ and sets
+ // ret = OB_SERVER_IS_STOPPING if stop_ is true. This causes the startup to fail.
+ //
+ // We need to call observer.destroy() to clean up, but then we need a way to reset
+ // stop_ before calling observer.start(). Since stop_ is private, we can't access it.
+ // Use OBSERVER macro directly like Python embed does
+ // Only destroy if observer was previously initialized
+ // This avoids setting stop_ = true unnecessarily
+ if (GCTX.is_inited()) {
+ OBSERVER.destroy();
+ ob_usleep(100 * 1000); // 100ms
+ }
+
+ // Set memory budget to unlimited before init. Critical for fork scenarios
+ // where inherited limits may be too low for embedded reopen.
+ oceanbase::lib::set_memory_budget(INT_MAX64);
+
+ // Note: global_thread_stack_size is already set by the library constructor
+ // (seekdb_library_init) when the library is loaded. This ensures it's set
+ // even in Node.js worker processes where the library is loaded via dlopen
+ // before seekdb_open() is called.
+
+ // CRITICAL: Explicitly initialize memory allocator and resource manager after fork
+ // In fork scenarios, the child process inherits the parent's memory state,
+ // but the memory allocator and resource manager may need explicit initialization to function correctly.
+ // Calling get_instance() ensures the singletons are initialized.
+ // This must be done BEFORE any memory allocations (including thread stack allocations).
+ try {
+ oceanbase::lib::ObMallocAllocator::get_instance();
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ try {
+ oceanbase::lib::ObResourceMgr::get_instance();
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ int ret = OB_SUCCESS;
+ ObServerOptions opts;
+ // Match Python embed's behavior:
+ // - Default: embed_mode = true, port = 2881
+ // - If port > 0: embed_mode = false (server mode), port = specified port
+ opts.port_ = 2881;
+ bool embed_mode = true; // Default to embed mode
+ if (port > 0) {
+ opts.port_ = port;
+ embed_mode = false; // Server mode when port is specified
+ }
+ opts.embedded_ = embed_mode;
+ opts.use_ipv6_ = false;
+
+
+ // Set default parameters
+ const char* params[][2] = {
+ {"memory_limit", "1G"},
+ {"log_disk_size", "2G"}
+ };
+ try {
+ for (int i = 0; OB_SUCC(ret) && i < 2; i++) {
+ ObString key = ObString::make_string(params[i][0]);
+ ObString value = ObString::make_string(params[i][1]);
+ if (OB_FAIL(opts.parameters_.push_back(std::make_pair(key, value)))) {
+ break;
+ }
+ }
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // Note: global_thread_stack_size is already set earlier (before memory allocator init)
+ // observer.init() will update it based on config (default is 512K), but we've set a safe default (2MB)
+ // If stack_size parameter is not set, observer.init() will use default 512K, but our library_init
+ // has already set a safe default, so threads can be created before observer.init()
+
+
+ char buffer[PATH_MAX];
+ ObSqlString work_abs_dir;
+ ObSqlString slog_dir;
+ ObSqlString sstable_dir;
+ int64_t start_time = ObTimeUtility::current_time();
+
+ ObWarningBuffer::set_warn_log_on(true);
+
+ if (OB_FAIL(ret)) {
+ } else if (SEEKDB_GETCWD(buffer, sizeof(buffer)) == nullptr) {
+ ret = OB_ERR_UNEXPECTED;
+ set_error(nullptr, "getcwd failed");
+ } else {
+ }
+
+ if (OB_SUCC(ret)) {
+ }
+ if (OB_FAIL(work_abs_dir.assign(buffer))) {
+ ret = OB_ERR_UNEXPECTED;
+ } else {
+ }
+
+ if (OB_SUCC(ret)) {
+ try {
+ if (OB_FAIL(opts.base_dir_.assign(db_dir))) {
+ set_error(nullptr, "assign base dir failed");
+ } else {
+ }
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+ }
+
+ // Continue with data_dir, redo_dir assignments
+ try {
+ if (OB_SUCC(ret) && OB_FAIL(opts.data_dir_.assign_fmt("%s/store", opts.base_dir_.ptr()))) {
+ set_error(nullptr, "assign data dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(opts.redo_dir_.assign_fmt("%s/store/redo", opts.data_dir_.ptr()))) {
+ set_error(nullptr, "assign redo dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(to_absolute_path(work_abs_dir.ptr(), opts.base_dir_))) {
+ set_error(nullptr, "get base dir absolute path failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(to_absolute_path(work_abs_dir.ptr(), opts.data_dir_))) {
+ set_error(nullptr, "get data dir absolute path failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(to_absolute_path(work_abs_dir.ptr(), opts.redo_dir_))) {
+ set_error(nullptr, "get redo dir absolute path failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(g_embedded_pid_file.assign("./run/seekdb.pid"))) {
+ // Note: pid file path is relative to base_dir after chdir
+ set_error(nullptr, "get pidfile path failed");
+ } else if (OB_SUCC(ret)) {
+ }
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+
+#ifndef _WIN32
+ struct statfs fs_info;
+#ifndef TMPFS_MAGIC
+ const long TMPFS_MAGIC = 0x01021994;
+#endif
+#endif
+ try {
+ if (OB_FAIL(ret)) {
+ } else if (OB_FAIL(FileDirectoryUtils::create_full_path(opts.base_dir_.ptr()))) {
+ set_error(nullptr, "create base dir failed");
+#ifndef _WIN32
+ } else if (statfs(opts.base_dir_.ptr(), &fs_info) != 0) {
+ ret = OB_ERR_UNEXPECTED;
+ set_error(nullptr, "stat base dir failed");
+ } else if (fs_info.f_type == TMPFS_MAGIC) {
+ ret = OB_NOT_SUPPORTED;
+ set_error(nullptr, "not support tmpfs directory");
+#endif
+ } else if (-1 == SEEKDB_CHDIR(opts.base_dir_.ptr())) {
+ ret = OB_ERR_UNEXPECTED;
+ set_error(nullptr, "change dir failed");
+ } else {
+ }
+
+ if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path(opts.data_dir_.ptr()))) {
+ set_error(nullptr, "create data dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path(opts.redo_dir_.ptr()))) {
+ set_error(nullptr, "create redo dir failed");
+ } else if (OB_SUCC(ret)) {
+ }
+
+ if (OB_SUCC(ret) && (OB_FAIL(slog_dir.assign_fmt("%s/slog", opts.data_dir_.ptr())) ||
+ OB_FAIL(sstable_dir.assign_fmt("%s/sstable", opts.data_dir_.ptr())))) {
+ set_error(nullptr, "calculate slog and sstable dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path(slog_dir.ptr()))) {
+ set_error(nullptr, "create slog dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path(sstable_dir.ptr()))) {
+ set_error(nullptr, "create sstable dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path("./run"))) {
+ set_error(nullptr, "create run dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path("./etc"))) {
+ set_error(nullptr, "create etc dir failed");
+ } else if (OB_SUCC(ret) && OB_FAIL(FileDirectoryUtils::create_full_path("./log"))) {
+ set_error(nullptr, "create log dir failed");
+ } else if (OB_SUCC(ret)) {
+ }
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ try {
+ if (OB_SUCC(ret) && OB_FAIL(start_daemon(g_embedded_pid_file.ptr(), true))) {
+ // Same-process reuse: if pid file is locked by us, db is already open (e.g. absolute path
+ // used by multiple clients in same process, or race between concurrent open() calls).
+ long pid_in_file = 0;
+ int read_ret = read_pid_from_file(g_embedded_pid_file.ptr(), pid_in_file);
+ if (read_ret == 0 && pid_in_file == SEEKDB_GETPID()) {
+ ret = OB_SUCCESS;
+ g_embedded_opened = true;
+ g_embedded_ever_opened = true;
+ strncpy(g_embedded_work_dir, work_abs_dir.ptr(), sizeof(g_embedded_work_dir) - 1);
+ g_embedded_work_dir[sizeof(g_embedded_work_dir) - 1] = '\0';
+ strncpy(g_embedded_base_dir, opts.base_dir_.ptr(), sizeof(g_embedded_base_dir) - 1);
+ g_embedded_base_dir[sizeof(g_embedded_base_dir) - 1] = '\0';
+ return SEEKDB_SUCCESS;
+ }
+ if (read_ret != 0) {
+ set_error(nullptr, "database already opened in this process (pid file locked)");
+ } else {
+ set_error(nullptr, "database opened by another process");
+ }
+ } else if (OB_SUCC(ret)) {
+ }
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ if (OB_SUCC(ret)) {
+
+ g_embedded_pid_locked = true;
+ strncpy(g_embedded_work_dir, work_abs_dir.ptr(), sizeof(g_embedded_work_dir) - 1);
+ g_embedded_work_dir[sizeof(g_embedded_work_dir) - 1] = '\0';
+ strncpy(g_embedded_base_dir, opts.base_dir_.ptr(), sizeof(g_embedded_base_dir) - 1);
+ g_embedded_base_dir[sizeof(g_embedded_base_dir) - 1] = '\0';
+
+ // Embedded mode defaults to WARN so tests / apps don't produce massive
+ // INFO log volume (which previously flooded the 2MB ring buffer and
+ // caused alloc_log_item -4013 drops). SEEKDB_LOG_LEVEL overrides.
+ const char *embed_log_level = getenv("SEEKDB_LOG_LEVEL");
+ if (nullptr != embed_log_level && '\0' != embed_log_level[0]) {
+ OB_LOGGER.set_log_level(embed_log_level);
+ } else {
+ OB_LOGGER.set_log_level("WARN");
+ }
+ // set_file_name for log file (same as Python embed ob_embed_impl.cpp do_open_)
+ ObSqlString log_file;
+ try {
+ if (OB_FAIL(log_file.assign_fmt("%s/log/seekdb.log", opts.base_dir_.ptr()))) {
+ set_error(nullptr, "calculate log file failed");
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+ OB_LOGGER.set_file_name(log_file.ptr(), true, false);
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // Create worker to make this thread having a binding worker (aligned with main.cpp)
+ oceanbase::lib::Worker worker;
+ oceanbase::lib::Worker::set_worker_to_thread_local(&worker);
+
+ ObPLogWriterCfg log_cfg;
+
+ if (OB_FAIL(ret)) {
+ } else {
+ try {
+ ret = OBSERVER.init(opts, log_cfg);
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+ }
+
+ int ret_check = ret;
+
+ if (ret_check != 0) {
+
+ // If OB_INIT_TWICE, it means some static initialization was already done
+ // This can happen if sql::init_sql_expr_static_var() was called before
+ // However, if observer.init() returns OB_INIT_TWICE early, startup_accel_handler_.init()
+ // may not have been called, causing startup_accel_handler_.start() to fail.
+ // We need to ensure that all initialization steps are completed even if OB_INIT_TWICE
+ // is returned. However, since startup_accel_handler_ is a private member, we cannot
+ // directly call its init() method. Instead, we need to ensure that observer.init()
+ // completes all initialization steps even when OB_INIT_TWICE is returned.
+ //
+ // Actually, from the code, observer.init() uses FAILEDx() macro which continues
+ // execution even if a function returns OB_INIT_TWICE. So if sql::init_sql_expr_static_var()
+ // returns OB_INIT_TWICE, observer.init() will continue and call startup_accel_handler_.init().
+ // The issue is that observer.init() may return OB_INIT_TWICE at the end, but all
+ // initialization steps should have been completed.
+ //
+ // Let's check if startup_accel_handler_ is initialized by checking if observer.start()
+ // can proceed. If startup_accel_handler_.start() fails with OB_NOT_INIT, we know
+ // that startup_accel_handler_.init() was not called.
+ if (OB_INIT_TWICE == ret) {
+ LOG_WARN("observer init returned OB_INIT_TWICE, continuing anyway", K(ret));
+ ret = OB_SUCCESS; // Ignore OB_INIT_TWICE and continue
+ // Note: observer.start() will reset stop_ flags (prepare_stop_, stop_, has_stopped_)
+ // at line 1099-1101 in ob_server.cpp, but only after all startup steps succeed.
+ // However, observer.start() checks stop_ status at line 1144 and 1184.
+ // If stop_ is true, it may cause some steps to fail or exit early.
+ // Since we removed the observer.destroy() call at the beginning, stop_ should
+ // be in its initial state (true from constructor, but observer.start() should
+ // handle this). However, if observer was previously destroyed, stop_ might be true.
+ // We rely on observer.start() to reset stop_ after successful startup.
+ // Continue to observer.start() below
+ } else {
+ LOG_WARN("observer init failed", K(ret));
+ const char* err_msg = ob_strerror(ret);
+ set_error(nullptr, "observer init failed");
+ // Clean up partially initialized observer
+ OBSERVER.destroy();
+ }
+ }
+
+ // Continue with observer.start() if init succeeded (or OB_INIT_TWICE was ignored)
+ if (OB_SUCC(ret) && OB_FAIL(OBSERVER.start())) {
+ // stdout already restored above
+ LOG_WARN("observer start failed", K(ret));
+ const char* err_msg = ob_strerror(ret);
+ set_error(nullptr, "observer start failed");
+ // Clean up partially initialized observer
+ OBSERVER.destroy();
+ } else if (-1 == SEEKDB_CHDIR(g_embedded_work_dir)) {
+ ret = OB_ERR_UNEXPECTED;
+ set_error(nullptr, "change dir failed");
+ } else {
+ FLOG_INFO("observer start finish wait service ", "cost", ObTimeUtility::current_time() - start_time);
+ // Wait until observer reports serving (replaces legacy ObRootService readiness checks).
+ while (GCTX.start_service_time_ <= 0) {
+ ob_usleep(100 * 1000); // 100ms
+ }
+ FLOG_INFO("seekdb start success ", "cost", ObTimeUtility::current_time() - start_time);
+#ifdef OB_BUILD_EMBED_MODE
+ // Ensure Change Stream threads leave bootstrap wait (embed may stop at IN_SERVICE).
+ if (GCTX.start_service_time_ <= 0) {
+ GCTX.start_service_time_ = ObTimeUtility::current_time();
+ }
+ GCTX.in_bootstrap_ = false;
+#endif
+ }
+ // stdout already restored above
+ }
+
+ if (OB_SUCCESS == ret) {
+ g_embedded_opened = true;
+ g_embedded_ever_opened = true;
+ return SEEKDB_SUCCESS;
+ } else {
+ if (g_embedded_pid_locked) {
+ SEEKDB_UNLINK(g_embedded_pid_file.ptr());
+ g_embedded_pid_locked = false;
+ }
+ return SEEKDB_ERROR_CONNECTION_FAILED;
+ }
+}
+
+int seekdb_open(const char* db_dir) {
+ if (!db_dir) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ std::lock_guard lock(g_init_mutex);
+
+ if (g_embedded_opened) {
+ // Absolute path: reuse open if same path.
+ if (g_embedded_base_dir[0] != '\0') {
+ char cwd_buf[PATH_MAX];
+ if (SEEKDB_GETCWD(cwd_buf, sizeof(cwd_buf)) != nullptr) {
+ ObSqlString req_abs;
+ if (req_abs.assign(db_dir) == OB_SUCCESS && to_absolute_path(cwd_buf, req_abs) == OB_SUCCESS &&
+ same_embedded_path(req_abs.ptr(), g_embedded_base_dir)) {
+ return SEEKDB_SUCCESS;
+ }
+ }
+ }
+ return SEEKDB_SUCCESS;
+ }
+
+ // Use CALL_WITH_NEW_STACK to execute on a dedicated stack (aligned with Python embed)
+ // This avoids issues with pthread_getattr_np returning invalid values in FFI environments
+ // The dedicated stack has known size and address, so OceanBase's stack overflow checks work correctly
+ const size_t stack_size = 1LL << 20; // 1MB (same as Python embed)
+ void* stack_addr = seekdb_mmap_anonymous_stack(stack_size);
+#ifdef _WIN32
+ if (stack_addr == nullptr) {
+#else
+ if (MAP_FAILED == stack_addr) {
+#endif
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // CRITICAL: Set a valid stack attribute before calling CALL_WITH_NEW_STACK
+ // call_with_new_stack() internally calls get_stackattr() to save the original stack attrs,
+ // but in FFI environments (Node.js/V8), pthread_getattr_np may return invalid values.
+ // By pre-setting a valid stack attribute, get_stackattr() will use our cached values.
+ // We use the mmap'd stack as the "original" stack - this is safe because:
+ // 1. All OceanBase code will run on the new stack anyway
+ // 2. After CALL_WITH_NEW_STACK returns, the stack attrs will be restored to this value
+ oceanbase::common::set_stackattr(stack_addr, stack_size);
+
+ int result;
+ { SuppressLogStdoutScope _; result = CALL_WITH_NEW_STACK(do_seekdb_open_inner(db_dir, 0), stack_addr, stack_size); }
+
+ // CRITICAL: After CALL_WITH_NEW_STACK returns, we're back on the original (Node.js) stack.
+ // Instead of clearing the stack attribute cache (which would cause pthread_getattr_np
+ // to return invalid values), we set a reasonable default stack attribute for subsequent
+ // operations. This allows connect/execute/execute_update to run on the main stack without
+ // needing CALL_WITH_NEW_STACK, aligning with Python embed behavior.
+ //
+ // We calculate a reasonable stack address from the current stack pointer and use a
+ // default stack size (8MB, Linux default).
+ const size_t default_stack_size = 8ULL << 20; // 8MB
+ char dummy;
+ uintptr_t cur_sp = (uintptr_t)&dummy;
+ // Align stack address down to page boundary, assume we're near top of stack
+ void* default_stack_addr = (void*)((cur_sp - default_stack_size + (1ULL << 20)) & ~((uintptr_t)0xFFF));
+ oceanbase::common::set_stackattr(default_stack_addr, default_stack_size);
+
+ seekdb_munmap_anonymous_stack(stack_addr, stack_size);
+
+ return result;
+}
+
+int seekdb_open_with_service(const char* db_dir, int port) {
+ if (!db_dir) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ std::lock_guard lock(g_init_mutex);
+
+ if (g_embedded_opened) {
+ if (g_embedded_base_dir[0] != '\0') {
+ char cwd_buf[PATH_MAX];
+ if (SEEKDB_GETCWD(cwd_buf, sizeof(cwd_buf)) != nullptr) {
+ ObSqlString req_abs;
+ if (req_abs.assign(db_dir) == OB_SUCCESS && to_absolute_path(cwd_buf, req_abs) == OB_SUCCESS &&
+ same_embedded_path(req_abs.ptr(), g_embedded_base_dir)) {
+ return SEEKDB_SUCCESS;
+ }
+ }
+ }
+ return SEEKDB_SUCCESS;
+ }
+
+ // Use CALL_WITH_NEW_STACK to execute on a dedicated stack (aligned with Python embed)
+ // This avoids issues with pthread_getattr_np returning invalid values in FFI environments
+ // The dedicated stack has known size and address, so OceanBase's stack overflow checks work correctly
+ const size_t stack_size = 1LL << 20; // 1MB (same as Python embed)
+ void* stack_addr = seekdb_mmap_anonymous_stack(stack_size);
+#ifdef _WIN32
+ if (stack_addr == nullptr) {
+#else
+ if (MAP_FAILED == stack_addr) {
+#endif
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // CRITICAL: Set a valid stack attribute before calling CALL_WITH_NEW_STACK
+ // call_with_new_stack() internally calls get_stackattr() to save the original stack attrs,
+ // but in FFI environments (Node.js/V8), pthread_getattr_np may return invalid values.
+ // By pre-setting a valid stack attribute, get_stackattr() will use our cached values.
+ // We use the mmap'd stack as the "original" stack - this is safe because:
+ // 1. All OceanBase code will run on the new stack anyway
+ // 2. After CALL_WITH_NEW_STACK returns, the stack attrs will be restored to this value
+ oceanbase::common::set_stackattr(stack_addr, stack_size);
+
+ int result;
+ { SuppressLogStdoutScope _; result = CALL_WITH_NEW_STACK(do_seekdb_open_inner(db_dir, port), stack_addr, stack_size); }
+
+ // CRITICAL: After CALL_WITH_NEW_STACK returns, we're back on the original (Node.js) stack.
+ // Instead of clearing the stack attribute cache (which would cause pthread_getattr_np
+ // to return invalid values), we set a reasonable default stack attribute for subsequent
+ // operations. This allows connect/execute/execute_update to run on the main stack without
+ // needing CALL_WITH_NEW_STACK, aligning with Python embed behavior.
+ //
+ // We calculate a reasonable stack address from the current stack pointer and use a
+ // default stack size (8MB, Linux default).
+ const size_t default_stack_size = 8ULL << 20; // 8MB
+ char dummy;
+ uintptr_t cur_sp = (uintptr_t)&dummy;
+ // Align stack address down to page boundary, assume we're near top of stack
+ void* default_stack_addr = (void*)((cur_sp - default_stack_size + (1ULL << 20)) & ~((uintptr_t)0xFFF));
+ oceanbase::common::set_stackattr(default_stack_addr, default_stack_size);
+
+ seekdb_munmap_anonymous_stack(stack_addr, stack_size);
+
+ return result;
+}
+
+// Gracefully stop embedded observer background threads without calling destroy().
+// destroy() tears down static singletons and can double-free at process exit.
+static void embed_observer_shutdown()
+{
+ OBSERVER.embed_shutdown();
+}
+
+void seekdb_close(void) {
+ std::lock_guard lock(g_init_mutex);
+ if (g_embedded_opened) {
+ // Set closing flag to indicate we're in cleanup process
+ // This allows the signal handler to recognize cleanup-related segfaults
+ g_closing = true;
+
+#ifndef _WIN32
+ // Re-install our SIGSEGV, SIGABRT and SIGBUS handlers so they are active during atexit/static destructors.
+ // Other runtimes (e.g. Rust, Node) may overwrite the handler; after seekdb_close()
+ // the process often exits and C++ destructors can trigger segfaults, ob_abort() or bus errors in worker threads.
+ struct sigaction sa;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_SIGINFO;
+ if (g_segv_handler_installed) {
+ sa.sa_sigaction = segv_handler_during_close;
+ (void)sigaction(SIGSEGV, &sa, &g_old_segv_handler);
+ }
+ if (g_sigabrt_handler_installed) {
+ sa.sa_sigaction = sigabrt_handler_during_close;
+ (void)sigaction(SIGABRT, &sa, &g_old_sigabrt_handler);
+ }
+ if (g_sigbus_handler_installed) {
+ sa.sa_sigaction = sigbus_handler_during_close;
+ (void)sigaction(SIGBUS, &sa, &g_old_sigbus_handler);
+ }
+#endif
+
+ embed_observer_shutdown();
+
+ // Skip observer.destroy(): static singleton ordering can segfault at exit.
+ // PID file cleanup below; exit-probe scripts remain as a last-resort fallback.
+
+ // Only clean up the PID file
+ if (g_embedded_pid_locked) {
+ SEEKDB_UNLINK(g_embedded_pid_file.ptr());
+ g_embedded_pid_locked = false;
+ }
+ g_embedded_opened = false;
+ g_embedded_base_dir[0] = '\0';
+
+ // Note: We keep g_closing = true to allow signal handler to catch
+ // segfaults during static destructors at program exit
+ // The signal handler will exit gracefully if segfault occurs
+ }
+}
+
+// =============================================================================
+// DDL refresh visibility: so listCollections / SHOW TABLES see latest tables after DDL
+// =============================================================================
+// - refresh_session_schema_version: refresh_and_add_schema then set session last_schema_version
+// - check_and_refresh_schema_for_embed: align with MySQL protocol (ObMPBase::check_and_refresh_schema):
+// only refresh when tenant local_version < session last_version; else skip to avoid heavy refresh on every read/write.
+// - is_write_sql: DDL/DML (create/drop/alter/insert/update/delete/truncate) -> refresh after execute_read
+
+static void refresh_session_schema_version(oceanbase::sql::ObSQLSessionInfo* session) {
+ if (OB_ISNULL(session) || OB_ISNULL(GCTX.schema_service_)) return;
+ (void)GCTX.schema_service_->refresh_and_add_schema(false);
+ int64_t schema_version = OB_INVALID_VERSION;
+ if (OB_SUCCESS != GCTX.schema_service_->get_runtime_refreshed_schema_version(schema_version)
+ || OB_INVALID_VERSION == schema_version) {
+ (void)oceanbase::sql::ObSQLUtils::update_session_last_schema_version(*GCTX.schema_service_, *session);
+ return;
+ }
+ session->set_last_ddl_schema_version(schema_version);
+}
+
+// Align with MySQL protocol: ObMPBase::check_and_refresh_schema. Only refresh when server (local) is behind session (last).
+static void check_and_refresh_schema_for_embed(oceanbase::sql::ObSQLSessionInfo* session) {
+ if (OB_ISNULL(session) || OB_ISNULL(GCTX.schema_service_)) return;
+ int64_t local_version = OB_INVALID_VERSION;
+ int64_t last_version = OB_INVALID_VERSION;
+ if (OB_SUCCESS != GCTX.schema_service_->get_runtime_refreshed_schema_version(local_version)) {
+ refresh_session_schema_version(session);
+ return;
+ }
+ last_version = session->get_last_ddl_schema_version();
+ if (OB_INVALID_VERSION == last_version) {
+ refresh_session_schema_version(session);
+ return;
+ }
+ if (local_version >= last_version) {
+ return; // skip: server already has at least what session has
+ }
+ refresh_session_schema_version(session);
+}
+
+static bool is_write_sql(const char* sql) {
+ if (!sql) return false;
+ std::string s(sql);
+ size_t start = s.find_first_not_of(" \t\n\r;");
+ if (start == std::string::npos) return false;
+ std::string lower = s.substr(start);
+ std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
+ return lower.find("create ") == 0 || lower.find("drop ") == 0 || lower.find("alter ") == 0
+ || lower.find("insert ") == 0 || lower.find("update ") == 0 || lower.find("delete ") == 0
+ || lower.find("truncate ") == 0;
+}
+
+// Internal implementation of seekdb_connect, called on a dedicated stack
+struct ConnectParams {
+ SeekdbHandle* handle;
+ const char* database;
+ bool autocommit;
+ int result;
+};
+
+static int do_seekdb_connect_inner(ConnectParams* params) {
+ SeekdbHandle* handle = params->handle;
+ const char* database = params->database;
+ bool autocommit = params->autocommit;
+
+ if (!GCTX.is_inited() || !GCTX.sql_proxy_ || !GCTX.session_mgr_ || !GCTX.schema_service_) {
+ params->result = SEEKDB_ERROR_NOT_INITIALIZED;
+ return OB_SUCCESS;
+ }
+
+ SeekdbConnection* conn = new (std::nothrow) SeekdbConnection();
+ if (!conn) {
+ params->result = SEEKDB_ERROR_MEMORY_ALLOC;
+ return OB_SUCCESS;
+ }
+
+ int ret = OB_SUCCESS;
+ uint32_t sid = ObSQLSessionInfo::INVALID_SESSID;
+ ObSQLSessionInfo* session = nullptr;
+ const schema::ObUserInfo* user_info = nullptr;
+ schema::ObSchemaGetterGuard schema_guard;
+ ObPrivSet db_priv_set = OB_PRIV_SET_EMPTY;
+ const schema::ObDatabaseSchema* database_schema = nullptr;
+
+ if (OB_FAIL(GCTX.session_mgr_->create_sessid(sid))) {
+ set_error(conn, "Failed to create sess id");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (OB_FAIL(GCTX.session_mgr_->create_session(sid, session))) {
+ session = nullptr;
+ set_error(conn, "Failed to create session");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (FALSE_IT(ob_setup_tsi_warning_buffer(&session->get_warnings_buffer()))) {
+ } else if (FALSE_IT(conn->embed_session = session)) {
+ } else if (OB_FAIL(GCTX.schema_service_->get_runtime_schema_guard(schema_guard))) {
+ set_error(conn, "failed to get schema guard");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (OB_FAIL(schema_guard.get_user_info(OB_SYS_USER_ID, user_info))) {
+ set_error(conn, "failed to get user info");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (OB_ISNULL(user_info)) {
+ set_error(conn, "schema user info is null");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (OB_NOT_NULL(database) && STRLEN(database) > 0) {
+ if (OB_FAIL(schema_guard.get_database_schema(ObString(database), database_schema))) {
+ set_error(conn, "failed to get database");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (OB_ISNULL(database_schema)) {
+ set_error(conn, "database is null");
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ }
+ }
+
+ if (OB_SUCC(ret)) {
+ if (OB_FAIL(session->load_default_sys_variable(false, true))) {
+ set_error(conn, "load_default_sys_variable failed");
+ } else if (OB_FAIL(session->load_default_configs_in_pc())) {
+ set_error(conn, "load_default_configs_in_pc failed");
+ } else if (OB_FAIL(session->load_all_sys_vars(schema_guard))) {
+ set_error(conn, "load_all_sys_vars failed");
+ } else {
+ if (OB_NOT_NULL(database) && STRLEN(database) > 0) {
+ if (OB_FAIL(session->set_default_database(database))) {
+ set_error(conn, "set_default_database failed");
+ }
+ }
+ if (OB_SUCC(ret)) {
+ session->set_user_session();
+ if (OB_FAIL(session->set_autocommit(autocommit))) {
+ set_error(conn, "set_autocommit failed");
+ } else if (OB_FAIL(session->set_user(user_info->get_user_name_str(),
+ user_info->get_host_name_str(),
+ user_info->get_user_id()))) {
+ set_error(conn, "set_user failed");
+ } else if (OB_FAIL(session->set_real_client_ip_and_port("127.0.0.1", 0))) {
+ set_error(conn, "set_real_client_ip_and_port failed");
+ } else {
+ session->set_priv_user_id(user_info->get_user_id());
+ session->set_user_priv_set(user_info->get_priv_set());
+ ObObj param_val;
+ param_val.set_int(60 * 1000 * 1000);
+ if (OB_FAIL(session->update_sys_variable(oceanbase::SYS_VAR_OB_QUERY_TIMEOUT, param_val))) {
+ // Non-critical, continue
+ }
+ if (OB_NOT_NULL(database) && STRLEN(database) > 0) {
+ if (OB_FAIL(schema_guard.get_db_priv_set(user_info->get_user_id(),
+ database, db_priv_set))) {
+ // Non-critical, continue
+ } else {
+ session->set_db_priv_set(db_priv_set);
+ }
+ }
+ // Set enable role array (aligned with Python embed)
+ session->get_enable_role_array().reuse();
+ for (int i = 0; OB_SUCC(ret) && i < user_info->get_role_id_array().count(); ++i) {
+ if (user_info->get_disable_option(user_info->get_role_id_option_array().at(i)) == 0) {
+ if (OB_FAIL(session->get_enable_role_array().push_back(user_info->get_role_id_array().at(i)))) {
+ // Non-critical, continue
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Use OBSERVER macro directly like Python embed does
+ if (OB_SUCC(ret)) {
+ if (OB_FAIL(ObInnerSQLConnection::create_connection_with_external_session(
+ session, conn->embed_conn_guard))) {
+ set_error(conn, "acquire conn failed");
+ if (session) {
+ GCTX.session_mgr_->revert_session(session);
+ }
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else if (!conn->embed_conn_guard.is_valid()) {
+ set_error(conn, "inner sql conn not ready");
+ if (session) {
+ GCTX.session_mgr_->revert_session(session);
+ }
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else {
+ conn->embed_conn = static_cast(
+ conn->embed_conn_guard.get_ptr());
+ conn->initialized = true;
+ *handle = static_cast(conn);
+ // Align with MySQL protocol: no schema refresh on connect; first query will do check_and_refresh_schema_for_embed.
+ // Reset warning buffer after connect (aligned with Python embed)
+ ob_setup_tsi_warning_buffer(NULL);
+ params->result = SEEKDB_SUCCESS;
+ return OB_SUCCESS;
+ }
+ } else {
+ if (session) {
+ GCTX.session_mgr_->revert_session(session);
+ }
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ }
+}
+
+int seekdb_connect(SeekdbHandle* handle, const char* database, bool autocommit) {
+ if (!handle || !database) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!g_embedded_opened) {
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ // Execute directly on main stack (aligned with Python embed)
+ // Stack attributes were set to reasonable defaults in seekdb_open()
+ ConnectParams params;
+ params.handle = handle;
+ params.database = database;
+ params.autocommit = autocommit;
+ params.result = SEEKDB_ERROR_CONNECTION_FAILED;
+
+ do_seekdb_connect_inner(¶ms);
+
+ // Explicit USE database so the physical connection is in the requested database (multi-connection
+ // same-path: each connection must run USE to avoid wrong-database reads).
+ if (params.result == SEEKDB_SUCCESS && database && strlen(database) > 0) {
+ (void)seekdb_select_db(*handle, database);
+ }
+ return params.result;
+}
+
+void seekdb_connect_close(SeekdbHandle handle) {
+ if (handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ delete conn;
+ }
+}
+
+// Internal implementation of seekdb_query/seekdb_real_query
+struct ExecuteParams {
+ SeekdbHandle handle;
+ const char* sql;
+ SeekdbResult* result;
+ int ret_code;
+};
+
+// Helper function to infer column names from SQL statement
+// Returns true if inference was successful, false otherwise
+static bool infer_column_names_from_sql(
+ const char* sql,
+ int column_count,
+ std::vector& column_names
+) {
+ if (!sql || column_count <= 0) {
+ return false;
+ }
+
+ std::string sql_str(sql);
+ // Convert to lowercase for case-insensitive matching
+ std::string sql_lower = sql_str;
+ std::transform(sql_lower.begin(), sql_lower.end(), sql_lower.begin(), ::tolower);
+
+ // Trim leading whitespace
+ size_t start = sql_lower.find_first_not_of(" \t\n\r");
+ if (start == std::string::npos) {
+ return false;
+ }
+ sql_lower = sql_lower.substr(start);
+
+ // Handle SHOW TABLES
+ if (sql_lower.find("show tables") == 0) {
+ column_names.push_back("Tables_in_" + std::string("database")); // Will be replaced if we know the database
+ return true;
+ }
+
+ // Handle SHOW CREATE TABLE
+ if (sql_lower.find("show create table") == 0) {
+ column_names.push_back("Table");
+ column_names.push_back("Create Table");
+ return column_count == 2;
+ }
+
+ // Handle DESCRIBE / DESC
+ if (sql_lower.find("describe ") == 0 || sql_lower.find("desc ") == 0) {
+ // DESCRIBE returns: Field, Type, Null, Key, Default, Extra
+ column_names.push_back("Field");
+ column_names.push_back("Type");
+ column_names.push_back("Null");
+ column_names.push_back("Key");
+ column_names.push_back("Default");
+ column_names.push_back("Extra");
+ return column_count == 6;
+ }
+
+ // Handle SELECT statements
+ if (sql_lower.find("select") == 0) {
+ // Find SELECT ... FROM pattern
+ size_t select_pos = sql_lower.find("select");
+ size_t from_pos = sql_lower.find(" from ");
+
+ if (from_pos == std::string::npos) {
+ // No FROM clause, might be SELECT without FROM (MySQL allows this)
+ from_pos = sql_lower.length();
+ }
+
+ if (select_pos != std::string::npos && from_pos > select_pos) {
+ // Extract SELECT clause
+ size_t select_start = select_pos + 6; // "select" length
+ std::string select_clause = sql_str.substr(select_start, from_pos - select_start);
+
+ // Trim whitespace
+ size_t clause_start = select_clause.find_first_not_of(" \t\n\r");
+ if (clause_start != std::string::npos) {
+ select_clause = select_clause.substr(clause_start);
+ }
+ size_t clause_end = select_clause.find_last_not_of(" \t\n\r");
+ if (clause_end != std::string::npos) {
+ select_clause = select_clause.substr(0, clause_end + 1);
+ }
+
+ // Handle SELECT *
+ if (select_clause == "*" || select_clause == " *") {
+ return false; // Cannot infer column names from SELECT *
+ }
+
+ // Split by comma, handling nested parentheses and quotes
+ std::vector parts;
+ int depth = 0;
+ bool in_single_quote = false;
+ bool in_double_quote = false;
+ bool in_backtick = false;
+ std::string current_part;
+
+ for (size_t i = 0; i < select_clause.length(); i++) {
+ char c = select_clause[i];
+
+ if (c == '\'' && !in_double_quote && !in_backtick) {
+ in_single_quote = !in_single_quote;
+ current_part += c;
+ } else if (c == '"' && !in_single_quote && !in_backtick) {
+ in_double_quote = !in_double_quote;
+ current_part += c;
+ } else if (c == '`' && !in_single_quote && !in_double_quote) {
+ in_backtick = !in_backtick;
+ current_part += c;
+ } else if (c == '(' && !in_single_quote && !in_double_quote && !in_backtick) {
+ depth++;
+ current_part += c;
+ } else if (c == ')' && !in_single_quote && !in_double_quote && !in_backtick) {
+ depth--;
+ current_part += c;
+ } else if (c == ',' && depth == 0 && !in_single_quote && !in_double_quote && !in_backtick) {
+ // Split point
+ if (!current_part.empty()) {
+ parts.push_back(current_part);
+ current_part.clear();
+ }
+ } else {
+ current_part += c;
+ }
+ }
+
+ if (!current_part.empty()) {
+ parts.push_back(current_part);
+ }
+
+ // Extract column names from parts
+ for (const std::string& part : parts) {
+ std::string trimmed = part;
+ // Trim whitespace
+ size_t trim_start = trimmed.find_first_not_of(" \t\n\r");
+ if (trim_start != std::string::npos) {
+ trimmed = trimmed.substr(trim_start);
+ }
+ size_t trim_end = trimmed.find_last_not_of(" \t\n\r");
+ if (trim_end != std::string::npos) {
+ trimmed = trimmed.substr(0, trim_end + 1);
+ }
+
+ if (trimmed.empty()) {
+ continue;
+ }
+
+ // Look for AS alias
+ std::string col_name;
+ size_t as_pos = std::string::npos;
+
+ // Case-insensitive search for AS
+ std::string trimmed_lower = trimmed;
+ std::transform(trimmed_lower.begin(), trimmed_lower.end(), trimmed_lower.begin(), ::tolower);
+
+ // Try to find " AS " or " as "
+ size_t as_pos1 = trimmed_lower.find(" as ");
+ if (as_pos1 != std::string::npos) {
+ as_pos = as_pos1;
+ }
+
+ if (as_pos != std::string::npos) {
+ // Has AS alias
+ std::string alias = trimmed.substr(as_pos + 4);
+ // Trim alias
+ size_t alias_start = alias.find_first_not_of(" \t\n\r");
+ if (alias_start != std::string::npos) {
+ alias = alias.substr(alias_start);
+ }
+ size_t alias_end = alias.find_last_not_of(" \t\n\r");
+ if (alias_end != std::string::npos) {
+ alias = alias.substr(0, alias_end + 1);
+ }
+
+ // Remove quotes if present
+ if ((alias.front() == '\'' && alias.back() == '\'') ||
+ (alias.front() == '"' && alias.back() == '"') ||
+ (alias.front() == '`' && alias.back() == '`')) {
+ alias = alias.substr(1, alias.length() - 2);
+ }
+
+ col_name = alias;
+ } else {
+ // No AS alias, try to extract column name
+ // Remove table prefix if present (e.g., "table.column")
+ size_t dot_pos = trimmed.find_last_of('.');
+ if (dot_pos != std::string::npos && dot_pos < trimmed.length() - 1) {
+ col_name = trimmed.substr(dot_pos + 1);
+ } else {
+ col_name = trimmed;
+ }
+
+ // Remove quotes if present
+ if ((col_name.front() == '\'' && col_name.back() == '\'') ||
+ (col_name.front() == '"' && col_name.back() == '"') ||
+ (col_name.front() == '`' && col_name.back() == '`')) {
+ col_name = col_name.substr(1, col_name.length() - 2);
+ }
+
+ // Trim whitespace
+ size_t name_start = col_name.find_first_not_of(" \t\n\r");
+ if (name_start != std::string::npos) {
+ col_name = col_name.substr(name_start);
+ }
+ size_t name_end = col_name.find_last_not_of(" \t\n\r");
+ if (name_end != std::string::npos) {
+ col_name = col_name.substr(0, name_end + 1);
+ }
+ }
+
+ if (!col_name.empty()) {
+ column_names.push_back(col_name);
+ } else {
+ // Fallback: use a generic name
+ char gen_name[64];
+ snprintf(gen_name, sizeof(gen_name), "col_%zu", column_names.size());
+ column_names.push_back(std::string(gen_name));
+ }
+ }
+
+ return column_names.size() == static_cast(column_count);
+ }
+ }
+
+ return false;
+}
+
+static int do_seekdb_execute_inner(ExecuteParams* params) {
+ SeekdbHandle handle = params->handle;
+ const char* sql = params->sql;
+ SeekdbResult* result = params->result;
+
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ params->ret_code = SEEKDB_ERROR_INVALID_PARAM;
+ return OB_SUCCESS;
+ }
+
+ SeekdbResultSet* result_set = new (std::nothrow) SeekdbResultSet();
+ if (!result_set) {
+ params->ret_code = SEEKDB_ERROR_MEMORY_ALLOC;
+ return OB_SUCCESS;
+ }
+
+ // Set owner connection for proper cleanup
+ result_set->owner_conn = conn;
+
+ int ret = OB_SUCCESS;
+ sqlclient::ObMySQLResult* sql_result = nullptr;
+
+ // Embedded mode only
+ if (!conn->embed_conn) {
+ delete result_set;
+ params->ret_code = SEEKDB_ERROR_INVALID_PARAM;
+ return OB_SUCCESS;
+ }
+
+ ObString sql_string(sql);
+ ObMemAttr mem_attr("FFIEmbedAlloc");
+
+ // Initialize trace ID (aligned with Python embed)
+ ObCurTraceId::init(GCTX.self_addr());
+
+ // Setup warning buffer (aligned with Python embed)
+ if (OB_NOT_NULL(conn->embed_session)) {
+ ob_setup_tsi_warning_buffer(&conn->embed_session->get_warnings_buffer());
+ }
+
+ // Reset previous result if exists
+ if (conn->embed_result) {
+ conn->embed_result->close();
+ conn->embed_result->~ReadResult();
+ ob_free(conn->embed_result);
+ conn->embed_result = nullptr;
+ }
+
+ // Allocate result
+ conn->embed_result = static_cast(
+ ob_malloc(sizeof(ObCommonSqlProxy::ReadResult), mem_attr));
+ if (!conn->embed_result) {
+ delete result_set;
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+ new (conn->embed_result) ObCommonSqlProxy::ReadResult();
+
+ // DDL / read-after-write visibility: align with MySQL protocol — check and refresh only when server is behind session.
+ if (OB_NOT_NULL(conn->embed_session)) {
+ check_and_refresh_schema_for_embed(conn->embed_session);
+ }
+
+ ret = conn->embed_conn->execute_read(sql_string, *conn->embed_result, true);
+
+ // Reset warning buffer after execute (aligned with Python embed)
+ if (OB_NOT_NULL(conn->embed_session)) {
+ conn->embed_session->reset_warnings_buf();
+ }
+ ob_setup_tsi_warning_buffer(NULL);
+
+ if (OB_SUCCESS != ret) {
+ delete result_set;
+ // Get detailed error message (aligned with Python embed)
+ std::string errmsg;
+ const oceanbase::common::ObWarningBuffer *wb = oceanbase::common::ob_get_tsi_warning_buffer();
+ if (nullptr != wb && wb->get_err_code() == ret) {
+ if (wb->get_err_msg() != nullptr && wb->get_err_msg()[0] != '\0') {
+ errmsg = std::string(wb->get_err_msg());
+ }
+ }
+ if (errmsg.empty()) {
+ errmsg = std::string(ob_errpkt_strerror(ret));
+ }
+ if (errmsg.empty()) {
+ errmsg = "Query execution failed";
+ }
+ // Append OB error code so logs always carry numeric ret even when the message is non-empty.
+ char code_suffix[32];
+ snprintf(code_suffix, sizeof(code_suffix), " (ret=%d)", ret);
+ errmsg += code_suffix;
+ set_error(conn, errmsg.c_str());
+ if (conn->embed_result) {
+ conn->embed_result->close();
+ conn->embed_result->~ReadResult();
+ ob_free(conn->embed_result);
+ conn->embed_result = nullptr;
+ }
+ params->ret_code = SEEKDB_ERROR_QUERY_FAILED;
+ return OB_SUCCESS;
+ }
+
+ // DDL / read-after-write visibility: after DDL/DML via execute_read, one refresh + sync session (align with MySQL DDL path).
+ if (is_write_sql(params->sql) && OB_NOT_NULL(conn->embed_session) && OB_NOT_NULL(GCTX.schema_service_)) {
+ refresh_session_schema_version(conn->embed_session);
+ (void)oceanbase::sql::ObSQLUtils::update_session_last_schema_version(*GCTX.schema_service_, *conn->embed_session);
+ }
+
+ sql_result = conn->embed_result->get_result();
+
+ if (!sql_result) {
+ delete result_set;
+ set_error(conn, "Result is null");
+ params->ret_code = SEEKDB_ERROR_QUERY_FAILED;
+ return OB_SUCCESS;
+ }
+
+ // Get column count
+ int64_t column_count = sql_result->get_column_count();
+
+ // Validate column_count to prevent vector::reserve errors
+ // Note:
+ // - column_count can be 0 for DML statements (INSERT/UPDATE/DELETE), which is normal
+ // - column_count can be -1 when result set is not opened or row_ is NULL (for DML statements), treat as 0
+ if (column_count < -1 || column_count > INT32_MAX) {
+ // Invalid column count (less than -1 or too large), return error
+ delete result_set;
+ set_error(conn, "Invalid column count");
+ params->ret_code = SEEKDB_ERROR_QUERY_FAILED;
+ return OB_SUCCESS;
+ }
+
+ // Treat -1 as 0 (for DML statements with no result set)
+ if (column_count == -1) {
+ column_count = 0;
+ }
+
+ result_set->column_count = static_cast(column_count);
+
+ // Get column names - try multiple approaches
+ // For DML statements, column_count may be 0, which is normal
+ result_set->column_names.clear();
+ if (column_count > 0) {
+ result_set->column_names.reserve(static_cast(column_count));
+ }
+
+ // Approach 1: Try to get column names from ObInnerSQLResult (aligned with MySQL mysql_fetch_fields)
+ oceanbase::observer::ObInnerSQLResult* inner_result =
+ static_cast(sql_result);
+ bool got_column_names = false;
+
+ // Store field information for seekdb_fetch_fields() (aligned with MySQL)
+ // For DML statements, fields may be empty, which is normal
+ result_set->fields.clear();
+ if (column_count > 0) {
+ result_set->fields.reserve(static_cast(column_count));
+ }
+
+ if (inner_result) {
+ // Try to get field columns from result set
+ const oceanbase::common::ColumnsFieldIArray* fields = nullptr;
+ if (OB_NOT_NULL(inner_result->get_result_set())) {
+ fields = inner_result->result_set().get_field_columns();
+ }
+
+ if (fields && fields->count() == column_count) {
+ // Build field information structures (aligned with MySQL MYSQL_FIELD)
+ for (int64_t i = 0; i < column_count; ++i) {
+ const oceanbase::common::ObField& ob_field = fields->at(i);
+
+ // Create SeekdbField structure (aligned with MYSQL_FIELD)
+ SeekdbField field;
+ memset(&field, 0, sizeof(SeekdbField));
+
+ // Store strings in result_set for lifetime management
+ std::string col_name;
+ std::string org_col_name;
+ std::string table_name;
+ std::string org_table_name;
+ std::string db_name;
+
+ // Extract column name
+ if (ob_field.cname_.ptr() && ob_field.cname_.length() > 0) {
+ col_name = std::string(ob_field.cname_.ptr(), ob_field.cname_.length());
+ field.name = col_name.c_str();
+ field.name_length = static_cast(col_name.length());
+ got_column_names = true;
+ } else {
+ got_column_names = false;
+ break;
+ }
+
+ // Extract original column name
+ if (ob_field.org_cname_.ptr() && ob_field.org_cname_.length() > 0) {
+ org_col_name = std::string(ob_field.org_cname_.ptr(), ob_field.org_cname_.length());
+ field.org_name = org_col_name.c_str();
+ field.org_name_length = static_cast(org_col_name.length());
+ } else {
+ field.org_name = field.name;
+ field.org_name_length = field.name_length;
+ }
+
+ // Extract table name
+ if (ob_field.tname_.ptr() && ob_field.tname_.length() > 0) {
+ table_name = std::string(ob_field.tname_.ptr(), ob_field.tname_.length());
+ field.table = table_name.c_str();
+ field.table_length = static_cast(table_name.length());
+ }
+
+ // Extract original table name
+ if (ob_field.org_tname_.ptr() && ob_field.org_tname_.length() > 0) {
+ org_table_name = std::string(ob_field.org_tname_.ptr(), ob_field.org_tname_.length());
+ field.org_table = org_table_name.c_str();
+ field.org_table_length = static_cast(org_table_name.length());
+ }
+
+ // Extract database name
+ if (ob_field.dname_.ptr() && ob_field.dname_.length() > 0) {
+ db_name = std::string(ob_field.dname_.ptr(), ob_field.dname_.length());
+ field.db = db_name.c_str();
+ field.db_length = static_cast(db_name.length());
+ }
+
+ // Set catalog (MySQL default is "def")
+ field.catalog = "def";
+ field.catalog_length = 3;
+
+ // Extract type information: map ObObjType to SeekdbFieldType (seekdb.h).
+ // OceanBase ObObjType (e.g. ObFloatType=11, ObDoubleType=12) does not match SEEKDB_TYPE (5, 6);
+ // bindings (e.g. js-bindings) rely on SEEKDB_TYPE_FLOAT/DOUBLE (5/6) to return number.
+ if (!ob_field.type_.is_null()) {
+ oceanbase::common::ObObjType obj_type = ob_field.type_.get_type();
+ if (oceanbase::common::ob_is_valid_obj_type(obj_type)) {
+ if (obj_type == oceanbase::common::ObCollectionSQLType) {
+ field.type = static_cast(SEEKDB_TYPE_VECTOR);
+ } else if (obj_type == oceanbase::common::ObFloatType || obj_type == oceanbase::common::ObUFloatType) {
+ field.type = static_cast(SEEKDB_TYPE_FLOAT);
+ } else if (obj_type == oceanbase::common::ObDoubleType || obj_type == oceanbase::common::ObUDoubleType) {
+ field.type = static_cast(SEEKDB_TYPE_DOUBLE);
+ } else {
+ // Map other ObObjType to SEEKDB_TYPE where they align or have a clear mapping
+ switch (obj_type) {
+ case oceanbase::common::ObNullType: field.type = static_cast(SEEKDB_TYPE_NULL); break;
+ case oceanbase::common::ObTinyIntType: field.type = static_cast(SEEKDB_TYPE_TINY); break;
+ case oceanbase::common::ObSmallIntType: field.type = static_cast(SEEKDB_TYPE_SHORT); break;
+ case oceanbase::common::ObMediumIntType:
+ case oceanbase::common::ObInt32Type: field.type = static_cast(SEEKDB_TYPE_LONG); break;
+ case oceanbase::common::ObIntType: field.type = static_cast(SEEKDB_TYPE_LONGLONG); break;
+ case oceanbase::common::ObUTinyIntType: field.type = static_cast(SEEKDB_TYPE_TINY); break;
+ case oceanbase::common::ObUSmallIntType: field.type = static_cast(SEEKDB_TYPE_SHORT); break;
+ case oceanbase::common::ObUMediumIntType:
+ case oceanbase::common::ObUInt32Type:
+ case oceanbase::common::ObUInt64Type: field.type = static_cast(SEEKDB_TYPE_LONGLONG); break;
+ case oceanbase::common::ObDateTimeType: field.type = static_cast(SEEKDB_TYPE_DATETIME); break;
+ case oceanbase::common::ObTimestampType: field.type = static_cast(SEEKDB_TYPE_TIMESTAMP); break;
+ case oceanbase::common::ObDateType: field.type = static_cast(SEEKDB_TYPE_DATE); break;
+ case oceanbase::common::ObTimeType: field.type = static_cast(SEEKDB_TYPE_TIME); break;
+ case oceanbase::common::ObYearType: field.type = static_cast(SEEKDB_TYPE_LONGLONG); break;
+ case oceanbase::common::ObVarcharType:
+ case oceanbase::common::ObCharType:
+ case oceanbase::common::ObHexStringType:
+ case oceanbase::common::ObNumberType:
+ case oceanbase::common::ObUNumberType:
+ case oceanbase::common::ObTinyTextType:
+ case oceanbase::common::ObTextType:
+ case oceanbase::common::ObMediumTextType:
+ case oceanbase::common::ObLongTextType:
+ case oceanbase::common::ObJsonType:
+ case oceanbase::common::ObDecimalIntType: field.type = static_cast(SEEKDB_TYPE_STRING); break;
+ case oceanbase::common::ObBitType:
+ case oceanbase::common::ObEnumType:
+ case oceanbase::common::ObSetType: field.type = static_cast(SEEKDB_TYPE_STRING); break;
+ case oceanbase::common::ObGeometryType:
+ case oceanbase::common::ObUserDefinedSQLType:
+ case oceanbase::common::ObMySQLDateType:
+ case oceanbase::common::ObMySQLDateTimeType:
+ case oceanbase::common::ObTimestampLTZType:
+ case oceanbase::common::ObTimestampNanoType:
+ default: field.type = static_cast(SEEKDB_TYPE_STRING); break;
+ }
+ }
+ }
+ }
+
+ // Extract flags
+ field.flags = ob_field.flags_;
+
+ // Extract length
+ field.length = static_cast(ob_field.length_);
+
+ // Extract charset
+ field.charsetnr = ob_field.charsetnr_;
+
+ // Extract decimals from accuracy
+ // ObAccuracy doesn't have is_valid(), but we can check if scale is valid (>= 0)
+ if (ob_field.accuracy_.get_scale() >= 0) {
+ field.decimals = static_cast(ob_field.accuracy_.get_scale());
+ }
+
+ // Store strings in result_set for lifetime management
+ result_set->field_strings.push_back({
+ col_name, org_col_name, table_name, org_table_name, db_name
+ });
+
+ // Update field pointers to point to stored strings
+ const auto& stored = result_set->field_strings.back();
+ field.name = stored.col_name.c_str();
+ field.org_name = stored.org_col_name.empty() ? field.name : stored.org_col_name.c_str();
+ field.table = stored.table_name.empty() ? nullptr : stored.table_name.c_str();
+ field.org_table = stored.org_table_name.empty() ? nullptr : stored.org_table_name.c_str();
+ field.db = stored.db_name.empty() ? nullptr : stored.db_name.c_str();
+
+ // Store field in result_set
+ result_set->fields.push_back(field);
+ result_set->column_names.push_back(col_name);
+ }
+ }
+ }
+
+ // Approach 2: If we didn't get column names, try SQL parsing inference
+ if (!got_column_names && column_count > 0) {
+ // Store SQL for inference (we'll use it if needed)
+ std::string sql_for_inference = sql;
+
+ // Try to infer column names from SQL
+ std::vector inferred_names;
+ if (infer_column_names_from_sql(sql_for_inference.c_str(), column_count, inferred_names)) {
+ if (inferred_names.size() == static_cast(column_count)) {
+ result_set->column_names = inferred_names;
+ got_column_names = true;
+ }
+ }
+ }
+
+ // Approach 3: Fallback to default names (col_0, col_1, etc.)
+ if (!got_column_names) {
+ for (int64_t i = 0; i < column_count; ++i) {
+ char col_name_buf[64];
+ snprintf(col_name_buf, sizeof(col_name_buf), "col_%ld", i);
+ result_set->column_names.push_back(std::string(col_name_buf));
+ }
+ }
+
+ // Fetch all rows (embed mode: modules are ready after observer start)
+ int64_t row_count = 0;
+ {
+ while (OB_SUCCESS == sql_result->next()) {
+ std::vector row;
+ std::vector row_null;
+ oceanbase::common::ObArenaAllocator row_lob_allocator(ObModIds::OB_MODULE_PAGE_ALLOCATOR);
+ for (int64_t i = 0; i < column_count; ++i) {
+ ObObj obj;
+ if (OB_SUCCESS == sql_result->get_obj(i, obj)) {
+ oceanbase::common::ObObjType col_type = oceanbase::common::ObNullType;
+ if (static_cast(i) < result_set->fields.size()) {
+ col_type = static_cast(result_set->fields[i].type);
+ }
+ if (obj.is_null()) {
+ row.push_back("");
+ row_null.push_back(true);
+ } else {
+ // Larger buffer for JSON/metadata (print_sql_literal may produce long escaped string)
+ char buf[32768];
+ int64_t pos = 0;
+ oceanbase::common::ObObjType obj_type = obj.get_type();
+
+ // Get raw value based on type (without SQL literal quotes or JSON format)
+ if (ob_is_integer_type(obj_type) || ob_is_enumset_tc(obj_type)) {
+ // Integer types: get value based on specific type using type-checked getters
+ int64_t int_val = 0;
+ int ret = OB_OBJ_TYPE_ERROR;
+
+ // Try signed integer types first
+ if (obj_type == ObTinyIntType) {
+ int8_t val = 0;
+ ret = obj.get_tinyint(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObSmallIntType) {
+ int16_t val = 0;
+ ret = obj.get_smallint(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObMediumIntType) {
+ int32_t val = 0;
+ ret = obj.get_mediumint(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObInt32Type) {
+ int32_t val = 0;
+ ret = obj.get_int32(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObIntType) {
+ ret = obj.get_int(int_val);
+ }
+ // Try unsigned integer types
+ else if (obj_type == ObUTinyIntType) {
+ uint8_t val = 0;
+ ret = obj.get_utinyint(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObUSmallIntType) {
+ uint16_t val = 0;
+ ret = obj.get_usmallint(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObUMediumIntType) {
+ uint32_t val = 0;
+ ret = obj.get_umediumint(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObUInt32Type) {
+ uint32_t val = 0;
+ ret = obj.get_uint32(val);
+ int_val = static_cast(val);
+ } else if (obj_type == ObUInt64Type) {
+ uint64_t val = 0;
+ ret = obj.get_uint64(val);
+ int_val = static_cast(val);
+ }
+
+ if (OB_SUCCESS == ret) {
+ pos = snprintf(buf, sizeof(buf), "%ld", int_val);
+ if (pos > 0 && pos < static_cast(sizeof(buf))) {
+ row.push_back(std::string(buf, pos));
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ } else {
+ // Fallback: use print_sql_literal and remove quotes
+ pos = 0;
+ if (OB_SUCCESS == obj.print_sql_literal(buf, sizeof(buf), pos)) {
+ std::string sql_literal(buf, pos);
+ // Remove surrounding quotes if present
+ if (sql_literal.length() >= 2 &&
+ sql_literal.front() == '\'' && sql_literal.back() == '\'') {
+ sql_literal = sql_literal.substr(1, sql_literal.length() - 2);
+ size_t quote_pos = 0;
+ while ((quote_pos = sql_literal.find("''", quote_pos)) != std::string::npos) {
+ sql_literal.replace(quote_pos, 2, "'");
+ quote_pos += 1;
+ }
+ }
+ row.push_back(sql_literal);
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ }
+ } else if (ob_is_json_tc(obj_type)) {
+ // JSON type: get_string() returns binary JSON (JSON_BIN). Always use print_sql_literal
+ // to get text JSON so JS JSON.parse() works (handles special chars in metadata).
+ pos = 0;
+ if (OB_SUCCESS == obj.print_sql_literal(buf, sizeof(buf), pos) && pos > 0) {
+ std::string sql_literal(buf, static_cast(pos));
+ if (sql_literal.length() >= 2 && sql_literal.front() == '\'' && sql_literal.back() == '\'') {
+ sql_literal = sql_literal.substr(1, sql_literal.length() - 2);
+ for (size_t q = 0; (q = sql_literal.find("''", q)) != std::string::npos; q += 1)
+ sql_literal.replace(q, 2, "'");
+ }
+ row.push_back(sql_literal);
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ } else if (ob_is_string_type(obj_type) || ob_is_text_tc(obj_type)) {
+ std::string cell_str;
+ seekdb_materialize_string_cell(conn, obj, obj_type, row_lob_allocator, cell_str);
+ row.push_back(std::move(cell_str));
+ row_null.push_back(false);
+ } else if (ob_is_float_tc(obj_type)) {
+ // Float types
+ float float_val = 0;
+ if (OB_SUCCESS == obj.get_float(float_val)) {
+ pos = snprintf(buf, sizeof(buf), "%.6g", float_val);
+ if (pos > 0 && pos < static_cast(sizeof(buf))) {
+ row.push_back(std::string(buf, pos));
+ } else {
+ row.push_back("");
+ }
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ } else if (ob_is_double_tc(obj_type)) {
+ // Double types
+ double double_val = 0;
+ if (OB_SUCCESS == obj.get_double(double_val)) {
+ pos = snprintf(buf, sizeof(buf), "%.15g", double_val);
+ if (pos > 0 && pos < static_cast(sizeof(buf))) {
+ row.push_back(std::string(buf, pos));
+ } else {
+ row.push_back("");
+ }
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ } else if (obj_type == oceanbase::common::ObCollectionSQLType) {
+ // VECTOR: engine returns raw float32 binary; convert to JSON string "[v1, v2, ...]" without rounding.
+ // Directly format each float so e.g. "[1.1, 2.2, 3.3]".
+ ObString str_val;
+ if (OB_SUCCESS == obj.get_string(str_val)) {
+ if (str_val.length() > 0 && str_val.ptr()) {
+ std::string json_vec;
+ if (vector_binary_to_json(str_val.ptr(), str_val.length(), json_vec)) {
+ row.push_back(std::move(json_vec));
+ } else {
+ row.push_back(std::string(str_val.ptr(), str_val.length()));
+ }
+ } else {
+ row.push_back("");
+ }
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ } else {
+ // For other types, use print_sql_literal and remove quotes if present
+ if (OB_SUCCESS == obj.print_sql_literal(buf, sizeof(buf), pos)) {
+ std::string sql_literal(buf, pos);
+ // Remove surrounding quotes if present (for string literals)
+ if (sql_literal.length() >= 2 &&
+ sql_literal.front() == '\'' && sql_literal.back() == '\'') {
+ sql_literal = sql_literal.substr(1, sql_literal.length() - 2);
+ // Unescape single quotes ('' -> ')
+ size_t quote_pos = 0;
+ while ((quote_pos = sql_literal.find("''", quote_pos)) != std::string::npos) {
+ sql_literal.replace(quote_pos, 2, "'");
+ quote_pos += 1;
+ }
+ }
+ row.push_back(sql_literal);
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ }
+ }
+ } else {
+ row.push_back("");
+ row_null.push_back(false);
+ }
+ }
+ result_set->rows.push_back(row);
+ result_set->row_nulls.push_back(row_null);
+ row_count++;
+ }
+ result_set->row_count = row_count;
+ }
+
+ // Update affected rows from result set for DML statements (INSERT/UPDATE/DELETE)
+ // This allows seekdb_affected_rows() to return correct value even when using seekdb_query()
+ // Aligned with Python embed implementation
+ oceanbase::observer::ObInnerSQLResult* inner_result_dml =
+ static_cast(sql_result);
+ if (inner_result_dml) {
+ oceanbase::sql::stmt::StmtType stmt_type = inner_result_dml->result_set().get_stmt_type();
+ if (stmt_type == oceanbase::sql::stmt::T_SELECT) {
+ // For SELECT, affected_rows is not meaningful, keep previous value
+ } else {
+ // For DML statements (INSERT/UPDATE/DELETE), get affected rows from result set
+ int64_t affected_rows = inner_result_dml->result_set().get_affected_rows();
+ if (affected_rows >= 0) {
+ conn->last_affected_rows = static_cast(affected_rows);
+ }
+ }
+ }
+
+ // Store result in connection for mysql_store_result() compatibility
+ // Note: conn is already defined at the beginning of this function
+ if (conn) {
+ // Free previous result set if exists and still owned by connection
+ // Note: If last_result_set is nullptr, it means it was transferred to user
+ // via seekdb_store_result(), so we don't need to free it
+ if (conn->last_result_set) {
+ // Check if already freed to prevent double free
+ if (!conn->last_result_set->freed) {
+ // Mark as freed before deleting to prevent double free
+ conn->last_result_set->freed = true;
+ delete conn->last_result_set;
+ }
+ // Clear the reference
+ conn->last_result_set = nullptr;
+ }
+ result_set->owner_conn = conn;
+ conn->last_result_set = result_set;
+ }
+
+ *result = static_cast(result_set);
+ params->ret_code = SEEKDB_SUCCESS;
+ return OB_SUCCESS;
+}
+
+int seekdb_query(SeekdbHandle handle, const char* query, SeekdbResult* result) {
+ if (!handle || !query || !result) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Use real_query with strlen
+ unsigned long length = static_cast(strlen(query));
+ return seekdb_real_query(handle, query, length, result);
+}
+
+int seekdb_real_query(SeekdbHandle handle, const char* stmt_str, unsigned long length, SeekdbResult* result) {
+ if (!handle || !stmt_str || !result || length == 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Create a null-terminated string for ObString
+ // Note: ObString can work with non-null-terminated strings, but we need to be careful
+ std::string sql_str(stmt_str, length);
+
+ // Execute directly on main stack (aligned with Python embed)
+ // Stack attributes were set to reasonable defaults in seekdb_open()
+ ExecuteParams params;
+ params.handle = handle;
+ params.sql = sql_str.c_str();
+ params.result = result;
+ params.ret_code = SEEKDB_ERROR_QUERY_FAILED;
+
+ do_seekdb_execute_inner(¶ms);
+
+ return params.ret_code;
+}
+
+
+// Helper function to convert SeekdbBind to parameter value string (for fallback)
+// This is used when we need to build SQL string (not recommended, but provided for compatibility)
+static std::string bind_to_string_value(SeekdbHandle handle, const SeekdbBind& bind) {
+ if (bind.is_null && *bind.is_null) {
+ return "NULL";
+ }
+
+ switch (bind.buffer_type) {
+ case SEEKDB_TYPE_TINY:
+ if (bind.buffer) {
+ int8_t val = *static_cast(bind.buffer);
+ return std::to_string(val);
+ }
+ break;
+ case SEEKDB_TYPE_SHORT:
+ if (bind.buffer) {
+ int16_t val = *static_cast(bind.buffer);
+ return std::to_string(val);
+ }
+ break;
+ case SEEKDB_TYPE_LONG:
+ if (bind.buffer) {
+ int32_t val = *static_cast(bind.buffer);
+ return std::to_string(val);
+ }
+ break;
+ case SEEKDB_TYPE_LONGLONG:
+ if (bind.buffer) {
+ int64_t val = *static_cast(bind.buffer);
+ return std::to_string(val);
+ }
+ break;
+ case SEEKDB_TYPE_FLOAT:
+ if (bind.buffer) {
+ float val = *static_cast(bind.buffer);
+ return std::to_string(val);
+ }
+ break;
+ case SEEKDB_TYPE_DOUBLE:
+ if (bind.buffer) {
+ double val = *static_cast(bind.buffer);
+ return std::to_string(val);
+ }
+ break;
+ case SEEKDB_TYPE_STRING:
+ if (bind.buffer && bind.length && *bind.length > 0) {
+ std::string str_val(static_cast(bind.buffer), *bind.length);
+ // Escape string
+ size_t escaped_len = str_val.length() * 2 + 1;
+ std::vector escaped_buf(escaped_len);
+
+ unsigned long escaped_length = seekdb_real_escape_string(
+ handle,
+ escaped_buf.data(),
+ static_cast(escaped_len),
+ str_val.c_str(),
+ static_cast(str_val.length())
+ );
+
+ if (escaped_length != static_cast(-1)) {
+ return "'" + std::string(escaped_buf.data(), escaped_length) + "'";
+ }
+ }
+ break;
+ case SEEKDB_TYPE_BLOB:
+ if (bind.buffer && bind.length && *bind.length > 0) {
+ size_t hex_len = *bind.length * 2 + 1;
+ std::vector hex_buf(hex_len);
+
+ unsigned long hex_length = seekdb_hex_string(
+ hex_buf.data(),
+ static_cast(hex_len),
+ static_cast(bind.buffer),
+ *bind.length
+ );
+
+ if (hex_length != static_cast(-1)) {
+ return "0x" + std::string(hex_buf.data(), hex_length);
+ }
+ }
+ break;
+ case SEEKDB_TYPE_VARBINARY_ID:
+ if (bind.buffer && bind.length) {
+ size_t data_len = *bind.length;
+ size_t copy_len = (data_len > VARBINARY_ID_LENGTH) ? VARBINARY_ID_LENGTH : data_len;
+ std::vector padded(VARBINARY_ID_LENGTH, 0);
+ if (copy_len > 0) {
+ memcpy(padded.data(), bind.buffer, copy_len);
+ }
+ size_t hex_len = VARBINARY_ID_LENGTH * 2 + 1;
+ std::vector hex_buf(hex_len);
+ unsigned long hex_length = seekdb_hex_string(
+ hex_buf.data(),
+ static_cast(hex_len),
+ padded.data(),
+ VARBINARY_ID_LENGTH
+ );
+ if (hex_length != static_cast(-1)) {
+ return "0x" + std::string(hex_buf.data(), hex_length);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return "NULL";
+}
+
+int seekdb_query_with_params(
+ SeekdbHandle handle,
+ const char* query,
+ SeekdbResult* result,
+ SeekdbBind* bind,
+ unsigned int param_count
+) {
+ if (!handle || !query || !result) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ unsigned long length = static_cast(strlen(query));
+ return seekdb_real_query_with_params(handle, query, length, result, bind, param_count);
+}
+
+// Helper function to get column types from INSERT statement using Schema Service API
+// Returns true if column types were successfully retrieved
+// Uses ObParser to parse SQL instead of manual string parsing
+static bool get_insert_column_types(
+ SeekdbConnection* conn,
+ const std::string& sql,
+ std::vector& param_types) {
+
+ if (!conn || !conn->embed_session || !conn->initialized) {
+ return false;
+ }
+
+ // Use ObParser to parse SQL instead of manual string parsing
+ // This is more reliable and handles complex SQL statements correctly
+ ObString sql_str;
+ sql_str.assign_ptr(sql.c_str(), static_cast(sql.length()));
+ ObArenaAllocator allocator(ObModIds::OB_SQL_PARSER);
+ ObParser parser(allocator, conn->embed_session->get_sql_mode(),
+ conn->embed_session->get_charsets4parser());
+ ParseResult parse_result;
+ ParseMode parse_mode = STD_MODE;
+
+ int parse_ret = parser.parse(sql_str, parse_result, parse_mode);
+ if (parse_ret != OB_SUCCESS || parse_result.result_tree_ == nullptr) {
+ // Parsing failed, cannot extract table/column information
+ return false;
+ }
+
+ // Check if it's an INSERT statement
+ const ParseNode* root = parse_result.result_tree_;
+ if (root->type_ != T_INSERT) {
+ return false;
+ }
+
+ // Extract table name and column names from parse tree
+ std::string table_name;
+ std::vector column_names;
+
+ // T_INSERT structure: children[0] = insert_into (T_INSERT_INTO)
+ if (root->num_child_ > 0 && root->children_[0] != nullptr) {
+ const ParseNode* insert_into = root->children_[0];
+ // T_INSERT_INTO structure: children[0] = table_node, children[1] = column_list (optional)
+ if (insert_into->num_child_ > 0 && insert_into->children_[0] != nullptr) {
+ const ParseNode* table_node = insert_into->children_[0];
+ // Extract table name from table_node
+ if (table_node->str_value_ != nullptr && table_node->str_len_ > 0) {
+ table_name = std::string(table_node->str_value_, table_node->str_len_);
+ }
+ }
+
+ // Extract column list if specified
+ if (insert_into->num_child_ > 1 && insert_into->children_[1] != nullptr) {
+ const ParseNode* column_list = insert_into->children_[1];
+ // Column list is typically T_COLUMN_LIST or similar
+ if (column_list->num_child_ > 0) {
+ for (int32_t i = 0; i < column_list->num_child_; i++) {
+ const ParseNode* col_node = column_list->children_[i];
+ if (col_node != nullptr && col_node->str_value_ != nullptr && col_node->str_len_ > 0) {
+ std::string col_name(col_node->str_value_, col_node->str_len_);
+ // Remove backticks if present
+ if (col_name.length() >= 2 && col_name[0] == '`' && col_name[col_name.length()-1] == '`') {
+ col_name = col_name.substr(1, col_name.length() - 2);
+ }
+ column_names.push_back(col_name);
+ }
+ }
+ }
+ }
+ }
+
+ if (table_name.empty()) {
+ return false;
+ }
+
+ // Get column types using Schema Service API (more efficient than executing SQL query)
+ // This approach directly accesses table schema without executing any SQL statements
+ // which is more efficient and avoids potential thread safety issues
+
+ // Get database name from session
+ ObString database_name = conn->embed_session->get_database_name();
+ if (database_name.empty()) {
+ // Database name is required for Schema Service API
+ return false;
+ }
+
+ // Get schema guard from schema service
+ schema::ObSchemaGetterGuard schema_guard;
+ int schema_ret = GCTX.schema_service_->get_runtime_schema_guard(schema_guard);
+ if (schema_ret != OB_SUCCESS) {
+ // Schema service is required
+ return false;
+ }
+
+ // Get table schema by database name and table name
+ const schema::ObTableSchema* table_schema = nullptr;
+ ObString table_name_str;
+ table_name_str.assign_ptr(table_name.c_str(), static_cast(table_name.length()));
+ schema_ret = schema_guard.get_table_schema(database_name, table_name_str, false, table_schema);
+ if (schema_ret != OB_SUCCESS || table_schema == nullptr) {
+ // Table schema is required
+ return false;
+ }
+
+ // Extract column types from table schema
+ param_types.clear();
+
+ if (!column_names.empty()) {
+ // If column list is specified, get types for those specific columns in order
+ param_types.reserve(column_names.size());
+ for (size_t i = 0; i < column_names.size(); i++) {
+ // Convert std::string to ObString for get_column_schema()
+ ObString column_name_str;
+ column_name_str.assign_ptr(column_names[i].c_str(), static_cast(column_names[i].length()));
+ const schema::ObColumnSchemaV2* column_schema = table_schema->get_column_schema(column_name_str);
+ if (column_schema != nullptr) {
+ oceanbase::common::ObObjType obj_type = column_schema->get_data_type();
+ param_types.push_back(obj_type);
+ } else {
+ // Column not found in schema
+ return false;
+ }
+ }
+ } else {
+ // If column list is not specified, get all user columns in table order
+ // Note: This may not match the parameter order, so it's better to specify column list
+ const schema::ObColumnSchemaV2* column_schema = nullptr;
+ int64_t column_count = table_schema->get_column_count();
+ param_types.reserve(static_cast(column_count));
+
+ for (int64_t i = 0; i < column_count; i++) {
+ column_schema = table_schema->get_column_schema_by_idx(i);
+ if (column_schema != nullptr && !column_schema->is_hidden()) {
+ oceanbase::common::ObObjType obj_type = column_schema->get_data_type();
+ param_types.push_back(obj_type);
+ }
+ }
+ }
+
+ return param_types.size() > 0;
+}
+
+int seekdb_real_query_with_params(
+ SeekdbHandle handle,
+ const char* stmt_str,
+ unsigned long length,
+ SeekdbResult* result,
+ SeekdbBind* bind,
+ unsigned int param_count
+) {
+ if (!handle || !stmt_str || !result || length == 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Auto-detect VECTOR type based on column type information from table schema
+ // Use column type information from prepared statement (obtained via table schema query)
+ // instead of hard-parsing parameter values
+ if (param_count > 0 && bind) {
+ SeekdbStmt stmt = seekdb_stmt_init(handle);
+ if (stmt) {
+ // Prepare statement to get column type information
+ int prep_ret = seekdb_stmt_prepare(stmt, stmt_str, length);
+ if (prep_ret == SEEKDB_SUCCESS) {
+ SeekdbStmtData* stmt_data = static_cast(stmt);
+
+ // Use column type information from table schema
+ // For multi-row INSERT: VALUES (?, ?, ?), (?, ?, ?), ...
+ // Parameters repeat in pattern: param[i] corresponds to column[i % column_count]
+ // Example: INSERT INTO table (col1, col2, col3, col4) VALUES (?, ?, ?, ?), (?, ?, ?, ?)
+ // param[0,4,8] -> col1, param[1,5,9] -> col2, param[2,6,10] -> col3, param[3,7,11] -> col4
+ size_t column_count = stmt_data->param_column_types.size();
+ if (column_count > 0) {
+ for (unsigned int i = 0; i < param_count; i++) {
+ // Only auto-detect if type is STRING or not explicitly set
+ if (bind[i].buffer_type == SEEKDB_TYPE_STRING || bind[i].buffer_type == SEEKDB_TYPE_NULL) {
+ // Map parameter index to column index (for multi-row inserts)
+ size_t col_idx = i % column_count;
+ if (col_idx < stmt_data->param_column_types.size()) {
+ oceanbase::common::ObObjType col_type = stmt_data->param_column_types[col_idx];
+
+ // Check if column type is VECTOR
+ // VECTOR type in OceanBase is represented as ObCollectionSQLType (40)
+ // We need to check if it's a collection type, which includes VECTOR
+ // Note: This may also match other collection types (varray, nested table),
+ // but for INSERT statements, VECTOR is the most common collection type
+ if (col_type == oceanbase::common::ObCollectionSQLType) {
+ // Auto-detect as VECTOR type based on column schema
+ bind[i].buffer_type = SEEKDB_TYPE_VECTOR;
+ }
+ }
+ }
+ }
+ }
+ }
+ // Note: We'll recreate the statement below, so we can close this one
+ seekdb_stmt_close(stmt);
+ }
+ }
+
+ // Use prepared statement internally (aligned with MySQL C API approach)
+ // This is more secure and efficient than string substitution
+ SeekdbStmt stmt = seekdb_stmt_init(handle);
+ if (!stmt) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // Prepare statement
+ int ret = seekdb_stmt_prepare(stmt, stmt_str, length);
+ if (ret != SEEKDB_SUCCESS) {
+ seekdb_stmt_close(stmt);
+ return ret;
+ }
+
+ // Bind parameters
+ if (param_count > 0 && bind) {
+ ret = seekdb_stmt_bind_param(stmt, bind);
+ if (ret != SEEKDB_SUCCESS) {
+ seekdb_stmt_close(stmt);
+ return ret;
+ }
+ }
+
+ // Execute statement
+ // Note: seekdb_stmt_execute() builds SQL with parameter substitution, calls seekdb_query(),
+ // then transfers result from conn->last_result_set to stmt_data->result_set
+ ret = seekdb_stmt_execute(stmt);
+
+ // Get result from statement (seekdb_stmt_execute stores it in stmt_data->result_set, not conn)
+ SeekdbStmtData* stmt_data = static_cast(stmt);
+ if (ret == SEEKDB_SUCCESS && stmt_data && stmt_data->result_set) {
+ *result = static_cast(stmt_data->result_set);
+ stmt_data->result_set = nullptr; // Transfer ownership to caller; stmt_close must not free it
+ }
+
+ // Close statement
+ seekdb_stmt_close(stmt);
+
+ return ret;
+}
+
+SeekdbResult seekdb_store_result(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return nullptr;
+ }
+
+ // Return the last result set stored in the connection
+ // This is set by seekdb_real_query() or seekdb_query()
+ // Transfer ownership to caller by removing reference from connection
+ // This prevents the result set from being deleted by subsequent queries
+ SeekdbResult result = static_cast(conn->last_result_set);
+ if (result) {
+ conn->last_result_set = nullptr; // Transfer ownership to caller
+ }
+ return result;
+}
+
+SeekdbResult seekdb_use_result(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !conn->embed_result) {
+ return nullptr;
+ }
+
+ // Create a streaming result set
+ if (!conn->use_result_set) {
+ SeekdbResultSet* result_set = new (std::nothrow) SeekdbResultSet();
+ if (!result_set) {
+ return nullptr;
+ }
+ result_set->owner_conn = conn;
+
+ sqlclient::ObMySQLResult* sql_result = conn->embed_result->get_result();
+ if (!sql_result) {
+ delete result_set;
+ return nullptr;
+ }
+
+ // Get column count
+ int64_t column_count = sql_result->get_column_count();
+ result_set->column_count = static_cast(column_count);
+ result_set->use_result_mode = true; // Mark as streaming mode
+
+ // Get column names (similar to store_result)
+ for (int64_t i = 0; i < column_count; ++i) {
+ char col_name_buf[64];
+ snprintf(col_name_buf, sizeof(col_name_buf), "col_%ld", i);
+ result_set->column_names.push_back(std::string(col_name_buf));
+ }
+
+ // In streaming mode, we don't pre-fetch rows
+ // Rows will be fetched on-demand in seekdb_fetch_row()
+ result_set->row_count = -1; // Unknown row count in streaming mode
+
+ result_set->owner_conn = conn;
+ conn->use_result_set = result_set;
+ }
+
+ return static_cast(conn->use_result_set);
+}
+
+my_ulonglong seekdb_num_rows(SeekdbResult result) {
+ if (!result) {
+ return static_cast(-1);
+ }
+ SeekdbResultSet* rs = static_cast(result);
+ return static_cast(rs->row_count);
+}
+
+unsigned int seekdb_num_fields(SeekdbResult result) {
+ if (!result) {
+ return static_cast(-1);
+ }
+ SeekdbResultSet* rs = static_cast(result);
+ return static_cast(rs->column_count);
+}
+
+unsigned int seekdb_field_count(SeekdbHandle handle) {
+ if (!handle) {
+ return 0;
+ }
+
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return 0;
+ }
+
+ // Get field count from last result set
+ if (conn->last_result_set) {
+ return static_cast(conn->last_result_set->column_count);
+ }
+
+ return 0;
+}
+
+size_t seekdb_result_column_name_len(SeekdbResult result, int32_t column_index) {
+ if (!result || column_index < 0) {
+ return static_cast(-1);
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+ if (column_index >= static_cast(rs->column_names.size())) {
+ return static_cast(-1);
+ }
+
+ return rs->column_names[column_index].length();
+}
+
+int seekdb_result_column_name(SeekdbResult result, int32_t column_index, char* name, size_t name_len) {
+ if (!result || !name || name_len == 0 || column_index < 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+ if (column_index >= static_cast(rs->column_names.size())) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ const std::string& col_name = rs->column_names[column_index];
+ size_t copy_len = std::min(col_name.length(), name_len - 1);
+ strncpy(name, col_name.c_str(), copy_len);
+ name[copy_len] = '\0';
+
+ return SEEKDB_SUCCESS;
+}
+
+SeekdbRow seekdb_fetch_row(SeekdbResult result) {
+ if (!result) {
+ return nullptr;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+ rs->current_row++;
+
+ if (rs->current_row >= rs->row_count) {
+ return nullptr; // No more rows (MySQL-compatible: returns NULL)
+ }
+
+ // Align with MySQL: row is borrowed, valid until next seekdb_fetch_row() or seekdb_result_free().
+ // Reuse a single current_row_data instead of allocating per row.
+ SeekdbRowData* row_data = rs->current_row_data;
+ if (!row_data) {
+ row_data = new (std::nothrow) SeekdbRowData(rs, rs->current_row);
+ if (!row_data) {
+ return nullptr;
+ }
+ rs->current_row_data = row_data;
+ } else {
+ row_data->row_index = rs->current_row;
+ }
+
+ // Pre-compute lengths for seekdb_fetch_lengths(); NULL -> 0, non-NULL -> actual byte length
+ rs->current_lengths.clear();
+ rs->current_lengths.resize(rs->column_count, 0);
+ if (rs->current_row < static_cast(rs->rows.size())) {
+ const std::vector& row_vec = rs->rows[rs->current_row];
+ for (int32_t i = 0; i < rs->column_count && i < static_cast(row_vec.size()); i++) {
+ bool is_null = (rs->current_row < static_cast(rs->row_nulls.size()) &&
+ i < static_cast(rs->row_nulls[rs->current_row].size()) &&
+ rs->row_nulls[rs->current_row][i]);
+ if (!is_null) {
+ rs->current_lengths[i] = static_cast(row_vec[i].length());
+ }
+ }
+ }
+
+ return static_cast(row_data);
+}
+
+size_t seekdb_row_get_string_len(SeekdbRow row, int32_t column_index) {
+ if (!row || column_index < 0) {
+ return static_cast(-1);
+ }
+
+ SeekdbRowData* row_data = static_cast(row);
+ SeekdbResultSet* rs = row_data->result_set;
+
+ if (row_data->row_index < 0 ||
+ row_data->row_index >= rs->row_count ||
+ column_index >= static_cast(rs->column_count)) {
+ return static_cast(-1);
+ }
+
+ const std::vector& row_vec = rs->rows[row_data->row_index];
+ if (column_index >= static_cast(row_vec.size())) {
+ return static_cast(-1);
+ }
+ // C ABI contract: NULL returns (size_t)-1; empty string '' returns 0; non-empty returns actual byte length
+ if (row_data->row_index < static_cast(rs->row_nulls.size()) &&
+ column_index < static_cast(rs->row_nulls[row_data->row_index].size()) &&
+ rs->row_nulls[row_data->row_index][column_index]) {
+ return static_cast(-1);
+ }
+ return row_vec[column_index].length();
+}
+
+int seekdb_row_get_string(SeekdbRow row, int32_t column_index, char* value, size_t value_len) {
+ if (!row || !value || value_len == 0 || column_index < 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbRowData* row_data = static_cast(row);
+ SeekdbResultSet* rs = row_data->result_set;
+
+ if (row_data->row_index < 0 ||
+ row_data->row_index >= rs->row_count ||
+ column_index >= static_cast(rs->column_count)) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ const std::vector& row_vec = rs->rows[row_data->row_index];
+ if (column_index >= static_cast(row_vec.size())) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+ // C ABI contract: NULL -> write '\0' and succeed; non-NULL requires value_len >= len+1 for full copy (no truncation)
+ bool is_null = (row_data->row_index < static_cast(rs->row_nulls.size()) &&
+ column_index < static_cast(rs->row_nulls[row_data->row_index].size()) &&
+ rs->row_nulls[row_data->row_index][column_index]);
+ if (is_null) {
+ value[0] = '\0';
+ return SEEKDB_SUCCESS;
+ }
+ const std::string& str_val = row_vec[column_index];
+ size_t len = str_val.length();
+ if (value_len < len + 1) {
+ return SEEKDB_ERROR_INVALID_PARAM; // Buffer too small; caller should use seekdb_row_get_string_len first
+ }
+ if (len > 0 && str_val.data()) {
+ memcpy(value, str_val.data(), len);
+ }
+ value[len] = '\0';
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_row_get_int64(SeekdbRow row, int32_t column_index, int64_t* value) {
+ if (!row || !value || column_index < 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ char buf[256];
+ int ret = seekdb_row_get_string(row, column_index, buf, sizeof(buf));
+ if (ret != SEEKDB_SUCCESS) {
+ return ret;
+ }
+
+ *value = strtoll(buf, nullptr, 10);
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_row_get_double(SeekdbRow row, int32_t column_index, double* value) {
+ if (!row || !value || column_index < 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ char buf[256];
+ int ret = seekdb_row_get_string(row, column_index, buf, sizeof(buf));
+ if (ret != SEEKDB_SUCCESS) {
+ return ret;
+ }
+
+ *value = strtod(buf, nullptr);
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_row_get_bool(SeekdbRow row, int32_t column_index, bool* value) {
+ if (!row || !value || column_index < 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ int64_t int_val;
+ int ret = seekdb_row_get_int64(row, column_index, &int_val);
+ if (ret != SEEKDB_SUCCESS) {
+ return ret;
+ }
+
+ *value = (int_val != 0);
+ return SEEKDB_SUCCESS;
+}
+
+bool seekdb_row_is_null(SeekdbRow row, int32_t column_index) {
+ if (!row || column_index < 0) {
+ return true;
+ }
+
+ SeekdbRowData* row_data = static_cast(row);
+ SeekdbResultSet* rs = row_data->result_set;
+
+ if (row_data->row_index < 0 ||
+ row_data->row_index >= rs->row_count ||
+ column_index >= static_cast(rs->column_count)) {
+ return true;
+ }
+
+ if (column_index >= static_cast(rs->rows[row_data->row_index].size())) {
+ return true;
+ }
+ // C ABI contract: only true for SQL NULL; empty string '' returns false (distinct from NULL)
+ if (row_data->row_index < static_cast(rs->row_nulls.size()) &&
+ column_index < static_cast(rs->row_nulls[row_data->row_index].size())) {
+ return rs->row_nulls[row_data->row_index][column_index];
+ }
+ return false; // Legacy result set without row_nulls (e.g. param metadata)
+}
+
+int seekdb_data_seek(SeekdbResult result, my_ulonglong offset) {
+ if (!result) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+
+ if (offset >= static_cast(rs->row_count)) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Set current row position (will be incremented on next fetch)
+ rs->current_row = static_cast(offset) - 1;
+
+ return SEEKDB_SUCCESS;
+}
+
+my_ulonglong seekdb_row_tell(SeekdbResult result) {
+ if (!result) {
+ return static_cast(-1);
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+
+ // current_row is -1 before first fetch, so we add 1 to get the actual position
+ int64_t current_pos = rs->current_row + 1;
+
+ if (current_pos < 0 || current_pos >= rs->row_count) {
+ return static_cast(-1);
+ }
+
+ return static_cast(current_pos);
+}
+
+SeekdbRow seekdb_row_seek(SeekdbResult result, SeekdbRow row) {
+ if (!result || !row) {
+ return nullptr;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+ SeekdbRowData* row_data = static_cast(row);
+
+ // Verify row belongs to this result set
+ if (row_data->result_set != rs) {
+ return nullptr;
+ }
+
+ // Set current row position (will be incremented on next fetch)
+ rs->current_row = row_data->row_index - 1;
+
+ // Return the row handle (can be used for future seeks)
+ return row;
+}
+
+unsigned long* seekdb_fetch_lengths(SeekdbResult result) {
+ if (!result) {
+ return nullptr;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+
+ // Return lengths for the current row (set by seekdb_fetch_row)
+ if (rs->current_lengths.empty() || rs->current_row < 0 || rs->current_row >= rs->row_count) {
+ return nullptr;
+ }
+
+ return rs->current_lengths.data();
+}
+
+void seekdb_result_free(SeekdbResult result) {
+ if (!result) {
+ return;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+
+ // Check if already freed to prevent double free
+ if (rs->freed) {
+ return; // Already freed, ignore
+ }
+
+ // Mark as freed first to prevent re-entry
+ rs->freed = true;
+
+ // Safely get owner connection (may be null if already cleared)
+ SeekdbConnection* conn = rs->owner_conn;
+
+ // Clear reference in connection to prevent double free
+ // Note: After seekdb_store_result(), last_result_set should already be nullptr
+ // but we check anyway for safety
+ if (conn) {
+ if (conn->last_result_set == rs) {
+ conn->last_result_set = nullptr;
+ }
+ if (conn->use_result_set == rs) {
+ conn->use_result_set = nullptr;
+ }
+ // Also remove from result_sets vector if present
+ auto it = std::find(conn->result_sets.begin(), conn->result_sets.end(), rs);
+ if (it != conn->result_sets.end()) {
+ conn->result_sets.erase(it);
+ }
+ }
+
+ delete rs;
+}
+
+const char* seekdb_last_error(void) {
+ if (g_thread_last_error.empty()) {
+ return nullptr;
+ }
+ return g_thread_last_error.c_str();
+}
+
+int seekdb_last_error_code(void) {
+ return g_thread_last_error_code;
+}
+
+int seekdb_get_last_error(SeekdbHandle handle, char* error_msg, size_t error_msg_len) {
+ if (!handle || !error_msg || error_msg_len == 0) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbConnection* conn = static_cast(handle);
+ size_t copy_len = std::min(conn->last_error.length(), error_msg_len - 1);
+ strncpy(error_msg, conn->last_error.c_str(), copy_len);
+ error_msg[copy_len] = '\0';
+
+ // Also update thread-local error
+ g_thread_last_error = conn->last_error;
+
+ return SEEKDB_SUCCESS;
+}
+
+// Internal implementation of seekdb_execute_update
+struct ExecuteUpdateParams {
+ SeekdbHandle handle;
+ const char* sql;
+ int64_t* affected_rows;
+ int ret_code;
+};
+
+static int do_seekdb_execute_update_inner(ExecuteUpdateParams* params) {
+ SeekdbHandle handle = params->handle;
+ const char* sql = params->sql;
+ int64_t* affected_rows = params->affected_rows;
+
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ params->ret_code = SEEKDB_ERROR_INVALID_PARAM;
+ return OB_SUCCESS;
+ }
+
+ int ret = OB_SUCCESS;
+ int64_t rows = 0;
+
+ // Embedded mode only
+ if (!conn->embed_conn) {
+ params->ret_code = SEEKDB_ERROR_INVALID_PARAM;
+ return OB_SUCCESS;
+ }
+
+ // Initialize trace ID (aligned with Python embed)
+ ObCurTraceId::init(GCTX.self_addr());
+
+ // Setup warning buffer (aligned with Python embed)
+ if (OB_NOT_NULL(conn->embed_session)) {
+ ob_setup_tsi_warning_buffer(&conn->embed_session->get_warnings_buffer());
+ }
+
+ // Reset previous result if exists
+ if (conn->embed_result) {
+ conn->embed_result->close();
+ conn->embed_result->~ReadResult();
+ ob_free(conn->embed_result);
+ conn->embed_result = nullptr;
+ }
+
+ // Align with MySQL protocol: check and refresh only when server is behind session.
+ if (OB_NOT_NULL(conn->embed_session)) {
+ check_and_refresh_schema_for_embed(conn->embed_session);
+ }
+
+ ObString sql_string(sql);
+ ret = conn->embed_conn->execute_write(sql_string, rows, true);
+
+ // Reset warning buffer after execute (aligned with Python embed)
+ if (OB_NOT_NULL(conn->embed_session)) {
+ conn->embed_session->reset_warnings_buf();
+ }
+ ob_setup_tsi_warning_buffer(NULL);
+
+ // DDL / read-after-write visibility: after execute_write, one refresh + sync session (align with MySQL DDL path).
+ if (OB_SUCCESS == ret && OB_NOT_NULL(conn->embed_session) && OB_NOT_NULL(GCTX.schema_service_)) {
+ refresh_session_schema_version(conn->embed_session);
+ (void)oceanbase::sql::ObSQLUtils::update_session_last_schema_version(*GCTX.schema_service_, *conn->embed_session);
+ }
+
+ if (OB_SUCCESS == ret) {
+ *affected_rows = rows;
+ params->ret_code = SEEKDB_SUCCESS;
+ } else {
+ // Get detailed error message (aligned with do_seekdb_execute_inner / Python embed)
+ // Surface the real OB error/warning so callers can diagnose failures instead of
+ // seeing only a generic "Update execution failed".
+ std::string errmsg;
+ const oceanbase::common::ObWarningBuffer *wb = oceanbase::common::ob_get_tsi_warning_buffer();
+ if (nullptr != wb && wb->get_err_code() == ret) {
+ if (wb->get_err_msg() != nullptr && wb->get_err_msg()[0] != '\0') {
+ errmsg = std::string(wb->get_err_msg());
+ }
+ }
+ if (errmsg.empty()) {
+ const char *err_str = ob_errpkt_strerror(ret);
+ if (nullptr != err_str && err_str[0] != '\0') {
+ errmsg = std::string(err_str);
+ }
+ }
+ if (errmsg.empty()) {
+ errmsg = "Update execution failed";
+ }
+ // Append OB error code so logs always carry numeric ret even when the message is non-empty.
+ char code_suffix[32];
+ snprintf(code_suffix, sizeof(code_suffix), " (ret=%d)", ret);
+ errmsg += code_suffix;
+ set_error(conn, errmsg.c_str());
+ params->ret_code = SEEKDB_ERROR_QUERY_FAILED;
+ }
+ return OB_SUCCESS;
+}
+
+// Internal/legacy: write-only path. MySQL-aligned usage is seekdb_query() for all SQL + seekdb_affected_rows().
+int seekdb_execute_update(SeekdbHandle handle, const char* sql, int64_t* affected_rows) {
+ if (!handle || !sql || !affected_rows) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Execute directly on main stack (aligned with Python embed)
+ // Stack attributes were set to reasonable defaults in seekdb_open()
+ ExecuteUpdateParams params;
+ params.handle = handle;
+ params.sql = sql;
+ params.affected_rows = affected_rows;
+ params.ret_code = SEEKDB_ERROR_QUERY_FAILED;
+
+ do_seekdb_execute_update_inner(¶ms);
+
+ // Store affected rows for seekdb_affected_rows()
+ if (params.ret_code == SEEKDB_SUCCESS && affected_rows) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (conn) {
+ conn->last_affected_rows = static_cast(*affected_rows);
+ }
+ }
+
+ return params.ret_code;
+}
+
+// SeekDB extension: Begin a transaction
+// In MySQL 5.7 and 8.0 C API, there is no mysql_begin() function.
+// To begin a transaction in MySQL C API:
+// - Use mysql_autocommit(mysql, 0) to disable autocommit mode, or
+// - Execute "START TRANSACTION" SQL statement using mysql_query() or mysql_real_query()
+// This function provides a convenient way to start a transaction, equivalent to executing "START TRANSACTION" SQL statement.
+int seekdb_begin(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!conn->embed_conn || !conn->embed_session) {
+ set_error(conn, "Connection not initialized");
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ int ret = OB_SUCCESS;
+
+ // Reset any previous result
+ if (conn->embed_result) {
+ conn->embed_result->close();
+ conn->embed_result->~ReadResult();
+ ob_free(conn->embed_result);
+ conn->embed_result = nullptr;
+ }
+
+ // If already in transaction, rollback first (like Python embed does)
+ if (conn->embed_session->is_in_transaction()) {
+ conn->embed_conn->set_is_in_trans(true);
+ if (OB_FAIL(conn->embed_conn->rollback())) {
+ set_error(conn, "Failed to rollback previous transaction");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+ }
+ // Session flag can be cleared while ObInnerSQLConnection::is_in_trans_ is still true (e.g.
+ // DDL or Room invalidation trigger paths). start_transaction_inner then fails with
+ // "inner conn is already in trans". Roll back to resync before START TRANSACTION.
+ if (conn->embed_conn->is_in_trans()) {
+ conn->embed_conn->set_is_in_trans(true);
+ if (OB_FAIL(conn->embed_conn->rollback())) {
+ set_error(conn, "Failed to rollback orphan inner transaction state");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+ }
+
+ // Start new transaction
+ // This is equivalent to executing "START TRANSACTION" SQL statement in MySQL 5.7
+ if (OB_FAIL(conn->embed_conn->start_transaction())) {
+ set_error(conn, "Failed to start transaction");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_commit(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!conn->embed_conn || !conn->embed_session) {
+ set_error(conn, "Connection not initialized");
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ // Reset any previous result
+ if (conn->embed_result) {
+ conn->embed_result->close();
+ conn->embed_result->~ReadResult();
+ ob_free(conn->embed_result);
+ conn->embed_result = nullptr;
+ }
+
+ int ret = OB_SUCCESS;
+
+ // Only commit if in transaction
+ if (conn->embed_session->is_in_transaction()) {
+ conn->embed_conn->set_is_in_trans(true);
+ if (OB_FAIL(conn->embed_conn->commit())) {
+ set_error(conn, "Failed to commit transaction");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+ }
+
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_rollback(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!conn->embed_conn || !conn->embed_session) {
+ set_error(conn, "Connection not initialized");
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ // Reset any previous result
+ if (conn->embed_result) {
+ conn->embed_result->close();
+ conn->embed_result->~ReadResult();
+ ob_free(conn->embed_result);
+ conn->embed_result = nullptr;
+ }
+
+ int ret = OB_SUCCESS;
+
+ // Only rollback if in transaction
+ if (conn->embed_session->is_in_transaction()) {
+ conn->embed_conn->set_is_in_trans(true);
+ if (OB_FAIL(conn->embed_conn->rollback())) {
+ set_error(conn, "Failed to rollback transaction");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+ }
+
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_autocommit(SeekdbHandle handle, bool mode) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!conn->embed_session) {
+ set_error(conn, "Connection not initialized");
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ int ret = OB_SUCCESS;
+ if (OB_FAIL(conn->embed_session->set_autocommit(mode))) {
+ set_error(conn, "Failed to set autocommit");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+
+ return SEEKDB_SUCCESS;
+}
+
+my_ulonglong seekdb_affected_rows(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return 0;
+ }
+ return conn->last_affected_rows;
+}
+
+my_ulonglong seekdb_insert_id(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !conn->embed_session) {
+ return 0;
+ }
+
+ // Execute SELECT LAST_INSERT_ID() to get the last inserted ID
+ SeekdbResult result = nullptr;
+ int ret = seekdb_query(handle, "SELECT LAST_INSERT_ID() as id", &result);
+ if (ret != SEEKDB_SUCCESS || !result) {
+ return 0;
+ }
+
+ my_ulonglong insert_id = 0;
+ SeekdbRow row = seekdb_fetch_row(result);
+ if (row) {
+ int64_t id_value = 0;
+ if (seekdb_row_get_int64(row, 0, &id_value) == SEEKDB_SUCCESS) {
+ insert_id = static_cast(id_value);
+ }
+ }
+ seekdb_result_free(result);
+ return insert_id;
+}
+
+int seekdb_ping(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast