diff --git a/.github/workflows/build-libseekdb.yml b/.github/workflows/build-libseekdb.yml
new file mode 100644
index 000000000..51a83cbec
--- /dev/null
+++ b/.github/workflows/build-libseekdb.yml
@@ -0,0 +1,598 @@
+# 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-14, 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
+# android-arm64-v8a: runner ubuntu-22.04, NDK + ./build.sh --android, zip libseekdb-android-arm64-v8a.zip
+#
+# macOS builds use runner macos-14 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.
+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) }}
+
+# Triggers on all branches; S3 upload runs only on main, master, develop, *.*.x, integration/* (see release-artifacts job).
+on:
+ push:
+ 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 (Linux, el7)
+ 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 (Linux)
+ uses: actions/cache@v4
+ with:
+ path: .ccache
+ # Static key per platform so second run always hits; ccache content is keyed by source hash internally
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}
+ 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 (Linux, manylinux2014)
+ 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"
+ 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
+ cd build_release && make -j$(nproc) libseekdb
+ [ -n "$USE_CCACHE" ] && ccache -s || true
+ cd "$GITHUB_WORKSPACE/package/libseekdb" && bash libseekdb-build.sh "$GITHUB_WORKSPACE/build_release/src/include"
+ '
+ - name: Fix ownership (container writes as root)
+ run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE"
+
+ - name: Setup Node.js (Linux)
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Rust (Linux)
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Setup Go (Linux)
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java (Linux)
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Test Node.js FFI binding (Linux)
+ run: |
+ cd unittest/include/nodejs
+ npm install
+ bash test.sh
+
+ - name: Test Node.js N-API binding (Linux)
+ 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 Python binding (Linux)
+ run: |
+ cd unittest/include/python
+ bash test.sh
+
+ - name: Test Rust binding (Linux)
+ run: |
+ cd unittest/include/rust
+ bash test.sh
+
+ - name: Test Go binding (Linux)
+ 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 (Linux)
+ run: |
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'deps/3rd' | tr '\n' ':' | sed 's/:$//')
+ cd unittest/include/java
+ bash test.sh
+
+ - 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 (Linux)
+ 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 (Linux)
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}
+
+ # ---------- 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-14
+ 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
+ run: brew install cmake dylibbundler googletest ccache pybind11 utf8proc thrift re2 brotli bzip2 || true
+
+ - name: Cache deps (macOS)
+ 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 (macOS)
+ uses: actions/cache@v4
+ with:
+ path: .ccache
+ # Static key per platform so second run always hits
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}
+ restore-keys: |
+ ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}
+
+ - name: Build init (macOS)
+ run: bash build.sh init
+
+ - name: Build libseekdb (macOS)
+ 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
+ cd build_release && make -j$(sysctl -n hw.ncpu) libseekdb
+ ccache -s
+
+ - name: Setup Node.js (macOS)
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Rust (macOS)
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Setup Go (macOS)
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java (macOS)
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Test Node.js FFI binding (macOS)
+ run: |
+ cd unittest/include/nodejs
+ npm install
+ bash test.sh
+
+ - name: Test Node.js N-API binding (macOS)
+ 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 Python binding (macOS)
+ run: |
+ cd unittest/include/python
+ bash test.sh
+
+ - name: Test Rust binding (macOS)
+ run: |
+ cd unittest/include/rust
+ bash test.sh
+
+ - name: Test Go binding (macOS)
+ 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 (macOS)
+ 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 (macOS)
+ 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: 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 (macOS)
+ 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 (macOS)
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-${{ matrix.platform }}
+
+ # ---------- Android NDK cross-compile (macOS arm64 host; target arm64-v8a) ----------
+ # ubuntu-* runners are x86_64 by default. macOS-14 uses Apple silicon + NDK darwin-arm64 prebuilts (see docs).
+ # Alternative: runs-on: ubuntu-24.04-arm for Linux ARM64.
+ build-android:
+ name: Build libseekdb (android-arm64-v8a)
+ runs-on: macos-14
+ 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 (Android)
+ 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 (Android)
+ 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: Build libseekdb (Android)
+ 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 --init --make libseekdb
+ ccache -s
+
+ - name: Pack libseekdb (Android)
+ run: cd package/libseekdb && bash libseekdb-build.sh --android
+
+ - 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 (Android)
+ 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 (Android)
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: .ccache
+ key: ${{ runner.os }}-ccache-libseekdb-android-arm64-v8a
+
+ # ---------- 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 (Windows)
+ 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: Build libseekdb (Windows)
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ $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 }
+ $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"
+ .\build.ps1 release --ninja --target libseekdb "-DBUILD_EMBED_MODE=ON" "-DPYTHON_VERSION=$py" "-DCMAKE_LINKER=$lldFwd"
+
+ - name: Setup Node.js (Windows binding tests)
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Setup Rust (Windows binding tests)
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Setup Go (Windows binding tests)
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21"
+
+ - name: Setup Java (Windows binding tests)
+ 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
+
+ - name: Binding tests (Windows)
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ run: |
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+ # Refresh PATH after Chocolatey (gcc / mvn)
+ $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"
+
+ - name: Pack libseekdb zip (Windows)
+ shell: pwsh
+ env:
+ BUILD_TYPE: release
+ run: .\package\libseekdb\libseekdb-build.ps1
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: libseekdb-windows-x64
+ path: package/libseekdb/libseekdb-windows-x64.zip
+
+ - name: Save Cache deps (Windows)
+ 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') }}
+
+ # ---------- Collect libseekdb artifacts and upload to S3 (runs only after all build jobs pass, 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/.gitignore b/.gitignore
index c6e6eedb9..d01c221f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -385,6 +385,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/
diff --git a/build.ps1 b/build.ps1
index ec43864d9..83935363e 100644
--- a/build.ps1
+++ b/build.ps1
@@ -18,7 +18,9 @@ $Action = "debug"
$Ninja = $false
$Init = $false
$Jobs = 0
-$h = $false
+$h = $false
+$NinjaTarget = "observer"
+$ExtraCmake = @()
$i = 0
while ($i -lt $args.Count) {
@@ -27,12 +29,17 @@ while ($i -lt $args.Count) {
{ $_ -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 -lt $args.Count) { $Jobs = [int]$args[$i] }
}
default {
if (-not $a.StartsWith("-")) { $Action = $a }
+ elseif ($a.StartsWith("-D")) { $ExtraCmake += $a }
else { Write-Host "[build.ps1][WARN] Unknown flag: $a" -ForegroundColor Yellow }
}
}
@@ -181,6 +188,7 @@ Usage:
.\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:
@@ -191,6 +199,8 @@ BuildType:
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)
@@ -286,7 +296,13 @@ function Do-Build {
[string[]]$ExtraCMakeArgs = @()
)
- $buildDir = "$TOPDIR\build_$($BuildType.ToLower())"
+ # Align directory names with build.sh: release -> build_release, debug -> build_debug
+ $folderName = switch ($BuildType) {
+ "RelWithDebInfo" { "release" }
+ "Debug" { "debug" }
+ default { $BuildType.ToLower() }
+ }
+ $buildDir = "$TOPDIR\build_$folderName"
if (-not (Test-Path $buildDir)) {
New-Item -ItemType Directory -Path $buildDir | Out-Null
}
@@ -300,9 +316,9 @@ function Do-Build {
"-DOB_VCPKG_DIR=$DefaultVcpkgDir",
"-DOB_OPENSSL_DIR=$DefaultOpenSSLDir",
"-DOB_LLVM_DIR=$DefaultLLVMDir"
- ) + $ExtraCMakeArgs
+ ) + $ExtraCMakeArgs + $ExtraCmake
- Write-Log "CMake configure: build_$($BuildType.ToLower())"
+ Write-Log "CMake configure: build_$folderName"
Write-Log " Build type : $BuildType"
Write-Log " VcpkgDir : $DefaultVcpkgDir"
Write-Log " OpenSSLDir : $DefaultOpenSSLDir"
@@ -334,12 +350,15 @@ function Do-Build {
# -- ninja build -----------------------------------------------------
function Do-Ninja {
- param([string]$BuildDir)
+ param(
+ [string]$BuildDir,
+ [string]$Target = "observer"
+ )
- Write-Log "Building with Ninja (-j $Jobs) in $BuildDir ..."
+ Write-Log "Building with Ninja (-j $Jobs) target=$Target in $BuildDir ..."
Push-Location $BuildDir
try {
- & ninja -j $Jobs observer | Out-Host
+ & ninja -j $Jobs $Target | Out-Host
if ($LASTEXITCODE -ne 0) {
Write-Err "Build failed (exit code $LASTEXITCODE)"
exit $LASTEXITCODE
@@ -404,7 +423,7 @@ function Do-Package {
}
$buildDir = Do-Build -BuildType "RelWithDebInfo" -ExtraCMakeArgs @("-DOB_BUILD_PACKAGE=ON")
- Do-Ninja -BuildDir $buildDir
+ Do-Ninja -BuildDir $buildDir -Target observer
# Sign binaries before they are packaged into the MSI
$exesToSign = @(
@@ -476,12 +495,12 @@ switch ($Action.ToLower()) {
{ $_ -in "release", "relwithdebinfo" } {
if ($Init) { Do-Init }
$buildDir = Do-Build -BuildType "RelWithDebInfo"
- if ($Ninja) { Do-Ninja -BuildDir $buildDir }
+ 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 }
+ if ($Ninja) { Do-Ninja -BuildDir $buildDir -Target $NinjaTarget }
}
"-h" {
Show-Usage
diff --git a/build.sh b/build.sh
index a647f3169..0ee00fa5d 100755
--- a/build.sh
+++ b/build.sh
@@ -6,13 +6,11 @@ BUILD_SH=$TOPDIR/build.sh
DEP_DIR=${TOPDIR}/deps/3rd/usr/local/oceanbase/deps/devel
TOOLS_DIR=${TOPDIR}/deps/3rd/usr/local/oceanbase/devtools
-# Get CPU cores and CMAKE command, compatible with macOS and Linux
+# Get CPU cores; cmake path is resolved in do_build() (Linux may use host cmake before deps devtools exist)
if [[ "$(uname -s)" == "Darwin" ]]; then
- CMAKE_COMMAND="cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1"
CPU_CORES=$(sysctl -n hw.ncpu)
KERNEL_RELEASE=""
else
- CMAKE_COMMAND="${TOOLS_DIR}/bin/cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1"
CPU_CORES=$(grep -c ^processor /proc/cpuinfo)
KERNEL_RELEASE=$(grep -Po 'release [0-9]{1}' /etc/issue 2>/dev/null)
fi
@@ -166,22 +164,25 @@ function do_init
# make build directory && cmake && make (if need)
function do_build
{
- # Check if cmake exists, compatible with macOS and Linux
+ # Resolve cmake: prefer OceanBase devtools on Linux; else Homebrew/common paths; else PATH (e.g. apt cmake on CI)
CMAKE_PATH=""
if [[ "$(uname -s)" == "Darwin" ]]; then
- # macOS: cmake may be at /opt/homebrew/bin/cmake or /usr/local/bin/cmake
if [ -f /opt/homebrew/bin/cmake ]; then
CMAKE_PATH="/opt/homebrew/bin/cmake"
elif [ -f /usr/local/bin/cmake ]; then
CMAKE_PATH="/usr/local/bin/cmake"
fi
else
- # Linux
- CMAKE_PATH="${TOOLS_DIR}/bin/cmake"
+ if [[ -x "${TOOLS_DIR}/bin/cmake" ]]; then
+ CMAKE_PATH="${TOOLS_DIR}/bin/cmake"
+ fi
+ fi
+ if [[ -z "$CMAKE_PATH" ]] && command -v cmake >/dev/null 2>&1; then
+ CMAKE_PATH="$(command -v cmake)"
fi
if [ -z "$CMAKE_PATH" ]; then
- echo_log "[NOTICE] Your workspace has not initialized dependencies, please append '--init' args to initialize dependencies"
+ echo_log "[NOTICE] cmake not found. On Linux install cmake (e.g. apt install cmake) or run --init so ${TOOLS_DIR}/bin/cmake exists"
exit 1
fi
@@ -200,7 +201,7 @@ function do_build
echo_log "Android NDK: $ANDROID_NDK_HOME"
fi
- ${CMAKE_COMMAND} ${TOPDIR} ${ANDROID_CMAKE_ARGS} "$@"
+ "${CMAKE_PATH}" -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ${TOPDIR} ${ANDROID_CMAKE_ARGS} "$@"
if [ $? -ne 0 ]; then
echo_err "Failed to generate Makefile"
exit 1
diff --git a/cmake/Env.cmake b/cmake/Env.cmake
index 5f70306e9..13c3c7830 100644
--- a/cmake/Env.cmake
+++ b/cmake/Env.cmake
@@ -209,9 +209,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)
+ # 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})
- set(_NDK_TOOLCHAIN_BIN "$ENV{ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/darwin-x86_64/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)
@@ -266,9 +272,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})
diff --git a/deps/init/dep_create.sh b/deps/init/dep_create.sh
index bb31682af..7232cf51e 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/oblib/src/lib/cpu/ob_cpu_topology.cpp b/deps/oblib/src/lib/cpu/ob_cpu_topology.cpp
index 061e07d15..2d1ab43ca 100644
--- a/deps/oblib/src/lib/cpu/ob_cpu_topology.cpp
+++ b/deps/oblib/src/lib/cpu/ob_cpu_topology.cpp
@@ -75,7 +75,12 @@ int CpuFlagSet::init_from_os(uint64_t& flags)
{
int ret = OB_SUCCESS;
flags = 0;
-#if defined(__linux__)
+#if defined(__ANDROID__)
+ // Android defines __linux__ as well, but app processes are sandboxed and shelling
+ // out to grep /proc/cpuinfo via system()/posix_spawn() is unsafe during startup.
+ // Rely on direct CPU probing instead.
+ init_from_cpu(flags);
+#elif defined(__linux__)
const char* const CPU_FLAG_CMDS[(int)CpuFlag::MAX] = {"grep -E ' sse4_2( |$)' /proc/cpuinfo > /dev/null 2>&1",
"grep -E ' avx( |$)' /proc/cpuinfo > /dev/null 2>&1",
"grep -E ' avx2( |$)' /proc/cpuinfo > /dev/null 2>&1",
@@ -95,8 +100,8 @@ int CpuFlagSet::init_from_os(uint64_t& flags)
flags |= (1 << i);
}
}
-#elif defined(__APPLE__) || defined(__ANDROID__)
- // On macOS/Android, /proc/cpuinfo doesn't exist or SSE/AVX features are irrelevant.
+#elif defined(__APPLE__)
+ // On macOS, /proc/cpuinfo doesn't exist or SSE/AVX features are irrelevant.
// We can use sysctl to check for features, but for now we rely on init_from_cpu
// and just return success here with flags set to a reasonable default or
// matched with cpu flags to avoid mismatch error in constructor.
diff --git a/deps/oblib/src/lib/signal/ob_signal_handlers.cpp b/deps/oblib/src/lib/signal/ob_signal_handlers.cpp
index 4ececf343..c40ab4f99 100644
--- a/deps/oblib/src/lib/signal/ob_signal_handlers.cpp
+++ b/deps/oblib/src/lib/signal/ob_signal_handlers.cpp
@@ -99,6 +99,13 @@ static inline void handler(int sig, siginfo_t *s, void *p)
int install_ob_signal_handler()
{
int ret = OB_SUCCESS;
+#ifdef __ANDROID__
+ // ART/libsigchain owns fatal signal dispatch for app processes. Installing
+ // OceanBase's process-wide crash handlers here interferes with JNI/runtime
+ // fault handling and can turn startup failures into opaque instrumentation
+ // crashes.
+ return ret;
+#else
struct sigaction sa;
sa.sa_flags = SA_SIGINFO | SA_RESTART | SA_NODEFER | SA_ONSTACK;
sa.sa_sigaction = handler;
@@ -114,6 +121,7 @@ int install_ob_signal_handler()
}
}
return ret;
+#endif
}
bool g_redirect_handler = false;
diff --git a/docs/developer-guide/en/android.md b/docs/developer-guide/en/android.md
index 367498055..658d060d9 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
```
@@ -114,4 +136,4 @@ adb shell "tail -100 /data/local/tmp/seekdb_data/log/seekdb.log"
```bash
adb shell "kill \$(pidof seekdb)"
-```
\ No newline at end of file
+```
diff --git a/docs/developer-guide/zh/android.md b/docs/developer-guide/zh/android.md
index 7dae627e1..77f4c63d1 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 000000000..1fe504f8e
--- /dev/null
+++ b/package/libseekdb/README.md
@@ -0,0 +1,86 @@
+# 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-14 (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`.
+
+### macOS compatibility
+
+CI macOS builds use **macOS 14** 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.
+
+## 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/ # Dependency dylibs (macOS only; collected by dylibbundler)
+ *.dylib
+```
+
+- **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.
+- **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 14** 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/libseekdb-build.ps1 b/package/libseekdb/libseekdb-build.ps1
new file mode 100644
index 000000000..9bbe4527b
--- /dev/null
+++ b/package/libseekdb/libseekdb-build.ps1
@@ -0,0 +1,55 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Pack libseekdb for Windows into libseekdb-windows-x64.zip (seekdb.h, seekdb.dll, seekdb.lib).
+
+.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
+$BuildType = if ($env:BUILD_TYPE) { $env:BUILD_TYPE } else { "release" }
+
+$WorkDir = if ($IncludeDir) {
+ (Resolve-Path $IncludeDir).Path
+} else {
+ Join-Path $TopDir "build_$BuildType\src\include"
+}
+
+$Dll = Join-Path $WorkDir "seekdb.dll"
+$Lib = Join-Path $WorkDir "seekdb.lib"
+if (-not (Test-Path $Dll)) {
+ Write-Error "seekdb.dll not found under $WorkDir (build libseekdb first: .\build.ps1 release --ninja --target libseekdb -DBUILD_EMBED_MODE=ON)"
+}
+
+$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())
+New-Item -ItemType Directory -Path $Staging -Force | Out-Null
+
+try {
+ Copy-Item $Header (Join-Path $Staging "seekdb.h")
+ Copy-Item $Dll (Join-Path $Staging "seekdb.dll")
+ if (Test-Path $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 000000000..83264289c
--- /dev/null
+++ b/package/libseekdb/libseekdb-build.sh
@@ -0,0 +1,259 @@
+#!/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/).
+#
+# 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; }
+
+# 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 [[ "$ANDROID_PACK" == true ]]; then
+ if [[ ! -d "$BUILD_DIR" ]]; then
+ (cd "$TOP_DIR" && ./build.sh "$BUILD_TYPE" --android --init --make) || exit 1
+ else
+ (cd "$TOP_DIR" && ./build.sh "$BUILD_TYPE" --android --make) || exit 1
+ fi
+ _j=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
+ (cd "$BUILD_DIR" && make libseekdb -j"${_j}") || exit 1
+ else
+ if [[ ! -d "$BUILD_DIR" ]]; then
+ (cd "$TOP_DIR" && ./build.sh "$BUILD_TYPE" --init --make) || exit 1
+ else
+ (cd "$TOP_DIR" && ./build.sh "$BUILD_TYPE" --make) || exit 1
+ fi
+ 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"
+ (cd "$TOP_DIR" && ./build.sh "$BUILD_TYPE" -DBUILD_EMBED_MODE=ON --make) || 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 100644
index 000000000..14133cd9d
--- /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/src/CMakeLists.txt b/src/CMakeLists.txt
index c138ac0f8..1d718dc00 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -41,3 +41,4 @@ else()
add_subdirectory(observer)
endif()
add_subdirectory(plugin)
+add_subdirectory(include)
\ No newline at end of file
diff --git a/src/include/CMakeLists.txt b/src/include/CMakeLists.txt
new file mode 100644
index 000000000..d52123daf
--- /dev/null
+++ b/src/include/CMakeLists.txt
@@ -0,0 +1,178 @@
+# C API for SeekDB
+# Provides C API for multi-language bindings (Node.js N-API, FFI, Rust, Go, etc.)
+
+# Note: ENABLE_INITIAL_EXEC_TLS_MODEL removal is handled in src/CMakeLists.txt
+# before observer subdirectory is added, ensuring liboceanbase.so uses local-dynamic TLS
+# TLS model is controlled by BUILD_EMBED_MODE in cmake/Env.cmake
+
+# 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
+)
+
+# 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 to avoid -all_load pulling in both
+ # oblib (with its snappy/lz4) and deps/3rd liblz4.a/libsnappy.a, which causes 68 duplicate symbols.
+ # Linking oceanbase_static without -all_load may yield undefined symbols; if so we need to revisit.
+ target_link_options(libseekdb PRIVATE "LINKER:-force_load,$")
+ target_link_libraries(libseekdb
+ PRIVATE
+ oceanbase_static
+ -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
+ -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:$")
+ 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 is linked only via /WHOLEARCHIVE above. ob_malloc + compress splits mirror observer_without_bolt on Windows.
+ target_link_libraries(libseekdb PRIVATE
+ ob_malloc
+ synchronization
+ ${CMAKE_BINARY_DIR}/deps/oblib/src/lib/compress/zstd_1_3_8/zstd_1_3_8_objs.lib
+ ${CMAKE_BINARY_DIR}/deps/oblib/src/lib/compress/zstd/zstd_objs.lib
+ ${CMAKE_BINARY_DIR}/deps/oblib/src/lib/compress/lz4/lz4-all.lib
+ )
+else()
+ target_link_libraries(libseekdb
+ PRIVATE
+ -Wl,--whole-archive
+ oceanbase_static
+ -Wl,--no-whole-archive
+ -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 000000000..acd8986f1
--- /dev/null
+++ b/src/include/seekdb.cpp
@@ -0,0 +1,5268 @@
+/*
+ * 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 "common/ob_common_utility.h" // For set_stackattr()
+#include "lib/mysqlclient/ob_mysql_proxy.h"
+#include "lib/mysqlclient/ob_mysql_result.h"
+#include "lib/string/ob_string.h"
+#include "lib/allocator/ob_malloc.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 "share/rc/ob_tenant_base.h" // MTL_SWITCH for read_lob_data (ObLobManager)
+#include "share/system_variable/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_TENANT_NAME, OB_SYS_USER_ID
+#include "lib/profile/ob_trace_id.h" // For ObCurTraceId
+#include "observer/omt/ob_tenant_node_balancer.h" // For ObTenantNodeBalancer
+#include "libtable/src/libobtable.h" // For ObTableServiceLibrary
+#include "lib/worker.h" // For lib::Worker
+#include "logservice/palf/election/interface/election.h" // For palf::election::INIT_TS
+#include "share/ob_thread_mgr.h" // For ob_init_create_func
+#include "lib/thread/threads.h" // For global_thread_stack_size
+#include "lib/signal/ob_signal_struct.h" // For SIG_STACK_SIZE
+#include "lib/alloc/alloc_assist.h" // For ACHUNK_PRESERVE_SIZE
+#include "common/ob_smart_call.h" // For CALL_WITH_NEW_STACK
+#include
+#include
+#ifdef __APPLE__
+#include // statfs on macOS
+#else
+#include
+#endif
+#include // For mmap/munmap
+#include
+#include
+#include
+
+using namespace oceanbase::table;
+using namespace oceanbase::common;
+using namespace oceanbase::sqlclient;
+using namespace oceanbase::observer;
+using namespace oceanbase::sql;
+using namespace oceanbase::share;
+namespace share = oceanbase::share; // MTL_SWITCH macro expands to share::ObTenantSwitchGuard
+using namespace oceanbase::lib;
+using namespace oceanbase::omt;
+using namespace oceanbase::palf::election;
+
+// 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() {
+ 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);
+ }
+ }
+ ~SuppressLogStdoutScope() {
+ if (saved_stderr >= 0) { dup2(saved_stderr, STDERR_FILENO); close(saved_stderr); }
+ if (saved_stdout >= 0) { dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); }
+ }
+};
+
+// 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 in connection for seekdb_affected_rows()
+// Define this early so it can be used in seekdb_execute_update()
+// Use unsigned long long directly since my_ulonglong is defined in ob_mysql_global.h
+static thread_local unsigned long long g_last_affected_rows = 0;
+
+// 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 {
+ 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;
+
+ SeekdbConnection() : embed_conn(nullptr), embed_session(nullptr),
+ embed_result(nullptr), last_result_set(nullptr),
+ use_result_set(nullptr), current_result_index(-1), initialized(false) {}
+ ~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 (using OBSERVER macro like Python embed)
+ if (embed_conn) {
+ OBSERVER.get_inner_sql_conn_pool().release(embed_conn, true);
+ embed_conn = nullptr;
+ }
+ // Release session (like Python embed does)
+ if (embed_session) {
+ uint32_t sid = embed_session->get_sid();
+ GCTX.session_mgr_->revert_session(embed_session);
+ GCTX.session_mgr_->mark_sessid_unused(sid);
+ 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;
+}
+
+// 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
+static struct sigaction g_old_segv_handler; // Store original SIGSEGV handler
+static bool g_segv_handler_installed = false;
+static struct sigaction g_old_sigabrt_handler; // Store original SIGABRT handler
+static bool g_sigabrt_handler_installed = false;
+static struct sigaction g_old_sigbus_handler; // Store original SIGBUS handler
+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;
+
+// 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);
+ }
+}
+
+// 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 int to_absolute_path(const char* cwd, ObSqlString& dir) {
+ int ret = OB_SUCCESS;
+ if (!dir.empty() && dir.ptr()[0] != '\0' && dir.ptr()[0] != '/') {
+ 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) {
+ 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;
+ 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 - SIG_STACK_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;
+ }
+
+ // 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;
+ }
+}
+
+// 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 (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);
+ 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 limit to unlimited before init (aligned with main.cpp inner_main)
+ // This is critical for fork scenarios where memory limits may be inherited
+ oceanbase::lib::set_memory_limit(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;
+ }
+
+ // Initialize thread group creation functions BEFORE ObTableServiceLibrary::init()
+ // This is critical for dynamic library linking, where static initialization order may be uncertain
+ // This ensures ServerGTimer and other observer-specific Timer Groups are registered
+ // before any thread group operations are attempted
+ // Use init_create_func() (strong version) which calls both lib_init_create_func() and ob_init_create_func()
+ try {
+ oceanbase::lib::init_create_func();
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // Mark create_func_inited_ as true to prevent TGMgr constructor from calling weak init_create_func()
+ // This ensures that the strong version (with ob_init_create_func()) is used
+ // Note: create_func_inited_ is declared as extern in thread_mgr.h
+ oceanbase::lib::create_func_inited_ = true;
+
+ // CRITICAL: TGMgr::instance() is a static singleton, so it may have been initialized
+ // before init_create_func() was called. We need to ensure TGMgr is re-initialized
+ // or that all Timer Groups are created after init_create_func().
+ // However, since TGMgr uses a static singleton, we cannot re-initialize it.
+ // Instead, we must ensure init_create_func() is called BEFORE any code that might
+ // trigger TGMgr::instance() initialization.
+ //
+ // For now, we just call TGMgr::instance() to ensure it's initialized with the
+ // correct create_funcs_ (which we just set via init_create_func()).
+ // If TGMgr was already initialized, its constructor would have called the weak
+ // init_create_func(), so we need to manually create any missing Timer Groups.
+ oceanbase::lib::TGMgr* tg_mgr_ptr = nullptr;
+ try {
+ tg_mgr_ptr = &oceanbase::lib::TGMgr::instance();
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+ oceanbase::lib::TGMgr& tg_mgr = *tg_mgr_ptr;
+
+ // After TGMgr is initialized, manually create all missing system-level Timer Groups
+ // This is a workaround for the case where TGMgr was initialized before ob_init_create_func()
+ // CRITICAL: For system-level Timer Groups (tg_def_id < TGDefIDs::END), the tg_id should equal tg_def_id
+ // because TG_START uses the enum value as the index. We need to ensure tgs_[enum_value] is set.
+ //
+ // TGMgr constructor creates Timer Groups for i=0 to i=255, but if create_funcs_[i] was null at that time,
+ // it only allocates an ID without creating the Timer Group object. We need to recreate them now.
+ // We try to create all missing Timer Groups. If create_funcs_[i] is null, create_tg will just allocate an ID.
+ // Only recreate Timer Groups that have create_funcs_ set (i.e., those that should have been created)
+ int recreated_count = 0;
+ try {
+ for (int i = 0; i < oceanbase::lib::TGDefIDs::END; i++) {
+ if (tg_mgr.tgs_[i] == nullptr) {
+ // Try to recreate the Timer Group. If create_funcs_[i] is now set, it will create the object.
+ int allocated_tg_id = -1;
+ int ret = tg_mgr.create_tg(i, allocated_tg_id, 0);
+ if (ret == oceanbase::common::OB_SUCCESS && allocated_tg_id >= 0 && allocated_tg_id != i && tg_mgr.tgs_[allocated_tg_id] != nullptr) {
+ // Move the Timer Group from allocated_tg_id to i (enum value)
+ tg_mgr.tgs_[i] = tg_mgr.tgs_[allocated_tg_id];
+ tg_mgr.tgs_[allocated_tg_id] = nullptr;
+ tg_mgr.free_tg_id(allocated_tg_id);
+ recreated_count++;
+ }
+ }
+ }
+ } catch (const std::exception& e) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // Note: We do NOT call ObTableServiceLibrary::init() here, aligning with Python embed
+ // Python embed directly calls OBSERVER.init() without ObTableServiceLibrary::init()
+ // ObTableServiceLibrary::init() is mainly for Table Service Client (server mode)
+ // In embedded mode, observer.init() should initialize all required resources
+
+
+ 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.embed_mode_ = embed_mode; // Set in opts for init() to use
+ 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 (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;
+ }
+
+
+ struct statfs fs_info;
+#ifndef TMPFS_MAGIC
+ const long TMPFS_MAGIC = 0x01021994;
+#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");
+ } 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");
+ } else if (-1 == 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 == static_cast(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';
+
+ OB_LOGGER.set_log_level("INFO");
+ // 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);
+
+ // TGMgr is already initialized earlier (after ob_init_create_func())
+ // No need to call it again here
+
+ ObPLogWriterCfg log_cfg;
+
+ // Use OBSERVER macro directly like Python embed does
+
+ // Set election INIT TS (aligned with main.cpp)
+ ATOMIC_STORE(&INIT_TS, get_monotonic_ts());
+
+ 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)
+ // Pass embed_mode directly (same value as opts.embed_mode_ used in init())
+ if (OB_SUCC(ret) && OB_FAIL(OBSERVER.start(embed_mode))) {
+ // 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 == 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 for service ready (aligned with Python embed - infinite wait)
+ while (true) {
+ if (OB_ISNULL(GCTX.root_service_)) {
+ // root_service_ not ready yet, wait
+ ob_usleep(100 * 1000); // 100ms
+ } else if (GCTX.root_service_->is_full_service()) {
+ break;
+ } else {
+ ob_usleep(100 * 1000); // 100ms
+ }
+ }
+ FLOG_INFO("seekdb start success ", "cost", ObTimeUtility::current_time() - start_time);
+ // Handle tenant node balancer (aligned with Python embed)
+ ObTenantNodeBalancer::get_instance().handle();
+ }
+ // 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) {
+ 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 (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 = ::mmap(nullptr, stack_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (MAP_FAILED == stack_addr) {
+ 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);
+
+ if (-1 == ::munmap(stack_addr, stack_size)) {
+ // munmap failed, but we still return the open result
+ // This is non-fatal as the memory will be reclaimed on process exit
+ }
+
+ 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 (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 = ::mmap(nullptr, stack_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (MAP_FAILED == stack_addr) {
+ 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);
+
+ if (-1 == ::munmap(stack_addr, stack_size)) {
+ // munmap failed, but we still return the open result
+ // This is non-fatal as the memory will be reclaimed on process exit
+ }
+
+ return result;
+}
+
+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;
+
+ // 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);
+ }
+
+ // Note: We skip observer.destroy() because:
+ // 1. It may cause segfault/OB_ABORT during cleanup (static destructor ordering issues)
+ // 2. The process will exit anyway, and OS will reclaim all resources
+ // 3. This aligns with common practice for embedded databases
+
+ // Only clean up the PID file
+ if (g_embedded_pid_locked) {
+ 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;
+ uint64_t tenant_id = session->get_effective_tenant_id();
+ oceanbase::common::ObArray tenant_ids;
+ if (OB_SUCCESS == tenant_ids.push_back(tenant_id)) {
+ (void)GCTX.schema_service_->refresh_and_add_schema(tenant_ids, false);
+ }
+ int64_t schema_version = OB_INVALID_VERSION;
+ oceanbase::share::schema::ObServerSchemaService* server_svc =
+ static_cast(GCTX.schema_service_);
+ if (OB_SUCCESS == server_svc->get_tenant_schema_version(tenant_id, schema_version)
+ && OB_INVALID_VERSION != schema_version) {
+ (void)session->update_sys_variable(oceanbase::share::SYS_VAR_OB_LAST_SCHEMA_VERSION, schema_version);
+ return;
+ }
+ if (OB_SUCCESS != GCTX.schema_service_->get_tenant_refreshed_schema_version(tenant_id, schema_version)
+ || OB_INVALID_VERSION == schema_version) {
+ (void)oceanbase::sql::ObSQLUtils::update_session_last_schema_version(*GCTX.schema_service_, *session);
+ return;
+ }
+ (void)session->update_sys_variable(oceanbase::share::SYS_VAR_OB_LAST_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;
+ uint64_t tenant_id = session->get_effective_tenant_id();
+ int64_t local_version = OB_INVALID_VERSION;
+ int64_t last_version = OB_INVALID_VERSION;
+ if (OB_SUCCESS != GCTX.schema_service_->get_tenant_refreshed_schema_version(tenant_id, local_version)) {
+ refresh_session_schema_version(session);
+ return;
+ }
+ if (OB_SUCCESS != session->get_ob_last_schema_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;
+ sqlclient::ObISQLConnection* inner_conn = nullptr;
+ 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(OB_SYS_TENANT_ID, sid, 0,
+ ObTimeUtility::current_time(), session))) {
+ GCTX.session_mgr_->mark_sessid_unused(sid);
+ 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_tenant_schema_guard(OB_SYS_TENANT_ID, 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_TENANT_ID, 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(OB_SYS_TENANT_ID, 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->init_tenant(OB_SYS_TENANT_NAME, OB_SYS_TENANT_ID))) {
+ set_error(conn, "init_tenant 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());
+ session->init_use_rich_format();
+ 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(OB_SYS_TENANT_ID,
+ 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(OBSERVER.get_inner_sql_conn_pool().acquire(session, inner_conn))) {
+ set_error(conn, "acquire conn failed");
+ if (session) {
+ GCTX.session_mgr_->revert_session(session);
+ GCTX.session_mgr_->mark_sessid_unused(sid);
+ }
+ delete conn;
+ params->result = SEEKDB_ERROR_CONNECTION_FAILED;
+ return OB_SUCCESS;
+ } else {
+ conn->embed_conn = static_cast(inner_conn);
+ 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);
+ GCTX.session_mgr_->mark_sessid_unused(sid);
+ }
+ 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(OB_SYS_TENANT_ID, "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(OB_SYS_TENANT_ID, 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) {
+ if (wb->get_err_code() == ret ||
+ (ret >= OB_MIN_RAISE_APPLICATION_ERROR && ret <= OB_MAX_RAISE_APPLICATION_ERROR)) {
+ 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, false));
+ }
+ if (errmsg.empty()) {
+ errmsg = "Query execution failed";
+ }
+ 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 (inner_result->has_tenant_resource()) {
+ fields = inner_result->result_set().get_field_columns();
+ } else {
+ // For remote results, try to get from remote_result_set
+ // Note: This may not be available in all cases
+ }
+
+ 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 (MTL_SWITCH so read_lob_data can access ObLobManager for out-of-row LOB, e.g. 100KB)
+ int64_t row_count = 0;
+ MTL_SWITCH(OB_SYS_TENANT_ID) {
+ 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)) {
+ // String/TEXT types: full content via get_string; LOB use read_lob_data when get_string fails.
+ ObString str_val;
+ int get_ret = obj.get_string(str_val);
+ if (OB_SUCCESS == get_ret) {
+ if (str_val.length() > 0 && str_val.ptr()) {
+ row.push_back(std::string(str_val.ptr(), str_val.length()));
+ } else {
+ 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_text_tc(obj_type)) && obj.is_lob_storage()) {
+ ObString lob_str;
+ if (OB_SUCCESS == obj.read_lob_data(row_lob_allocator, lob_str)) {
+ if (lob_str.ptr() && lob_str.length() > 0) {
+ row.push_back(std::string(lob_str.ptr(), lob_str.length()));
+ } else {
+ row.push_back("");
+ }
+ } else {
+ row.push_back("");
+ }
+ row_null.push_back(false);
+ } else {
+ 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_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;
+ } // MTL_SWITCH
+
+ // 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) {
+ g_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_tenant_schema_guard(OB_SYS_TENANT_ID, 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(OB_SYS_TENANT_ID, 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(OB_SYS_TENANT_ID, 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 {
+ set_error(conn, "Update execution failed");
+ 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) {
+ g_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(OB_SYS_TENANT_ID))) {
+ 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) {
+ // Return the last affected rows count
+ // This should be set after INSERT/UPDATE/DELETE operations
+ return g_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(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!conn->embed_conn || !conn->embed_session) {
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ // Execute a simple query to check if connection is alive
+ SeekdbResult result = nullptr;
+ int ret = seekdb_query(handle, "SELECT 1", &result);
+ if (ret == SEEKDB_SUCCESS && result) {
+ seekdb_result_free(result);
+ return SEEKDB_SUCCESS;
+ }
+
+ return SEEKDB_ERROR_CONNECTION_FAILED;
+}
+
+// Store server version in connection for seekdb_get_server_info()
+static thread_local std::string g_server_version;
+
+const char* seekdb_get_server_info(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return nullptr;
+ }
+
+ if (!conn->embed_conn || !conn->embed_session) {
+ return nullptr;
+ }
+
+ // Execute SELECT VERSION() to get server version
+ SeekdbResult result = nullptr;
+ int ret = seekdb_query(handle, "SELECT VERSION() as version", &result);
+ if (ret != SEEKDB_SUCCESS || !result) {
+ return nullptr;
+ }
+
+ SeekdbRow row = seekdb_fetch_row(result);
+ if (row) {
+ char buf[256];
+ if (seekdb_row_get_string(row, 0, buf, sizeof(buf)) == SEEKDB_SUCCESS) {
+ g_server_version = std::string(buf);
+ }
+ }
+ seekdb_result_free(result);
+ return g_server_version.c_str();
+}
+
+const char* seekdb_character_set_name(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !conn->embed_session) {
+ return nullptr;
+ }
+
+ // Get character set from session
+ ObObj charset_obj;
+ if (OB_SUCCESS != conn->embed_session->get_sys_variable(SYS_VAR_CHARACTER_SET_CONNECTION, charset_obj)) {
+ return nullptr;
+ }
+
+ int64_t collation_type = 0;
+ if (OB_SUCCESS != charset_obj.get_int(collation_type)) {
+ return nullptr;
+ }
+
+ // Convert collation type to charset name
+ ObCollationType coll_type = static_cast(collation_type);
+ ObCharsetType cs_type = ObCharset::charset_type_by_coll(coll_type);
+ const char* cs_name = ObCharset::charset_name(cs_type);
+
+ // Store in connection for lifetime management
+ static thread_local std::string g_charset_name;
+ if (cs_name) {
+ g_charset_name = std::string(cs_name);
+ return g_charset_name.c_str();
+ }
+
+ return nullptr;
+}
+
+int seekdb_set_character_set(SeekdbHandle handle, const char* csname) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !conn->embed_session || !csname) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Build SET NAMES statement
+ char sql[256];
+ int ret = snprintf(sql, sizeof(sql), "SET NAMES %s", csname);
+ if (ret < 0 || ret >= static_cast(sizeof(sql))) {
+ set_error(conn, "Character set name too long");
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Execute SET NAMES
+ SeekdbResult result = nullptr;
+ int query_ret = seekdb_query(handle, sql, &result);
+ if (query_ret != SEEKDB_SUCCESS) {
+ set_error(conn, "Failed to set character set");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+
+ if (result) {
+ seekdb_result_free(result);
+ }
+
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_select_db(SeekdbHandle handle, const char* db) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !db) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Build USE statement
+ char sql[512];
+ int ret = snprintf(sql, sizeof(sql), "USE %s", db);
+ if (ret < 0 || ret >= static_cast(sizeof(sql))) {
+ set_error(conn, "Database name too long");
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Execute USE
+ SeekdbResult result = nullptr;
+ int query_ret = seekdb_query(handle, sql, &result);
+ if (query_ret != SEEKDB_SUCCESS) {
+ set_error(conn, "Failed to select database");
+ return SEEKDB_ERROR_QUERY_FAILED;
+ }
+
+ if (result) {
+ seekdb_result_free(result);
+ }
+ // Align with MySQL protocol: after USE db, check and refresh only when server is behind session.
+ if (conn->embed_session) {
+ check_and_refresh_schema_for_embed(conn->embed_session);
+ }
+
+ return SEEKDB_SUCCESS;
+}
+
+const char* seekdb_get_host_info(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return nullptr;
+ }
+
+ // For embedded mode, return "localhost via TCP/IP" or similar
+ static thread_local std::string g_host_info = "localhost via embedded connection";
+ return g_host_info.c_str();
+}
+
+const char* seekdb_get_client_info(void) {
+ // Return client version
+ static const char* client_info = "SeekDB Embedded Client";
+ return client_info;
+}
+
+const char* seekdb_info(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return nullptr;
+ }
+
+ // Get info from last query (e.g., "Rows matched: 5 Changed: 5 Warnings: 0")
+ // For now, return a simple message
+ static thread_local std::string g_info;
+ my_ulonglong affected = seekdb_affected_rows(handle);
+ char buf[256];
+ snprintf(buf, sizeof(buf), "Rows matched: %llu Changed: %llu Warnings: 0",
+ affected, affected);
+ g_info = std::string(buf);
+ return g_info.c_str();
+}
+
+unsigned int seekdb_warning_count(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !conn->embed_session) {
+ return 0;
+ }
+
+ // Get warning count from session
+ const oceanbase::common::ObWarningBuffer *wb = oceanbase::common::ob_get_tsi_warning_buffer();
+ if (wb) {
+ return static_cast(wb->get_total_warning_count());
+ }
+
+ return 0;
+}
+
+const char* seekdb_sqlstate(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return nullptr;
+ }
+
+ // Map error code to SQLSTATE
+ // For now, return a generic SQLSTATE based on error code
+ static thread_local char sqlstate[6] = "00000";
+ unsigned int errno_val = seekdb_errno(handle);
+
+ // Map common error codes to SQLSTATE
+ // This is a simplified mapping
+ if (errno_val == SEEKDB_SUCCESS) {
+ strcpy(sqlstate, "00000"); // Success
+ } else if (errno_val == SEEKDB_ERROR_CONNECTION_FAILED) {
+ strcpy(sqlstate, "08001"); // SQLSTATE for connection failure
+ } else if (errno_val == SEEKDB_ERROR_QUERY_FAILED) {
+ strcpy(sqlstate, "42000"); // SQLSTATE for syntax error or access rule violation
+ } else if (errno_val == SEEKDB_ERROR_INVALID_PARAM) {
+ strcpy(sqlstate, "HY000"); // General error
+ } else {
+ strcpy(sqlstate, "HY000"); // General error
+ }
+
+ return sqlstate;
+}
+
+int seekdb_get_character_set_info(SeekdbHandle handle, const char* csname, SeekdbCharsetInfo* charset_info) {
+ if (!handle || !csname || !charset_info) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ // Get charset type from name
+ ObCharsetType cs_type = ObCharset::charset_type(ObString(csname));
+ if (cs_type == CHARSET_INVALID) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Fill charset info
+ memset(charset_info, 0, sizeof(SeekdbCharsetInfo));
+ charset_info->number = static_cast(cs_type);
+
+ // Get charset name
+ const char* cs_name = ObCharset::charset_name(cs_type);
+ if (cs_name) {
+ charset_info->name = cs_name;
+ }
+
+ // Get default collation
+ ObCollationType coll_type = ObCharset::get_default_collation(cs_type);
+ ObString coll_name;
+ if (OB_SUCCESS == ObCharset::collation_name(coll_type, coll_name)) {
+ // Store in thread-local buffer
+ static thread_local char coll_name_buf[64];
+ size_t copy_len = std::min(coll_name.length(), static_cast(sizeof(coll_name_buf) - 1));
+ memcpy(coll_name_buf, coll_name.ptr(), copy_len);
+ coll_name_buf[copy_len] = '\0';
+ charset_info->collation = coll_name_buf;
+ }
+
+ // Set comment (simplified - use charset name as comment)
+ charset_info->comment = cs_name;
+
+ return SEEKDB_SUCCESS;
+}
+
+unsigned long seekdb_real_escape_string(SeekdbHandle handle, char* to, unsigned long to_len,
+ const char* from, unsigned long from_len) {
+ if (!handle || !to || to_len == 0 || !from || from_len == 0) {
+ return static_cast(-1);
+ }
+
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return static_cast(-1);
+ }
+
+ // Escape special characters: \0, \n, \r, \, ', ", and \x1a
+ unsigned long j = 0;
+ for (unsigned long i = 0; i < from_len && j < to_len - 1; i++) {
+ char c = from[i];
+ switch (c) {
+ case '\0':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '0';
+ }
+ break;
+ case '\n':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = 'n';
+ }
+ break;
+ case '\r':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = 'r';
+ }
+ break;
+ case '\\':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '\\';
+ }
+ break;
+ case '\'':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '\'';
+ }
+ break;
+ case '"':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '"';
+ }
+ break;
+ case '\x1a': // Ctrl+Z
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = 'Z';
+ }
+ break;
+ default:
+ to[j++] = c;
+ break;
+ }
+ }
+
+ to[j] = '\0';
+ return j;
+}
+
+unsigned long seekdb_real_escape_string_quote(SeekdbHandle handle, char* to, unsigned long to_len,
+ const char* from, unsigned long from_len, char quote) {
+ if (!handle || !to || to_len == 0 || !from || from_len == 0) {
+ return static_cast(-1);
+ }
+
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return static_cast(-1);
+ }
+
+ // Escape special characters, considering quote context
+ // If quote is '\'', escape single quotes; if quote is '"', escape double quotes
+ unsigned long j = 0;
+ for (unsigned long i = 0; i < from_len && j < to_len - 1; i++) {
+ char c = from[i];
+ switch (c) {
+ case '\0':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '0';
+ }
+ break;
+ case '\n':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = 'n';
+ }
+ break;
+ case '\r':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = 'r';
+ }
+ break;
+ case '\\':
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '\\';
+ }
+ break;
+ case '\'':
+ // Escape single quote only if quote context is single quote
+ if (quote == '\'') {
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '\'';
+ }
+ } else {
+ to[j++] = c;
+ }
+ break;
+ case '"':
+ // Escape double quote only if quote context is double quote
+ if (quote == '"') {
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = '"';
+ }
+ } else {
+ to[j++] = c;
+ }
+ break;
+ case '\x1a': // Ctrl+Z
+ if (j + 1 < to_len - 1) {
+ to[j++] = '\\';
+ to[j++] = 'Z';
+ }
+ break;
+ default:
+ to[j++] = c;
+ break;
+ }
+ }
+
+ to[j] = '\0';
+ return j;
+}
+
+unsigned long seekdb_hex_string(char* to, unsigned long to_len, const char* from, unsigned long from_len) {
+ if (!to || to_len == 0 || !from || from_len == 0) {
+ return static_cast(-1);
+ }
+
+ // Each byte becomes 2 hex characters
+ if (to_len < from_len * 2 + 1) {
+ return static_cast(-1);
+ }
+
+ static const char hex_chars[] = "0123456789ABCDEF";
+ unsigned long j = 0;
+
+ for (unsigned long i = 0; i < from_len && j < to_len - 1; i++) {
+ unsigned char c = static_cast(from[i]);
+ to[j++] = hex_chars[(c >> 4) & 0x0F];
+ to[j++] = hex_chars[c & 0x0F];
+ }
+
+ to[j] = '\0';
+ return j;
+}
+
+unsigned long seekdb_get_server_version(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return 0;
+ }
+
+ // Get server version string
+ const char* version_str = seekdb_get_server_info(handle);
+ if (!version_str) {
+ return 0;
+ }
+
+ // Parse version string (format: "major.minor.patch" or "major.minor.patch.extra")
+ unsigned long major = 0, minor = 0, patch = 0;
+ if (sscanf(version_str, "%lu.%lu.%lu", &major, &minor, &patch) >= 2) {
+ // Format: major * 10000 + minor * 100 + patch
+ return major * 10000 + minor * 100 + patch;
+ }
+
+ return 0;
+}
+
+int seekdb_change_user(SeekdbHandle handle, const char* user, const char* password, const char* database) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized || !user) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // For embedded mode, changing user is not typically needed
+ // But we can switch database if provided
+ if (database) {
+ int ret = seekdb_select_db(handle, database);
+ if (ret != SEEKDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ // Note: In embedded mode, user/password authentication is typically not used
+ // This function mainly switches the database context
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_reset_connection(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ if (!conn->embed_session) {
+ return SEEKDB_ERROR_NOT_INITIALIZED;
+ }
+
+ // Reset session state
+ // This includes: session variables, temporary tables, locks, etc.
+ // In embedded mode, we can reset the session by clearing state
+
+ // Clear any pending transactions
+ if (conn->embed_session->is_in_transaction()) {
+ // Rollback any pending transaction
+ int ret = seekdb_rollback(handle);
+ if (ret != SEEKDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ // Reset autocommit to default (true)
+ conn->embed_session->set_autocommit(true);
+
+ // Clear last error
+ conn->last_error.clear();
+
+ // Note: In embedded mode, we don't need to reset network connection state
+ // as there's no network connection. Session variables and temporary tables
+ // are managed by the session itself.
+
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_next_result(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ // Check if there are more result sets in the queue
+ if (conn->current_result_index >= 0 &&
+ conn->current_result_index + 1 < static_cast(conn->result_sets.size())) {
+ conn->current_result_index++;
+ return 0; // More results available
+ }
+
+ // For now, we don't support multiple result sets from a single query
+ // This would require parsing multi-statement queries or stored procedures
+ // Return -1 to indicate no more results
+ return -1;
+}
+
+bool seekdb_more_results(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return false;
+ }
+
+ // Check if there are more result sets
+ if (conn->current_result_index >= 0 &&
+ conn->current_result_index + 1 < static_cast(conn->result_sets.size())) {
+ return true;
+ }
+
+ return false;
+}
+
+const char* seekdb_error(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return nullptr;
+ }
+
+ return conn->last_error.empty() ? nullptr : conn->last_error.c_str();
+}
+
+unsigned int seekdb_errno(SeekdbHandle handle) {
+ SeekdbConnection* conn = static_cast(handle);
+ if (!conn || !conn->initialized) {
+ return 0;
+ }
+
+ // Return error code from thread-local storage
+ return static_cast(seekdb_last_error_code());
+}
+
+int seekdb_result_get_all_column_names(
+ SeekdbResult result,
+ char** names,
+ char* name_bufs,
+ size_t name_buf_size,
+ int32_t* column_count
+) {
+ if (!result || !names || !name_bufs || name_buf_size == 0 || !column_count) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+ int32_t actual_count = rs->column_count;
+
+ if (actual_count > *column_count) {
+ return SEEKDB_ERROR_INVALID_PARAM; // Buffer too small
+ }
+
+ for (int32_t i = 0; i < actual_count; i++) {
+ if (i >= static_cast(rs->column_names.size())) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ const std::string& col_name = rs->column_names[i];
+ size_t copy_len = std::min(col_name.length(), name_buf_size - 1);
+
+ char* buf = name_bufs + i * name_buf_size;
+ strncpy(buf, col_name.c_str(), copy_len);
+ buf[copy_len] = '\0';
+ names[i] = buf;
+ }
+
+ *column_count = actual_count;
+ return SEEKDB_SUCCESS;
+}
+
+int seekdb_result_get_all_column_names_alloc(
+ SeekdbResult result,
+ char*** names,
+ int32_t* column_count
+) {
+ if (!result || !names || !column_count) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+ int32_t actual_count = rs->column_count;
+
+ // Allocate array of char* pointers
+ char** name_array = static_cast(malloc(sizeof(char*) * actual_count));
+ if (!name_array) {
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+
+ // Allocate and copy column names
+ for (int32_t i = 0; i < actual_count; i++) {
+ if (i >= static_cast(rs->column_names.size())) {
+ // Free already allocated names
+ for (int32_t j = 0; j < i; j++) {
+ free(name_array[j]);
+ }
+ free(name_array);
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ const std::string& col_name = rs->column_names[i];
+ size_t name_len = col_name.length() + 1; // +1 for null terminator
+ name_array[i] = static_cast(malloc(name_len));
+ if (!name_array[i]) {
+ // Free already allocated names
+ for (int32_t j = 0; j < i; j++) {
+ free(name_array[j]);
+ }
+ free(name_array);
+ return SEEKDB_ERROR_MEMORY_ALLOC;
+ }
+ strncpy(name_array[i], col_name.c_str(), name_len - 1);
+ name_array[i][name_len - 1] = '\0';
+ }
+
+ *names = name_array;
+ *column_count = actual_count;
+ return SEEKDB_SUCCESS;
+}
+
+void seekdb_free_column_names(char** names, int32_t column_count) {
+ if (!names || column_count <= 0) {
+ return;
+ }
+
+ for (int32_t i = 0; i < column_count; i++) {
+ if (names[i]) {
+ free(names[i]);
+ }
+ }
+
+ free(names);
+}
+
+int seekdb_result_fetch_all(
+ SeekdbResult result,
+ seekdb_cell_callback_t callback,
+ void* user_data
+) {
+ if (!result || !callback) {
+ return SEEKDB_ERROR_INVALID_PARAM;
+ }
+
+ SeekdbResultSet* rs = static_cast(result);
+
+ for (int64_t row_idx = 0; row_idx < rs->row_count; row_idx++) {
+ if (row_idx >= static_cast(rs->rows.size())) {
+ break;
+ }
+
+ const std::vector& row = rs->rows[row_idx];
+
+ for (int32_t col_idx = 0; col_idx < rs->column_count; col_idx++) {
+ if (col_idx >= static_cast(row.size())) {
+ break;
+ }
+
+ const std::string& cell_value = row[col_idx];
+ bool is_null = (row_idx < static_cast