diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..43b8b9ae --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +spikes/wasix-postgres-build/patches/*.patch whitespace=-blank-at-eol,-space-before-tab diff --git a/.github/actions/setup-rust-tools/action.yml b/.github/actions/setup-rust-tools/action.yml new file mode 100644 index 00000000..51c709c3 --- /dev/null +++ b/.github/actions/setup-rust-tools/action.yml @@ -0,0 +1,50 @@ +name: Set up Rust tools +description: Install the pinned Rust toolchain, cache Cargo output, and install optional Cargo tools. + +inputs: + toolchain: + description: Rust toolchain version. + required: false + default: "1.92" + components: + description: Comma-separated Rust components. + required: false + default: "" + cache: + description: Whether to enable the Cargo cache. + required: false + default: "true" + cache-workspaces: + description: Workspace mapping for Swatinem/rust-cache. + required: false + default: ". -> target" + cache-save-if: + description: Expression string passed to Swatinem/rust-cache save-if. + required: false + default: "false" + tools: + description: Comma-separated tools for taiki-e/install-action. + required: false + default: "" + +runs: + using: composite + steps: + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: ${{ inputs.toolchain }} + components: ${{ inputs.components }} + + - name: Cache Cargo output + if: ${{ inputs.cache == 'true' }} + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: ${{ inputs.cache-workspaces }} + save-if: ${{ inputs.cache-save-if }} + + - name: Install Cargo tools + if: ${{ inputs.tools != '' }} + uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 + with: + tool: ${{ inputs.tools }} diff --git a/.github/actions/setup-wasmer-llvm/action.yml b/.github/actions/setup-wasmer-llvm/action.yml new file mode 100644 index 00000000..8d5fb2d0 --- /dev/null +++ b/.github/actions/setup-wasmer-llvm/action.yml @@ -0,0 +1,109 @@ +name: Set up Wasmer LLVM +description: Restore or install the pinned Wasmer LLVM toolchain used for WASIX and AOT generation. + +inputs: + url: + description: Wasmer LLVM archive URL. + required: true + version: + description: Expected LLVM major.minor version. + required: false + default: "22.1" + cache: + description: Whether to restore and save the extracted LLVM toolchain. + required: false + default: "true" + cache-save-if: + description: Whether to save a cache miss after installation. + required: false + default: "false" + +runs: + using: composite + steps: + - name: Derive LLVM cache key + id: cache-key + shell: bash + env: + LLVM_URL: ${{ inputs.url }} + LLVM_VERSION: ${{ inputs.version }} + run: | + if command -v shasum >/dev/null 2>&1; then + url_hash="$(printf '%s' "$LLVM_URL" | shasum -a 256 | cut -c1-16)" + else + url_hash="$(printf '%s' "$LLVM_URL" | sha256sum | cut -c1-16)" + fi + echo "key=wasmer-llvm-${RUNNER_OS}-${RUNNER_ARCH}-${LLVM_VERSION}-${url_hash}" >> "$GITHUB_OUTPUT" + + - name: Restore Wasmer LLVM cache + id: cache + if: ${{ inputs.cache == 'true' }} + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: ${{ runner.temp }}/wasmer-llvm/${{ steps.cache-key.outputs.key }} + key: ${{ steps.cache-key.outputs.key }} + + - name: Install Wasmer LLVM + shell: bash + env: + LLVM_URL: ${{ inputs.url }} + LLVM_VERSION: ${{ inputs.version }} + CACHE_KEY: ${{ steps.cache-key.outputs.key }} + run: | # zizmor: ignore[github-env] repo-owned LLVM URLs and validated tool paths are exported for later workflow steps. + runner_temp="$RUNNER_TEMP" + if [ "$RUNNER_OS" = "Windows" ]; then + runner_temp="$(cygpath -u "$RUNNER_TEMP")" + fi + + cache_root="$runner_temp/wasmer-llvm/$CACHE_KEY" + install_dir="$cache_root/llvm" + if [ "$RUNNER_OS" = "Windows" ]; then + llvm_config="$install_dir/bin/llvm-config.exe" + else + llvm_config="$install_dir/bin/llvm-config" + fi + + if [ ! -x "$llvm_config" ]; then + archive="$runner_temp/llvm-${LLVM_VERSION}.tar.xz" + rm -rf "$install_dir" + mkdir -p "$install_dir" + curl -L --fail --retry 3 --output "$archive" "$LLVM_URL" + tar -xJf "$archive" -C "$install_dir" + fi + + if [ ! -x "$llvm_config" ]; then + echo "LLVM extraction did not produce bin/llvm-config" >&2 + find "$install_dir" -maxdepth 3 -type f -name 'llvm-config*' -print >&2 + exit 1 + fi + + if [ "$RUNNER_OS" = "Windows" ]; then + env_prefix="$(cygpath -w "$install_dir")" + path_entry="$(cygpath -w "$install_dir/bin")" + else + env_prefix="$install_dir" + path_entry="$install_dir/bin" + fi + + echo "LLVM_PATH=$env_prefix" >> "$GITHUB_ENV" + echo "LLVM_SYS_221_PREFIX=$env_prefix" >> "$GITHUB_ENV" + echo "$path_entry" >> "$GITHUB_PATH" + + version="$("$llvm_config" --version)" + case "$version" in + "$LLVM_VERSION".*) ;; + *) echo "expected LLVM $LLVM_VERSION.x, got $version" >&2; exit 1 ;; + esac + + targets="$("$llvm_config" --targets-built)" + case "$targets" in + *LoongArch*WebAssembly*|*WebAssembly*LoongArch*) ;; + *) echo "expected Wasmer LLVM build with LoongArch and WebAssembly targets; got $targets" >&2; exit 1 ;; + esac + + - name: Save Wasmer LLVM cache + if: ${{ inputs.cache == 'true' && inputs.cache-save-if == 'true' && steps.cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: ${{ runner.temp }}/wasmer-llvm/${{ steps.cache-key.outputs.key }} + key: ${{ steps.cache-key.outputs.key }} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dea2f8b9..82e297ff 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,9 +4,13 @@ - [ ] Package/API/runtime change: PR title uses `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, or a breaking `!`. - [ ] Docs/CI/repository-only change: no release intended. +- [ ] Asset/source-spine change: source pins/fingerprints are current and the Assets workflow will generate/test release artifacts. ## Verification -- [ ] `scripts/validate.sh ci` -- [ ] `scripts/validate.sh release` +- [ ] `scripts/validate.sh repo` +- [ ] `scripts/validate.sh artifacts` +- [ ] `scripts/validate.sh lint` +- [ ] `scripts/validate.sh test` +- [ ] `scripts/validate.sh package` when published package contents changed - [ ] `cargo deny check` diff --git a/.github/scripts/check-release-intent.sh b/.github/scripts/check-release-intent.sh index bc9075e1..dc1f46c4 100755 --- a/.github/scripts/check-release-intent.sh +++ b/.github/scripts/check-release-intent.sh @@ -21,10 +21,18 @@ if [[ "${subject}" =~ ${release_pr_pattern} && "${head_branch}" == release-plz-* is_release_pr=true fi -package_version_from_ref() { - local ref="${1:?package_version_from_ref requires a git ref}" - - git show "${ref}:Cargo.toml" | awk ' +package_versions_from_ref() { + local ref="${1:?package_versions_from_ref requires a git ref}" + local files + + files="$( + git ls-tree -r --name-only "${ref}" | + grep -E '(^Cargo.toml$|^crates/.*/Cargo.toml$)' || true + )" + + while IFS= read -r file; do + [[ -z "${file}" ]] && continue + git show "${ref}:${file}" | awk -v file="${file}" ' /^\[package\][[:space:]]*$/ { in_package = 1 next @@ -32,27 +40,43 @@ package_version_from_ref() { /^\[/ && in_package { exit } + in_package && $0 ~ /^[[:space:]]*name[[:space:]]*=/ { + name = $0 + sub(/^[^=]*=[[:space:]]*"/, "", name) + sub(/".*$/, "", name) + } in_package && $0 ~ /^[[:space:]]*version[[:space:]]*=/ { line = $0 sub(/^[^=]*=[[:space:]]*"/, "", line) sub(/".*$/, "", line) - print line + if (name == "") { + name = file + } + print name "=" line exit } ' + done <<< "${files}" | sort } -base_version="$(package_version_from_ref "${base_ref}")" -head_version="$(package_version_from_ref "${head_ref}")" +base_versions="$(package_versions_from_ref "${base_ref}")" +head_versions="$(package_versions_from_ref "${head_ref}")" -if [[ -z "${base_version}" || -z "${head_version}" ]]; then - echo "could not read package version from Cargo.toml" >&2 +if [[ -z "${base_versions}" || -z "${head_versions}" ]]; then + echo "could not read package versions from Cargo.toml files" >&2 exit 1 fi -if [[ "${base_version}" != "${head_version}" && "${is_release_pr}" != true ]]; then +changed_existing_versions="$( + join -t $'\t' \ + <(printf '%s\n' "${base_versions}" | sed 's/=/\t/' | sort -t $'\t' -k1,1) \ + <(printf '%s\n' "${head_versions}" | sed 's/=/\t/' | sort -t $'\t' -k1,1) | + awk -F '\t' '$2 != $3 { print $1 "=" $2 " -> " $3 }' +)" + +if [[ -n "${changed_existing_versions}" && "${is_release_pr}" != true ]]; then cat >&2 <&2 + exit 2 + ;; + esac +done + +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${GH_REPO:?GH_REPO is required}" + +required_artifacts_present() { + run_id="$1" + if [[ "${#required_artifacts[@]}" -eq 0 ]]; then + return 0 + fi + + artifacts="$(gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts" \ + --paginate \ + --jq '.artifacts[].name')" || return 1 + for expected in "${required_artifacts[@]}"; do + if ! printf '%s\n' "$artifacts" | grep -Fxq "$expected"; then + return 1 + fi + done +} + +deadline=$((SECONDS + timeout)) +while true; do + runs="$(gh run list \ + --workflow "$workflow" \ + --commit "$sha" \ + --limit 10 \ + --json databaseId,status,conclusion,url,event \ + --jq '.[] | [.databaseId, .status, (.conclusion // ""), .url, .event] | @tsv')" + if [ -n "$runs" ]; then + echo "$runs" + for run_id in $(echo "$runs" | awk -F '\t' '$2 == "completed" && $3 == "success" { print $1 }'); do + if required_artifacts_present "$run_id"; then + exit 0 + fi + echo "$workflow run $run_id is successful but is missing one or more required artifacts" + done + if echo "$runs" | awk -F '\t' '$2 != "completed" { active=1 } END { exit active ? 0 : 1 }'; then + echo "$workflow is still running for $sha" + elif echo "$runs" | awk -F '\t' '$2 == "completed" && $3 != "success" && $5 != "workflow_dispatch" { failed=1 } END { exit failed ? 0 : 1 }'; then + echo "$workflow failed for $sha" >&2 + exit 1 + else + echo "waiting for successful $workflow workflow for $sha" + fi + else + echo "waiting for $workflow workflow for $sha" + fi + if [ "$SECONDS" -ge "$deadline" ]; then + echo "timed out waiting for successful $workflow workflow for $sha" >&2 + exit 1 + fi + sleep 60 +done diff --git a/.github/workflows/assets.yml b/.github/workflows/assets.yml new file mode 100644 index 00000000..f8c5afd8 --- /dev/null +++ b/.github/workflows/assets.yml @@ -0,0 +1,225 @@ +name: Assets +run-name: Assets / ${{ github.event_name == 'workflow_dispatch' && inputs.target || (github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || github.ref_name) }} + +on: + pull_request: + paths: + - ".github/workflows/assets.yml" + - ".github/actions/setup-wasmer-llvm/**" + - "Cargo.lock" + - "Cargo.toml" + - "assets/**" + - "crates/aot/**" + - "crates/assets/**" + - "xtask/**" + push: + branches: [main] + paths: + - ".github/workflows/assets.yml" + - ".github/actions/setup-wasmer-llvm/**" + - "Cargo.lock" + - "Cargo.toml" + - "assets/**" + - "crates/aot/**" + - "crates/assets/**" + - "xtask/**" + workflow_dispatch: + inputs: + target: + description: Native AOT target to build + required: true + default: all + type: choice + options: + - all + - aarch64-apple-darwin + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - x86_64-pc-windows-msvc + schedule: + - cron: "17 3 * * 1" + +permissions: + contents: read + +concurrency: + group: assets-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.target || 'all' }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + ASSET_PROFILE: release-o3 + RUST_CACHE_SAVE_IF: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + WASMER_LLVM_VERSION: "22.1" + WASMER_LLVM_LINUX_X64_URL: https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-amd64.tar.xz + +defaults: + run: + shell: bash + +jobs: + portable-wasix: + name: Build portable WASIX assets + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + contents: read + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Verify source-controlled asset inputs + run: cargo run -p xtask -- assets verify-committed + + - name: Fetch pinned asset sources + run: cargo run -p xtask -- assets fetch + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + + - name: Build WASIX builder image and save cache + if: ${{ github.ref == 'refs/heads/main' }} + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + with: + context: assets/wasix-build/docker + file: assets/wasix-build/docker/Dockerfile + tags: pglite-oxide-wasix-build:ci + load: true + cache-from: type=gha,scope=wasix-builder + cache-to: type=gha,mode=max,scope=wasix-builder + + - name: Build WASIX builder image + if: ${{ github.ref != 'refs/heads/main' }} + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + with: + context: assets/wasix-build/docker + file: assets/wasix-build/docker/Dockerfile + tags: pglite-oxide-wasix-build:ci + load: true + cache-from: type=gha,scope=wasix-builder + + - name: Install Wasmer LLVM 22.1 for WASIX template generation + uses: ./.github/actions/setup-wasmer-llvm + with: + url: ${{ env.WASMER_LLVM_LINUX_X64_URL }} + version: ${{ env.WASMER_LLVM_VERSION }} + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Build portable WASIX modules and package runtime assets + env: + IMAGE: pglite-oxide-wasix-build:ci + run: | + cargo run -p xtask --features template-runner -- assets release-build \ + --profile "$ASSET_PROFILE" \ + --target-triple x86_64-unknown-linux-gnu \ + --skip-aot \ + --skip-package-size + + - name: Validate generated portable assets + run: cargo run -p xtask -- assets check --strict-generated + + - name: Upload portable WASIX build outputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: pglite-oxide-portable-wasix + path: | + assets/wasix-build/build/** + target/pglite-oxide/assets/** + assets/generated/** + if-no-files-found: error + + native-targets: + name: Select native AOT targets + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + matrix: ${{ steps.targets.outputs.matrix }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Build target matrix + id: targets + env: + REQUESTED_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.target || 'all' }} + run: cargo run --quiet -p xtask -- assets ci-matrix --target "$REQUESTED_TARGET" --github-output >> "$GITHUB_OUTPUT" + + native-aot: + name: Native AOT / ${{ matrix.target }} + needs: + - portable-wasix + - native-targets + runs-on: ${{ matrix.os }} + timeout-minutes: 180 + permissions: + contents: read + actions: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.native-targets.outputs.matrix) }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Download portable WASIX build outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: pglite-oxide-portable-wasix + path: . + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Install Wasmer LLVM 22.1 for AOT generation + uses: ./.github/actions/setup-wasmer-llvm + with: + url: ${{ matrix.llvm_url }} + version: ${{ env.WASMER_LLVM_VERSION }} + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Generate and package target AOT artifacts + env: + AOT_TARGET: ${{ matrix.target }} + run: | + cargo run -p xtask -- assets aot --target-triple "$AOT_TARGET" + cargo run -p xtask -- assets package-aot --target-triple "$AOT_TARGET" + cargo run -p xtask -- assets check-aot --target-triple "$AOT_TARGET" + + - name: Check target AOT crate + env: + AOT_PACKAGE: ${{ matrix.package }} + run: cargo check -p "$AOT_PACKAGE" --locked + + - name: Run asset smoke tests + run: cargo run -p xtask -- assets smoke + + - name: Upload target artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: ${{ matrix.artifact }} + path: | + target/pglite-oxide/aot/${{ matrix.target }}/** + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e33dae2e..72091ffa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ name: CI +run-name: CI / ${{ github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || github.ref_name }} on: pull_request: @@ -16,31 +17,247 @@ env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 PREK_VERSION: 0.3.10 + CARGO_HACK_VERSION: 0.6.44 + RUST_CACHE_SAVE_IF: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} jobs: + scope: + name: Determine changed surfaces + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + repo: ${{ steps.scope.outputs.repo }} + rust: ${{ steps.scope.outputs.rust }} + examples: ${{ steps.scope.outputs.examples }} + package: ${{ steps.scope.outputs.package }} + assets: ${{ steps.scope.outputs.assets }} + ci: ${{ steps.scope.outputs.ci }} + docs: ${{ steps.scope.outputs.docs }} + docs_only: ${{ steps.scope.outputs.docs_only }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Classify changed paths + id: scope + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: scripts/ci-scope.sh "$BASE_SHA" "$HEAD_SHA" + + repo-hygiene: + name: Repository hygiene + needs: scope + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + components: rustfmt + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + tools: prek@${{ env.PREK_VERSION }} + + - name: Validate repository hygiene + run: scripts/validate.sh repo + + - name: Verify asset inputs + if: ${{ github.event_name == 'push' || needs.scope.outputs.assets == 'true' || needs.scope.outputs.package == 'true' || needs.scope.outputs.ci == 'true' }} + run: scripts/validate.sh artifacts + workflow-lint: name: Workflow lint + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.ci == 'true' }} runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 + permissions: + actions: read + contents: read steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Lint GitHub Actions workflows - uses: raven-actions/actionlint@v2.1.2 + uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 + + - name: Audit GitHub Actions workflows + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e + with: + advanced-security: false + config: .github/zizmor.yml + inputs: .github/workflows .github/actions + min-severity: medium + persona: auditor + version: 1.24.1 + + rust-lint: + name: Rust lint + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + components: clippy + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - checks: - name: Rust checks (MSRV) + - name: Validate lint gates + run: scripts/validate.sh lint + + rust-tests: + name: Rust tests + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' }} runs-on: ubuntu-latest timeout-minutes: 90 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Validate test gates + run: scripts/validate.sh test + + runtime-targets: + name: Select runtime AOT targets + needs: scope + if: ${{ needs.scope.outputs.rust == 'true' && needs.scope.outputs.assets != 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + matrix: ${{ steps.targets.outputs.matrix }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Build target matrix + id: targets + run: cargo run --quiet -p xtask -- assets ci-matrix --github-output >> "$GITHUB_OUTPUT" + + runtime-aot-tests: + name: Runtime AOT smoke / ${{ matrix.target }} + needs: + - scope + - runtime-targets + if: ${{ needs.scope.outputs.rust == 'true' && needs.scope.outputs.assets != 'true' }} + runs-on: ${{ matrix.os }} + timeout-minutes: 180 + permissions: + contents: read + actions: read + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.runtime-targets.outputs.matrix) }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download compatible runtime artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AOT_TARGET: ${{ matrix.target }} + run: | + cargo run -p xtask -- assets download \ + --latest-compatible \ + --target-triple "$AOT_TARGET" + + - name: Check target AOT crate + env: + AOT_PACKAGE: ${{ matrix.package }} + run: cargo check -p "$AOT_PACKAGE" --locked + + - name: Run runtime tests against target AOT + run: scripts/validate.sh runtime-smoke + + asset-status: + name: Wait for same-SHA Assets + needs: scope + if: ${{ needs.scope.outputs.assets == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Wait for successful same-SHA Assets workflow + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + ASSET_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + args=() + while IFS= read -r artifact; do + args+=(--artifact "$artifact") + done < <(cargo run --quiet -p xtask -- assets ci-artifacts) + bash .github/scripts/require-workflow-success.sh Assets "$ASSET_SHA" 21000 "${args[@]}" + + examples: + name: Examples + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.examples == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - name: Install Tauri Linux dependencies run: | @@ -53,102 +270,123 @@ jobs: patchelf \ pkg-config - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools with: - toolchain: 1.92 - components: rustfmt, clippy + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + cache-workspaces: | + . -> target + examples/tauri-sqlx-vanilla/src-tauri -> target - name: Install Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 with: node-version: 22 cache: npm cache-dependency-path: examples/tauri-sqlx-vanilla/package-lock.json - - uses: Swatinem/rust-cache@v2 + - name: Validate examples + run: scripts/validate.sh examples + + package: + name: Package checks + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.package == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - workspaces: | - . -> target - examples/tauri-sqlx-vanilla/src-tauri -> target + fetch-depth: 0 + persist-credentials: false - - name: Install prek - uses: taiki-e/install-action@v2 + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools with: - tool: prek@${{ env.PREK_VERSION }} + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - name: Validate package checks - run: scripts/validate.sh ci - - - name: Validate publish dry run - run: scripts/validate.sh release + run: scripts/validate.sh package feature-powerset: name: Feature powerset + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' }} runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.92 - - - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-hack - uses: taiki-e/install-action@v2 + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools with: - tool: cargo-hack + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + tools: cargo-hack@${{ env.CARGO_HACK_VERSION }} - name: Check feature combinations - run: cargo hack check --feature-powerset --no-dev-deps + run: scripts/validate.sh feature-powerset semver: name: Public API compatibility + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.package == 'true' }} runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - name: Check semver compatibility - uses: obi1kenobi/cargo-semver-checks-action@v2 + uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 supply-chain: name: Supply chain + needs: scope + if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' || needs.scope.outputs.ci == 'true' }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - - uses: EmbarkStudios/cargo-deny-action@v2 + - name: Check dependency policy + uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb required: name: Required checks if: always() needs: + - scope + - repo-hygiene - workflow-lint - - checks + - rust-lint + - rust-tests + - runtime-targets + - runtime-aot-tests + - asset-status + - examples + - package - feature-powerset - semver - supply-chain runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Fail if any required job did not pass - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') + - name: Fail if any required job failed + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') run: exit 1 - name: All required jobs passed - run: echo "All required CI jobs passed." + run: echo "All required CI jobs passed or were intentionally skipped." diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index ce06912b..d5d53f52 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -1,4 +1,5 @@ name: Conventional Commits +run-name: Commit policy / ${{ github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || github.ref_name }} on: pull_request: @@ -24,22 +25,17 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools with: - toolchain: 1.92 - - - uses: Swatinem/rust-cache@v2 - - - name: Install prek - uses: taiki-e/install-action@v2 - with: - tool: prek@${{ env.PREK_VERSION }} + tools: prek@${{ env.PREK_VERSION }} - name: Check PR title if: github.event_name == 'pull_request' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a3ce526..3ff2f9e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ name: Release +run-name: Release / ${{ inputs.operation }} / ${{ github.ref_name }} on: workflow_dispatch: @@ -14,9 +15,7 @@ on: - publish permissions: - contents: write - id-token: write - pull-requests: write + contents: read concurrency: group: release-${{ github.ref }} @@ -25,14 +24,17 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - PREK_VERSION: 0.3.10 jobs: prepare-release-pr: - name: Prepare Release PR + name: Prepare release PR runs-on: ubuntu-latest timeout-minutes: 20 if: ${{ github.repository == 'f0rr0/pglite-oxide' && inputs.operation == 'prepare-release-pr' }} + environment: release-pr + permissions: + contents: write + pull-requests: write steps: - name: Require main run: | @@ -42,29 +44,33 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools with: - toolchain: 1.92 + cache: "false" - name: Create or update release PR - uses: release-plz/action@v0.5 + uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 with: command: release-pr env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }} publish: - name: Publish Release + name: Publish release runs-on: ubuntu-latest timeout-minutes: 120 if: ${{ github.repository == 'f0rr0/pglite-oxide' && inputs.operation != 'prepare-release-pr' }} environment: ${{ inputs.operation == 'publish' && 'crates-io' || 'release-dry-run' }} + permissions: + actions: read + contents: write + id-token: write steps: - name: Require main run: | @@ -74,71 +80,52 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 persist-credentials: false - - name: Install Tauri Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libayatana-appindicator3-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - librsvg2-dev \ - patchelf \ - pkg-config - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.92 - components: rustfmt, clippy - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - cache-dependency-path: examples/tauri-sqlx-vanilla/package-lock.json - - - uses: Swatinem/rust-cache@v2 + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools with: - workspaces: | - . -> target - examples/tauri-sqlx-vanilla/src-tauri -> target + cache-save-if: "true" - - name: Install prek - uses: taiki-e/install-action@v2 - with: - tool: prek@${{ env.PREK_VERSION }} + - name: Require successful same-SHA CI and Assets workflows + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + args=() + while IFS= read -r artifact; do + args+=(--artifact "$artifact") + done < <(cargo run --quiet -p xtask -- assets ci-artifacts) + bash .github/scripts/require-workflow-success.sh CI "$GITHUB_SHA" 7200 + bash .github/scripts/require-workflow-success.sh Assets "$GITHUB_SHA" 21600 "${args[@]}" - name: Validate release changelog run: .github/scripts/check-release-changelog.sh - - name: Validate package checks - run: scripts/validate.sh ci + - name: Download release asset and AOT artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: .github/scripts/download-aot-artifacts.sh - - name: Validate publish dry run + - name: Validate staged release packages and dry-runs run: scripts/validate.sh release - - name: Supply-chain policy - uses: EmbarkStudios/cargo-deny-action@v2 - - - name: Run release-plz dry run + - name: Dry-run release-plz publish if: ${{ inputs.operation == 'publish-dry-run' }} - uses: release-plz/action@v0.5 + uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 with: command: release dry_run: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run release-plz publish + - name: Publish with release-plz if: ${{ inputs.operation == 'publish' }} id: release_plz_publish - uses: release-plz/action@v0.5 + uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 with: command: release env: diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..14d9f6e3 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,2 @@ +# Shared by `scripts/validate.sh workflows` and the workflow-lint CI job. +rules: {} diff --git a/.gitignore b/.gitignore index 5e35b429..50ca2105 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ /target/ +node_modules/ +/scripts/perf/node-bench/node_modules/ /tmp/ +/assets/checkouts/ +/assets/wasix-build/build/ +/assets/wasix-build/work/ +/target/pglite-oxide/ +/crates/assets/assets/ +/crates/aot/*/artifacts/ **/.DS_Store # Build artifacts from cargo package pglite_oxide-*.crates.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a391f7..8cce1cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,12 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `PgliteRuntimeOptions::default` now selects the optimized embedded-template startup path. - `ensure_cluster` now requires runtime options. -- Runtime packaging now uses the uncompressed `pglite-wasi.tar` asset. +- Runtime packaging now uses a bundled optimized runtime archive. ### Added -- Reusable Wasmtime engine/module caching and on-disk compiled `.cwasm` cache - support for faster startup. +- Reusable embedded runtime caching and on-disk compiled-module cache support + for faster startup. - Embedded prepopulated PGDATA template with manifest validation. - Vanilla Tauri v2 SQLx profiler example with release-mode workload reporting. - Repo hooks for Conventional Commit validation, formatting, and pre-push checks. @@ -45,8 +45,7 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Added the high-level `Pglite` and `PgliteServer` APIs for direct embedded use and PostgreSQL client compatibility. -- Added process-local template cluster reuse for fast temporary databases, with - `fresh_temporary` escape hatches for initialization-specific tests. +- Added process-local template cluster reuse for fast temporary databases. - Added SQLx and `tokio-postgres` compatibility coverage, runtime/proxy smoke tests, CI, cargo-deny policy checks, Conventional Commit validation, and documented runtime asset provenance. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04e3539e..51693f81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,11 +19,12 @@ scripts/install-hooks.sh ``` Hooks stay deliberately smaller than CI: pre-commit handles file hygiene and -formatting, while pre-push runs whitespace diff checking, -`cargo clippy --all-targets`, and `cargo test --all-targets`. CI repeats those -hook checks and remains the source of truth for no-default builds, docs, -packaging, Tauri, frontend, feature combinations, public API compatibility, and -supply-chain checks. +formatting, while pre-push runs whitespace diff checking, clippy, and +`scripts/validate.sh test`. That test gate compiles runtime tests when host AOT +artifacts are unavailable and runs them when artifacts have been materialized. +CI repeats those hook checks and remains the source of truth for generated AOT +runtime matrices, packaging, Tauri, frontend, feature combinations, public API +compatibility, and supply-chain checks. In GitHub branch protection, require the aggregate `Required checks` status and the Conventional Commit status before merging. Local hooks are convenience diff --git a/Cargo.lock b/Cargo.lock index 9998cfce..aaaa8c89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,11 @@ version = 4 [[package]] name = "addr2line" -version = "0.26.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", ] [[package]] @@ -33,19 +33,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "android_system_properties" -version = "0.1.5" +name = "any_ascii" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" [[package]] name = "anyhow" @@ -54,10 +45,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arbitrary" -version = "1.4.2" +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" @@ -67,7 +64,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -85,17 +82,92 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", +] + [[package]] name = "bitflags" -version = "2.11.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] [[package]] name = "block-buffer" @@ -116,109 +188,84 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.20.2" +name = "bstr" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ - "allocator-api2", + "memchr", + "serde", ] [[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" +name = "bumpalo" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] -name = "cap-fs-ext" -version = "3.4.5" +name = "bus" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes", - "windows-sys 0.59.0", + "crossbeam-channel", + "num_cpus", + "parking_lot_core", ] [[package]] -name = "cap-net-ext" -version = "3.4.5" +name = "bytecheck" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" dependencies = [ - "cap-primitives", - "cap-std", - "rustix 1.1.4", - "smallvec", + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", ] [[package]] -name = "cap-primitives" -version = "3.4.5" +name = "bytecheck_derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix 1.1.4", - "rustix-linux-procfs", - "windows-sys 0.59.0", - "winx", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "cap-rand" -version = "3.4.5" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" -dependencies = [ - "ambient-authority", - "rand 0.8.5", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "cap-std" -version = "3.4.5" +name = "bytes" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", + "serde", ] [[package]] -name = "cap-time-ext" -version = "3.4.5" +name = "bytesize" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" dependencies = [ - "ambient-authority", - "cap-primitives", - "iana-time-zone", - "once_cell", - "rustix 1.1.4", - "winx", + "serde_core", ] [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -226,6 +273,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -244,210 +300,154 @@ dependencies = [ ] [[package]] -name = "cmov" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" - -[[package]] -name = "cobs" -version = "0.3.0" +name = "chrono" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "thiserror 2.0.18", + "num-traits", ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "ciborium" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ - "crossbeam-utils", + "ciborium-io", + "ciborium-ll", + "serde", ] [[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "ciborium-io" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] -name = "cpp_demangle" -version = "0.4.5" +name = "ciborium-ll" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ - "cfg-if", + "ciborium-io", + "half", ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ + "glob", "libc", + "libloading", ] [[package]] -name = "cpufeatures" -version = "0.3.0" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "libc", + "cc", ] [[package]] -name = "cranelift-assembler-x64" -version = "0.131.0" +name = "cmov" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb5bdd1af46714e3224a017fabbbd57f70df4e840eb5ad6a7429dc456119d6" -dependencies = [ - "cranelift-assembler-x64-meta", -] +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] -name = "cranelift-assembler-x64-meta" -version = "0.131.0" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a819599186e1b1a1f88d464e06045696afc7aa3e0cc018aa0b2999cb63d1d088" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "cranelift-srcgen", + "crossbeam-utils", ] [[package]] -name = "cranelift-bforest" -version = "0.131.0" +name = "console" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e2c152d488e03c87b913bc2ed3414416eb1e0d66d61b49af60bf456a9665c7" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ - "cranelift-entity", - "wasmtime-internal-core", + "encode_unicode", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "cranelift-bitset" -version = "0.131.0" +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6559d4fbc253d1396e1f6beeae57fa88a244f02aaf0cde2a735afd3492d9b2e" -dependencies = [ - "serde", - "serde_derive", - "wasmtime-internal-core", -] +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] -name = "cranelift-codegen" -version = "0.131.0" +name = "constant_time_eq" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d9315d98d6e0a64454d4c83be2ee0e8055c3f80c3b2d7bcad7079f281a06ff" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.16.1", - "libm", - "log", - "pulley-interpreter", - "regalloc2", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon", - "wasmtime-internal-core", -] +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] -name = "cranelift-codegen-meta" -version = "0.131.0" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89c00a88081c55e3087c45bebc77e0cc973de2d7b44ef6a943c7122647b89f5" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "cranelift-assembler-x64-meta", - "cranelift-codegen-shared", - "cranelift-srcgen", - "heck", - "pulley-interpreter", + "unicode-segmentation", ] [[package]] -name = "cranelift-codegen-shared" -version = "0.131.0" +name = "cooked-waker" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f77c497a1eb6273482aa1ac3b23cb8563ff04edb39ed5dfcfd28c8deff8f5" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" [[package]] -name = "cranelift-control" -version = "0.131.0" +name = "corosensei" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498dc1f17a6910c88316d49c7176d8fa97cf10c30859c32a266040449317f963" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" dependencies = [ - "arbitrary", + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", ] [[package]] -name = "cranelift-entity" -version = "0.131.0" +name = "cpp_demangle" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2acba797f6a46042ce82aaf7680d0c3567fe2001e238db9df649fd104a2727f" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" dependencies = [ - "cranelift-bitset", - "serde", - "serde_derive", - "wasmtime-internal-core", + "cfg-if", ] [[package]] -name = "cranelift-frontend" -version = "0.131.0" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dca3df1d107d98d88f159ad1d5eaa2d5cdb678b3d5bcfadc6fc83d8ebb448ea" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", + "libc", ] [[package]] -name = "cranelift-isle" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f62dd18116d88bed649871feceda79dad7b59cc685ea8998c2b3e64d0e689602" - -[[package]] -name = "cranelift-native" -version = "0.131.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f843b80360d7fdf61a6124642af7597f6d55724cf521210c34af8a1c66daca6e" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "cranelift-codegen", "libc", - "target-lexicon", ] -[[package]] -name = "cranelift-srcgen" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090ee5de58c6f17eb5e3a5ae8cf1695c7efea04ec4dd0ecba6a5b996c9bad7dc" - [[package]] name = "crc" version = "3.4.0" @@ -459,9 +459,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -472,6 +472,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -506,6 +515,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -535,115 +550,337 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.10.7" +name = "darling" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", - "subtle", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] -name = "digest" -version = "0.11.2" +name = "darling" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "block-buffer 0.12.0", - "const-oid", - "crypto-common 0.2.1", - "ctutils", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] -name = "directories" -version = "6.0.0" +name = "darling_core" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "dirs-sys", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] -name = "directories-next" -version = "2.0.0" +name = "darling_core" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ - "cfg-if", - "dirs-sys-next", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "dirs-sys" -version = "0.5.0" +name = "darling_macro" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.59.0", + "darling_core 0.20.11", + "quote", + "syn 2.0.117", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "darling_macro" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "dashmap" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "debugid" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] [[package]] -name = "either" -version = "1.15.0" +name = "defmt" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" dependencies = [ - "serde", + "defmt 1.0.1", ] [[package]] -name = "embedded-io" -version = "0.4.0" +name = "defmt" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + [[package]] -name = "embedded-io" -version = "0.6.1" +name = "defmt-macros" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "defmt-parser" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "cfg-if", + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -659,7 +896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -692,31 +929,19 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fd-lock" -version = "4.0.4" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.59.0", -] +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] @@ -725,6 +950,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -763,15 +994,10 @@ dependencies = [ ] [[package]] -name = "fs-set-times" -version = "0.20.3" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" -dependencies = [ - "io-lifetimes", - "rustix 1.1.4", - "windows-sys 0.59.0", -] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" @@ -781,6 +1007,7 @@ checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -803,6 +1030,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-intrusive" version = "0.5.0" @@ -820,6 +1058,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -838,8 +1087,10 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -875,9 +1126,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -887,13 +1140,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "gimli" version = "0.33.0" @@ -906,6 +1167,51 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -922,11 +1228,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", - "serde", - "serde_core", -] [[package]] name = "hashbrown" @@ -935,6 +1236,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" dependencies = [ "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -946,12 +1249,37 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -995,46 +1323,33 @@ dependencies = [ ] [[package]] -name = "hybrid-array" -version = "0.4.10" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "typenum", + "bytes", + "itoa", ] [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "hybrid-array" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", + "typenum", ] [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1042,9 +1357,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1055,9 +1370,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1069,15 +1384,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1089,15 +1404,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1114,6 +1429,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1127,14 +1448,30 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1148,20 +1485,42 @@ dependencies = [ ] [[package]] -name = "io-extras" -version = "0.18.4" +name = "inkwell" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +checksum = "7decbc9dfa45a4a827a6ff7b822c113b1285678a937e84213417d4ca8a095782" dependencies = [ - "io-lifetimes", - "windows-sys 0.59.0", + "bitflags 2.11.1", + "inkwell_internals", + "libc", + "llvm-sys", + "thiserror", +] + +[[package]] +name = "inkwell_internals" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfe97ee860815a90ed17e09639513269e39420a7440f3f4c996f238c514cf8d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "io-lifetimes" -version = "2.0.4" +name = "insta" +version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +dependencies = [ + "console", + "once_cell", + "regex", + "serde", + "similar", + "tempfile", +] [[package]] name = "ipnet" @@ -1169,6 +1528,24 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "iprange" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" +dependencies = [ + "ipnet", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1180,9 +1557,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1196,19 +1573,27 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" [[package]] name = "leb128fmt" @@ -1216,6 +1601,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + [[package]] name = "libc" version = "0.2.186" @@ -1223,27 +1617,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libm" -version = "0.2.16" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", + "plain", "redox_syscall 0.7.4", ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] [[package]] name = "linux-raw-sys" @@ -1253,9 +1667,29 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "llvm-sys" +version = "221.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abcc34a3b190f03c2a61b555f218f529589ff13657bdd2ff8ac3e85f2abe6bb" +dependencies = [ + "anyhow", + "cc", + "lazy_static", + "libc", + "regex-lite", + "semver", +] [[package]] name = "lock_api" @@ -1272,6 +1706,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lz4_flex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" +dependencies = [ + "twox-hash", +] + [[package]] name = "mach2" version = "0.4.3" @@ -1282,10 +1725,27 @@ dependencies = [ ] [[package]] -name = "maybe-owned" -version = "0.3.4" +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "managed" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" [[package]] name = "md-5" @@ -1314,14 +1774,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "memfd" -version = "0.6.5" +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ - "rustix 1.1.4", + "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1339,10 +1823,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "msvc-demangler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" +dependencies = [ + "bitflags 2.11.1", + "itoa", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1352,13 +1899,45 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -1370,6 +1949,15 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.39.1" @@ -1377,9 +1965,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", + "flate2", "hashbrown 0.17.0", "indexmap", "memchr", + "ruzstd", ] [[package]] @@ -1423,21 +2013,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + [[package]] name = "pglite-oxide" version = "0.3.0" dependencies = [ "anyhow", + "async-trait", "directories", + "dunce", + "filetime", "flate2", - "getrandom 0.4.2", "hex", + "pglite-oxide-aot-aarch64-apple-darwin", + "pglite-oxide-aot-aarch64-unknown-linux-gnu", + "pglite-oxide-aot-x86_64-pc-windows-msvc", + "pglite-oxide-aot-x86_64-unknown-linux-gnu", + "pglite-oxide-assets", "regex", "serde", "serde_json", @@ -1448,54 +2069,125 @@ dependencies = [ "tokio", "tokio-postgres", "tracing", - "wasmtime", - "wasmtime-wasi", + "wasmer", + "wasmer-config", + "wasmer-types", + "wasmer-wasix", + "webc", "zstd", ] +[[package]] +name = "pglite-oxide-aot-aarch64-apple-darwin" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-aot-x86_64-pc-windows-msvc" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-assets" +version = "0.3.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ + "phf_macros", "phf_shared", "serde", ] [[package]] -name = "phf_shared" +name = "phf_generator" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "siphasher", + "fastrand", + "phf_shared", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "phf_macros" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "phf_shared" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] [[package]] -name = "postcard" -version = "1.1.3" +name = "pin-project" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", + "pin-project-internal", ] +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "postgres-protocol" version = "0.6.11" @@ -1527,13 +2219,19 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1550,7 +2248,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1563,26 +2292,34 @@ dependencies = [ ] [[package]] -name = "pulley-interpreter" -version = "44.0.0" +name = "ptr_meta" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df866b7fd522992ccc6682e58b2741cc7972b163b661db24c4328f4c914cb09d" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" dependencies = [ - "cranelift-bitset", - "log", - "pulley-macros", - "wasmtime-internal-core", + "ptr_meta_derive", ] [[package]] -name = "pulley-macros" -version = "44.0.0" +name = "ptr_meta_derive" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7dfa8354acc622b3857e1bb1a4e4315d3bc1a44ad31d5653c3e87c0da9306d7" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", ] [[package]] @@ -1606,17 +2343,36 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.1" @@ -1638,6 +2394,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1647,17 +2413,32 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1679,7 +2460,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -1688,43 +2469,38 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 1.0.69", + "thiserror", ] [[package]] -name = "redox_users" -version = "0.5.2" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", + "ref-cast-impl", ] [[package]] -name = "regalloc2" -version = "0.15.1" +name = "ref-cast-impl" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.17.0", - "log", - "rustc-hash", - "smallvec", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1750,17 +2526,80 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" + [[package]] name = "regex-syntax" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "rkyv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.0", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -1769,16 +2608,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] -name = "rustix" -version = "0.38.44" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "semver", ] [[package]] @@ -1787,28 +2622,92 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "rustix-linux-procfs" -version = "0.1.1" +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty_pool" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" dependencies = [ - "once_cell", - "rustix 1.1.4", + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "ruzstd" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "saffron" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" +dependencies = [ + "chrono", + "nom 5.1.3", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] [[package]] name = "scopeguard" @@ -1816,11 +2715,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -1836,6 +2741,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1853,7 +2769,18 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1878,6 +2805,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1900,6 +2840,16 @@ dependencies = [ "digest 0.11.2", ] +[[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1908,13 +2858,25 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "siphasher" -version = "1.0.2" +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" @@ -1933,6 +2895,20 @@ dependencies = [ "serde", ] +[[package]] +name = "smoltcp" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac729b0a77bd092a3f06ddaddc59fe0d67f48ba0de45a9abe707c2842c7f8767" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "managed", +] + [[package]] name = "socket2" version = "0.6.3" @@ -1981,7 +2957,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -1998,7 +2974,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.117", ] [[package]] @@ -2009,7 +2985,7 @@ checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -2019,7 +2995,7 @@ dependencies = [ "sha2 0.10.9", "sqlx-core", "sqlx-postgres", - "syn", + "syn 2.0.117", "tokio", "url", ] @@ -2032,7 +3008,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.1", "byteorder", "crc", "dotenvy", @@ -2049,14 +3025,14 @@ dependencies = [ "md-5 0.10.6", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "tracing", "whoami 1.6.1", ] @@ -2078,17 +3054,47 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symbolic-common" +version = "12.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332615d90111d8eeaf86a84dc9bbe9f65d0d8c5cf11b4caccedc37754eb0dcfd" +dependencies = [ + "debugid", + "memmap2 0.9.10", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "12.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912017718eb4d21930546245af9a3475c9dccf15675a5c215664e76621afc471" +dependencies = [ + "cpp_demangle", + "msvc-demangler", + "rustc-demangle", + "symbolic-common", +] + [[package]] name = "syn" -version = "2.0.117" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2096,30 +3102,25 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] -name = "system-interface" -version = "0.27.3" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "bitflags", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.59.0", - "winx", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2148,26 +3149,27 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "terminal_size" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "winapi-util", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "termios" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ - "thiserror-impl 1.0.69", + "libc", ] [[package]] @@ -2176,36 +3178,56 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "time" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", ] [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2249,7 +3271,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2275,7 +3297,7 @@ dependencies = [ "socket2", "tokio", "tokio-util", - "whoami 2.1.1", + "whoami 2.1.2", ] [[package]] @@ -2287,6 +3309,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -2311,12 +3334,27 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -2326,6 +3364,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -2361,7 +3420,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2373,11 +3432,23 @@ dependencies = [ "once_cell", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -2406,12 +3477,30 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -2422,20 +3511,201 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtual-fs" +version = "0.702.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8091f35d7e5531288dbfccd8ec8c6942c510e04147e4f191d4ed1086d3b5722" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "dashmap", + "derive_more", + "dunce", + "filetime", + "fs_extra", + "futures", + "getrandom 0.4.2", + "indexmap", + "libc", + "pin-project-lite", + "replace_with", + "shared-buffer", + "slab", + "thiserror", + "tokio", + "tracing", + "virtual-mio", + "wasmer-package", + "webc", +] + +[[package]] +name = "virtual-mio" +version = "0.702.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db61f3409d96c79cb68ea15458d6ee1ad415235efc9896f0a878dc7d2832640c" +dependencies = [ + "async-trait", + "bytes", + "futures", + "mio", + "parking", + "serde", + "socket2", + "thiserror", + "tracing", +] + +[[package]] +name = "virtual-net" +version = "0.702.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17529a707ebafa6a085170700ea1055e3785a10026dc04c2d5eb98576df26647" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "futures-util", + "ipnet", + "iprange", + "libc", + "mio", + "pin-project-lite", + "rkyv", + "serde", + "smoltcp", + "socket2", + "thiserror", + "tokio", + "tracing", + "virtual-mio", +] + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wai-bindgen-gen-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa3dc41b510811122b3088197234c27e08fcad63ef936306dd8e11e2803876c" +dependencies = [ + "anyhow", + "wai-parser", +] + +[[package]] +name = "wai-bindgen-gen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc05e8380515c4337c40ef03b2ff233e391315b178a320de8640703d522efe" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", +] + +[[package]] +name = "wai-bindgen-gen-rust-wasm" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f35ce5e74086fac87f3a7bd50f643f00fe3559adb75c88521ecaa01c8a6199" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", +] + +[[package]] +name = "wai-bindgen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5601c6f448c063e83a5e931b8fefcdf7e01ada424ad42372c948d2e3d67741" +dependencies = [ + "bitflags 1.3.2", + "wai-bindgen-rust-impl", +] + +[[package]] +name = "wai-bindgen-rust-impl" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeb5c1170246de8425a3e123e7ef260dc05ba2b522a1d369fe2315376efea4" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wai-parser" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd0acb6d70885ea0c343749019ba74f015f64a9d30542e66db69b49b7e28186" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2486,9 +3756,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -2499,9 +3769,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2509,22 +3779,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -2541,12 +3811,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.246.2" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ "leb128fmt", - "wasmparser 0.246.2", + "wasmparser 0.247.0", ] [[package]] @@ -2562,494 +3832,456 @@ dependencies = [ ] [[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wasmparser" -version = "0.246.2" +name = "wasmer" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +checksum = "4605aab3837fdddf33ecafec6d90aa012d99dc9201570deed0d6b32c6459d36e" dependencies = [ - "bitflags", - "hashbrown 0.16.1", + "bindgen", + "bytes", + "cfg-if", + "cmake", + "corosensei", + "dashmap", + "derive_more", + "futures", "indexmap", - "semver", + "js-sys", + "more-asserts", + "paste", "serde", + "serde-wasm-bindgen", + "shared-buffer", + "symbolic-demangle", + "tar", + "target-lexicon", + "thiserror", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-compiler-llvm", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "windows-sys 0.61.2", ] [[package]] -name = "wasmprinter" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e41f7493ba994b8a779430a4c25ff550fd5a40d291693af43a6ef48688f00e3" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.246.2", -] - -[[package]] -name = "wasmtime" -version = "44.0.0" +name = "wasmer-compiler" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca3f777dfb4db45915f95eeb25cac7f2eeb268797a27e5eb78b072618135c7f" +checksum = "2ef403f60d7977ff8571575a083edae36b8c3cf56b326ce20c315fcc812751d6" dependencies = [ - "addr2line", - "async-trait", - "bitflags", - "bumpalo", - "cc", + "backtrace", + "bytes", "cfg-if", - "encoding_rs", + "crossbeam-channel", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", "libc", - "log", - "mach2", - "memfd", - "object", - "once_cell", - "postcard", - "pulley-interpreter", + "macho-unwind-info", + "memmap2 0.9.10", + "more-asserts", + "object 0.39.1", + "rangemap", "rayon", - "rustix 1.1.4", - "semver", - "serde", - "serde_derive", + "region", + "rkyv", + "self_cell", + "shared-buffer", "smallvec", "target-lexicon", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-cache", - "wasmtime-internal-component-macro", - "wasmtime-internal-component-util", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", - "wasmtime-internal-fiber", - "wasmtime-internal-jit-debug", - "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", - "wasmtime-internal-winch", + "tempfile", + "thiserror", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.247.0", + "which", "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-environ" -version = "44.0.0" +name = "wasmer-compiler-llvm" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5ca1af838cec374931242d07af5d354aedf63f297f95b3625ac863e516ef67" +checksum = "faeecedd667206354e52cc146cba5f1ec5735e884096109ec69a8ea6c7993560" dependencies = [ - "anyhow", - "cpp_demangle", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-entity", - "gimli", - "hashbrown 0.16.1", - "indexmap", - "log", - "object", - "postcard", - "rustc-demangle", + "byteorder", + "cc", + "crossbeam-channel", + "enum-iterator", + "enumset", + "inkwell", + "itertools 0.14.0", + "libc", + "object 0.39.1", + "phf", + "rayon", + "regex", + "rustc_version", "semver", - "serde", - "serde_derive", - "sha2 0.10.9", "smallvec", "target-lexicon", - "wasm-encoder 0.246.2", - "wasmparser 0.246.2", - "wasmprinter", - "wasmtime-internal-component-util", - "wasmtime-internal-core", -] - -[[package]] -name = "wasmtime-internal-cache" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2004f7c86ebeb116550655377cdf16dbf7b03ae5aa6b4b1c1458cfa23aaa306" -dependencies = [ - "base64", - "directories-next", - "log", - "postcard", - "rustix 1.1.4", - "serde", - "serde_derive", - "sha2 0.10.9", - "toml", - "wasmtime-environ", - "windows-sys 0.61.2", - "zstd", + "tracing", + "wasmer-compiler", + "wasmer-types", + "wasmer-vm", ] [[package]] -name = "wasmtime-internal-component-macro" -version = "44.0.0" +name = "wasmer-config" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b31927f7b613d8fe019609744e226f6458d8aa5e6289e92fbbc60e521cd026" +checksum = "18d113f780598913bda441a38471810cefe2cda54a621aad77d65d82fd60a703" dependencies = [ "anyhow", - "proc-macro2", - "quote", - "syn", - "wasmtime-internal-component-util", - "wasmtime-internal-wit-bindgen", - "wit-parser 0.246.2", -] - -[[package]] -name = "wasmtime-internal-component-util" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc29e3478928b93979831ba02a997ce7f707c673ce47180d643091cf4fa4f561" - -[[package]] -name = "wasmtime-internal-core" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816a61a75275c6be435131fc625a4f5956daf24d9f9f59443e81cbef228929b3" -dependencies = [ - "hashbrown 0.16.1", - "libm", + "bytesize", + "ciborium", + "derive_builder", + "hex", + "indexmap", + "saffron", + "schemars", + "semver", "serde", + "serde_json", + "serde_yaml", + "thiserror", + "toml 1.1.2+spec-1.1.0", + "url", ] [[package]] -name = "wasmtime-internal-cranelift" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ceb5e079877e7e4565c1e2d86d9db889175d55f7ca0001315576d08c71e634" -dependencies = [ - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli", - "itertools", - "log", - "object", - "pulley-interpreter", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", -] - -[[package]] -name = "wasmtime-internal-fiber" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18f8bb05d25e0d4cca7278147c9f9e2f26f66886ef754b562bf729128f1e537" -dependencies = [ - "cc", - "cfg-if", - "libc", - "rustix 1.1.4", - "wasmtime-environ", - "wasmtime-internal-versioned-export-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "wasmtime-internal-jit-debug" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357f1070b31154ee463937b477ca0b2962bf450b40fc59799bef2f656b15da73" -dependencies = [ - "cc", - "wasmtime-internal-versioned-export-macros", -] - -[[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd683a94490bf755d016a09697b0955602c50106b1ded97d16983ab2ded9fed" -dependencies = [ - "cfg-if", - "libc", - "wasmtime-internal-core", - "windows-sys 0.61.2", -] - -[[package]] -name = "wasmtime-internal-unwinder" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4471746ce113c3c1862ce2c0674acb35399a4b3ed3ef4531dc087f333c74f064" -dependencies = [ - "cfg-if", - "cranelift-codegen", - "log", - "object", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-internal-versioned-export-macros" -version = "44.0.0" +name = "wasmer-derive" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6af582ec18b674bf7a17775d6fbfbddfcc143f0edbd89c9c1778239c8aa92ed" +checksum = "9e83cb1ef2f745694abfd55eec5f95a1898e011143dc7adf1efb90edc746f473" dependencies = [ + "proc-macro-error2", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "wasmtime-internal-winch" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d31be8916bb60ea756d2f0ae1f634d9258442aa71e773c893e2f4cead30501b5" -dependencies = [ - "cranelift-codegen", - "gimli", - "log", - "object", - "target-lexicon", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-cranelift", - "winch-codegen", -] - -[[package]] -name = "wasmtime-internal-wit-bindgen" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2150e63d502ab2d64754e5abe8eb737ae674b7dd4ad53144fd16bbeceaf4a19" -dependencies = [ - "anyhow", - "bitflags", - "heck", - "indexmap", - "wit-parser 0.246.2", -] - -[[package]] -name = "wasmtime-wasi" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f5109b4fd619b9796b9c9901de59d83e3575cd1226c1a36d1901371f43db28" -dependencies = [ - "async-trait", - "bitflags", - "bytes", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "futures", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", - "system-interface", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "wasmtime", - "wasmtime-wasi-io", - "wiggle", - "windows-sys 0.61.2", + "syn 2.0.117", ] [[package]] -name = "wasmtime-wasi-io" -version = "44.0.0" +name = "wasmer-journal" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ebe14c586e98d2fdc32c76ca0005ef28348e98ed737e776d378b3b0cc2afd0" +checksum = "1fddba7b9a7a5fb75e09e9ed8a7d12cb711d98f5d1eefa8702759063451772e6" dependencies = [ + "anyhow", "async-trait", + "base64", + "bincode", + "bytecheck", "bytes", - "futures", + "derive_more", + "lz4_flex", + "num_enum", + "rkyv", + "serde", + "serde_json", + "thiserror", "tracing", - "wasmtime", + "virtual-fs", + "virtual-net", + "wasmer", + "wasmer-config", + "wasmer-wasix-types", ] [[package]] -name = "wast" -version = "35.0.2" +name = "wasmer-package" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +checksum = "45f8d4543ba2aae126a39ffa0b3856c6794c112ac3b4458772df2309123414ca" dependencies = [ - "leb128", + "anyhow", + "bytes", + "cfg-if", + "ciborium", + "flate2", + "ignore", + "insta", + "libc", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "shared-buffer", + "tar", + "tempfile", + "thiserror", + "toml 1.1.2+spec-1.1.0", + "url", + "wasmer-config", + "wasmer-types", + "webc", ] [[package]] -name = "web-sys" -version = "0.3.95" +name = "wasmer-types" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "573557fe88edc93098d66f4eaad1b58ddcd5454be22dae2ed91677a7c96d4057" dependencies = [ - "js-sys", - "wasm-bindgen", + "bytecheck", + "crc32fast", + "enum-iterator", + "enumset", + "getrandom 0.4.2", + "hex", + "indexmap", + "itertools 0.14.0", + "more-asserts", + "rkyv", + "serde", + "sha2 0.11.0", + "target-lexicon", + "thiserror", + "wasmparser 0.247.0", ] [[package]] -name = "whoami" -version = "1.6.1" +name = "wasmer-vm" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +checksum = "e0511df85bc7bad5feb66ba6da79afd37e03bb1aae95f5a352aeda8e2babe724" dependencies = [ - "libredox", - "wasite 0.1.0", + "backtrace", + "bytesize", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror", + "wasmer-types", + "windows-sys 0.61.2", ] [[package]] -name = "whoami" -version = "2.1.1" +name = "wasmer-wasix" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +checksum = "4a5dfefe1640080a492a940b279f0e661354450884218abb8921417159b90f4a" dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "blake3", + "bus", + "bytecheck", + "bytes", + "cfg-if", + "cooked-waker", + "crossbeam-channel", + "dashmap", + "derive_more", + "flate2", + "fnv", + "futures", + "getrandom 0.3.4", + "getrandom 0.4.2", + "heapless", + "hex", + "http", "libc", - "libredox", - "objc2-system-configuration", - "wasite 1.0.2", - "web-sys", + "linked_hash_set", + "lz4_flex", + "num_enum", + "once_cell", + "petgraph", + "pin-project", + "pin-utils", + "rand 0.10.1", + "rkyv", + "rusty_pool", + "semver", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "shared-buffer", + "tempfile", + "terminal_size", + "termios", + "thiserror", + "tokio", + "tokio-stream", + "toml 1.1.2+spec-1.1.0", + "tracing", + "url", + "urlencoding", + "virtual-fs", + "virtual-mio", + "virtual-net", + "waker-fn", + "wasm-encoder 0.247.0", + "wasmer", + "wasmer-config", + "wasmer-journal", + "wasmer-package", + "wasmer-types", + "wasmer-wasix-types", + "wasmparser 0.247.0", + "webc", + "weezl", + "windows-sys 0.61.2", + "xxhash-rust", + "zstd", ] [[package]] -name = "wiggle" -version = "44.0.0" +name = "wasmer-wasix-types" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cff414ef7dce0cc1cf8a033ff80d3f38e3987c37e3efeec7926ecb5ffaaae6" +checksum = "dd4bfe23433cbf2c8e4d1157fa96edead70dc8f87f716720dc15613bded3880b" dependencies = [ - "bitflags", - "thiserror 2.0.18", + "anyhow", + "bitflags 2.11.1", + "byteorder", + "cfg-if", + "num_enum", + "serde", + "time", "tracing", - "wasmtime", - "wasmtime-environ", - "wiggle-macro", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", + "wai-bindgen-gen-rust-wasm", + "wai-bindgen-rust", + "wai-parser", + "wasmer", + "wasmer-derive", + "wasmer-types", ] [[package]] -name = "wiggle-generate" -version = "44.0.0" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf9dc7272b151a9616a2699e7f94ea1d4ae253b47b63a79fbc8f38e2cca5fa6" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", - "wasmtime-environ", - "witx", + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] -name = "wiggle-macro" -version = "44.0.0" +name = "wasmparser" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa7c29fcf738630cba4e35f1805da5e42dde20ee9809ee9202b0648ae671602f" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" dependencies = [ - "proc-macro2", - "quote", - "syn", - "wiggle-generate", + "bitflags 2.11.1", + "hashbrown 0.17.0", + "indexmap", + "semver", + "serde", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "web-sys" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" +name = "webc" +version = "11.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "20b8b523112384e8c1caf77cffdd371ca7e0013779f081e6beafc537f5b5325d" dependencies = [ - "windows-sys 0.48.0", + "anyhow", + "base64", + "bytes", + "cfg-if", + "ciborium", + "document-features", + "ignore", + "indexmap", + "leb128", + "lexical-sort", + "libc", + "once_cell", + "path-clean", + "rand 0.9.4", + "serde", + "serde_json", + "sha2 0.10.9", + "shared-buffer", + "thiserror", + "url", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "weezl" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] -name = "winch-codegen" -version = "44.0.0" +name = "which" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9339858ad222412200fd8b1af9e270712201aaec440c7618991443af3446481f" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "cranelift-assembler-x64", - "cranelift-codegen", - "gimli", - "regalloc2", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", + "libc", ] [[package]] -name = "windows-core" -version = "0.62.2" +name = "whoami" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "libredox", + "wasite 0.1.0", ] [[package]] -name = "windows-implement" -version = "0.60.2" +name = "whoami" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "libc", + "libredox", + "objc2-system-configuration", + "wasite 1.0.2", + "web-sys", ] [[package]] -name = "windows-interface" -version = "0.59.3" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-sys 0.61.2", ] [[package]] @@ -3058,24 +4290,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3087,20 +4301,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.53.5", + "windows-targets 0.52.6", ] [[package]] @@ -3136,30 +4350,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3172,12 +4369,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3190,12 +4381,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3208,24 +4393,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3238,12 +4411,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3256,12 +4423,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3274,12 +4435,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3292,12 +4447,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" @@ -3309,15 +4458,8 @@ name = "winnow" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" - -[[package]] -name = "winx" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags", - "windows-sys 0.59.0", + "memchr", ] [[package]] @@ -3342,8 +4484,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", - "wit-parser 0.244.0", + "heck 0.5.0", + "wit-parser", ] [[package]] @@ -3353,10 +4495,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3372,7 +4514,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3384,7 +4526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -3393,7 +4535,7 @@ dependencies = [ "wasm-encoder 0.244.0", "wasm-metadata", "wasmparser 0.244.0", - "wit-parser 0.244.0", + "wit-parser", ] [[package]] @@ -3414,42 +4556,11 @@ dependencies = [ "wasmparser 0.244.0", ] -[[package]] -name = "wit-parser" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" -dependencies = [ - "anyhow", - "hashbrown 0.16.1", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.246.2", -] - -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror 1.0.69", - "wast", -] - [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xattr" @@ -3458,14 +4569,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix", +] + +[[package]] +name = "xtask" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "directories", + "futures-util", + "pglite-oxide", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "tar", + "tokio", + "tokio-postgres", + "toml 0.9.12+spec-1.1.0", + "walkdir", + "wasmer", + "wasmer-types", + "wasmer-wasix", + "wasmparser 0.247.0", + "webc", + "zstd", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3474,62 +4617,62 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3538,9 +4681,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3549,13 +4692,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b0ccf410..34b80683 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "pglite-oxide" version = "0.3.0" edition = "2024" rust-version = "1.92" -description = "Rust helpers for embedding the Electric SQL pglite WebAssembly PostgreSQL runtime" +description = "Embedded Postgres for Rust tests and local apps. No Docker, works with SQLx and any Postgres client." readme = "README.md" repository = "https://github.com/f0rr0/pglite-oxide" homepage = "https://github.com/f0rr0/pglite-oxide" @@ -14,39 +14,58 @@ license = "MIT AND Apache-2.0 AND PostgreSQL" exclude = [ ".github/**", "Cargo.toml.orig", - "assets/bin/pg_dump.wasm", - "assets/extensions/vector.tar.gz", + "assets/checkouts/**", + "assets/wasix-build/build/**", + "assets/wasix-build/work/**", + "crates/**", "examples/tauri-sqlx-vanilla/**", "release-plz.toml", + "xtask/**", ] +[workspace] +members = [ + ".", + "crates/assets", + "crates/aot/aarch64-apple-darwin", + "crates/aot/x86_64-unknown-linux-gnu", + "crates/aot/aarch64-unknown-linux-gnu", + "crates/aot/x86_64-pc-windows-msvc", + "xtask", +] +exclude = ["examples/tauri-sqlx-vanilla/src-tauri"] +resolver = "3" + [features] -default = ["runtime-cache"] -runtime-cache = ["wasmtime/cache"] +default = ["bundled", "extensions"] +bundled = [ + "dep:pglite-oxide-assets", + "dep:pglite-oxide-aot-aarch64-apple-darwin", + "dep:pglite-oxide-aot-x86_64-unknown-linux-gnu", + "dep:pglite-oxide-aot-aarch64-unknown-linux-gnu", + "dep:pglite-oxide-aot-x86_64-pc-windows-msvc", +] +extensions = ["bundled"] [package.metadata.pglite-oxide.assets] postgres-version = "17.5" postgres-pglite-branch = "REL_17_5-pglite" pglite-build-repo = "electric-sql/pglite-build" -pglite-build-branch = "gh-pages" -pglite-build-commit = "4c78ee29513799a51d4e1f75008cf9c3f00b11e9" -pglite-npm-version-checked = "0.4.4" -runtime-archive-sha256 = "f6f90bf571c7f5bc925ff22f94233893c2c740466da2ad23bcd4e8e1ea8c498a" -pglite-wasi-sha256 = "ad423e536096ede1870f4802e7dbe4b49599c6e3bafc45aa9a4ddeeae2c5f4f8" -pgdata-template-archive-sha256 = "63e398b3cd4fec134d06539f064018fc2aa7fef75b9c331472f9b2d7385b913d" +pglite-build-branch = "portable" +pglite-build-commit = "c195113dbaf09488f8d5eeb2db91dacd123b74d0" +pglite-npm-version-checked = "0.4.5" +runtime-archive-sha256 = "8c2271c53d4f2786f7406ec0211e679e1a13352fc14ae87215ae3569b547fc1f" +pglite-wasix-sha256 = "4ce77a543675b25a5b1fd93c62bd175576bf1fee0266b9fd96fac193bf13b811" +pgdata-template-archive-sha256 = "a0a91f4fbd0428787ce78b351ee84f0c33f9ce8578448701b0f6080f7d8b052e" +pg-dump-wasix-sha256 = "59482f1193c35147c1b50e6a5fd9bc2dfed4b15e0624102c4da93a266d8303ed" +initdb-wasix-sha256 = "" [dependencies] anyhow = "1" +async-trait = "0.1" tar = "0.4" zstd = { version = "0.13", default-features = false } directories = "6" -wasmtime = { version = "44", default-features = false, features = [ - "cranelift", - "parallel-compilation", - "runtime", -] } -wasmtime-wasi = { version = "44", default-features = false, features = ["p1"] } -getrandom = "0.4" tracing = "0.1" flate2 = "1" serde = { version = "1", features = ["derive"] } @@ -55,6 +74,37 @@ regex = "1" tempfile = "3" hex = "0.4" sha2 = "0.10" +dunce = "1" +filetime = "0.2" +pglite-oxide-assets = { version = "=0.3.0", path = "crates/assets", optional = true } +tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } +wasmer = { version = "7.2.0-alpha.2", default-features = false, features = [ + "sys", + "headless", + "compiler", + "wasmer-artifact-load", +] } +wasmer-config = "0.702.0-alpha.2" +wasmer-types = "7.2.0-alpha.2" +wasmer-wasix = { version = "0.702.0-alpha.2", default-features = false, features = [ + "sys-minimal", + "sys-poll", + "host-vnet", + "time", +] } +webc = "11.0.0" + +[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] +pglite-oxide-aot-aarch64-apple-darwin = { version = "=0.3.0", path = "crates/aot/aarch64-apple-darwin", optional = true } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] +pglite-oxide-aot-x86_64-unknown-linux-gnu = { version = "=0.3.0", path = "crates/aot/x86_64-unknown-linux-gnu", optional = true } + +[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies] +pglite-oxide-aot-aarch64-unknown-linux-gnu = { version = "=0.3.0", path = "crates/aot/aarch64-unknown-linux-gnu", optional = true } + +[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies] +pglite-oxide-aot-x86_64-pc-windows-msvc = { version = "=0.3.0", path = "crates/aot/x86_64-pc-windows-msvc", optional = true } [dev-dependencies] sqlx = { version = "0.8", default-features = false, features = [ @@ -71,10 +121,3 @@ path = "src/bin/pglite_dump.rs" [[bin]] name = "pglite-proxy" path = "src/bin/pglite_proxy.rs" - -[profile.dev.package.wasmtime-internal-cache] -# Wasmtime's internal cache crate keys debug builds by the current executable -# mtime. That defeats the compiled-module cache after ordinary edit/rebuild -# cycles, so keep this dependency on the stable release-style cache namespace -# during local dev and tests. -debug-assertions = false diff --git a/README.md b/README.md index f536f00f..64f62cae 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,135 @@ -# pglite-oxide - -[![CI](https://github.com/f0rr0/pglite-oxide/actions/workflows/ci.yml/badge.svg)](https://github.com/f0rr0/pglite-oxide/actions/workflows/ci.yml) -[![crates.io](https://img.shields.io/crates/v/pglite-oxide.svg)](https://crates.io/crates/pglite-oxide) -[![docs.rs](https://docs.rs/pglite-oxide/badge.svg)](https://docs.rs/pglite-oxide) -[![MSRV](https://img.shields.io/badge/msrv-1.92-blue)](https://www.rust-lang.org) -[![License](https://img.shields.io/badge/license-MIT%20AND%20Apache--2.0%20AND%20PostgreSQL-blue)](https://github.com/f0rr0/pglite-oxide#license) - -`pglite-oxide` embeds the [Electric SQL PGlite](https://github.com/electric-sql/pglite) -WASI PostgreSQL runtime in Rust. It gives Rust apps a local Postgres-compatible -database without shipping a native Postgres sidecar. - -Use it when you want: - -- local Postgres semantics in a Rust or Tauri app -- fast Postgres-backed tests without Docker or testcontainers -- a PostgreSQL connection URI for crates such as SQLx or `tokio-postgres` -- a small, embedded database boundary that stays on the Rust side of the app - -The crate currently targets PostgreSQL 17.x PGlite builds, Rust 1.92+, and -Wasmtime 44. - -## Install +

+ pglite-oxide logo +

+ +

pglite-oxide

+ +

+ Embedded Postgres for Rust tests and local apps.
+ Real PostgreSQL. Instant testing. Packaged runtime. Direct Rust API or a local Postgres URL. +

+ +

+ Usage + · + Performance + · + Extensions + · + Dump & Upgrade + · + Testing + · + Tauri +

+ +

+ CI + crates.io + docs.rs + MSRV + License +

+ +`pglite-oxide` brings PGlite/Postgres to Rust with a small API. Open a database +directly with `Pglite`, or hand `PgliteServer` to SQLx and any standard +Postgres client. No local Postgres install, no Docker, no runtime build +toolchain. + +## Add Postgres In One Minute âš¡ + +Already using SQLx or another Postgres client? Add the crate and point your +client at an embedded database URL: ```sh -cargo add pglite-oxide serde_json +cargo add pglite-oxide ``` -The default path uses the bundled PGDATA template and compiled Wasmtime module -cache. There are no startup flags to remember for ordinary apps. - -## Direct Embedded API - -Use `Pglite` when your Rust code owns the database calls. - ```rust,no_run -use pglite_oxide::Pglite; -use serde_json::json; - -fn main() -> Result<(), Box> { - let mut db = Pglite::open("./.pglite")?; +use pglite_oxide::PgliteServer; +use sqlx::{Connection, Row}; - db.exec("CREATE TABLE IF NOT EXISTS items(value TEXT)", None)?; - db.query("INSERT INTO items(value) VALUES ($1)", &[json!("alpha")], None)?; +#[tokio::main] +async fn main() -> Result<(), Box> { + let server = PgliteServer::temporary_tcp()?; + // For a persistent TCP server: + // let server = PgliteServer::builder().path("./.pglite").start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; - let result = db.query("SELECT value FROM items", &[], None)?; - println!("{:?}", result.rows); + let row = sqlx::query("SELECT $1::int4 + 1 AS answer") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("answer")?, 42); - db.close()?; + conn.close().await?; + server.shutdown()?; Ok(()) } ``` -For tests, use `Pglite::temporary()?`. Temporary databases clone a process-local -template cluster, so repeated tests avoid fresh `initdb` work. +That's it. Real PostgreSQL, no service setup. -## PostgreSQL Client URI +## Why pglite-oxide ✨ -Use `PgliteServer` when an existing library expects a PostgreSQL URL. Configure -client pools with one connection because the embedded runtime owns one backend. +Postgres should be as easy to add to a Rust project as SQLite. -For SQLx: +- âš¡ **No service tax**: no Docker, no local Postgres, no testcontainers. +- 🔌 **Use your real stack**: SQLx, `tokio-postgres`, CLIs, and other clients + connect through a normal local URL. +- 🌉 **Proxy included**: expose an embedded database to non-Rust tools with + `pglite-proxy`. +- 🧪 **Clean tests**: temporary databases are isolated, fast, and removed on + drop. +- 💾 **Persistent apps**: keep local app data across restarts when you want it. +- 🧩 **Extensions included**: `pgvector`, `pg_trgm`, `hstore`, `citext`, and + more. +- 📦 **Portable dumps**: use bundled `pg_dump` for logical backups and upgrade + paths. +- 🚀 **Near-native feel**: close to native Postgres, fully embedded. -```sh -cargo add sqlx --features postgres,runtime-tokio -cargo add tokio --features macros,rt-multi-thread -``` +## Near-Native Performance 🚀 + +Current local snapshot on `Apple M1 Pro`, `16 GB RAM`, and `macOS 26.4.1`. +Full numbers and reproduction steps live in the +[performance guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/PERFORMANCE.md). Lower is better. + +| Operation | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx | +|---|---:|---:|---:| +| 25,000 INSERTs in one transaction | 132.36 ms | 149.54 ms | 257.02 ms | +| 25,000 INSERTs in one statement | 46.14 ms | 59.39 ms | 117.19 ms | +| 25,000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | +| 5,000 indexed SELECTs | 81.39 ms | 125.31 ms | 203.05 ms | +| 25,000 indexed UPDATEs | 351.05 ms | 578.96 ms | 720.63 ms | + +`pglite-oxide` stays close to native Postgres while running entirely embedded +and consistently performs better than vanilla PGlite. + +## Extensions 🧩 + +Bundled extensions are supported, including `pgvector`, `pg_trgm`, `hstore`, +`citext`, `ltree`, and more. See the +[extensions guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/EXTENSIONS.md) +for the full catalog and usage details. ```rust,no_run -use pglite_oxide::PgliteServer; -use sqlx::{Connection, Row}; +use pglite_oxide::{extensions, PgliteServer}; +use sqlx::Connection; #[tokio::main] async fn main() -> Result<(), Box> { - let server = PgliteServer::temporary_tcp()?; - let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; - - let row = sqlx::query("SELECT $1::int4 + 1 AS answer") - .bind(41_i32) - .fetch_one(&mut conn) + let server = PgliteServer::builder() + .path("./.pglite") + .extension(extensions::VECTOR) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; + + sqlx::query("CREATE TABLE IF NOT EXISTS items (embedding vector(3))") + .execute(&mut conn) + .await?; + sqlx::query("INSERT INTO items VALUES ('[1,2,3]')") + .execute(&mut conn) .await?; - assert_eq!(row.try_get::("answer")?, 42); conn.close().await?; server.shutdown()?; @@ -87,14 +137,12 @@ async fn main() -> Result<(), Box> { } ``` -For app persistence, use `PgliteServer::builder().path("./.pglite").start()?`. - ## Docs - [Usage guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/USAGE.md) -- [Runtime and performance notes](https://github.com/f0rr0/pglite-oxide/blob/main/docs/RUNTIME.md) +- [Extensions](https://github.com/f0rr0/pglite-oxide/blob/main/docs/EXTENSIONS.md) +- [Performance guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/PERFORMANCE.md) +- [Dump and upgrade guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/PG_DUMP.md) +- [Testing guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/TESTING.md) - [Tauri usage](https://github.com/f0rr0/pglite-oxide/blob/main/docs/TAURI.md) -- [Tauri SQLx profiler example](https://github.com/f0rr0/pglite-oxide/blob/main/examples/tauri-sqlx-vanilla) -- [Development guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/DEVELOPMENT.md) -- [Runtime asset provenance](https://github.com/f0rr0/pglite-oxide/blob/main/docs/ASSETS.md) -- [Release process](https://github.com/f0rr0/pglite-oxide/blob/main/docs/RELEASE.md) +- [Runtime guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/RUNTIME.md) diff --git a/assets/bin/pg_dump.wasm b/assets/bin/pg_dump.wasm deleted file mode 100644 index fcaa5964..00000000 Binary files a/assets/bin/pg_dump.wasm and /dev/null differ diff --git a/assets/extensions.promoted.toml b/assets/extensions.promoted.toml new file mode 100644 index 00000000..7844e8d7 --- /dev/null +++ b/assets/extensions.promoted.toml @@ -0,0 +1,167 @@ +format-version = 1 + +[[extensions]] +id = "age" +stable = true + +[[extensions]] +id = "amcheck" +stable = true + +[[extensions]] +id = "auto_explain" +stable = true + +[[extensions]] +id = "bloom" +stable = true + +[[extensions]] +id = "btree_gin" +stable = true + +[[extensions]] +id = "btree_gist" +stable = true + +[[extensions]] +id = "citext" +stable = true + +[[extensions]] +id = "cube" +stable = true + +[[extensions]] +id = "dict_int" +stable = true + +[[extensions]] +id = "dict_xsyn" +stable = true + +[[extensions]] +id = "earthdistance" +stable = true + +[[extensions]] +id = "file_fdw" +stable = true + +[[extensions]] +id = "fuzzystrmatch" +stable = true + +[[extensions]] +id = "hstore" +stable = true + +[[extensions]] +id = "intarray" +stable = true + +[[extensions]] +id = "isn" +stable = true + +[[extensions]] +id = "lo" +stable = true + +[[extensions]] +id = "ltree" +stable = true + +[[extensions]] +id = "pageinspect" +stable = true + +[[extensions]] +id = "pg_buffercache" +stable = true + +[[extensions]] +id = "pg_freespacemap" +stable = true + +[[extensions]] +id = "pg_hashids" +stable = true + +[[extensions]] +id = "pg_ivm" +stable = true + +[[extensions]] +id = "pg_surgery" +stable = true + +[[extensions]] +id = "pg_textsearch" +stable = true + +[[extensions]] +id = "pg_trgm" +stable = true + +[[extensions]] +id = "pg_uuidv7" +stable = true + +[[extensions]] +id = "pg_visibility" +stable = true + +[[extensions]] +id = "pg_walinspect" +stable = true + +[[extensions]] +id = "pgcrypto" +build = false +stable = false +blocker = "Requires a pinned WASIX OpenSSL/libcrypto sysroot; current generic contrib build fails on openssl/evp.h." + +[[extensions]] +id = "pgtap" +stable = true + +[[extensions]] +id = "postgis" +build = false +stable = false +blocker = "Requires a pinned WASIX geospatial dependency stack and PostGIS configure/install-delta packaging before smoke." + +[[extensions]] +id = "seg" +stable = true + +[[extensions]] +id = "tablefunc" +stable = true + +[[extensions]] +id = "tcn" +stable = true + +[[extensions]] +id = "tsm_system_rows" +stable = true + +[[extensions]] +id = "tsm_system_time" +stable = true + +[[extensions]] +id = "unaccent" +stable = true + +[[extensions]] +id = "uuid_ossp" +build = false +stable = false +blocker = "Requires a pinned WASIX OSSP UUID/libuuid sysroot; upstream Emscripten builder provides this separately." + +[[extensions]] +id = "vector" +stable = true diff --git a/assets/extensions.smoke.toml b/assets/extensions.smoke.toml new file mode 100644 index 00000000..d2ae5dcf --- /dev/null +++ b/assets/extensions.smoke.toml @@ -0,0 +1,260 @@ +format-version = 1 + +[[extensions]] +id = "age" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "amcheck" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "auto_explain" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "bloom" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "btree_gin" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "btree_gist" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "citext" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "cube" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "dict_int" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "dict_xsyn" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "earthdistance" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "file_fdw" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "fuzzystrmatch" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "hstore" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "intarray" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "isn" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "lo" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "ltree" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pageinspect" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_buffercache" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_freespacemap" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_hashids" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_ivm" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_surgery" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_textsearch" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_trgm" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_uuidv7" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_visibility" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pg_walinspect" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "pgtap" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "seg" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "tablefunc" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "tcn" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "tsm_system_rows" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "tsm_system_time" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "unaccent" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "not-run" + +[[extensions]] +id = "vector" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" diff --git a/assets/extensions/vector.tar.gz b/assets/extensions/vector.tar.gz deleted file mode 100644 index df6e8b30..00000000 Binary files a/assets/extensions/vector.tar.gz and /dev/null differ diff --git a/assets/generated/asset-inputs.sha256 b/assets/generated/asset-inputs.sha256 new file mode 100644 index 00000000..48cbdf55 --- /dev/null +++ b/assets/generated/asset-inputs.sha256 @@ -0,0 +1 @@ +24f6fd55deae757f3219d82e8e0b020b9a7adfac64554337b09fdac0249de16b diff --git a/assets/generated/contrib-build.tsv b/assets/generated/contrib-build.tsv new file mode 100644 index 00000000..fd3fab14 --- /dev/null +++ b/assets/generated/contrib-build.tsv @@ -0,0 +1,31 @@ +# id sql_name contrib_dir module_file archive stable +amcheck amcheck amcheck amcheck.so extensions/amcheck.tar.zst true +auto_explain auto_explain auto_explain auto_explain.so extensions/auto_explain.tar.zst true +bloom bloom bloom bloom.so extensions/bloom.tar.zst true +btree_gin btree_gin btree_gin btree_gin.so extensions/btree_gin.tar.zst true +btree_gist btree_gist btree_gist btree_gist.so extensions/btree_gist.tar.zst true +citext citext citext citext.so extensions/citext.tar.zst true +cube cube cube cube.so extensions/cube.tar.zst true +dict_int dict_int dict_int dict_int.so extensions/dict_int.tar.zst true +dict_xsyn dict_xsyn dict_xsyn dict_xsyn.so extensions/dict_xsyn.tar.zst true +earthdistance earthdistance earthdistance earthdistance.so extensions/earthdistance.tar.zst true +file_fdw file_fdw file_fdw file_fdw.so extensions/file_fdw.tar.zst true +fuzzystrmatch fuzzystrmatch fuzzystrmatch fuzzystrmatch.so extensions/fuzzystrmatch.tar.zst true +hstore hstore hstore hstore.so extensions/hstore.tar.zst true +intarray intarray intarray _int.so extensions/intarray.tar.zst true +isn isn isn isn.so extensions/isn.tar.zst true +lo lo lo lo.so extensions/lo.tar.zst true +ltree ltree ltree ltree.so extensions/ltree.tar.zst true +pageinspect pageinspect pageinspect pageinspect.so extensions/pageinspect.tar.zst true +pg_buffercache pg_buffercache pg_buffercache pg_buffercache.so extensions/pg_buffercache.tar.zst true +pg_freespacemap pg_freespacemap pg_freespacemap pg_freespacemap.so extensions/pg_freespacemap.tar.zst true +pg_surgery pg_surgery pg_surgery pg_surgery.so extensions/pg_surgery.tar.zst true +pg_trgm pg_trgm pg_trgm pg_trgm.so extensions/pg_trgm.tar.zst true +pg_visibility pg_visibility pg_visibility pg_visibility.so extensions/pg_visibility.tar.zst true +pg_walinspect pg_walinspect pg_walinspect pg_walinspect.so extensions/pg_walinspect.tar.zst true +seg seg seg seg.so extensions/seg.tar.zst true +tablefunc tablefunc tablefunc tablefunc.so extensions/tablefunc.tar.zst true +tcn tcn tcn tcn.so extensions/tcn.tar.zst true +tsm_system_rows tsm_system_rows tsm_system_rows tsm_system_rows.so extensions/tsm_system_rows.tar.zst true +tsm_system_time tsm_system_time tsm_system_time tsm_system_time.so extensions/tsm_system_time.tar.zst true +unaccent unaccent unaccent unaccent.so extensions/unaccent.tar.zst true diff --git a/assets/generated/extensions.build-plan.json b/assets/generated/extensions.build-plan.json new file mode 100644 index 00000000..e72df8b5 --- /dev/null +++ b/assets/generated/extensions.build-plan.json @@ -0,0 +1,1320 @@ +{ + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "assets/generated/extensions.catalog.json" + }, + { + "name": "promotion-config", + "path": "assets/extensions.promoted.toml" + }, + { + "name": "asset-manifest-evidence", + "path": "target/pglite-oxide/assets/manifest.json" + } + ], + "extensions": [ + { + "id": "age", + "sql-name": "age", + "display-name": "Apache AGE", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/age", + "make-args": [ + "SIZEOF_DATUM=4" + ], + "module-file": "age.so", + "archive": "extensions/age.tar.zst", + "control-file": "assets/checkouts/age/age.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "ag_catalog", + "load-sql": [ + "LOAD 'age';" + ], + "post-create-sql": [ + "SET search_path = ag_catalog, \"$user\", public;" + ], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/age.test.ts" + ] + }, + { + "id": "amcheck", + "sql-name": "amcheck", + "display-name": "amcheck", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/amcheck", + "contrib-dir": "amcheck", + "module-file": "amcheck.so", + "archive": "extensions/amcheck.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/amcheck/amcheck.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/amcheck.test.js" + ] + }, + { + "id": "auto_explain", + "sql-name": "auto_explain", + "display-name": "auto_explain", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/auto_explain", + "contrib-dir": "auto_explain", + "module-file": "auto_explain.so", + "archive": "extensions/auto_explain.tar.zst", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": false, + "load-sql": [ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';" + ], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/auto_explain.test.js" + ] + }, + { + "id": "bloom", + "sql-name": "bloom", + "display-name": "bloom", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/bloom", + "contrib-dir": "bloom", + "module-file": "bloom.so", + "archive": "extensions/bloom.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/bloom/bloom.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/bloom.test.js" + ] + }, + { + "id": "btree_gin", + "sql-name": "btree_gin", + "display-name": "btree_gin", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/btree_gin", + "contrib-dir": "btree_gin", + "module-file": "btree_gin.so", + "archive": "extensions/btree_gin.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gin/btree_gin.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gin.test.js" + ] + }, + { + "id": "btree_gist", + "sql-name": "btree_gist", + "display-name": "btree_gist", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/btree_gist", + "contrib-dir": "btree_gist", + "module-file": "btree_gist.so", + "archive": "extensions/btree_gist.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gist/btree_gist.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gist.test.js" + ] + }, + { + "id": "citext", + "sql-name": "citext", + "display-name": "citext", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/citext", + "contrib-dir": "citext", + "module-file": "citext.so", + "archive": "extensions/citext.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/citext/citext.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/citext.test.js" + ] + }, + { + "id": "cube", + "sql-name": "cube", + "display-name": "cube", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/cube", + "contrib-dir": "cube", + "module-file": "cube.so", + "archive": "extensions/cube.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/cube/cube.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/cube.test.js" + ] + }, + { + "id": "dict_int", + "sql-name": "dict_int", + "display-name": "dict_int", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/dict_int", + "contrib-dir": "dict_int", + "module-file": "dict_int.so", + "archive": "extensions/dict_int.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/dict_int/dict_int.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_int.test.js" + ] + }, + { + "id": "dict_xsyn", + "sql-name": "dict_xsyn", + "display-name": "dict_xsyn", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/dict_xsyn", + "contrib-dir": "dict_xsyn", + "module-file": "dict_xsyn.so", + "archive": "extensions/dict_xsyn.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/dict_xsyn/dict_xsyn.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_xsyn.test.ts" + ] + }, + { + "id": "earthdistance", + "sql-name": "earthdistance", + "display-name": "earthdistance", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/earthdistance", + "contrib-dir": "earthdistance", + "module-file": "earthdistance.so", + "archive": "extensions/earthdistance.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/earthdistance/earthdistance.control", + "stable": true, + "dependencies": [ + "cube" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/earthdistance.test.js" + ] + }, + { + "id": "file_fdw", + "sql-name": "file_fdw", + "display-name": "file_fdw", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/file_fdw", + "contrib-dir": "file_fdw", + "module-file": "file_fdw.so", + "archive": "extensions/file_fdw.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/file_fdw/file_fdw.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/file_fdw.test.js" + ] + }, + { + "id": "fuzzystrmatch", + "sql-name": "fuzzystrmatch", + "display-name": "fuzzystrmatch", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/fuzzystrmatch", + "contrib-dir": "fuzzystrmatch", + "module-file": "fuzzystrmatch.so", + "archive": "extensions/fuzzystrmatch.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/fuzzystrmatch/fuzzystrmatch.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/fuzzystrmatch.test.js" + ] + }, + { + "id": "hstore", + "sql-name": "hstore", + "display-name": "hstore", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/hstore", + "contrib-dir": "hstore", + "module-file": "hstore.so", + "archive": "extensions/hstore.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/hstore/hstore.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/hstore.test.js" + ] + }, + { + "id": "intarray", + "sql-name": "intarray", + "display-name": "intarray", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/intarray", + "contrib-dir": "intarray", + "module-file": "_int.so", + "archive": "extensions/intarray.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/intarray/intarray.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/intarray.test.js" + ] + }, + { + "id": "isn", + "sql-name": "isn", + "display-name": "isn", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/isn", + "contrib-dir": "isn", + "module-file": "isn.so", + "archive": "extensions/isn.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/isn/isn.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/isn.test.js" + ] + }, + { + "id": "lo", + "sql-name": "lo", + "display-name": "lo", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/lo", + "contrib-dir": "lo", + "module-file": "lo.so", + "archive": "extensions/lo.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/lo/lo.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/lo.test.js" + ] + }, + { + "id": "ltree", + "sql-name": "ltree", + "display-name": "ltree", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/ltree", + "contrib-dir": "ltree", + "module-file": "ltree.so", + "archive": "extensions/ltree.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/ltree/ltree.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/ltree.test.js" + ] + }, + { + "id": "pageinspect", + "sql-name": "pageinspect", + "display-name": "pageinspect", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pageinspect", + "contrib-dir": "pageinspect", + "module-file": "pageinspect.so", + "archive": "extensions/pageinspect.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pageinspect/pageinspect.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pageinspect.test.js" + ] + }, + { + "id": "pg_buffercache", + "sql-name": "pg_buffercache", + "display-name": "pg_buffercache", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_buffercache", + "contrib-dir": "pg_buffercache", + "module-file": "pg_buffercache.so", + "archive": "extensions/pg_buffercache.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_buffercache/pg_buffercache.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_buffercache.test.js" + ] + }, + { + "id": "pg_freespacemap", + "sql-name": "pg_freespacemap", + "display-name": "pg_freespacemap", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_freespacemap", + "contrib-dir": "pg_freespacemap", + "module-file": "pg_freespacemap.so", + "archive": "extensions/pg_freespacemap.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_freespacemap/pg_freespacemap.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_freespacemap.test.ts" + ] + }, + { + "id": "pg_hashids", + "sql-name": "pg_hashids", + "display-name": "pg_hashids", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/pg_hashids", + "module-file": "pg_hashids.so", + "archive": "extensions/pg_hashids.tar.zst", + "control-file": "assets/checkouts/pg_hashids/pg_hashids.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_hashids.test.ts" + ] + }, + { + "id": "pg_ivm", + "sql-name": "pg_ivm", + "display-name": "pg_ivm", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/pg_ivm", + "module-file": "pg_ivm.so", + "archive": "extensions/pg_ivm.tar.zst", + "control-file": "assets/checkouts/pg_ivm/pg_ivm.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_ivm.test.ts" + ] + }, + { + "id": "pg_surgery", + "sql-name": "pg_surgery", + "display-name": "pg_surgery", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_surgery", + "contrib-dir": "pg_surgery", + "module-file": "pg_surgery.so", + "archive": "extensions/pg_surgery.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_surgery/pg_surgery.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_surgery.test.js" + ] + }, + { + "id": "pg_textsearch", + "sql-name": "pg_textsearch", + "display-name": "pg_textsearch", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/pg_textsearch", + "module-file": "pg_textsearch.so", + "archive": "extensions/pg_textsearch.tar.zst", + "control-file": "assets/checkouts/pg_textsearch/pg_textsearch.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_textsearch.test.ts" + ] + }, + { + "id": "pg_trgm", + "sql-name": "pg_trgm", + "display-name": "pg_trgm", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_trgm", + "contrib-dir": "pg_trgm", + "module-file": "pg_trgm.so", + "archive": "extensions/pg_trgm.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_trgm/pg_trgm.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_trgm.test.js" + ] + }, + { + "id": "pg_uuidv7", + "sql-name": "pg_uuidv7", + "display-name": "pg_uuidv7", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/pg_uuidv7", + "module-file": "pg_uuidv7.so", + "archive": "extensions/pg_uuidv7.tar.zst", + "control-file": "assets/checkouts/pg_uuidv7/pg_uuidv7.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_uuidv7.test.ts" + ] + }, + { + "id": "pg_visibility", + "sql-name": "pg_visibility", + "display-name": "pg_visibility", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_visibility", + "contrib-dir": "pg_visibility", + "module-file": "pg_visibility.so", + "archive": "extensions/pg_visibility.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_visibility/pg_visibility.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_visibility.test.js" + ] + }, + { + "id": "pg_walinspect", + "sql-name": "pg_walinspect", + "display-name": "pg_walinspect", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_walinspect", + "contrib-dir": "pg_walinspect", + "module-file": "pg_walinspect.so", + "archive": "extensions/pg_walinspect.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_walinspect/pg_walinspect.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_walinspect.test.js" + ] + }, + { + "id": "pgtap", + "sql-name": "pgtap", + "display-name": "pgtap", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/pgtap", + "archive": "extensions/pgtap.tar.zst", + "control-file": "assets/checkouts/pgtap/pgtap.control", + "stable": true, + "dependencies": [ + "plpgsql" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pgtap.test.ts" + ] + }, + { + "id": "seg", + "sql-name": "seg", + "display-name": "seg", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/seg", + "contrib-dir": "seg", + "module-file": "seg.so", + "archive": "extensions/seg.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/seg/seg.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/seg.test.js" + ] + }, + { + "id": "tablefunc", + "sql-name": "tablefunc", + "display-name": "tablefunc", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/tablefunc", + "contrib-dir": "tablefunc", + "module-file": "tablefunc.so", + "archive": "extensions/tablefunc.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/tablefunc/tablefunc.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tablefunc.test.js" + ] + }, + { + "id": "tcn", + "sql-name": "tcn", + "display-name": "tcn", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/tcn", + "contrib-dir": "tcn", + "module-file": "tcn.so", + "archive": "extensions/tcn.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/tcn/tcn.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tcn.test.js" + ] + }, + { + "id": "tsm_system_rows", + "sql-name": "tsm_system_rows", + "display-name": "tsm_system_rows", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/tsm_system_rows", + "contrib-dir": "tsm_system_rows", + "module-file": "tsm_system_rows.so", + "archive": "extensions/tsm_system_rows.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_rows/tsm_system_rows.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_rows.test.js" + ] + }, + { + "id": "tsm_system_time", + "sql-name": "tsm_system_time", + "display-name": "tsm_system_time", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/tsm_system_time", + "contrib-dir": "tsm_system_time", + "module-file": "tsm_system_time.so", + "archive": "extensions/tsm_system_time.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_time/tsm_system_time.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_time.test.js" + ] + }, + { + "id": "unaccent", + "sql-name": "unaccent", + "display-name": "unaccent", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "assets/checkouts/postgres-pglite/contrib/unaccent", + "contrib-dir": "unaccent", + "module-file": "unaccent.so", + "archive": "extensions/unaccent.tar.zst", + "control-file": "assets/checkouts/postgres-pglite/contrib/unaccent/unaccent.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/unaccent.test.js" + ] + }, + { + "id": "vector", + "sql-name": "vector", + "display-name": "pgvector", + "source-kind": "pglite-other-extension", + "build-kind": "pgxs-external", + "source-dir": "assets/checkouts/pgvector", + "module-file": "vector.so", + "archive": "extensions/vector.tar.zst", + "control-file": "assets/checkouts/pgvector/vector.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pgvector.test.ts" + ] + } + ] +} diff --git a/assets/generated/extensions.catalog.json b/assets/generated/extensions.catalog.json new file mode 100644 index 00000000..7f19ee16 --- /dev/null +++ b/assets/generated/extensions.catalog.json @@ -0,0 +1,2262 @@ +{ + "format-version": 1, + "generated-from": [ + { + "name": "pglite-repl-exports", + "path": "assets/checkouts/pglite/docs/repl/allExtensions.ts" + }, + { + "name": "pglite-docs-catalog", + "path": "assets/checkouts/pglite/docs/extensions/extensions.data.ts" + }, + { + "name": "pglite-package-exports", + "path": "assets/checkouts/pglite/packages/pglite/package.json" + }, + { + "name": "pglite-contrib-modules", + "path": "assets/checkouts/pglite/packages/pglite/src/contrib" + }, + { + "name": "postgres-contrib", + "path": "assets/checkouts/postgres-pglite/contrib" + }, + { + "name": "postgres-pglite-other-extensions", + "path": "assets/checkouts/postgres-pglite/pglite/other_extensions" + }, + { + "name": "extension-promotion-config", + "path": "assets/extensions.promoted.toml" + }, + { + "name": "extension-smoke-evidence", + "path": "assets/extensions.smoke.toml" + }, + { + "name": "asset-manifest-evidence", + "path": "target/pglite-oxide/assets/manifest.json" + } + ], + "extensions": [ + { + "id": "age", + "sql-name": "age", + "rust-constant": "AGE", + "display-name": "Apache AGE", + "source-kind": "pglite-other-extension", + "pglite-import-name": "age", + "pglite-import-path": "@electric-sql/pglite/age", + "package-export": "./age", + "tags": [ + "postgres extension" + ], + "bundle-size": 141551, + "control-file": "assets/checkouts/age/age.control", + "control": { + "default-version": "1.7.0", + "module-pathname": "$libdir/age", + "requires": [], + "schema": "ag_catalog" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "ag_catalog", + "load-sql": [ + "LOAD 'age';" + ], + "post-create-sql": [ + "SET search_path = ag_catalog, \"$user\", public;" + ], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/age.test.ts" + ], + "native-module-file": "age.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/age.tar.zst", + "module-sha256": "db961a6cd82c7712c1270c4fa6ccb2dccdb24d83b6a3d2781af2d62e8685de0a" + }, + "notes": [ + "postgres-pglite submodule https://github.com/apache/age.git pinned at e1467f12e0b1d15dd35d3ab93f057a7112d425b8" + ] + }, + { + "id": "amcheck", + "sql-name": "amcheck", + "rust-constant": "AMCHECK", + "display-name": "amcheck", + "source-kind": "postgres-contrib", + "pglite-import-name": "amcheck", + "pglite-import-path": "@electric-sql/pglite/contrib/amcheck", + "package-export": "./contrib/amcheck", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 18815, + "control-file": "assets/checkouts/postgres-pglite/contrib/amcheck/amcheck.control", + "control": { + "default-version": "1.4", + "module-pathname": "$libdir/amcheck", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/amcheck.test.js" + ], + "native-module-file": "amcheck.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/amcheck.tar.zst", + "module-sha256": "46f8569941bd68b3660bf0239017c5818f13d0d4b6109b25ef701a6886a17d45" + }, + "notes": [] + }, + { + "id": "auto_explain", + "sql-name": "auto_explain", + "rust-constant": "AUTO_EXPLAIN", + "display-name": "auto_explain", + "source-kind": "postgres-contrib", + "pglite-import-name": "auto_explain", + "pglite-import-path": "@electric-sql/pglite/contrib/auto_explain", + "package-export": "./contrib/auto_explain", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 3125, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": false, + "load-sql": [ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';" + ], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/auto_explain.test.js" + ], + "native-module-file": "auto_explain.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/auto_explain.tar.zst", + "module-sha256": "cd4e51ac361ee24a9e7ce798d44be00994db625deccda7f510d57b486e072354" + }, + "notes": [] + }, + { + "id": "bloom", + "sql-name": "bloom", + "rust-constant": "BLOOM", + "display-name": "bloom", + "source-kind": "postgres-contrib", + "pglite-import-name": "bloom", + "pglite-import-path": "@electric-sql/pglite/contrib/bloom", + "package-export": "./contrib/bloom", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 6197, + "control-file": "assets/checkouts/postgres-pglite/contrib/bloom/bloom.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/bloom", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/bloom.test.js" + ], + "native-module-file": "bloom.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/bloom.tar.zst", + "module-sha256": "62b098457eca4dc5559adda558e090379949948a179b25840931d86dcd0f0461" + }, + "notes": [] + }, + { + "id": "btree_gin", + "sql-name": "btree_gin", + "rust-constant": "BTREE_GIN", + "display-name": "btree_gin", + "source-kind": "postgres-contrib", + "pglite-import-name": "btree_gin", + "pglite-import-path": "@electric-sql/pglite/contrib/btree_gin", + "package-export": "./contrib/btree_gin", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 6347, + "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gin/btree_gin.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/btree_gin", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gin.test.js" + ], + "native-module-file": "btree_gin.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/btree_gin.tar.zst", + "module-sha256": "3ef71d9930b3bba882c67a8981d45a72c91d7ac547cc476ffef9539bc30cb678" + }, + "notes": [] + }, + { + "id": "btree_gist", + "sql-name": "btree_gist", + "rust-constant": "BTREE_GIST", + "display-name": "btree_gist", + "source-kind": "postgres-contrib", + "pglite-import-name": "btree_gist", + "pglite-import-path": "@electric-sql/pglite/contrib/btree_gist", + "package-export": "./contrib/btree_gist", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 24181, + "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gist/btree_gist.control", + "control": { + "default-version": "1.7", + "module-pathname": "$libdir/btree_gist", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gist.test.js" + ], + "native-module-file": "btree_gist.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/btree_gist.tar.zst", + "module-sha256": "1074bc00fef8fa8631e5b01bd1811b15cba12e203cc5fff6f58f43dbba97fd87" + }, + "notes": [] + }, + { + "id": "citext", + "sql-name": "citext", + "rust-constant": "CITEXT", + "display-name": "citext", + "source-kind": "postgres-contrib", + "pglite-import-name": "citext", + "pglite-import-path": "@electric-sql/pglite/contrib/citext", + "package-export": "./contrib/citext", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4983, + "control-file": "assets/checkouts/postgres-pglite/contrib/citext/citext.control", + "control": { + "default-version": "1.6", + "module-pathname": "$libdir/citext", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/citext.test.js" + ], + "native-module-file": "citext.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/citext.tar.zst", + "module-sha256": "63f974647782a25b233ba787d7fcbab24a672234d534c5f25425878e1cdf22ec" + }, + "notes": [] + }, + { + "id": "cube", + "sql-name": "cube", + "rust-constant": "CUBE", + "display-name": "cube", + "source-kind": "postgres-contrib", + "pglite-import-name": "cube", + "pglite-import-path": "@electric-sql/pglite/contrib/cube", + "package-export": "./contrib/cube", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 15104, + "control-file": "assets/checkouts/postgres-pglite/contrib/cube/cube.control", + "control": { + "default-version": "1.5", + "module-pathname": "$libdir/cube", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/cube.test.js" + ], + "native-module-file": "cube.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/cube.tar.zst", + "module-sha256": "e63cd4629af7d6a94dd5c4443c828211162bfa0321b5f4c8b136e00d1fe4fe13" + }, + "notes": [] + }, + { + "id": "dict_int", + "sql-name": "dict_int", + "rust-constant": "DICT_INT", + "display-name": "dict_int", + "source-kind": "postgres-contrib", + "pglite-import-name": "dict_int", + "pglite-import-path": "@electric-sql/pglite/contrib/dict_int", + "package-export": "./contrib/dict_int", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1361, + "control-file": "assets/checkouts/postgres-pglite/contrib/dict_int/dict_int.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/dict_int", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_int.test.js" + ], + "native-module-file": "dict_int.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/dict_int.tar.zst", + "module-sha256": "44955defc617a97878d7ad47fb37eb2a6b4d62d6566256f3852d468afc14a009" + }, + "notes": [] + }, + { + "id": "dict_xsyn", + "sql-name": "dict_xsyn", + "rust-constant": "DICT_XSYN", + "display-name": "dict_xsyn", + "source-kind": "postgres-contrib", + "pglite-import-name": "dict_xsyn", + "pglite-import-path": "@electric-sql/pglite/contrib/dict_xsyn", + "package-export": "./contrib/dict_xsyn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1948, + "control-file": "assets/checkouts/postgres-pglite/contrib/dict_xsyn/dict_xsyn.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/dict_xsyn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_xsyn.test.ts" + ], + "native-module-file": "dict_xsyn.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/dict_xsyn.tar.zst", + "module-sha256": "ed92af122102ebcf520801456bfb90231dcce50495aa59cba7a3464fceff77f9" + }, + "notes": [] + }, + { + "id": "earthdistance", + "sql-name": "earthdistance", + "rust-constant": "EARTHDISTANCE", + "display-name": "earthdistance", + "source-kind": "postgres-contrib", + "pglite-import-name": "earthdistance", + "pglite-import-path": "@electric-sql/pglite/contrib/earthdistance", + "package-export": "./contrib/earthdistance", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2220, + "control-file": "assets/checkouts/postgres-pglite/contrib/earthdistance/earthdistance.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/earthdistance", + "requires": [ + "cube" + ], + "relocatable": "true" + }, + "dependencies": [ + "cube" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/earthdistance.test.js" + ], + "native-module-file": "earthdistance.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/earthdistance.tar.zst", + "module-sha256": "a2fd0268a07e14df2b8cd0624f4c37d806aad5ce0a82f40bb8622cb1500af5c4" + }, + "notes": [] + }, + { + "id": "file_fdw", + "sql-name": "file_fdw", + "rust-constant": "FILE_FDW", + "display-name": "file_fdw", + "source-kind": "postgres-contrib", + "pglite-import-name": "file_fdw", + "pglite-import-path": "@electric-sql/pglite/contrib/file_fdw", + "package-export": "./contrib/file_fdw", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4467, + "control-file": "assets/checkouts/postgres-pglite/contrib/file_fdw/file_fdw.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/file_fdw", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/file_fdw.test.js" + ], + "native-module-file": "file_fdw.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/file_fdw.tar.zst", + "module-sha256": "7bd1a071edb2596f389ebb8c0a486da368c7e00191b0987c536fadf061256d50" + }, + "notes": [] + }, + { + "id": "fuzzystrmatch", + "sql-name": "fuzzystrmatch", + "rust-constant": "FUZZYSTRMATCH", + "display-name": "fuzzystrmatch", + "source-kind": "postgres-contrib", + "pglite-import-name": "fuzzystrmatch", + "pglite-import-path": "@electric-sql/pglite/contrib/fuzzystrmatch", + "package-export": "./contrib/fuzzystrmatch", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 12026, + "control-file": "assets/checkouts/postgres-pglite/contrib/fuzzystrmatch/fuzzystrmatch.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/fuzzystrmatch", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/fuzzystrmatch.test.js" + ], + "native-module-file": "fuzzystrmatch.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/fuzzystrmatch.tar.zst", + "module-sha256": "329cbb5f46e13529987934094660b827e65552cbeaa5f2b2c0405a193ae75687" + }, + "notes": [] + }, + { + "id": "hstore", + "sql-name": "hstore", + "rust-constant": "HSTORE", + "display-name": "hstore", + "source-kind": "postgres-contrib", + "pglite-import-name": "hstore", + "pglite-import-path": "@electric-sql/pglite/contrib/hstore", + "package-export": "./contrib/hstore", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 21380, + "control-file": "assets/checkouts/postgres-pglite/contrib/hstore/hstore.control", + "control": { + "default-version": "1.8", + "module-pathname": "$libdir/hstore", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/hstore.test.js" + ], + "native-module-file": "hstore.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/hstore.tar.zst", + "module-sha256": "51359ef4a23523ccf0cedcf26ac47e3594f6b22bedc8d6d5d9a41dce066e0ba6" + }, + "notes": [] + }, + { + "id": "intarray", + "sql-name": "intarray", + "rust-constant": "INTARRAY", + "display-name": "intarray", + "source-kind": "postgres-contrib", + "pglite-import-name": "intarray", + "pglite-import-path": "@electric-sql/pglite/contrib/intarray", + "package-export": "./contrib/intarray", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 14712, + "control-file": "assets/checkouts/postgres-pglite/contrib/intarray/intarray.control", + "control": { + "default-version": "1.5", + "module-pathname": "$libdir/_int", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/intarray.test.js" + ], + "native-module-file": "_int.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/intarray.tar.zst", + "module-sha256": "77abeef93372180e39e969a955f7c8201e34b08319ea429e7b07703970c8cc4e" + }, + "notes": [] + }, + { + "id": "isn", + "sql-name": "isn", + "rust-constant": "ISN", + "display-name": "isn", + "source-kind": "postgres-contrib", + "pglite-import-name": "isn", + "pglite-import-path": "@electric-sql/pglite/contrib/isn", + "package-export": "./contrib/isn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 31417, + "control-file": "assets/checkouts/postgres-pglite/contrib/isn/isn.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/isn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/isn.test.js" + ], + "native-module-file": "isn.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/isn.tar.zst", + "module-sha256": "cc0e86b65c59209df4e7c12fd8b2fedfd52de08117f597c73d5d04ef989275e5" + }, + "notes": [] + }, + { + "id": "lo", + "sql-name": "lo", + "rust-constant": "LO", + "display-name": "lo", + "source-kind": "postgres-contrib", + "pglite-import-name": "lo", + "pglite-import-path": "@electric-sql/pglite/contrib/lo", + "package-export": "./contrib/lo", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1822, + "control-file": "assets/checkouts/postgres-pglite/contrib/lo/lo.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/lo", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/lo.test.js" + ], + "native-module-file": "lo.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/lo.tar.zst", + "module-sha256": "36a2862ccc365f83ad5808eaa496e79d34d006d05b353bd1121b04246ea78ce8" + }, + "notes": [] + }, + { + "id": "ltree", + "sql-name": "ltree", + "rust-constant": "LTREE", + "display-name": "ltree", + "source-kind": "postgres-contrib", + "pglite-import-name": "ltree", + "pglite-import-path": "@electric-sql/pglite/contrib/ltree", + "package-export": "./contrib/ltree", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 19553, + "control-file": "assets/checkouts/postgres-pglite/contrib/ltree/ltree.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/ltree", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/ltree.test.js" + ], + "native-module-file": "ltree.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/ltree.tar.zst", + "module-sha256": "56507f0debc47adda14f5d35a3265f8f00f2471c023ecc1fbaffc704779c321e" + }, + "notes": [] + }, + { + "id": "pageinspect", + "sql-name": "pageinspect", + "rust-constant": "PAGEINSPECT", + "display-name": "pageinspect", + "source-kind": "postgres-contrib", + "pglite-import-name": "pageinspect", + "pglite-import-path": "@electric-sql/pglite/contrib/pageinspect", + "package-export": "./contrib/pageinspect", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 15923, + "control-file": "assets/checkouts/postgres-pglite/contrib/pageinspect/pageinspect.control", + "control": { + "default-version": "1.12", + "module-pathname": "$libdir/pageinspect", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pageinspect.test.js" + ], + "native-module-file": "pageinspect.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pageinspect.tar.zst", + "module-sha256": "9c5fc1aa2243810a123d2868c2405227d7d25d9866e273b3a6f24797115f8322" + }, + "notes": [] + }, + { + "id": "pg_buffercache", + "sql-name": "pg_buffercache", + "rust-constant": "PG_BUFFERCACHE", + "display-name": "pg_buffercache", + "source-kind": "postgres-contrib", + "pglite-import-name": "pg_buffercache", + "pglite-import-path": "@electric-sql/pglite/contrib/pg_buffercache", + "package-export": "./contrib/pg_buffercache", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 3133, + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_buffercache/pg_buffercache.control", + "control": { + "default-version": "1.5", + "module-pathname": "$libdir/pg_buffercache", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_buffercache.test.js" + ], + "native-module-file": "pg_buffercache.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_buffercache.tar.zst", + "module-sha256": "3ac0bed8c504ed7af629f32454e323924bb28fcaec28da403dd8e4fc50e0f2e6" + }, + "notes": [] + }, + { + "id": "pg_freespacemap", + "sql-name": "pg_freespacemap", + "rust-constant": "PG_FREESPACEMAP", + "display-name": "pg_freespacemap", + "source-kind": "postgres-contrib", + "pglite-import-name": "pg_freespacemap", + "pglite-import-path": "@electric-sql/pglite/contrib/pg_freespacemap", + "package-export": "./contrib/pg_freespacemap", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1485, + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_freespacemap/pg_freespacemap.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/pg_freespacemap", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_freespacemap.test.ts" + ], + "native-module-file": "pg_freespacemap.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_freespacemap.tar.zst", + "module-sha256": "38809d986cd421965060d030a59102aa44dbe1707a90ad74e322ce41318b4ec7" + }, + "notes": [] + }, + { + "id": "pg_hashids", + "sql-name": "pg_hashids", + "rust-constant": "PG_HASHIDS", + "display-name": "pg_hashids", + "source-kind": "pglite-other-extension", + "pglite-import-name": "pg_hashids", + "pglite-import-path": "@electric-sql/pglite/pg_hashids", + "package-export": "./pg_hashids", + "tags": [ + "postgres extension" + ], + "bundle-size": 4212, + "control-file": "assets/checkouts/pg_hashids/pg_hashids.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/pg_hashids", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_hashids.test.ts" + ], + "native-module-file": "pg_hashids.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_hashids.tar.zst", + "module-sha256": "b7bdf822c03ff5fcba6ba2a89f6dcaab90d30bf1c531062165bb3d125b4b95ee" + }, + "notes": [ + "postgres-pglite submodule https://github.com/iCyberon/pg_hashids pinned at 8c404dd86408f3a987a3ff6825ac7e42bd618b98" + ] + }, + { + "id": "pg_ivm", + "sql-name": "pg_ivm", + "rust-constant": "PG_IVM", + "display-name": "pg_ivm", + "source-kind": "pglite-other-extension", + "pglite-import-name": "pg_ivm", + "pglite-import-path": "@electric-sql/pglite/pg_ivm", + "package-export": "./pg_ivm", + "tags": [ + "postgres extension" + ], + "bundle-size": 24865, + "control-file": "assets/checkouts/pg_ivm/pg_ivm.control", + "control": { + "default-version": "1.13", + "module-pathname": "$libdir/pg_ivm", + "requires": [], + "relocatable": "false", + "schema": "pg_catalog" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_ivm.test.ts" + ], + "native-module-file": "pg_ivm.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_ivm.tar.zst", + "module-sha256": "7aefdd437c0a3ee8b4f246b9fadf01734ad991c505dff8f6d004ac6e74a9154a" + }, + "notes": [ + "postgres-pglite submodule https://github.com/sraoss/pg_ivm.git pinned at b66487f7a6f8deee3998e858d773e19923e4bd4b" + ] + }, + { + "id": "pg_surgery", + "sql-name": "pg_surgery", + "rust-constant": "PG_SURGERY", + "display-name": "pg_surgery", + "source-kind": "postgres-contrib", + "pglite-import-name": "pg_surgery", + "pglite-import-path": "@electric-sql/pglite/contrib/pg_surgery", + "package-export": "./contrib/pg_surgery", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2635, + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_surgery/pg_surgery.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/pg_surgery", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_surgery.test.js" + ], + "native-module-file": "pg_surgery.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_surgery.tar.zst", + "module-sha256": "dd1b8daebf7da458eec04b9ba5733913fd1f0d9bad41447e7ee3e0d3611f0ae2" + }, + "notes": [] + }, + { + "id": "pg_textsearch", + "sql-name": "pg_textsearch", + "rust-constant": "PG_TEXTSEARCH", + "display-name": "pg_textsearch", + "source-kind": "pglite-other-extension", + "pglite-import-name": "pg_textsearch", + "pglite-import-path": "@electric-sql/pglite/pg_textsearch", + "package-export": "./pg_textsearch", + "tags": [ + "postgres extension", + "experimental" + ], + "bundle-size": 55062, + "control-file": "assets/checkouts/pg_textsearch/pg_textsearch.control", + "control": { + "default-version": "0.5.1", + "module-pathname": "$libdir/pg_textsearch", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_textsearch.test.ts" + ], + "native-module-file": "pg_textsearch.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_textsearch.tar.zst", + "module-sha256": "70566a2191130c20f703aa20016dcc0a2c90312d6b05b0c04a5c8fc4a1e1b896" + }, + "notes": [ + "postgres-pglite submodule https://github.com/timescale/pg_textsearch.git pinned at 5c5147bf2610d786f1bd139951b9fb7fe4ac68fb" + ] + }, + { + "id": "pg_trgm", + "sql-name": "pg_trgm", + "rust-constant": "PG_TRGM", + "display-name": "pg_trgm", + "source-kind": "postgres-contrib", + "pglite-import-name": "pg_trgm", + "pglite-import-path": "@electric-sql/pglite/contrib/pg_trgm", + "package-export": "./contrib/pg_trgm", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 16208, + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_trgm/pg_trgm.control", + "control": { + "default-version": "1.6", + "module-pathname": "$libdir/pg_trgm", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_trgm.test.js" + ], + "native-module-file": "pg_trgm.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_trgm.tar.zst", + "module-sha256": "997b99a690c538d0034997c3f9224c2d6e108438e679535a5fa67ecaec476863" + }, + "notes": [] + }, + { + "id": "pg_uuidv7", + "sql-name": "pg_uuidv7", + "rust-constant": "PG_UUIDV7", + "display-name": "pg_uuidv7", + "source-kind": "pglite-other-extension", + "pglite-import-name": "pg_uuidv7", + "pglite-import-path": "@electric-sql/pglite/pg_uuidv7", + "package-export": "./pg_uuidv7", + "tags": [ + "postgres extension" + ], + "bundle-size": 1522, + "control-file": "assets/checkouts/pg_uuidv7/pg_uuidv7.control", + "control": { + "default-version": "1.7", + "module-pathname": "$libdir/pg_uuidv7", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pg_uuidv7.test.ts" + ], + "native-module-file": "pg_uuidv7.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_uuidv7.tar.zst", + "module-sha256": "96e9903d134288ca7f7f5e094dd3c09ec83bd6ce5dc2d87c3d93732c4b4040b4" + }, + "notes": [ + "postgres-pglite submodule https://github.com/fboulnois/pg_uuidv7/ pinned at c707aae2411181be4802f5fa565b44d9c0bcbc29" + ] + }, + { + "id": "pg_visibility", + "sql-name": "pg_visibility", + "rust-constant": "PG_VISIBILITY", + "display-name": "pg_visibility", + "source-kind": "postgres-contrib", + "pglite-import-name": "pg_visibility", + "pglite-import-path": "@electric-sql/pglite/contrib/pg_visibility", + "package-export": "./contrib/pg_visibility", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4159, + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_visibility/pg_visibility.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/pg_visibility", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_visibility.test.js" + ], + "native-module-file": "pg_visibility.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_visibility.tar.zst", + "module-sha256": "0c5fcce9a85cb57422a7c201352fe30848ba8d67714e49283515f343c41401d6" + }, + "notes": [] + }, + { + "id": "pg_walinspect", + "sql-name": "pg_walinspect", + "rust-constant": "PG_WALINSPECT", + "display-name": "pg_walinspect", + "source-kind": "postgres-contrib", + "pglite-import-name": "pg_walinspect", + "pglite-import-path": "@electric-sql/pglite/contrib/pg_walinspect", + "package-export": "./contrib/pg_walinspect", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4689, + "control-file": "assets/checkouts/postgres-pglite/contrib/pg_walinspect/pg_walinspect.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/pg_walinspect", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_walinspect.test.js" + ], + "native-module-file": "pg_walinspect.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_walinspect.tar.zst", + "module-sha256": "7a6ce46998a15d4487b51d3a0c7125f3468372c1e2ddbeb31b66a8ad106a41e8" + }, + "notes": [] + }, + { + "id": "pgcrypto", + "sql-name": "pgcrypto", + "rust-constant": "PGCRYPTO", + "display-name": "pgcrypto", + "source-kind": "postgres-contrib", + "pglite-import-name": "pgcrypto", + "pglite-import-path": "@electric-sql/pglite/contrib/pgcrypto", + "package-export": "./contrib/pgcrypto", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1148162, + "control-file": "assets/checkouts/postgres-pglite/contrib/pgcrypto/pgcrypto.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/pgcrypto", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "not-run", + "server": "not-run", + "restart": "not-run", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/pgcrypto.test.js" + ], + "native-module-file": "pgcrypto.so", + "promotion": { + "configured": true, + "requested": false, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/pgcrypto.tar.zst", + "blocker": "Requires a pinned WASIX OpenSSL/libcrypto sysroot; current generic contrib build fails on openssl/evp.h." + }, + "notes": [ + "promotion blocker: Requires a pinned WASIX OpenSSL/libcrypto sysroot; current generic contrib build fails on openssl/evp.h." + ] + }, + { + "id": "pgtap", + "sql-name": "pgtap", + "rust-constant": "PGTAP", + "display-name": "pgtap", + "source-kind": "pglite-other-extension", + "pglite-import-name": "pgtap", + "pglite-import-path": "@electric-sql/pglite/pgtap", + "package-export": "./pgtap", + "tags": [ + "postgres extension" + ], + "bundle-size": 239428, + "control-file": "assets/checkouts/pgtap/pgtap.control", + "control": { + "default-version": "1.3.5", + "requires": [ + "plpgsql" + ], + "relocatable": "true" + }, + "dependencies": [ + "plpgsql" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pgtap.test.ts" + ], + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pgtap.tar.zst", + "module-sha256": "" + }, + "notes": [ + "postgres-pglite submodule https://github.com/theory/pgtap.git pinned at b89585a64ffef012ff0f219de9197c669aa8485b" + ] + }, + { + "id": "postgis", + "sql-name": "postgis", + "rust-constant": "POSTGIS", + "display-name": "PostGIS", + "source-kind": "postgis", + "pglite-import-name": "postgis", + "pglite-import-path": "@electric-sql/pglite-postgis", + "tags": [ + "postgres extension", + "experimental" + ], + "bundle-size": 8551161, + "dependencies": [], + "native-dependencies": [], + "load-order": [ + "lib/postgresql/postgis-3.so", + "lib/postgresql/postgis_topology-3.so", + "lib/postgresql/postgis_raster-3.so" + ], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "not-run", + "server": "not-run", + "restart": "not-run", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite-postgis/tests/postgis.test.ts" + ], + "promotion": { + "configured": true, + "requested": false, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/postgis.tar.zst", + "blocker": "Requires a pinned WASIX geospatial dependency stack and PostGIS configure/install-delta packaging before smoke." + }, + "notes": [ + "control file unavailable in current checkout; source submodule may not be initialized", + "postgres-pglite submodule https://github.com/postgis/postgis.git pinned at 08d9b9f749fa3531591055db2a736bfb6df47006", + "promotion blocker: Requires a pinned WASIX geospatial dependency stack and PostGIS configure/install-delta packaging before smoke." + ] + }, + { + "id": "seg", + "sql-name": "seg", + "rust-constant": "SEG", + "display-name": "seg", + "source-kind": "postgres-contrib", + "pglite-import-name": "seg", + "pglite-import-path": "@electric-sql/pglite/contrib/seg", + "package-export": "./contrib/seg", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 10426, + "control-file": "assets/checkouts/postgres-pglite/contrib/seg/seg.control", + "control": { + "default-version": "1.4", + "module-pathname": "$libdir/seg", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/seg.test.js" + ], + "native-module-file": "seg.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/seg.tar.zst", + "module-sha256": "bf0650bbc399138f0b62c25bff3f5f57dc17e216fa0325b5b109eb31015682fe" + }, + "notes": [] + }, + { + "id": "tablefunc", + "sql-name": "tablefunc", + "rust-constant": "TABLEFUNC", + "display-name": "tablefunc", + "source-kind": "postgres-contrib", + "pglite-import-name": "tablefunc", + "pglite-import-path": "@electric-sql/pglite/contrib/tablefunc", + "package-export": "./contrib/tablefunc", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 5824, + "control-file": "assets/checkouts/postgres-pglite/contrib/tablefunc/tablefunc.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tablefunc", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tablefunc.test.js" + ], + "native-module-file": "tablefunc.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tablefunc.tar.zst", + "module-sha256": "c15351f6a1dad0b1ff43a20c8966be666a0d4a6dc0ef2d87b9aae8d04671aa11" + }, + "notes": [] + }, + { + "id": "tcn", + "sql-name": "tcn", + "rust-constant": "TCN", + "display-name": "tcn", + "source-kind": "postgres-contrib", + "pglite-import-name": "tcn", + "pglite-import-path": "@electric-sql/pglite/contrib/tcn", + "package-export": "./contrib/tcn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1914, + "control-file": "assets/checkouts/postgres-pglite/contrib/tcn/tcn.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tcn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tcn.test.js" + ], + "native-module-file": "tcn.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tcn.tar.zst", + "module-sha256": "ed94b83055df5cb5e64884cc556f76e17ef663fd272adfb4fbcba80f894fb604" + }, + "notes": [] + }, + { + "id": "tsm_system_rows", + "sql-name": "tsm_system_rows", + "rust-constant": "TSM_SYSTEM_ROWS", + "display-name": "tsm_system_rows", + "source-kind": "postgres-contrib", + "pglite-import-name": "tsm_system_rows", + "pglite-import-path": "@electric-sql/pglite/contrib/tsm_system_rows", + "package-export": "./contrib/tsm_system_rows", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2048, + "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_rows/tsm_system_rows.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tsm_system_rows", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_rows.test.js" + ], + "native-module-file": "tsm_system_rows.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tsm_system_rows.tar.zst", + "module-sha256": "d66231a731c3a66e16226ef90023208c8659133c1af5059ddf7d09a29f4b56a3" + }, + "notes": [] + }, + { + "id": "tsm_system_time", + "sql-name": "tsm_system_time", + "rust-constant": "TSM_SYSTEM_TIME", + "display-name": "tsm_system_time", + "source-kind": "postgres-contrib", + "pglite-import-name": "tsm_system_time", + "pglite-import-path": "@electric-sql/pglite/contrib/tsm_system_time", + "package-export": "./contrib/tsm_system_time", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2099, + "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_time/tsm_system_time.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tsm_system_time", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_time.test.js" + ], + "native-module-file": "tsm_system_time.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tsm_system_time.tar.zst", + "module-sha256": "c4b0a9770859c988e7c278d8a6518a0e892bcae96cf218c337a0aef188bcb8c8" + }, + "notes": [] + }, + { + "id": "unaccent", + "sql-name": "unaccent", + "rust-constant": "UNACCENT", + "display-name": "unaccent", + "source-kind": "postgres-contrib", + "pglite-import-name": "unaccent", + "pglite-import-path": "@electric-sql/pglite/contrib/unaccent", + "package-export": "./contrib/unaccent", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 9323, + "control-file": "assets/checkouts/postgres-pglite/contrib/unaccent/unaccent.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/unaccent", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/unaccent.test.js" + ], + "native-module-file": "unaccent.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/unaccent.tar.zst", + "module-sha256": "f966e59cf9431a5b880d4a5e091c6a3eb1eac0c84173466631216bc0c8c0ea67" + }, + "notes": [] + }, + { + "id": "uuid_ossp", + "sql-name": "uuid-ossp", + "rust-constant": "UUID_OSSP", + "display-name": "uuid-ossp", + "source-kind": "postgres-contrib", + "pglite-import-name": "uuid_ossp", + "pglite-import-path": "@electric-sql/pglite/contrib/uuid_ossp", + "package-export": "./contrib/uuid_ossp", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 17936, + "control-file": "assets/checkouts/postgres-pglite/contrib/uuid-ossp/uuid-ossp.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/uuid-ossp", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "not-run", + "server": "not-run", + "restart": "not-run", + "dump-restore": "not-run" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/contrib/uuid_ossp.test.js" + ], + "native-module-file": "uuid-ossp.so", + "promotion": { + "configured": true, + "requested": false, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/uuid-ossp.tar.zst", + "blocker": "Requires a pinned WASIX OSSP UUID/libuuid sysroot; upstream Emscripten builder provides this separately." + }, + "notes": [ + "promotion blocker: Requires a pinned WASIX OSSP UUID/libuuid sysroot; upstream Emscripten builder provides this separately." + ] + }, + { + "id": "vector", + "sql-name": "vector", + "rust-constant": "VECTOR", + "display-name": "pgvector", + "source-kind": "pglite-other-extension", + "pglite-import-name": "vector", + "pglite-import-path": "@electric-sql/pglite/vector", + "package-export": "./vector", + "tags": [ + "postgres extension" + ], + "bundle-size": 43953, + "control-file": "assets/checkouts/pgvector/vector.control", + "control": { + "default-version": "0.8.2", + "module-pathname": "$libdir/vector", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "assets/checkouts/pglite/packages/pglite/tests/pgvector.test.ts" + ], + "native-module-file": "vector.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/vector.tar.zst", + "module-sha256": "860a9dda5d86bfb5008fa0281a30454b8de6b0754e6fd25be5d1cd6308a36629" + }, + "notes": [ + "postgres-pglite submodule https://github.com/pgvector/pgvector.git pinned at 35ab919bf5da677709b2ebb8be07480bb25e97cf" + ] + } + ] +} diff --git a/assets/generated/pgxs-build.tsv b/assets/generated/pgxs-build.tsv new file mode 100644 index 00000000..274a9c57 --- /dev/null +++ b/assets/generated/pgxs-build.tsv @@ -0,0 +1,8 @@ +# id sql_name source_dir module_file archive stable make_args +age age assets/checkouts/age age.so extensions/age.tar.zst true SIZEOF_DATUM=4 +pg_hashids pg_hashids assets/checkouts/pg_hashids pg_hashids.so extensions/pg_hashids.tar.zst true - +pg_ivm pg_ivm assets/checkouts/pg_ivm pg_ivm.so extensions/pg_ivm.tar.zst true - +pg_textsearch pg_textsearch assets/checkouts/pg_textsearch pg_textsearch.so extensions/pg_textsearch.tar.zst true - +pg_uuidv7 pg_uuidv7 assets/checkouts/pg_uuidv7 pg_uuidv7.so extensions/pg_uuidv7.tar.zst true - +pgtap pgtap assets/checkouts/pgtap - extensions/pgtap.tar.zst true - +vector vector assets/checkouts/pgvector vector.so extensions/vector.tar.zst true - diff --git a/assets/generated/wasix-dl.exports b/assets/generated/wasix-dl.exports new file mode 100644 index 00000000..2f11f960 --- /dev/null +++ b/assets/generated/wasix-dl.exports @@ -0,0 +1,1317 @@ +AcceptInvalidationMessages +AcquireRewriteLocks +AllocSetContextCreateInternal +AlterSequence +AlterTable +ArrayGetIntegerTypmods +ArrayGetNItems +Async_Notify +AtEOXact_GUC +BeginCopyFrom +BeginInternalSubTransaction +BlessTupleDesc +BlockSampler_HasMore +BlockSampler_Init +BlockSampler_Next +BufferBlocks +BufferDescriptors +BufferGetBlockNumber +BuildIndexInfo +BuildTupleFromCStrings +CacheMemoryContext +CacheRegisterRelcacheCallback +CacheRegisterSyscacheCallback +CachedPlanAllowsSimpleValidityCheck +CachedPlanIsSimplyValid +CallerFInfoFunctionCall2 +CatalogCloseIndexes +CatalogOpenIndexes +CatalogTupleDelete +CatalogTupleInsert +CatalogTupleInsertWithInfo +CatalogTupleUpdate +ChangeVarNodes +CheckFunctionValidatorAccess +CheckIndexCompatible +CheckTableNotInUse +CheckXidAlive +ChooseRelationName +CommandCounterIncrement +ConditionVariableCancelSleep +ConditionVariableInit +ConditionVariableSignal +ConditionVariableSleep +ConditionalLockBuffer +ConditionalLockRelationOid +CopyErrorData +CopyFromErrorCallback +CreateCacheMemoryContext +CreateDestReceiver +CreateExecutorState +CreateExprContext +CreateParallelContext +CreateQueryDesc +CreateSchemaCommand +CreateTableAsRelExists +CreateTemplateTupleDesc +CreateTransientRelDestReceiver +CreateTrigger +CreateTupleDescCopy +CritSectionCount +CurrentMemoryContext +CurrentResourceOwner +DatumGetEOHP +DecrTupleDescRefCount +DefineCustomBoolVariable +DefineCustomEnumVariable +DefineCustomIntVariable +DefineCustomRealVariable +DefineCustomStringVariable +DefineIndex +DefineRelation +DefineSequence +DeleteExpandedObject +DestroyParallelContext +DirectFunctionCall1Coll +DirectFunctionCall2Coll +DirectFunctionCall3Coll +DirectFunctionCall4Coll +DirectFunctionCall5Coll +EOH_flatten_into +EOH_get_flat_size +EncodeDateOnly +EncodeDateTime +EncodeSpecialDate +EncodeSpecialTimestamp +EncodeTimeOnly +EndCopyFrom +EnsurePortalSnapshotExists +EnterParallelMode +EvictUnpinnedBuffer +ExecAssignExprContext +ExecAssignProjectionInfo +ExecCloseIndices +ExecCloseRangeTableRelations +ExecCloseResultRelations +ExecConstraints +ExecDropSingleTupleTableSlot +ExecEndNode +ExecFetchSlotHeapTuple +ExecGetResultType +ExecInitExpr +ExecInitExprWithParams +ExecInitExtraTupleSlot +ExecInitNode +ExecInitQual +ExecInitRangeTable +ExecInitResultRelation +ExecInitScanTupleSlot +ExecInsertIndexTuples +ExecOpenIndices +ExecPrepareQual +ExecReScan +ExecStoreHeapTuple +ExecStoreVirtualTuple +ExecUpdateLockMode +ExecWithCheckOptions +ExecuteTruncateGuts +ExecutorEnd +ExecutorEnd_hook +ExecutorFinish +ExecutorFinish_hook +ExecutorRun +ExecutorRun_hook +ExecutorStart +ExecutorStart_hook +ExitParallelMode +ExplainBeginOutput +ExplainEndOutput +ExplainPrintJITSummary +ExplainPrintPlan +ExplainPrintTriggers +ExplainPropertyInteger +ExplainPropertyText +ExplainQueryParameters +ExplainQueryText +ExprEvalPushStep +ExtendBufferedRel +FigureColname +Float8GetDatum +FlushErrorState +FlushOneBuffer +FlushRelationBuffers +FreeAccessStrategy +FreeBulkInsertState +FreeCachedExpression +FreeExecutorState +FreeExprContext +FreeQueryDesc +FunctionCall0Coll +FunctionCall1Coll +FunctionCall2Coll +FunctionCall4Coll +GUC_check_errdetail_string +GenerationContextCreate +GenericXLogAbort +GenericXLogFinish +GenericXLogRegisterBuffer +GenericXLogStart +GetAccessStrategy +GetActiveSnapshot +GetBulkInsertState +GetCachedExpression +GetCommandTagName +GetCurrentCommandId +GetCurrentSubTransactionId +GetCurrentTimestamp +GetDatabaseEncoding +GetDatabaseEncodingName +GetDefaultOpClass +GetErrorContextStack +GetFlushRecPtr +GetForeignColumnOptions +GetForeignDataWrapper +GetForeignServer +GetForeignTable +GetFreeIndexPage +GetOldestNonRemovableTransactionId +GetRecordedFreeSpace +GetRunningTransactionData +GetSearchPathMatcher +GetSysCacheOid +GetTopFullTransactionId +GetTransactionSnapshot +GetUserId +GetUserIdAndSecContext +GetXLogReplayRecPtr +HeapTupleGetUpdateXid +HeapTupleHeaderGetDatum +HeapTupleSatisfiesVacuum +IncrementVarSublevelsUp +IndexFreeSpaceMapVacuum +IndexGetRelation +InitMaterializedSRF +InitResultRelInfo +InitializeParallelDSM +InputFunctionCall +InstrAlloc +InstrEndLoop +Int64GetDatum +InterruptPending +InvalidObjectAddress +IsValidJsonNumber +ItemPointerCompare +ItemPointerEquals +JsonbValueToJsonb +LWLockAcquire +LWLockHeldByMe +LWLockInitialize +LWLockNewTrancheId +LWLockRegisterTranche +LWLockRelease +LaunchParallelWorkers +LocalBufferBlockPointers +LockBufHdr +LockBuffer +LockBufferForCleanup +LockPage +LockRelationForExtension +LockRelationOid +LookupFuncName +MainLWLockArray +MakeExpandedObjectReadOnlyInternal +MakePerTupleExprContext +MakeSingleTupleTableSlot +MakeTupleTableSlot +MarkBufferDirty +MarkGUCPrefixReserved +MemoryContextAlloc +MemoryContextAllocExtended +MemoryContextAllocHuge +MemoryContextAllocZero +MemoryContextDelete +MemoryContextDeleteChildren +MemoryContextGetParent +MemoryContextMemAllocated +MemoryContextReset +MemoryContextSetIdentifier +MemoryContextSetParent +MemoryContextStrdup +MultiXactIdPrecedes +MultiXactIdPrecedesOrEquals +MyProc +NBuffers +NameListToString +NewExplainState +NewGUCNestLevel +NewRelationCreateToastTable +NextCopyFrom +NextCopyFromRawFields +None_Receiver +OidFunctionCall1Coll +OidOutputFunctionCall +OpernameGetOprid +OutputFunctionCall +PG_exception_stack +PageAddItemExtended +PageGetExactFreeSpace +PageGetFreeSpace +PageIndexMultiDelete +PageIndexTupleOverwrite +PageInit +ParallelWorkerNumber +ParseFuncOrColumn +PinPortal +PopActiveSnapshot +PostgresMainLongJmp +PostgresMainLoopOnce +PostgresSendReadyForQueryIfNecessary +ProcessCopyOptions +ProcessInterrupts +ProcessStartupPacket +ProcessUtility +ProcessUtility_hook +PushActiveSnapshot +PushCopiedSnapshot +QueryRewrite +RangeVarCallbackMaintainsTable +RangeVarGetRelidExtended +ReThrowError +ReadBuffer +ReadBufferExtended +ReadMultiXactIdRange +ReadNextMultiXactId +RecentXmin +RecordFreeIndexPage +RecoveryInProgress +RegisterExtensibleNodeMethods +RegisterSnapshot +RegisterSubXactCallback +RegisterXactCallback +RelationGetIndexList +RelationGetIndexScan +RelationGetNumberOfBlocksInFork +RelationIsVisible +ReleaseAllPlanCacheRefsInOwner +ReleaseBuffer +ReleaseCachedPlan +ReleaseCatCacheList +ReleaseCurrentSubTransaction +ReleaseSysCache +RelnameGetRelid +RemoveObjects +RemoveRelations +RenameSchema +RequestAddinShmemSpace +ResourceOwnerCreate +ResourceOwnerDelete +RestoreBlockImage +RestrictSearchPath +RmgrNotFound +RmgrTable +RollbackAndReleaseCurrentSubTransaction +SPI_commit +SPI_commit_and_chain +SPI_connect +SPI_connect_ext +SPI_copytuple +SPI_cursor_close +SPI_cursor_fetch +SPI_cursor_find +SPI_cursor_open_with_paramlist +SPI_cursor_parse_open +SPI_datumTransfer +SPI_exec +SPI_execute +SPI_execute_extended +SPI_execute_plan +SPI_execute_plan_extended +SPI_execute_plan_with_paramlist +SPI_finish +SPI_fnumber +SPI_freeplan +SPI_freetuptable +SPI_getbinval +SPI_getvalue +SPI_keepplan +SPI_palloc +SPI_plan_get_cached_plan +SPI_plan_get_plan_sources +SPI_plan_is_valid +SPI_prepare +SPI_prepare_extended +SPI_processed +SPI_register_relation +SPI_register_trigger_data +SPI_result +SPI_result_code_string +SPI_returntuple +SPI_rollback +SPI_rollback_and_chain +SPI_scroll_cursor_fetch +SPI_scroll_cursor_move +SPI_tuptable +SS_process_sublinks +ScanKeyInit +ScanKeywordLookup +SearchPathMatchesCurrentEnvironment +SearchSysCache1 +SearchSysCacheAttName +SearchSysCacheList +SetConfigOption +SetTuplestoreDestReceiverParams +SetUserIdAndSecContext +ShmemInitStruct +SnapshotAnyData +SplitIdentifierString +SysCacheGetAttrNotNull +SystemFuncName +SystemTypeName +TTSOpsHeapTuple +TTSOpsMinimalTuple +TTSOpsVirtual +TopMemoryContext +TopTransactionContext +TopTransactionResourceOwner +TransactionIdDidCommit +TransactionIdIsCurrentTransactionId +TransactionIdIsInProgress +TransactionIdPrecedes +TransamVariables +TransferExpandedObject +TupleDescGetAttInMetadata +TupleDescInitEntry +TupleDescInitEntryCollation +TypenameGetTypid +UnlockPage +UnlockRelationForExtension +UnlockReleaseBuffer +UnpinPortal +UnregisterSnapshot +UpdateActiveSnapshotCommandId +WaitForParallelWorkersToAttach +WaitForParallelWorkersToFinish +XLogBeginInsert +XLogFindNextRecord +XLogFlush +XLogInsert +XLogReadRecord +XLogReaderAllocate +XLogReaderFree +XLogRecGetBlockRefInfo +XLogRecGetBlockTagExtended +XLogRecStoreStats +XLogRegisterData +XactIsoLevel +XidInMVCCSnapshot +_bt_allequalimage +_bt_binsrch_insert +_bt_check_natts +_bt_checkpage +_bt_compare +_bt_form_posting +_bt_freestack +_bt_metaversion +_bt_mkscankey +_bt_relbuf +_bt_search +_hash_get_indextuple_hashkey +_hash_getbuf +_hash_ovflblkno_to_bitno +_hash_relbuf +_start +accumArrayResult +aclcheck_error +acos +addNSItemToQuery +addRangeTableEntry +addRangeTableEntryForENR +addRangeTableEntryForFunction +addRangeTableEntryForJoin +addRangeTableEntryForRelation +addRangeTableEntryForSubquery +addTargetToSortList +add_exact_object_address +add_int_reloption +add_local_int_reloption +add_path +add_real_reloption +add_reloption_kind +add_size +add_string_reloption +aligned_alloc +appendBinaryStringInfo +appendStringInfo +appendStringInfoChar +appendStringInfoSpaces +appendStringInfoString +appendStringInfoStringQuoted +appendStringInfoVA +array_contains_nulls +array_create_iterator +array_iterate +arraycontjoinsel +arraycontsel +asin +assignSortGroupRef +assign_expr_collations +assign_list_collations +assign_query_collations +atof +atoi +be_lo_unlink +bit_in +bitcmp +biteq +bitge +bitgt +bitle +bitlt +bloom_add_element +bloom_create +bloom_free +bloom_lacks_element +bloom_prop_bits_set +bms_add_member +bms_copy +bms_del_member +bms_del_members +bms_equal +bms_free +bms_int_members +bms_is_member +bms_is_subset +bms_make_singleton +bms_next_member +bms_num_members +bms_union +bool_int4 +boolin +boolout +bpcharcmp +bpchareq +bpcharge +bpchargt +bpcharle +bpcharlt +brin_build_desc +brin_deform_tuple +brin_free_desc +bsearch +bsysscan +btboolcmp +btcharcmp +btfloat4cmp +btfloat8cmp +btint2cmp +btint4cmp +btint8cmp +btnamecmp +btoidcmp +btrim1 +bttextcmp +build_column_default +build_reloptions +buildoidvector +byteacmp +byteaeq +byteage +byteagt +byteale +bytealt +can_coerce_type +cancel_parser_errposition_callback +cash_cmp +checkExprHasSubLink +checkNameSpaceConflicts +check_amop_signature +check_amoptsproc_signature +check_amproc_signature +check_enable_rls +check_function_bodies +check_functional_grouping +check_object_ownership +check_stack_depth +clamp_row_est +clauselist_selectivity +clearerr +clock_gettime +coerce_to_boolean +coerce_to_common_type +coerce_to_specific_type +coerce_to_target_type +coerce_type +colNameToVar +construct_array +construct_array_builtin +construct_empty_array +construct_md_array +contain_aggs_of_level +contain_mutable_functions +contain_nonstrict_functions +contain_vars_of_level +convert_network_to_scalar +convert_tuples_by_position +copyObjectImpl +core_yylex +cos +count_nonjunk_tlist_entries +cpu_tuple_cost +create_foreignscan_path +create_queryEnv +cstring_to_text +cstring_to_text_with_len +dacos +dasin +datan +datan2 +date_cmp +date_eq +date_ge +date_gt +date_le +date_lt +date_mi +datumCopy +datumIsEqual +datumTransfer +datum_image_eq +datum_image_hash +dcos +dcot +debackslash +debug_query_string +deconstruct_array +deconstruct_array_builtin +deconstruct_expanded_record +defGetBoolean +defGetString +degrees +detoast_external_attr +dexp +domain_check +downcase_truncate_identifier +dpi +drandom +dsa_allocate_extended +dsa_attach +dsa_create_ext +dsa_detach +dsa_free +dsa_get_address +dsa_get_handle +dsa_get_total_size +dsa_pin +dsa_pin_mapping +dsa_trim +dshash_attach +dshash_create +dshash_delete_current +dshash_delete_key +dshash_destroy +dshash_detach +dshash_find +dshash_find_or_insert +dshash_get_hash_table_handle +dshash_memcmp +dshash_memcpy +dshash_memhash +dshash_release_lock +dshash_seq_init +dshash_seq_next +dshash_seq_term +dsin +dtan +dtoi2 +dtoi4 +dtoi8 +end_MultiFuncCall +enlargeStringInfo +enum_cmp +enum_ge +enum_gt +enum_le +enum_lt +equal +err_generic_string +errcode +errcode_for_file_access +errcontext_msg +errdetail +errdetail_internal +errdetail_relkind_not_supported +errfinish +errhidestmt +errhint +errmsg +errmsg_internal +errmsg_plural +errno +error_context_stack +errposition +errsave_finish +errsave_start +errstart +errstart_cold +escape_json +estimate_expression_value +eval_const_expressions +execute_attr_map_tuple +expandNSItemAttrs +expandNSItemVars +expandRTE +expand_array +expanded_record_fetch_field +expanded_record_fetch_tupdesc +expanded_record_get_tuple +expanded_record_lookup_field +expanded_record_set_field_internal +expanded_record_set_fields +expanded_record_set_tuple +exprCollation +exprLocation +exprType +exprTypmod +expression_tree_mutator_impl +expression_tree_walker_impl +extract_actual_clauses +extract_variadic_args +fclose +ferror +fetch_search_path +fflush +find_coercion_pathway +find_inheritance_children +find_nonnullable_rels +find_rendezvous_variable +finish_heap_swap +fix_opfuncids +flatten_join_alias_vars +float4_numeric +float4in_internal +float8_accum +float8_numeric +float8_stddev_pop +float8_stddev_samp +float8in +float8in_internal +float8out +float8out_internal +float8pl +float_overflow_error +float_to_shortest_decimal_buf +float_to_shortest_decimal_bufn +float_underflow_error +fmgr_info +fmgr_info_copy +fmgr_info_cxt +fmod +fopen +forkname_to_number +format_elog_string +format_operator +format_procedure +format_type_be +format_type_be_qualified +format_type_extended +format_type_with_typemod +fread +free +free_attstatsslot +free_object_addresses +free_parsestate +function_parse_error_transpose +generate_operator_clause +generic_restriction_selectivity +genericcostestimate +getBaseType +getClosestMatch +getExtensionOfObject +getTypeInputInfo +getTypeOutputInfo +get_am_oid +get_array_type +get_attname +get_attstatsslot +get_base_element_type +get_call_expr_argtype +get_call_result_type +get_collation_oid +get_element_type +get_extension_name +get_fn_expr_arg_stable +get_fn_expr_argtype +get_fn_expr_rettype +get_fn_expr_variadic +get_fn_opclass_options +get_func_arg_info +get_func_name +get_func_namespace +get_mergejoin_opfamilies +get_namespace_name +get_namespace_name_or_temp +get_namespace_oid +get_object_address +get_opcode +get_primary_key_attnos +get_rel_name +get_rel_namespace +get_rel_relkind +get_rel_type_id +get_relids_in_jointree +get_relkind_objtype +get_relname_relid +get_restriction_variable +get_row_security_policies +get_rte_attribute_name +get_sort_group_operators +get_sortgroupclause_tle +get_tablespace_name +get_tablespace_page_costs +get_ts_config_oid +get_tsearch_config_filename +get_typcollation +get_typlenbyval +get_typlenbyvalalign +get_typsubscript +get_typtype +getc +geterrcode +geterrposition +getinternalerrposition +getmissingattr +getpid +ginPostingListDecode +guc_malloc +has_fn_opclass_options +has_privs_of_role +has_superclass +hash_bytes +hash_bytes_extended +hash_create +hash_destroy +hash_freeze +hash_get_num_entries +hash_numeric_extended +hash_search +hash_seq_init +hash_seq_search +hash_seq_term +hashcharextended +hashfloat8extended +hashint8extended +heap_deform_tuple +heap_delete +heap_fetch +heap_form_tuple +heap_freetuple +heap_getnext +heap_getsysattr +heap_insert +heap_lock_tuple +heap_modify_tuple +heap_multi_insert +heap_reloptions +heap_tuple_needs_eventual_freeze +identify_opfamily_groups +index_close +index_deform_tuple +index_form_tuple +index_getprocid +index_getprocinfo +index_open +inet_in +initArrayResult +initClosestMatch +initStringInfo +init_MultiFuncCall +init_local_reloptions +init_toast_snapshot +int2_numeric +int4_bool +int4_numeric +int4in +int64_to_numeric +int82 +int84 +int8_numeric +int8in +int8out +int8pl +internalerrposition +internalerrquery +interval_cmp +interval_eq +interval_ge +interval_gt +interval_le +interval_lt +interval_mi +interval_um +isTempNamespace +isalnum +j2date +json_in +json_validate +lappend +lappend_int +lappend_oid +lcons +list_append_unique +list_concat +list_concat_copy +list_copy +list_delete_cell +list_delete_last +list_delete_nth_cell +list_delete_ptr +list_free +list_free_deep +list_insert_nth +list_intersection_int +list_make1_impl +list_make2_impl +list_make3_impl +list_make4_impl +list_member_int +list_sort +list_truncate +list_union_int +locate_agg_of_level +locate_var_of_level +log +log_newpage_buffer +log_newpage_range +lookup_rowtype_tupdesc +lookup_rowtype_tupdesc_domain +lookup_ts_dictionary_cache +lookup_type_cache +lowerstr +lowerstr_with_len +ltrim1 +macaddr8_cmp +macaddr8_eq +macaddr8_ge +macaddr8_gt +macaddr8_le +macaddr8_lt +macaddr_cmp +macaddr_eq +macaddr_ge +macaddr_gt +macaddr_le +macaddr_lt +maintenance_work_mem +makeA_Expr +makeAlias +makeArrayResult +makeBoolExpr +makeBoolean +makeColumnDef +makeConst +makeDefElem +makeFloat +makeFromExpr +makeFuncCall +makeFuncExpr +makeInteger +makeNullConst +makeObjectName +makeParamList +makeRangeVar +makeRangeVarFromNameList +makeSimpleA_Expr +makeString +makeStringInfo +makeTargetEntry +makeTypeName +makeTypeNameFromNameList +makeVar +makeWholeRowVar +make_expanded_record_from_exprecord +make_expanded_record_from_tupdesc +make_expanded_record_from_typeid +make_foreignscan +make_new_heap +make_op +make_opclause +make_parsestate +make_scalar_array_op +malloc +markNullableIfNeeded +markTargetListOrigins +markVarForSelectPriv +max_parallel_maintenance_workers +memchr +memcmp +namein +namestrcpy +network_cmp +new_object_addresses +nextval_internal +nocache_index_getattr +nocachegetattr +nodeRead +nodeToString +numeric_abs +numeric_add +numeric_ceil +numeric_cmp +numeric_div +numeric_eq +numeric_exp +numeric_float4 +numeric_float8 +numeric_float8_no_overflow +numeric_floor +numeric_ge +numeric_gt +numeric_in +numeric_int2 +numeric_int4 +numeric_int8 +numeric_is_nan +numeric_le +numeric_ln +numeric_log +numeric_lt +numeric_mod +numeric_mul +numeric_normalize +numeric_out +numeric_power +numeric_round +numeric_sign +numeric_sqrt +numeric_sub +numeric_uminus +object_access_hook +object_ownercheck +oidin +oidout +op_hashjoinable +op_mergejoinable +outNode +outToken +pairingheap_add +pairingheap_allocate +pairingheap_first +pairingheap_remove_first +palloc +palloc0 +palloc_extended +parserOpenTable +parser_errposition +per_MultiFuncCall +performDeletion +performMultipleDeletions +pfree +pg_any_to_server +pg_bindtextdomain +pg_checksum_page +pg_class_aclcheck +pg_crc32_table +pg_database_encoding_max_length +pg_detoast_datum +pg_detoast_datum_copy +pg_detoast_datum_packed +pg_fprintf +pg_get_indexdef_columns_extended +pg_get_querydef +pg_global_prng_state +pg_lltoa +pg_ltoa +pg_mb2wchar_with_len +pg_mblen +pg_mbstrlen_with_len +pg_number_of_ones +pg_parse_query +pg_plan_query +pg_popcount_optimized +pg_printf +pg_prng_double +pg_prng_uint32 +pg_prng_uint64 +pg_qsort +pg_re_throw +pg_reg_colorisbegin +pg_reg_colorisend +pg_reg_getcharacters +pg_reg_getfinalstate +pg_reg_getinitialstate +pg_reg_getnumcharacters +pg_reg_getnumcolors +pg_reg_getnumoutarcs +pg_reg_getoutarcs +pg_regcomp +pg_regerror +pg_server_to_any +pg_snprintf +pg_sprintf +pg_strcasecmp +pg_strncasecmp +pg_strong_random +pg_strtoint64 +pg_strtok +pg_tolower +pg_toupper +pg_utf_mblen_private +pg_vfprintf +pg_wchar2mb_with_len +pgl_getMyProcPort +pgl_pq_flush +pgl_sendConnData +pgl_setPGliteActive +pgl_set_force_host_error_recovery +pgl_set_protocol_stdio +pgl_startPGlite +pgl_wasix_input_available +pgl_wasix_input_reset +pgl_wasix_input_write +pgl_wasix_output_len +pgl_wasix_output_read +pgl_wasix_output_reset +pgstat_assoc_relation +pgstat_count_heap_insert +pgstat_count_truncate +pgstat_progress_update_param +pgstat_report_activity +plan_create_index_workers +planner_hook +pnstrdup +post_parse_analyze_hook +pow +pq_begintypsend +pq_buffer_remaining_data +pq_copymsgbytes +pq_endtypsend +pq_getmsgbyte +pq_getmsgfloat4 +pq_getmsgfloat8 +pq_getmsgint +pq_getmsgint64 +pq_getmsgtext +pq_sendbytes +pq_sendfloat4 +pq_sendfloat8 +pq_sendtext +pre_format_elog_string +process_shared_preload_libraries_in_progress +psprintf +pstrdup +pthread_mutex_lock +pthread_mutex_unlock +pull_varattnos +pull_varnos +pull_vars_of_level +pushJsonbValue +qsort_arg +query_tree_walker_impl +quote_identifier +quote_literal_cstr +quote_qualified_identifier +radians +raw_expression_tree_walker_impl +raw_parser +read_local_xlog_page_no_wait +readstoplist +realloc +realpath +recordDependencyOn +recordDependencyOnExpr +refnameNamespaceItem +regconfigout +regexp_split_to_array +register_ENR +register_reloptions_validator +regtypeout +relation_close +relation_open +relation_openrv +remove_nulling_relids +repalloc +replace_text +reservoir_get_next_S +reservoir_init_selection_state +resetStringInfo +resolve_polymorphic_argtypes +row_security_policy_hook_permissive +row_security_policy_hook_restrictive +rtrim1 +s_lock +sampler_random_fract +sampler_random_init_state +scanNSItemForColumn +scanner_finish +scanner_init +scanner_isspace +searchstoplist +select_common_collation +select_common_type +seq_page_cost +set_errcontext_domain +set_rel_pathlist_hook +setup_parser_errposition_callback +setval_oid +shm_toc_allocate +shm_toc_insert +shm_toc_lookup +shmem_request_hook +shmem_startup_hook +sigsetjmp +sin +slot_getsomeattrs_int +smgrexists +smgrnblocks +smgropen +smgrpin +smgrtruncate +smgrtruncate2 +standard_ExecutorEnd +standard_ExecutorFinish +standard_ExecutorRun +standard_ExecutorStart +standard_ProcessUtility +standard_planner +stat +stdin +stdout +str_tolower +strcasecmp +strcat +strchr +strcmp +strcpy +stringToNode +stringToQualifiedNameList +strip_implicit_coercions +strlcpy +strlen +strncat +strncmp +strncpy +strspn +strstr +strtod +strtof +strtoint +strtol +strtoll +strtoul +superuser +systable_beginscan +systable_beginscan_ordered +systable_endscan +systable_endscan_ordered +systable_getnext +systable_getnext_ordered +t_isalnum +t_isdigit +t_isspace +table_beginscan_parallel +table_close +table_open +table_parallelscan_estimate +table_parallelscan_initialize +table_slot_callbacks +table_slot_create +tag_hash +targetIsInSortList +tbm_add_tuples +textToQualifiedNameList +text_ge +text_gt +text_le +text_left +text_lt +text_reverse +text_right +text_substr +text_substr_no_len +text_to_cstring +texteq +textregexeq +time2tm +time_cmp +time_eq +time_ge +time_gt +time_le +time_lt +time_mi_time +timestamp2tm +timestamp_cmp +timestamp_eq +timestamp_ge +timestamp_gt +timestamp_le +timestamp_lt +timestamp_mi +timetz2tm +timetz_cmp +to_hex32 +to_tsvector +to_tsvector_byid +toast_close_indexes +toast_open_indexes +tolower +toupper +transformDistinctClause +transformExpr +transformRelOptions +transformSortClause +transformStmt +truncate_identifier +tsearch_readline +tsearch_readline_begin +tsearch_readline_end +tuplesort_attach_shared +tuplesort_begin_datum +tuplesort_begin_heap +tuplesort_end +tuplesort_estimate_shared +tuplesort_getdatum +tuplesort_gettupleslot +tuplesort_initialize_shared +tuplesort_performsort +tuplesort_putdatum +tuplesort_puttupleslot +tuplesort_rescan +tuplesort_reset +tuplesort_skiptuples +tuplestore_begin_heap +tuplestore_clear +tuplestore_end +tuplestore_gettupleslot +tuplestore_puttuple +tuplestore_puttupleslot +tuplestore_putvalues +tuplestore_rescan +tuplestore_tuple_count +typeByVal +typeLen +typeStringToTypeName +typeTypeCollation +type_is_collatable +type_is_rowtype +typeidType +typenameTypeIdAndMod +unpack_sql_state +untransformRelOptions +updateClosestMatch +uuid_cmp +vacuum_delay_point +varbit_in +varstr_cmp +varstr_levenshtein +varstr_levenshtein_less_equal +verify_common_type +visibilitymap_clear +visibilitymap_get_status +visibilitymap_pin +visibilitymap_prepare_truncate +wal_level +wal_segment_close +wal_segment_open +wal_segment_size +work_mem diff --git a/assets/pglite-wasi.tar.zst b/assets/pglite-wasi.tar.zst deleted file mode 100644 index eedc23d2..00000000 Binary files a/assets/pglite-wasi.tar.zst and /dev/null differ diff --git a/assets/prepopulated/pgdata-template.json b/assets/prepopulated/pgdata-template.json deleted file mode 100644 index b29d3007..00000000 --- a/assets/prepopulated/pgdata-template.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "postgresVersion": "17", - "wasmSha256": "ad423e536096ede1870f4802e7dbe4b49599c6e3bafc45aa9a4ddeeae2c5f4f8", - "archiveSha256": "63e398b3cd4fec134d06539f064018fc2aa7fef75b9c331472f9b2d7385b913d", - "architectureIndependent": true -} diff --git a/assets/prepopulated/pgdata-template.tar.zst b/assets/prepopulated/pgdata-template.tar.zst deleted file mode 100644 index 1f076ee5..00000000 Binary files a/assets/prepopulated/pgdata-template.tar.zst and /dev/null differ diff --git a/assets/sources.toml b/assets/sources.toml new file mode 100644 index 00000000..109071c1 --- /dev/null +++ b/assets/sources.toml @@ -0,0 +1,88 @@ +[toolchain] +wasmer = "7.2.0-alpha.2" +wasmer-wasix = "0.702.0-alpha.2" +wasixcc = "2026-03-02.1" +llvm = "22.1" +docker_image = "ghcr.io/f0rr0/pglite-oxide-wasix-build" +docker_image_digest = "sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b" + +[build] +postgres_prefix = "/" +postgres_pkglibdir = "/lib/postgresql" +postgres_sharedir = "/share/postgresql" +main_flags = ["-fwasm-exceptions"] +extension_flags = ["-fwasm-exceptions", "-fPIC", "-Wl,-shared"] +archive_format = "tar.zst" +deterministic_archives = true + +[[sources]] +name = "pglite" +url = "https://github.com/electric-sql/pglite.git" +branch = "main" +commit = "1337be6e33b7c294f8987c918b1e64d2421365ee" + +[[sources]] +name = "pglite-build" +url = "https://github.com/electric-sql/pglite-build" +branch = "portable" +commit = "c195113dbaf09488f8d5eeb2db91dacd123b74d0" + +[[sources]] +name = "postgres-pglite" +url = "https://github.com/electric-sql/postgres-pglite" +branch = "REL_17_5-pglite" +commit = "01792c31a62b7045eb22e93d7dad022bb64b1184" + +[[sources]] +name = "pgvector" +url = "https://github.com/pgvector/pgvector.git" +branch = "master" +commit = "d238409becebb8172fe696ffa776badfad4b631c" + +[[sources]] +name = "pgtap" +url = "https://github.com/theory/pgtap.git" +branch = "postgres-pglite-submodule" +commit = "b89585a64ffef012ff0f219de9197c669aa8485b" + +[[sources]] +name = "pg_ivm" +url = "https://github.com/sraoss/pg_ivm.git" +branch = "postgres-pglite-submodule" +commit = "b66487f7a6f8deee3998e858d773e19923e4bd4b" + +[[sources]] +name = "pg_uuidv7" +url = "https://github.com/fboulnois/pg_uuidv7/" +branch = "postgres-pglite-submodule" +commit = "c707aae2411181be4802f5fa565b44d9c0bcbc29" + +[[sources]] +name = "pg_hashids" +url = "https://github.com/iCyberon/pg_hashids" +branch = "postgres-pglite-submodule" +commit = "8c404dd86408f3a987a3ff6825ac7e42bd618b98" + +[[sources]] +name = "age" +url = "https://github.com/apache/age.git" +branch = "PG17" +commit = "e1467f12e0b1d15dd35d3ab93f057a7112d425b8" + +[[sources]] +name = "pg_textsearch" +url = "https://github.com/timescale/pg_textsearch.git" +branch = "postgres-pglite-submodule" +commit = "5c5147bf2610d786f1bd139951b9fb7fe4ac68fb" + +[[sources]] +name = "postgis" +url = "https://github.com/postgis/postgis.git" +branch = "postgres-pglite-submodule" +commit = "08d9b9f749fa3531591055db2a736bfb6df47006" + +[[sources]] +name = "pglite-bindings" +url = "https://github.com/electric-sql/pglite-bindings" +branch = "main" +commit = "0d31326a41f7251548103f73e601699b0ebae2fa" diff --git a/assets/wasix-build/.gitignore b/assets/wasix-build/.gitignore new file mode 100644 index 00000000..5998c97c --- /dev/null +++ b/assets/wasix-build/.gitignore @@ -0,0 +1,2 @@ +build/ +work/ diff --git a/assets/wasix-build/analyze_pgl_stubs.sh b/assets/wasix-build/analyze_pgl_stubs.sh new file mode 100755 index 00000000..f365f019 --- /dev/null +++ b/assets/wasix-build/analyze_pgl_stubs.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + test -f "$BUILD_DIR/src/backend/pglite" + + mkdir -p /work/assets/wasix-build/build/link-analysis + out=/work/assets/wasix-build/build/link-analysis/wasix-host-abi-used.txt + stubs="pgl_system pgl_popen pgl_pclose pgl_geteuid pgl_getuid pgl_getpwuid pgl_exit pgl_atexit pgl_longjmp pgl_siglongjmp pgl_recv pgl_send pgl_shmget pgl_shmat pgl_shmdt pgl_shmctl ProcessStartupPacket" + + runtime_inputs=( + "$BUILD_DIR/libpgcore.a" + "$BUILD_DIR/libpgcore.o" + "$BUILD_DIR/src/common/libpgcommon_srv.a" + "$BUILD_DIR/src/port/libpgport_srv.a" + "$BUILD_DIR/src/backend/snowball/libdict_snowball.a" + "$BUILD_DIR/src/pl/plpgsql/src/libplpgsql.a" + ) + frontend_tool_inputs=( + "$BUILD_DIR/src/bin/pg_dump/"*.o + "$BUILD_DIR/src/interfaces/libpq/libpq.a" + "$BUILD_DIR/src/common/libpgcommon.a" + "$BUILD_DIR/src/common/libpgcommon_shlib.a" + "$BUILD_DIR/src/port/libpgport.a" + "$BUILD_DIR/src/port/libpgport_shlib.a" + "$BUILD_DIR/src/fe_utils/libpgfeutils.a" + ) + compiled_sources=( + "$PGSRC/pglite/src/pglitec" + "$PGSRC/src/bin/initdb/initdb.c" + "$PGSRC/src/bin/initdb/findtimezone.c" + "$PGSRC/src/fe_utils/option_utils.c" + ) + + print_undefined_refs() { + for obj in "$@"; do + [ -e "$obj" ] || continue + undef=$(wasixnm -u "$obj" 2>/dev/null || true) + for sym in $stubs; do + if printf "%s\n" "$undef" | grep -Eq "(^| )U $sym$"; then + echo "$sym $obj" + fi + done + done | sort -u + } + + symbol_defined_in() { + obj="$1" + sym="$2" + wasixnm "$obj" 2>/dev/null | awk -v sym="$sym" \ + '\''$2 ~ /^[TtWw]$/ && $3 == sym { found = 1 } END { exit(found ? 0 : 1) }'\'' + } + + { + echo "# WASIX host ABI link-symbol analysis" + echo + echo "Generated from $BUILD_DIR with wasixnm." + echo "Source tree: $PGSRC" + echo + echo "## Definitions compiled into final pglite" + for sym in $stubs; do + printf "%-30s" "$sym" + if symbol_defined_in "$BUILD_DIR/src/backend/pglite" "$sym"; then + printf " final" + fi + printf "\n" + done + echo + echo "## Runtime link inputs requiring WASIX host ABI ownership" + print_undefined_refs "${runtime_inputs[@]}" + echo + echo "## Frontend tool inputs requiring frontend/common ownership" + echo "These references are reported for pg_dump and future tool packaging." + echo "They do not by themselves justify adding symbols to the production WASIX bridge." + print_undefined_refs "${frontend_tool_inputs[@]}" + echo + echo "## Runtime compiled-source call sites" + echo "This includes upstream pglitec plus initdb/frontend source files when present." + for sym in $stubs; do + matches=$(grep -R --line-number -E "\\b${sym}\\s*\\(" "${compiled_sources[@]}" 2>/dev/null || true) + if [ -n "$matches" ]; then + printf "%s\n%s\n" "$sym" "$matches" + fi + done + } > "$out" + + cat "$out" + ' diff --git a/assets/wasix-build/configure_wasix_dl.sh b/assets/wasix-build/configure_wasix_dl.sh new file mode 100755 index 00000000..dc182668 --- /dev/null +++ b/assets/wasix-build/configure_wasix_dl.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" +DEFAULT_PGSRC="$ROOT/work/postgres-pglite-wasix-src" +if [ ! -d "$DEFAULT_PGSRC" ]; then + DEFAULT_PGSRC="$REPO_ROOT/assets/checkouts/postgres-pglite" +fi +PGSRC="${PGSRC:-$DEFAULT_PGSRC}" +BUILD="${BUILD_DIR:-$ROOT/work/configure-smoke}" + +WASIX_HOME="${WASIX_HOME:-/tmp/wasixcc-home/.wasixcc}" +export HOME="${WASIX_HOME%/.wasixcc}" +export PATH="$WASIX_HOME/bin:$PATH" + +mkdir -p "$BUILD" + +. "$ROOT/profile_flags.sh" +pglite_oxide_apply_wasix_profile configure + +COMMON_CPPFLAGS="-I$PGSRC/src/include/port/wasix-dl" +if [ "${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" = "1" ]; then + COMMON_CPPFLAGS="$COMMON_CPPFLAGS -DPGLITE_WASIX_BACKEND_TIMING" +fi +COMMON_CFLAGS="$PGLITE_OXIDE_PROFILE_CFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes -Wno-unused-command-line-argument" +COMMON_LDFLAGS="$PGLITE_OXIDE_PROFILE_LDFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes" +MAIN_LDFLAGS="-sMODULE_KIND=dynamic-main -sSTACK_SIZE=8MB -sINITIAL_MEMORY=128MB" +SIDE_MODULE_LDFLAGS="-Wl,-shared" + +if [ "${PGLITE_MODE:-0}" = "1" ]; then + mkdir -p "$ROOT/build/wasix-pglite" + PGLITE_SHIM="$ROOT/build/wasix-pglite/pglite_wasix_bridge.o" + + wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ + -include stdbool.h \ + -include stdlib.h \ + -I"$PGSRC/src/include/port/wasix-dl" \ + -c "$ROOT/wasix_shim/pglite_wasix_bridge.c" \ + -o "$PGLITE_SHIM" + + PGLITE_CFLAGS="\ + -D__PGLITE__\ + -DPGLITE_WASIX_DL\ + -Dsystem=pgl_system -Dpopen=pgl_popen -Dpclose=pgl_pclose\ + -Dgeteuid=pgl_geteuid -Dgetuid=pgl_getuid -Dgetpwuid=pgl_getpwuid\ + -Dexit=pgl_exit\ + -Dmunmap=pgl_munmap\ + -Dfcntl=pgl_fcntl\ + -Datexit=pgl_atexit\ + -Dsetsockopt=pgl_setsockopt -Dgetsockopt=pgl_getsockopt -Dgetsockname=pgl_getsockname\ + -Drecv=pgl_recv -Dsend=pgl_send -Dconnect=pgl_connect\ + -Dpoll=pgl_poll\ + -Dshmget=pgl_shmget -Dshmat=pgl_shmat -Dshmdt=pgl_shmdt -Dshmctl=pgl_shmctl\ + -Dlongjmp=pgl_longjmp -Dsiglongjmp=pgl_siglongjmp\ + -Wno-declaration-after-statement\ + -Wno-macro-redefined\ + -Wno-unused-function\ + -Wno-missing-prototypes\ + -Wno-incompatible-pointer-types" + LDFLAGS_EXTRA=" $PGLITE_SHIM" +else + mkdir -p "$ROOT/build/wasix-shim" + GENERIC_SHIM="$ROOT/build/wasix-shim/pglite_wasix_shim.o" + + wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ + -I"$PGSRC/src/include/port/wasix-dl" \ + -c "$ROOT/wasix_shim/pglite_wasix_shim.c" \ + -o "$GENERIC_SHIM" + + PGLITE_CFLAGS="" + LDFLAGS_EXTRA=" $GENERIC_SHIM" +fi + +cd "$BUILD" + +CC=wasixcc \ +AR=wasixar \ +RANLIB=wasixranlib \ +NM=wasixnm \ +CPPFLAGS="$COMMON_CPPFLAGS" \ +CFLAGS="$COMMON_CFLAGS$PGLITE_CFLAGS" \ +LDFLAGS="$COMMON_LDFLAGS" \ +LDFLAGS_EX="$MAIN_LDFLAGS$LDFLAGS_EXTRA" \ +LDFLAGS_SL="$SIDE_MODULE_LDFLAGS" \ +"$PGSRC/configure" \ + --prefix=/ \ + --libdir=/lib \ + --datadir=/share/postgresql \ + --bindir=/bin \ + --host=wasm32-wasix \ + --with-template=wasix-dl \ + --without-readline \ + --without-icu \ + --without-zlib \ + --without-llvm \ + --disable-largefile \ + --without-pam \ + --with-openssl=no diff --git a/assets/wasix-build/docker/Dockerfile b/assets/wasix-build/docker/Dockerfile new file mode 100644 index 00000000..1e471ed3 --- /dev/null +++ b/assets/wasix-build/docker/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1.7 +FROM ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b + +ENV DEBIAN_FRONTEND=noninteractive + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + bash \ + bison \ + build-essential \ + ca-certificates \ + curl \ + file \ + flex \ + gawk \ + git \ + jq \ + lz4 \ + make \ + patch \ + perl \ + pkg-config \ + python3 \ + rsync \ + tar \ + wget \ + xz-utils \ + zstd + +ENV HOME=/opt/wasixcc-home +ENV WASIX_HOME=/opt/wasixcc-home/.wasixcc +ENV PATH=/opt/wasixcc-home/.wasixcc/bin:$PATH + +RUN mkdir -p /opt/wasixcc-home \ + && curl -fsSL https://wasix.cc -o /tmp/wasixcc-install.sh \ + && HOME=/opt/wasixcc-home sh /tmp/wasixcc-install.sh \ + && wasixcc --version + +WORKDIR /work diff --git a/assets/wasix-build/docker_contrib_extensions.sh b/assets/wasix-build/docker_contrib_extensions.sh new file mode 100755 index 00000000..b7802bd0 --- /dev/null +++ b/assets/wasix-build/docker_contrib_extensions.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +CONTAINER_PLAN="${CONTAINER_PLAN:-/work/assets/generated/contrib-build.tsv}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$ROOT/prepare_patched_source.sh" + +if [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e PLAN="$CONTAINER_PLAN" \ + -e JOBS="$JOBS" \ + -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ + -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ + -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ + -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ + -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ + -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + . ./assets/wasix-build/profile_flags.sh + pglite_oxide_apply_wasix_profile build + + test -f "$BUILD_DIR/config.status" + test -f "$BUILD_DIR/src/backend/pglite" + cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" + cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" + sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null + test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" + + if [ ! -f "$PLAN" ]; then + echo "generated contrib build plan missing: $PLAN" >&2 + exit 1 + fi + + while IFS=$'\''\t'\'' read -r id sql_name contrib_dir module_file archive stable; do + case "$id" in ""|"#"*) continue ;; esac + test -n "$sql_name" + test -n "$contrib_dir" + echo "building contrib extension $id from contrib/$contrib_dir" + test -d "$BUILD_DIR/contrib/$contrib_dir" + make -s -j"$JOBS" -C "$BUILD_DIR/contrib/$contrib_dir" all + if [ "$module_file" = "-" ]; then + continue + fi + if [ ! -f "$BUILD_DIR/contrib/$contrib_dir/$module_file" ]; then + echo "expected WASIX side module missing: $BUILD_DIR/contrib/$contrib_dir/$module_file" >&2 + find "$BUILD_DIR/contrib/$contrib_dir" -maxdepth 1 -type f -name "*.so" -print >&2 + exit 1 + fi + done < "$PLAN" + ' diff --git a/assets/wasix-build/docker_initdb.sh b/assets/wasix-build/docker_initdb.sh new file mode 100755 index 00000000..27339c0e --- /dev/null +++ b/assets/wasix-build/docker_initdb.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$ROOT/prepare_patched_source.sh" + +if [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e JOBS="$JOBS" \ + -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ + -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ + -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ + -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ + -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ + -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + . ./assets/wasix-build/profile_flags.sh + pglite_oxide_apply_wasix_profile build + export AR=wasixar + export RANLIB=wasixranlib + export NM=wasixnm + export LLVM_NM=wasixnm + + test -f "$BUILD_DIR/config.status" + cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" + cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" + sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null + test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" + + COMMON_CPPFLAGS="-I$PGSRC/src/include/port/wasix-dl" + COMMON_CFLAGS="$PGLITE_OXIDE_PROFILE_CFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes -Wno-unused-command-line-argument" + COMMON_LDFLAGS="$PGLITE_OXIDE_PROFILE_LDFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes" + MAIN_LDFLAGS="-sMODULE_KIND=dynamic-main -sSTACK_SIZE=8MB -sINITIAL_MEMORY=128MB -Wl,--wrap=system -Wl,--wrap=popen -Wl,--wrap=pclose" + + INITDB_BUILD_DIR="$CONTAINER_ROOT/build/wasix-initdb" + mkdir -p "$INITDB_BUILD_DIR" + GENERIC_SHIM="$INITDB_BUILD_DIR/pglite_wasix_shim.o" + INITDB_SHIM="$INITDB_BUILD_DIR/pglite_wasix_initdb_shim.o" + wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ + -I"$BUILD_DIR/src/include" \ + -I"$PGSRC/src/include/port/wasix-dl" \ + -c "$CONTAINER_ROOT/wasix_shim/pglite_wasix_shim.c" \ + -o "$GENERIC_SHIM" + wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ + -I"$BUILD_DIR/src/include" \ + -I"$PGSRC/src/include/port/wasix-dl" \ + -c "$CONTAINER_ROOT/wasix_shim/pglite_wasix_initdb_shim.c" \ + -o "$INITDB_SHIM" + + make -s -C "$BUILD_DIR/src/bin/initdb" clean + make -s -j"$JOBS" -C "$BUILD_DIR/src/bin/initdb" initdb \ + CFLAGS="$COMMON_CFLAGS -Dsystem=pgl_initdb_system -Dpopen=pgl_initdb_popen -Dpclose=pgl_initdb_pclose -Dgeteuid=pgl_geteuid -Dgetuid=pgl_getuid -Dgetpwuid=pgl_getpwuid -Wno-unused-function -Wno-missing-prototypes" \ + LDFLAGS="$COMMON_LDFLAGS -L$BUILD_DIR/src/common -L$BUILD_DIR/src/port" \ + LDFLAGS_EX="$MAIN_LDFLAGS $GENERIC_SHIM $INITDB_SHIM $BUILD_DIR/src/fe_utils/libpgfeutils.a $BUILD_DIR/src/interfaces/libpq/libpq.a $BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a" + test -f "$BUILD_DIR/src/bin/initdb/initdb" + ' diff --git a/assets/wasix-build/docker_pgdump.sh b/assets/wasix-build/docker_pgdump.sh new file mode 100755 index 00000000..257aa271 --- /dev/null +++ b/assets/wasix-build/docker_pgdump.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$ROOT/prepare_patched_source.sh" + +if [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e JOBS="$JOBS" \ + -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ + -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ + -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ + -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ + -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ + -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + . ./assets/wasix-build/profile_flags.sh + pglite_oxide_apply_wasix_profile build + export AR=wasixar + export RANLIB=wasixranlib + export NM=wasixnm + export LLVM_NM=wasixnm + + test -f "$BUILD_DIR/config.status" + cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" + cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" + sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null + test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" + make -s -C "$BUILD_DIR/src/bin/pg_dump" clean + make -s -C "$BUILD_DIR/src/bin/pg_dump" pg_dump \ + libpq="$BUILD_DIR/src/interfaces/libpq/libpq.a" \ + LIBS="$BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a -lm" + test -f "$BUILD_DIR/src/bin/pg_dump/pg_dump" + if wasixnm -u "$BUILD_DIR/src/bin/pg_dump/pg_dump" | grep -E " PQ[A-Za-z0-9_]+$"; then + echo "pg_dump still imports libpq symbols; expected standalone WASIX pg_dump" >&2 + exit 1 + fi + ' diff --git a/assets/wasix-build/docker_pglite.sh b/assets/wasix-build/docker_pglite.sh new file mode 100755 index 00000000..b06c546c --- /dev/null +++ b/assets/wasix-build/docker_pglite.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$ROOT/prepare_patched_source.sh" + +if [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e FORCE_RECONFIGURE="${FORCE_RECONFIGURE:-0}" \ + -e JOBS="$JOBS" \ + -e PGLITE_MODE=1 \ + -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ + -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ + -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ + -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ + -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ + -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + . ./assets/wasix-build/profile_flags.sh + pglite_oxide_apply_wasix_profile configure + profile_signature="$(pglite_oxide_wasix_profile_signature)" + + needs_configure=0 + if [ "${FORCE_RECONFIGURE:-0}" = "1" ] || [ ! -f "$BUILD_DIR/config.status" ]; then + needs_configure=1 + elif ! cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head"; then + needs_configure=1 + elif ! cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256"; then + needs_configure=1 + elif [ ! -f "$BUILD_DIR/.pglite-oxide-bridge-sha256" ]; then + needs_configure=1 + elif ! sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null 2>&1; then + needs_configure=1 + elif [ ! -f "$BUILD_DIR/.pglite-oxide-build-profile" ]; then + needs_configure=1 + elif [ "$profile_signature" != "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" ]; then + needs_configure=1 + fi + + if [ "$needs_configure" = "1" ]; then + rm -rf "$BUILD_DIR" + ./assets/wasix-build/configure_wasix_dl.sh + cp "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" + cp "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" + sha256sum ./assets/wasix-build/wasix_shim/pglite_wasix_bridge.c \ + > "$BUILD_DIR/.pglite-oxide-bridge-sha256" + printf "%s\n" "$profile_signature" > "$BUILD_DIR/.pglite-oxide-build-profile" + else + echo "reusing configured PGlite build at $BUILD_DIR" + fi + pglite_oxide_apply_wasix_profile build + rm -rf "$BUILD_DIR/src/timezone/compiled" + mkdir -p "$BUILD_DIR/src/timezone/compiled" + /usr/sbin/zic \ + -d "$BUILD_DIR/src/timezone/compiled" \ + "$PGSRC/src/timezone/data/tzdata.zi" + test -f "$BUILD_DIR/src/timezone/compiled/UTC" + test -f "$BUILD_DIR/src/timezone/compiled/GMT" + test -f "$BUILD_DIR/src/timezone/compiled/Etc/UTC" + test -f "$BUILD_DIR/src/timezone/compiled/America/New_York" + make -s -C "$BUILD_DIR/src/backend" generated-headers + make -s -C "$BUILD_DIR/src/backend" submake-libpgport + make -s -j"$JOBS" -C "$BUILD_DIR/src/backend" pglite + ' diff --git a/assets/wasix-build/docker_pgxs_extensions.sh b/assets/wasix-build/docker_pgxs_extensions.sh new file mode 100755 index 00000000..d96f7721 --- /dev/null +++ b/assets/wasix-build/docker_pgxs_extensions.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +CONTAINER_PLAN="${CONTAINER_PLAN:-/work/assets/generated/pgxs-build.tsv}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$ROOT/prepare_patched_source.sh" + +if [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e PLAN="$CONTAINER_PLAN" \ + -e JOBS="$JOBS" \ + -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ + -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ + -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ + -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ + -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ + -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + . ./assets/wasix-build/profile_flags.sh + pglite_oxide_apply_wasix_profile build + + test -f "$BUILD_DIR/config.status" + test -f "$BUILD_DIR/src/backend/pglite" + cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" + cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" + sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null + test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" + + if [ ! -f "$PLAN" ]; then + echo "generated PGXS build plan missing: $PLAN" >&2 + exit 1 + fi + + mkdir -p "$BUILD_DIR/src/makefiles" "$BUILD_DIR/install/lib" + ln -sf "$PGSRC/src/makefiles/pgxs.mk" "$BUILD_DIR/src/makefiles/pgxs.mk" + ln -sf "$PGSRC/src/Makefile.shlib" "$BUILD_DIR/src/Makefile.shlib" + + while IFS=$'\''\t'\'' read -r id sql_name source_dir module_file archive stable make_args; do + case "$id" in ""|"#"*) continue ;; esac + test -n "$sql_name" + test -n "$source_dir" + extension_source="/work/$source_dir" + test -d "$extension_source" + extension_dir="$BUILD_DIR/pgxs/$id" + rm -rf "$extension_dir" + mkdir -p "$(dirname "$extension_dir")" + cp -a "$extension_source" "$extension_dir" + rm -rf "$extension_dir/.git" + extra_make_args=() + if [ "${make_args:-"-"}" != "-" ]; then + read -r -a extra_make_args <<< "$make_args" + fi + make -s -C "$extension_dir" \ + PG_CONFIG=/work/assets/wasix-build/pg_config_wasix.sh \ + clean >/dev/null 2>&1 || true + make -s -j"$JOBS" -C "$extension_dir" \ + PG_CONFIG=/work/assets/wasix-build/pg_config_wasix.sh \ + CPPFLAGS="-I$BUILD_DIR/src/include -I$PGSRC/src/include -I$PGSRC/src/include/port/wasix-dl" \ + OPTFLAGS="" \ + "${extra_make_args[@]}" \ + all + if [ "$id" = "age" ] && grep -q "^ PASSEDBYVALUE,$" "$extension_dir/age--1.7.0.sql"; then + echo "AGE generated SQL still declares graphid PASSEDBYVALUE on wasm32" >&2 + exit 1 + fi + if [ "$module_file" = "-" ]; then + continue + fi + if [ ! -f "$extension_dir/$module_file" ]; then + echo "expected WASIX side module missing: $extension_dir/$module_file" >&2 + find "$extension_dir" -maxdepth 1 -type f -name "*.so" -print >&2 + exit 1 + fi + done < "$PLAN" + ' diff --git a/assets/wasix-build/docker_runtime_support.sh b/assets/wasix-build/docker_runtime_support.sh new file mode 100755 index 00000000..fd33571f --- /dev/null +++ b/assets/wasix-build/docker_runtime_support.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" + +IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +"$ROOT/prepare_patched_source.sh" + +if [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e JOBS="$JOBS" \ + -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ + -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ + -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ + -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ + -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ + -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ + -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./assets/wasix-build/docker_wasix_env.sh + . ./assets/wasix-build/profile_flags.sh + pglite_oxide_apply_wasix_profile build + + test -f "$BUILD_DIR/config.status" + test -f "$BUILD_DIR/src/backend/pglite" + cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" + cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" + sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null + test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" + + make -s -j"$JOBS" -C "$BUILD_DIR/src/pl/plpgsql/src" all + make -s -j"$JOBS" -C "$BUILD_DIR/src/backend/snowball" all + test -f "$BUILD_DIR/src/pl/plpgsql/src/plpgsql.so" + test -f "$BUILD_DIR/src/backend/snowball/dict_snowball.so" + test -f "$BUILD_DIR/src/backend/snowball/snowball_create.sql" + ' diff --git a/assets/wasix-build/docker_wasix_env.sh b/assets/wasix-build/docker_wasix_env.sh new file mode 100755 index 00000000..2f2df26b --- /dev/null +++ b/assets/wasix-build/docker_wasix_env.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${WASIX_HOME:=/opt/wasixcc-home/.wasixcc}" + +if [ "${HOME:-}" != "${WASIX_HOME%/.wasixcc}" ] && [ ! -e "$HOME/.wasixcc" ]; then + ln -s "$WASIX_HOME" "$HOME/.wasixcc" +fi + +export PATH="$WASIX_HOME/bin:$PATH" diff --git a/assets/wasix-build/patches/postgres-pglite-wasix-dl.patch b/assets/wasix-build/patches/postgres-pglite-wasix-dl.patch new file mode 100644 index 00000000..7765655f --- /dev/null +++ b/assets/wasix-build/patches/postgres-pglite-wasix-dl.patch @@ -0,0 +1,1187 @@ +diff --git a/src/Makefile.shlib b/src/Makefile.shlib +index 723fe2b7fa..1f99174a1f 100644 +--- a/src/Makefile.shlib ++++ b/src/Makefile.shlib +@@ -239,6 +239,17 @@ ifeq ($(PORTNAME), emscripten) + # endif + endif + ++ifeq ($(PORTNAME), wasix-dl) ++ LINK.shared = $(COMPILER) -shared -Wno-unused-function ++ ifdef soname ++ # wasm side modules use unversioned shared libraries ++ shlib = $(shlib_bare) ++ soname = $(shlib_bare) ++ endif ++ BUILD.exports = ( $(AWK) '/^[^\#]/ {printf "%s\n",$$1}' $< ) | sort -u >$@ ++ exports_file = $(SHLIB_EXPORTS:%.txt=%.list) ++endif ++ + ## + ## BUILD + ## +diff --git a/src/backend/Makefile b/src/backend/Makefile +index a215e39386..dbafda3a7e 100644 +--- a/src/backend/Makefile ++++ b/src/backend/Makefile +@@ -60,7 +60,7 @@ override LDFLAGS := $(LDFLAGS) $(LDFLAGS_EX) $(LDFLAGS_EX_BE) + + all: submake-libpgport submake-catalog-headers submake-utils-headers postgres $(POSTGRES_IMP) + +-ifneq ($(PORTNAME), emscripten) ++ifeq (,$(filter emscripten wasix-dl,$(PORTNAME))) + ifneq ($(PORTNAME), cygwin) + ifneq ($(PORTNAME), win32) + +@@ -109,6 +109,30 @@ pglite-libc: + + endif + ++ifeq ($(PORTNAME), wasix-dl) ++AR ?= llvm-ar ++WASM_LD ?= $(shell $(CC) -print-prog-name=wasm-ld) ++LIBPGCORE ?= $(top_builddir)/libpgcore.a ++LIBPG = $(top_builddir)/libpostgres.a ++PGCORE = $(top_builddir)/src/common/libpgcommon_srv.a $(top_builddir)/src/port/libpgport_srv.a $(LIBPG) ++PGMAIN = main/main.o tcop/postgres.o ++PGBACKEND = $(filter-out $(PGMAIN) $(top_builddir)/src/common/libpgcommon_srv.a $(top_builddir)/src/port/libpgport_srv.a,$(call expand_subsys,$(OBJS))) ++ ++postgres: $(OBJS) ++ $(AR) rcs $(top_builddir)/libpgmain.a $(PGMAIN) ++ $(AR) rcs $(LIBPG) $(PGBACKEND) ++ $(WASM_LD) --relocatable -o $(top_builddir)/libpgcore.o --whole-archive $(PGCORE) --no-whole-archive ++ $(AR) rcs $(LIBPGCORE) $(top_builddir)/libpgcore.o ++ COPTS="$(LOPTS)" $(CC) $(MAIN_MODULE) $(CFLAGS) $(LDFLAGS) -nostartfiles -o $@ $(LIBPGCORE) $(top_builddir)/libpgmain.a $(LIBS) ++ ++pglite: $(OBJS) ++ $(AR) rcs $(top_builddir)/libpgmain.a $(PGMAIN) ++ $(AR) rcs $(LIBPG) $(PGBACKEND) ++ $(WASM_LD) --relocatable -o $(top_builddir)/libpgcore.o --whole-archive $(PGCORE) --no-whole-archive ++ $(AR) rcs $(LIBPGCORE) $(top_builddir)/libpgcore.o ++ COPTS="$(LOPTS)" $(CC) $(MAIN_MODULE) $(CFLAGS) $(LDFLAGS) -nostartfiles -o $@ $(LIBPGCORE) $(top_builddir)/libpgmain.a $(LIBS) ++endif ++ + ifeq ($(PORTNAME), cygwin) + + postgres: $(OBJS) +diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c +index 97a4c387a3..5b5a19b7f2 100644 +--- a/src/backend/commands/copyfromparse.c ++++ b/src/backend/commands/copyfromparse.c +@@ -174,6 +174,9 @@ ReceiveCopyBegin(CopyFromState cstate) + int16 format = (cstate->opts.binary ? 1 : 0); + int i; + ++#ifdef PGLITE_WASIX_DL ++ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_IN); ++#endif + pq_beginmessage(&buf, PqMsg_CopyInResponse); + pq_sendbyte(&buf, format); /* overall format */ + pq_sendint16(&buf, natts); +diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c +index 84dc465cba..63526776be 100644 +--- a/src/backend/commands/copyto.c ++++ b/src/backend/commands/copyto.c +@@ -137,6 +137,9 @@ SendCopyBegin(CopyToState cstate) + int16 format = (cstate->opts.binary ? 1 : 0); + int i; + ++#ifdef PGLITE_WASIX_DL ++ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_OUT); ++#endif + pq_beginmessage(&buf, PqMsg_CopyOutResponse); + pq_sendbyte(&buf, format); /* overall format */ + pq_sendint16(&buf, natts); +diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c +index e0257cc49b..5137013369 100644 +--- a/src/backend/replication/walsender.c ++++ b/src/backend/replication/walsender.c +@@ -701,6 +701,9 @@ UploadManifest(void) + ib = CreateIncrementalBackupInfo(mcxt); + + /* Send a CopyInResponse message */ ++#ifdef PGLITE_WASIX_DL ++ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_IN); ++#endif + pq_beginmessage(&buf, PqMsg_CopyInResponse); + pq_sendbyte(&buf, 0); + pq_sendint16(&buf, 0); +@@ -954,6 +957,9 @@ StartReplication(StartReplicationCmd *cmd) + WalSndSetState(WALSNDSTATE_CATCHUP); + + /* Send a CopyBothResponse message, and start streaming */ ++#ifdef PGLITE_WASIX_DL ++ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_BOTH); ++#endif + pq_beginmessage(&buf, PqMsg_CopyBothResponse); + pq_sendbyte(&buf, 0); + pq_sendint16(&buf, 0); +@@ -1496,6 +1502,9 @@ StartLogicalReplication(StartReplicationCmd *cmd) + WalSndSetState(WALSNDSTATE_CATCHUP); + + /* Send a CopyBothResponse message, and start streaming */ ++#ifdef PGLITE_WASIX_DL ++ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_BOTH); ++#endif + pq_beginmessage(&buf, PqMsg_CopyBothResponse); + pq_sendbyte(&buf, 0); + pq_sendint16(&buf, 0); +diff --git a/src/common/file_utils.c b/src/common/file_utils.c +index 35458d1844..a04ee56efa 100644 +--- a/src/common/file_utils.c ++++ b/src/common/file_utils.c +@@ -416,1 +416,1 @@ fsync_fname(const char *fname, bool isdir) +- if (returncode != 0 && !(isdir && (errno == EBADF || errno == EINVAL))) ++ if (returncode != 0 && !(isdir && (errno == EBADF || errno == EINVAL || errno == EISDIR))) +diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c +index dd3307cb76..eb14fa5c13 100644 +--- a/src/backend/tcop/backend_startup.c ++++ b/src/backend/tcop/backend_startup.c +@@ -47,7 +47,7 @@ bool Trace_connection_negotiation = false; + static void BackendInitialize(ClientSocket *client_sock, CAC_state cac); + static int ProcessSSLStartup(Port *port); + +-#if defined(__EMSCRIPTEN__) ++#if defined(__EMSCRIPTEN__) || defined(PGLITE_WASIX_DL) + int ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done); + #else + static int ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done); +@@ -459,7 +459,9 @@ reject: + * should make no assumption here about the order in which the client may make + * requests. + */ +-#if defined(__EMSCRIPTEN__) ++#if defined(PGLITE_WASIX_DL) ++__attribute__((export_name("ProcessStartupPacket"))) int ++#elif defined(__EMSCRIPTEN__) + int EMSCRIPTEN_KEEPALIVE + #else + static int +diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c +index 1b41bf4de9..2394275215 100644 +--- a/src/backend/tcop/postgres.c ++++ b/src/backend/tcop/postgres.c +@@ -81,6 +81,15 @@ + #include "utils/timestamp.h" + #include "utils/varlena.h" + ++#ifdef PGLITE_WASIX_DL ++#include "port/wasix-dl.h" ++#define PGLITE_HOST_EXPORT(name) __attribute__((export_name(name))) ++extern void pglite_wasix_process_startup_options(struct Port *port); ++extern volatile int pglite_wasix_startup_error_capture_active; ++#else ++#define PGLITE_HOST_EXPORT(name) ++#endif ++ + /* ---------------- + * global variables + * ---------------- +@@ -243,7 +251,7 @@ void initDummyPort() { + MemoryContextSwitchTo(oldcontext); + } + +-void pgl_startPGlite() { ++PGLITE_HOST_EXPORT("pgl_startPGlite") void pgl_startPGlite() { + initDummyPort(); + whereToSendOutput = DestRemote; + // initdb execs postgres in single mode, which sets this to true +@@ -267,15 +275,18 @@ void pgl_startPGlite() { + + } + +-void pgl_pq_flush() { ++PGLITE_HOST_EXPORT("pgl_pq_flush") void pgl_pq_flush() { + pq_flush(); + } + +-struct Port* pgl_getMyProcPort() { ++PGLITE_HOST_EXPORT("pgl_getMyProcPort") struct Port* pgl_getMyProcPort() { + return MyProcPort; + } + +-void pgl_sendConnData() { ++PGLITE_HOST_EXPORT("pgl_sendConnData") void pgl_sendConnData() { ++#ifdef PGLITE_WASIX_DL ++ pglite_wasix_process_startup_options(MyProcPort); ++#endif + ClientAuthInProgress = false; + + { +@@ -297,6 +308,28 @@ void pgl_sendConnData() { + ReadyForQuery(DestRemote); + } + ++#ifdef PGLITE_WASIX_DL ++static CommandDest pglite_wasix_startup_error_saved_dest = DestDebug; ++ ++static void ++pglite_wasix_begin_startup_error_capture(void) ++{ ++ if (MyProcPort == NULL) ++ initDummyPort(); ++ pglite_wasix_startup_error_saved_dest = whereToSendOutput; ++ pglite_wasix_startup_error_capture_active = 1; ++ whereToSendOutput = DestRemote; ++} ++ ++static void ++pglite_wasix_end_startup_error_capture(void) ++{ ++ pglite_wasix_startup_error_capture_active = 0; ++ if (whereToSendOutput == DestRemote) ++ whereToSendOutput = pglite_wasix_startup_error_saved_dest; ++} ++#endif ++ + #endif // ifdef __PGLITE__ + + /* ---------------------------------------------------------------- +@@ -1125,6 +1156,8 @@ exec_simple_query(const char *query_string) + bool use_implicit_block; + char msec_str[32]; + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_SIMPLE_QUERY); ++ + /* + * Report query to various monitoring facilities. + */ +@@ -1148,7 +1181,9 @@ exec_simple_query(const char *query_string) + * one of those, else bad things will happen in xact.c. (Note that this + * will normally change current memory context.) + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_START_XACT); + start_xact_command(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_START_XACT); + + /* + * Zap any pre-existing unnamed statement. (While not strictly necessary, +@@ -1156,7 +1191,9 @@ exec_simple_query(const char *query_string) + * statement and portal; this ensures we recover any storage used by prior + * unnamed operations.) + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_DROP_UNNAMED); + drop_unnamed_stmt(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_DROP_UNNAMED); + + /* + * Switch to appropriate context for constructing parsetrees. +@@ -1167,7 +1204,9 @@ exec_simple_query(const char *query_string) + * Do basic parsing of the query or queries (this should be safe even if + * we are in aborted transaction state!) + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PARSE); + parsetree_list = pg_parse_query(query_string); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PARSE); + + /* Log immediately if dictated by log_statement */ + if (check_log_statement(parsetree_list)) +@@ -1244,7 +1283,9 @@ exec_simple_query(const char *query_string) + errdetail_abort())); + + /* Make sure we are in a transaction command */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_START_XACT); + start_xact_command(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_START_XACT); + + /* + * If using an implicit transaction block, and we're not already in a +@@ -1264,7 +1305,9 @@ exec_simple_query(const char *query_string) + */ + if (analyze_requires_snapshot(parsetree)) + { ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_SNAPSHOT); + PushActiveSnapshot(GetTransactionSnapshot()); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_SNAPSHOT); + snapshot_set = true; + } + +@@ -1291,11 +1334,15 @@ exec_simple_query(const char *query_string) + else + oldcontext = MemoryContextSwitchTo(MessageContext); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_ANALYZE_REWRITE); + querytree_list = pg_analyze_and_rewrite_fixedparams(parsetree, query_string, + NULL, 0, NULL); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_ANALYZE_REWRITE); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PLAN); + plantree_list = pg_plan_queries(querytree_list, query_string, + CURSOR_OPT_PARALLEL_OK, NULL); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PLAN); + + /* + * Done with the snapshot used for parsing/planning. +@@ -1336,7 +1383,9 @@ exec_simple_query(const char *query_string) + /* + * Start the portal. No parameters here. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PORTAL_START); + PortalStart(portal, NULL, 0, InvalidSnapshot); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PORTAL_START); + + /* + * Select the appropriate output format: text unless we are doing a +@@ -1363,9 +1412,11 @@ exec_simple_query(const char *query_string) + /* + * Now we can create the destination receiver object. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_DEST_RECEIVER); + receiver = CreateDestReceiver(dest); + if (dest == DestRemote) + SetRemoteDestReceiverParams(receiver, portal); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_DEST_RECEIVER); + + /* + * Switch back to transaction context for execution. +@@ -1375,6 +1426,7 @@ exec_simple_query(const char *query_string) + /* + * Run the portal to completion, and then drop it (and the receiver). + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PORTAL_RUN); + (void) PortalRun(portal, + FETCH_ALL, + true, /* always top level */ +@@ -1382,6 +1434,7 @@ exec_simple_query(const char *query_string) + receiver, + receiver, + &qc); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PORTAL_RUN); + + receiver->rDestroy(receiver); + +@@ -1400,7 +1453,9 @@ exec_simple_query(const char *query_string) + */ + if (use_implicit_block) + EndImplicitTransactionBlock(); ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); + finish_xact_command(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); + } + else if (IsA(parsetree->stmt, TransactionStmt)) + { +@@ -1408,7 +1463,9 @@ exec_simple_query(const char *query_string) + * If this was a transaction control statement, commit it. We will + * start a new xact command for the next command. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); + finish_xact_command(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); + } + else + { +@@ -1423,7 +1480,9 @@ exec_simple_query(const char *query_string) + * We need a CommandCounterIncrement after every query, except + * those that start or end a transaction block. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_COMMAND_COUNTER); + CommandCounterIncrement(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_COMMAND_COUNTER); + + /* + * Disable statement timeout between queries of a multi-query +@@ -1439,7 +1498,9 @@ exec_simple_query(const char *query_string) + * command the client sent, regardless of rewriting. (But a command + * aborted by error will not send an EndCommand report at all.) + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_END_COMMAND); + EndCommand(&qc, dest, false); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_END_COMMAND); + + /* Now we may drop the per-parsetree context, if one was created. */ + if (per_parsetree_context) +@@ -1451,7 +1512,9 @@ exec_simple_query(const char *query_string) + * something if the parsetree list was empty; otherwise the last loop + * iteration already did it.) + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); + finish_xact_command(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); + + /* + * If there were no parsetrees, return EmptyQueryResponse message. +@@ -1484,6 +1547,7 @@ exec_simple_query(const char *query_string) + TRACE_POSTGRESQL_QUERY_DONE(query_string); + + debug_query_string = NULL; ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_SIMPLE_QUERY); + } + + /* +@@ -4231,20 +4295,28 @@ PostgresSingleUserMain(int argc, char *argv[], + { + const char *dbname = NULL; + ++ PGL_BACKEND_TIMING_RESET(); ++ + Assert(!IsUnderPostmaster); + + /* Initialize startup process environment. */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_STANDALONE_PROCESS); + InitStandaloneProcess(argv[0]); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_STANDALONE_PROCESS); + + /* + * Set default values for command-line options. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_GUC_INIT); + InitializeGUCOptions(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_GUC_INIT); + + /* + * Parse command-line options. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SWITCH_PARSE); + process_postgres_switches(argc, argv, PGC_POSTMASTER, &dbname); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SWITCH_PARSE); + + /* Must have gotten a database name, or have a default (the username) */ + if (dbname == NULL) +@@ -4258,13 +4330,16 @@ PostgresSingleUserMain(int argc, char *argv[], + } + + /* Acquire configuration parameters */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CONFIG_FILES); + if (!SelectConfigFiles(userDoption, progname)) + proc_exit(1); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CONFIG_FILES); + + /* + * Validate we have been given a reasonable-looking DataDir and change + * into it. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATA_DIR_LOCK); + checkDataDir(); + ChangeToDataDir(); + +@@ -4272,17 +4347,25 @@ PostgresSingleUserMain(int argc, char *argv[], + * Create lockfile for data directory. + */ + CreateDataDirLockFile(false); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATA_DIR_LOCK); + + /* read control file (error checking and contains config ) */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CONTROL_FILE); + LocalProcessControlFile(false); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CONTROL_FILE); + + /* + * process any libraries that should be preloaded at postmaster start + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_PRELOAD_LIBS); + process_shared_preload_libraries(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_PRELOAD_LIBS); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SHARED_MEMORY); + /* Initialize MaxBackends */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_MAX_BACKENDS); + InitializeMaxBackends(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_MAX_BACKENDS); + + /* + * Give preloaded libraries a chance to request additional shared memory. +@@ -4302,7 +4385,9 @@ PostgresSingleUserMain(int argc, char *argv[], + */ + InitializeWalConsistencyChecking(); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CREATE_SHARED_MEMORY); + CreateSharedMemoryAndSemaphores(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CREATE_SHARED_MEMORY); + + /* + * Remember stand-alone backend startup time,roughly at the same point +@@ -4314,7 +4399,10 @@ PostgresSingleUserMain(int argc, char *argv[], + * Create a per-backend PGPROC struct in shared memory. We must do this + * before we can use LWLocks. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_PROCESS); + InitProcess(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_PROCESS); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SHARED_MEMORY); + + /* + * Now that sufficient infrastructure has been initialized, PostgresMain() +@@ -4323,7 +4411,7 @@ PostgresSingleUserMain(int argc, char *argv[], + PostgresMain(dbname, username); + } + +-void PostgresSendReadyForQueryIfNecessary() { ++PGLITE_HOST_EXPORT("PostgresSendReadyForQueryIfNecessary") void PostgresSendReadyForQueryIfNecessary() { + /* + * (1) If we've reached idle state, tell the frontend we're ready for + * a new query. +@@ -4431,7 +4519,7 @@ void PostgresSendReadyForQueryIfNecessary() { + } + } + +-void PostgresMainLoopOnce() { ++PGLITE_HOST_EXPORT("PostgresMainLoopOnce") void PostgresMainLoopOnce() { + + int firstchar; + StringInfoData input_message; +@@ -4805 +4893 @@ void PostgresMainLoopOnce() { +-void PostgresMainLongJmp() { ++PGLITE_HOST_EXPORT("PostgresMainLongJmp") void PostgresMainLongJmp() { +@@ -4894,6 +4982,9 @@ void PostgresMainLongJmp() { + if (doing_extended_query_message) + ignore_till_sync = true; + ++ if (!ignore_till_sync) ++ send_ready_for_query = true; ++ + /* We don't have a transaction command open anymore */ + xact_started = false; + +@@ -4994,7 +5085,9 @@ PostgresMain(const char *dbname, const char *username) + } + + /* Early initialization */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_BASE_INIT); + BaseInit(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_BASE_INIT); + + /* We need to allow SIGINT, etc during the initial transaction */ + sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); +@@ -5008,10 +5101,18 @@ PostgresMain(const char *dbname, const char *username) + * + * Honor session_preload_libraries if not dealing with a WAL sender. + */ ++#ifdef PGLITE_WASIX_DL ++ pglite_wasix_begin_startup_error_capture(); ++#endif ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_POSTGRES); + InitPostgres(dbname, InvalidOid, /* database to connect to */ + username, InvalidOid, /* role to connect as */ + (!am_walsender) ? INIT_PG_LOAD_SESSION_LIBS : 0, + NULL); /* no out_dbname */ ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_POSTGRES); ++#ifdef PGLITE_WASIX_DL ++ pglite_wasix_end_startup_error_capture(); ++#endif + + /* + * If the PostmasterContext is still around, recycle the space; we don't +@@ -5025,6 +5126,7 @@ PostgresMain(const char *dbname, const char *username) + + SetProcessingMode(NormalProcessing); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_POST_INIT); + /* + * Now all GUC states are fully set up. Report them to client if + * appropriate. +@@ -5061,7 +5163,9 @@ PostgresMain(const char *dbname, const char *username) + /* Welcome banner for standalone case */ + if (whereToSendOutput == DestDebug) + printf("\nPostgreSQL stand-alone backend %s\n", PG_VERSION); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_POST_INIT); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_MESSAGE_CONTEXTS); + /* + * Create the memory context we will use in the main loop. + * +@@ -5084,6 +5188,7 @@ PostgresMain(const char *dbname, const char *username) + MemoryContextSwitchTo(row_description_context); + initStringInfo(&row_description_buf); + MemoryContextSwitchTo(TopMemoryContext); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_MESSAGE_CONTEXTS); + + /* Fire any defined login event triggers, if appropriate */ + EventTriggerOnLogin(); +diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c +index 288c55a90d..0a4ba03fb2 100644 +--- a/src/backend/utils/init/postinit.c ++++ b/src/backend/utils/init/postinit.c +@@ -65,6 +65,9 @@ + #include "utils/snapmgr.h" + #include "utils/syscache.h" + #include "utils/timeout.h" ++#ifdef PGLITE_WASIX_DL ++#include "port/wasix-dl.h" ++#endif + + static HeapTuple GetDatabaseTuple(const char *dbname); + static HeapTuple GetDatabaseTupleByOid(Oid dboid); +@@ -759,6 +762,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + * + * Once I have done this, I am visible to other backends! + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_PROC_PHASE2); + InitProcessPhase2(); + + /* +@@ -786,6 +790,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT, + IdleStatsUpdateTimeoutHandler); + } ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_PROC_PHASE2); + + /* + * If this is either a bootstrap process or a standalone backend, start up +@@ -802,7 +807,9 @@ InitPostgres(const char *in_dbname, Oid dboid, + */ + CreateAuxProcessResourceOwner(); + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_STARTUP_XLOG); + StartupXLOG(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_STARTUP_XLOG); + /* Release (and warn about) any buffer pins leaked in StartupXLOG */ + ReleaseAuxProcessResources(true); + /* Reset CurrentResourceOwner to nothing for the moment */ +@@ -822,6 +829,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + * We must do this before starting a transaction because transaction abort + * would try to touch these hashtables. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_RELCACHE_CATCACHE_INIT); + RelationCacheInitialize(); + InitCatalogCache(); + InitPlanCache(); +@@ -837,6 +845,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + * at least entries for pg_database and catalogs used for authentication. + */ + RelationCacheInitializePhase2(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_RELCACHE_CATCACHE_INIT); + + /* + * Set up process-exit callback to do pre-shutdown cleanup. This is the +@@ -872,6 +881,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + */ + if (!bootstrap) + { ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_TRANSACTION_SNAPSHOT); + /* statement_timestamp must be set for timeouts to work correctly */ + SetCurrentStatementStartTimestamp(); + StartTransactionCommand(); +@@ -885,6 +895,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + XactIsoLevel = XACT_READ_COMMITTED; + + (void) GetTransactionSnapshot(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_TRANSACTION_SNAPSHOT); + } + + /* +@@ -895,6 +906,7 @@ InitPostgres(const char *in_dbname, Oid dboid, + * process, we use a fixed ID, otherwise we figure it out from the + * authenticated user name. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SESSION_USER); + if (bootstrap || AmAutoVacuumWorkerProcess() || AmLogicalSlotSyncWorkerProcess()) + { + InitializeSessionUserIdStandalone(); +@@ -902,12 +914,12 @@ InitPostgres(const char *in_dbname, Oid dboid, + } + else if (!IsUnderPostmaster) + { +-#if defined(__EMSCRIPTEN__) ++#if defined(__EMSCRIPTEN__) || defined(PGLITE_WASIX_DL) + if (!strcmp( username , WASM_USERNAME )) { + #endif + InitializeSessionUserIdStandalone(); + am_superuser = true; +-#if defined(__EMSCRIPTEN__) ++#if defined(__EMSCRIPTEN__) || defined(PGLITE_WASIX_DL) + } else { + //puts("# 894: switching session id"); + InitializeSessionUserId(username, InvalidOid, false); +@@ -947,6 +959,7 @@ if (!strcmp( username , WASM_USERNAME )) { + hba_authname(MyClientConnectionInfo.auth_method)); + am_superuser = superuser(); + } ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SESSION_USER); + + /* + * Binary upgrades only allowed super-user connections +@@ -1043,7 +1056,9 @@ if (!strcmp( username , WASM_USERNAME )) { + HeapTuple tuple; + Form_pg_database dbform; + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_LOOKUP); + tuple = GetDatabaseTuple(in_dbname); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_LOOKUP); + if (!HeapTupleIsValid(tuple)) + ereport(FATAL, + (errcode(ERRCODE_UNDEFINED_DATABASE), +@@ -1089,7 +1104,11 @@ if (!strcmp( username , WASM_USERNAME )) { + * CREATE DATABASE. + */ + if (!bootstrap) ++ { ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); + LockSharedObject(DatabaseRelationId, dboid, 0, RowExclusiveLock); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); ++ } + + /* + * Recheck pg_database to make sure the target database hasn't gone away. +@@ -1101,6 +1120,7 @@ if (!strcmp( username , WASM_USERNAME )) { + HeapTuple tuple; + Form_pg_database datform; + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); + tuple = GetDatabaseTupleByOid(dboid); + if (HeapTupleIsValid(tuple)) + datform = (Form_pg_database) GETSTRUCT(tuple); +@@ -1134,6 +1154,7 @@ if (!strcmp( username , WASM_USERNAME )) { + /* pass the database name back to the caller */ + if (out_dbname) + strcpy(out_dbname, dbname); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); + } + + /* +@@ -1175,6 +1196,7 @@ if (!strcmp( username , WASM_USERNAME )) { + * Now we should be able to access the database directory safely. Verify + * it's there and looks reasonable. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_PATH); + fullpath = GetDatabasePath(MyDatabaseId, MyDatabaseTableSpace); + + if (!bootstrap) +@@ -1200,6 +1222,7 @@ if (!strcmp( username , WASM_USERNAME )) { + + SetDatabasePath(fullpath); + pfree(fullpath); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_PATH); + + /* + * It's now possible to do real access to the system catalogs. +@@ -1207,10 +1230,16 @@ if (!strcmp( username , WASM_USERNAME )) { + * Load relcache entries for the system catalogs. This must create at + * least the minimum set of "nailed-in" cache entries. + */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_RELCACHE_PHASE3); ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_RELATION_CACHE_PHASE3); + RelationCacheInitializePhase3(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_RELATION_CACHE_PHASE3); + + /* set up ACL framework (so CheckMyDatabase can check permissions) */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INITIALIZE_ACL); + initialize_acl(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INITIALIZE_ACL); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_RELCACHE_PHASE3); + + /* + * Re-read the pg_database row for our database, check permissions and set +@@ -1219,8 +1248,12 @@ if (!strcmp( username , WASM_USERNAME )) { + * user is a superuser, so the above stuff has to happen first.) + */ + if (!bootstrap) ++ { ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CHECK_MY_DATABASE); + CheckMyDatabase(dbname, am_superuser, + (flags & INIT_PG_OVERRIDE_ALLOW_CONNS) != 0); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CHECK_MY_DATABASE); ++ } + + /* + * Now process any command-line switches and any additional GUC variable +@@ -1228,10 +1261,16 @@ if (!strcmp( username , WASM_USERNAME )) { + * because we didn't know if client is a superuser. + */ + if (MyProcPort != NULL) ++ { ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_STARTUP_OPTIONS); + process_startup_options(MyProcPort, am_superuser); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_STARTUP_OPTIONS); ++ } + + /* Process pg_db_role_setting options */ ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_PROCESS_SETTINGS); + process_settings(MyDatabaseId, GetSessionUserId()); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_PROCESS_SETTINGS); + + /* Apply PostAuthDelay as soon as we've read all options */ + if (PostAuthDelay > 0) +@@ -1242,6 +1281,7 @@ if (!strcmp( username , WASM_USERNAME )) { + * selected the active user and gotten the right GUC settings. + */ + ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SESSION_INITIALIZATION); + /* set default namespace search path */ + InitializeSearchPath(); + +@@ -1250,6 +1290,7 @@ if (!strcmp( username , WASM_USERNAME )) { + + /* Initialize this backend's session state. */ + InitializeSession(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SESSION_INITIALIZATION); + + /* + * If this is an interactive session, load any libraries that should be +@@ -1259,7 +1300,11 @@ if (!strcmp( username , WASM_USERNAME )) { + * access needs to be done. + */ + if ((flags & INIT_PG_LOAD_SESSION_LIBS) != 0) ++ { ++ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SESSION_PRELOAD_LIBS); + process_session_preload_libraries(); ++ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SESSION_PRELOAD_LIBS); ++ } + + /* report this backend in the PgBackendStatus array */ + if (!bootstrap) +@@ -1333,6 +1378,15 @@ process_startup_options(Port *port, bool am_superuser) + } + } + ++#ifdef PGLITE_WASIX_DL ++void ++pglite_wasix_process_startup_options(Port *port) ++{ ++ if (port != NULL) ++ process_startup_options(port, true); ++} ++#endif ++ + /* + * Load GUC settings from pg_db_role_setting. + * +diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c +index 93137820ac..4fd3eed96e 100644 +--- a/src/backend/utils/mmgr/portalmem.c ++++ b/src/backend/utils/mmgr/portalmem.c +@@ -28,6 +28,10 @@ + #include "utils/snapmgr.h" + #include "utils/timestamp.h" + ++#ifdef PGLITE_WASIX_DL ++extern int is_pglite_active; ++#endif ++ + /* + * Estimate of the maximum number of open portals a user would have, + * used in initially sizing the PortalHashTable in EnablePortalManager(). +@@ -795,6 +799,10 @@ AtAbort_Portals(void) + */ + if (portal->status == PORTAL_ACTIVE && shmem_exit_inprogress) + MarkPortalFailed(portal); ++#ifdef PGLITE_WASIX_DL ++ else if (portal->status == PORTAL_ACTIVE && is_pglite_active) ++ MarkPortalFailed(portal); ++#endif + + /* + * Do nothing else to cursors held over from a previous transaction. +diff --git a/src/include/port/wasix-dl.h b/src/include/port/wasix-dl.h +new file mode 100644 +index 0000000000..acbaf6cd8c +--- /dev/null ++++ b/src/include/port/wasix-dl.h +@@ -0,0 +1,133 @@ ++#pragma once ++ ++#ifndef I_WASIX_DL ++#define I_WASIX_DL ++ ++#undef HAVE_PTHREAD ++ ++#ifdef HAVE_GETRLIMIT ++#undef HAVE_GETRLIMIT ++#endif ++ ++#ifdef HAVE_SETSID ++#undef HAVE_SETSID ++#endif ++ ++#define PLATFORM_DEFAULT_SYNC_METHOD SYNC_METHOD_FDATASYNC ++ ++#ifndef EMSCRIPTEN_KEEPALIVE ++#define EMSCRIPTEN_KEEPALIVE __attribute__((used)) ++#endif ++ ++#ifndef __declspec ++#define __declspec(x) __attribute__((used)) ++#endif ++ ++#define em_callback_func void ++#define emscripten_set_main_loop(...) ++#define emscripten_force_exit(...) ++#define EM_JS(...) ++ ++#if defined(PGLITE_WASIX_DL) && defined(PGLITE_WASIX_BACKEND_TIMING) ++#define PGL_BACKEND_TIMING_MAIN_PRE 1 ++#define PGL_BACKEND_TIMING_RESTART_SINGLE_USER_MAIN 2 ++#define PGL_BACKEND_TIMING_ASYNC_SINGLE_USER_MAIN 3 ++#define PGL_BACKEND_TIMING_STANDALONE_PROCESS 4 ++#define PGL_BACKEND_TIMING_GUC_INIT 5 ++#define PGL_BACKEND_TIMING_SWITCH_PARSE 6 ++#define PGL_BACKEND_TIMING_CONFIG_FILES 7 ++#define PGL_BACKEND_TIMING_DATA_DIR_LOCK 8 ++#define PGL_BACKEND_TIMING_CONTROL_FILE 9 ++#define PGL_BACKEND_TIMING_PRELOAD_LIBS 10 ++#define PGL_BACKEND_TIMING_SHARED_MEMORY 11 ++#define PGL_BACKEND_TIMING_BASE_INIT 12 ++#define PGL_BACKEND_TIMING_INIT_POSTGRES 13 ++#define PGL_BACKEND_TIMING_POST_INIT 14 ++#define PGL_BACKEND_TIMING_MESSAGE_CONTEXTS 15 ++#define PGL_BACKEND_TIMING_POSTMASTER_ENVIRONMENT 16 ++#define PGL_BACKEND_TIMING_INIT_PROC_PHASE2 17 ++#define PGL_BACKEND_TIMING_STARTUP_XLOG 18 ++#define PGL_BACKEND_TIMING_RELCACHE_CATCACHE_INIT 19 ++#define PGL_BACKEND_TIMING_TRANSACTION_SNAPSHOT 20 ++#define PGL_BACKEND_TIMING_SESSION_USER 21 ++#define PGL_BACKEND_TIMING_DATABASE_LOOKUP 22 ++#define PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK 23 ++#define PGL_BACKEND_TIMING_DATABASE_PATH 24 ++#define PGL_BACKEND_TIMING_RELCACHE_PHASE3 25 ++#define PGL_BACKEND_TIMING_CHECK_MY_DATABASE 26 ++#define PGL_BACKEND_TIMING_STARTUP_OPTIONS 27 ++#define PGL_BACKEND_TIMING_PROCESS_SETTINGS 28 ++#define PGL_BACKEND_TIMING_SESSION_INITIALIZATION 29 ++#define PGL_BACKEND_TIMING_SESSION_PRELOAD_LIBS 30 ++#define PGL_BACKEND_TIMING_INIT_MAX_BACKENDS 31 ++#define PGL_BACKEND_TIMING_CREATE_SHARED_MEMORY 32 ++#define PGL_BACKEND_TIMING_INIT_PROCESS 33 ++#define PGL_BACKEND_TIMING_RELATION_CACHE_PHASE3 34 ++#define PGL_BACKEND_TIMING_INITIALIZE_ACL 35 ++#define PGL_BACKEND_TIMING_EXEC_SIMPLE_QUERY 36 ++#define PGL_BACKEND_TIMING_EXEC_START_XACT 37 ++#define PGL_BACKEND_TIMING_EXEC_DROP_UNNAMED 38 ++#define PGL_BACKEND_TIMING_EXEC_PARSE 39 ++#define PGL_BACKEND_TIMING_EXEC_SNAPSHOT 40 ++#define PGL_BACKEND_TIMING_EXEC_ANALYZE_REWRITE 41 ++#define PGL_BACKEND_TIMING_EXEC_PLAN 42 ++#define PGL_BACKEND_TIMING_EXEC_PORTAL_START 43 ++#define PGL_BACKEND_TIMING_EXEC_DEST_RECEIVER 44 ++#define PGL_BACKEND_TIMING_EXEC_PORTAL_RUN 45 ++#define PGL_BACKEND_TIMING_EXEC_FINISH_XACT 46 ++#define PGL_BACKEND_TIMING_EXEC_COMMAND_COUNTER 47 ++#define PGL_BACKEND_TIMING_EXEC_END_COMMAND 48 ++ ++extern void pgl_backend_timing_reset(void); ++extern void pgl_backend_timing_start(int id); ++extern void pgl_backend_timing_end(int id); ++ ++#define PGL_BACKEND_TIMING_RESET() pgl_backend_timing_reset() ++#define PGL_BACKEND_TIMING_START(id) pgl_backend_timing_start(id) ++#define PGL_BACKEND_TIMING_END(id) pgl_backend_timing_end(id) ++#else ++#define PGL_BACKEND_TIMING_RESET() ++#define PGL_BACKEND_TIMING_START(id) ++#define PGL_BACKEND_TIMING_END(id) ++#endif ++ ++#define PGL_WASIX_PROTOCOL_COPY_IN 1 ++#define PGL_WASIX_PROTOCOL_COPY_OUT 2 ++#define PGL_WASIX_PROTOCOL_COPY_BOTH 3 ++extern volatile int pglite_wasix_startup_error_capture_active; ++extern void pgl_protocol_report_copy_response(int state); ++ ++#ifdef __PGLITE__ ++#include ++#define fe_utils_quote_all_identifiers quote_all_identifiers ++#define sdk_sock_flush() ((void) 0) ++#define fork() (errno = ENOSYS, -1) ++#ifndef PDEBUG ++#define PDEBUG(...) ((void) 0) ++#endif ++#ifndef WASM_USERNAME ++#define WASM_USERNAME "postgres" ++#endif ++#ifndef WASM_PREFIX ++#define WASM_PREFIX "" ++#endif ++#ifndef WASM_PGOPTS ++#define WASM_PGOPTS \ ++ "-c", "log_checkpoints=false", \ ++ "-c", "search_path=pg_catalog", \ ++ "-c", "exit_on_error=true", \ ++ "-c", "ignore_invalid_pages=on", \ ++ "-c", "temp_buffers=8MB", \ ++ "-c", "work_mem=4MB", \ ++ "-c", "fsync=on", \ ++ "-c", "synchronous_commit=on", \ ++ "-c", "wal_buffers=4MB", \ ++ "-c", "min_wal_size=80MB", \ ++ "-c", "shared_buffers=128MB" ++#endif ++extern int pgl_system(const char *command); ++#define system_wasi(command) pgl_system(command) ++#define proc_exit(arg) pg_proc_exit(arg) ++#endif ++ ++#endif /* I_WASIX_DL */ +diff --git a/src/include/port/wasix-dl/sys/ipc.h b/src/include/port/wasix-dl/sys/ipc.h +new file mode 100644 +index 0000000000..0872fdb47f +--- /dev/null ++++ b/src/include/port/wasix-dl/sys/ipc.h +@@ -0,0 +1,37 @@ ++#pragma once ++ ++#include ++ ++#ifndef IPC_PRIVATE ++#define IPC_PRIVATE ((key_t) 0) ++#endif ++#ifndef IPC_CREAT ++#define IPC_CREAT 01000 ++#endif ++#ifndef IPC_EXCL ++#define IPC_EXCL 02000 ++#endif ++#ifndef IPC_NOWAIT ++#define IPC_NOWAIT 04000 ++#endif ++ ++#ifndef IPC_RMID ++#define IPC_RMID 0 ++#endif ++#ifndef IPC_SET ++#define IPC_SET 1 ++#endif ++#ifndef IPC_STAT ++#define IPC_STAT 2 ++#endif ++ ++struct ipc_perm ++{ ++ key_t __key; ++ uid_t uid; ++ gid_t gid; ++ uid_t cuid; ++ gid_t cgid; ++ mode_t mode; ++ unsigned short __seq; ++}; +diff --git a/src/include/port/wasix-dl/sys/shm.h b/src/include/port/wasix-dl/sys/shm.h +new file mode 100644 +index 0000000000..91f59d9e39 +--- /dev/null ++++ b/src/include/port/wasix-dl/sys/shm.h +@@ -0,0 +1,30 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++#ifndef SHM_RDONLY ++#define SHM_RDONLY 010000 ++#endif ++#ifndef SHM_RND ++#define SHM_RND 020000 ++#endif ++#ifndef SHMLBA ++#define SHMLBA 4096 ++#endif ++ ++struct shmid_ds ++{ ++ struct ipc_perm shm_perm; ++ size_t shm_segsz; ++ time_t shm_atime; ++ time_t shm_dtime; ++ time_t shm_ctime; ++ unsigned long shm_nattch; ++}; ++ ++int shmget(key_t key, size_t size, int shmflg); ++void *shmat(int shmid, const void *shmaddr, int shmflg); ++int shmdt(const void *shmaddr); ++int shmctl(int shmid, int cmd, struct shmid_ds *buf); +diff --git a/src/makefiles/Makefile.wasix-dl b/src/makefiles/Makefile.wasix-dl +new file mode 100644 +index 0000000000..c0e3bac8c8 +--- /dev/null ++++ b/src/makefiles/Makefile.wasix-dl +@@ -0,0 +1,14 @@ ++# Use unversioned shared objects for WebAssembly side modules. ++rpath = ++AROPT = crs ++ ++# Rule for building a shared library from a single .o file. ++%.so: %.o ++ $(CC) $(CFLAGS) $< $(LDFLAGS) $(LDFLAGS_SL) -shared -o $@ ++ ++# WASIX side modules install import lists under the same layout contract that ++# PGlite's dynamic loader consumes, without reusing Emscripten-named variables. ++wasm_dl_include_dir := $(pkgincludedir)/wasix-dl ++wasm_dl_base_dir := $(wasm_dl_include_dir)/base ++wasm_dl_imports_dir := $(wasm_dl_base_dir)/imports ++wasm_dl_extension_dir := $(wasm_dl_include_dir)/extension +diff --git a/src/makefiles/pgxs.mk b/src/makefiles/pgxs.mk +index 79705050cf..6780d837e0 100644 +--- a/src/makefiles/pgxs.mk ++++ b/src/makefiles/pgxs.mk +@@ -249,11 +249,11 @@ ifdef MODULES + ifeq ($(with_llvm), yes) + $(foreach mod, $(MODULES), $(call install_llvm_module,$(mod),$(mod).bc)) + endif # with_llvm +-ifeq ($(PORTNAME), emscripten) ++ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) + find . -name "*.o" -exec $(LLVM_NM) --undefined-only {} \; | awk '{print $$2}' | sed '/^$$/d' | sort -u > '$(MODULES).undef.txt' + find . -type f \( -name "*.o" -o -name "*.so" \) -exec $(LLVM_NM) --defined-only {} \; | awk '$$2 ~ /^[TDB]$$/ {print $$3}' | sed '/^$$/d' | sort -u > '$(MODULES).defs.txt' +- comm -23 '$(MODULES).undef.txt' '$(MODULES).defs.txt' > '$(emscripten_extension_imports_dir)/$(MODULES).imports' +-endif # PORTNAME=emscripten ++ comm -23 '$(MODULES).undef.txt' '$(MODULES).defs.txt' > '$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULES).imports' ++endif # PORTNAME=emscripten wasix-dl + endif # MODULES + ifdef DOCS + ifdef docdir +@@ -276,11 +276,11 @@ ifdef MODULE_big + ifeq ($(with_llvm), yes) + $(call install_llvm_module,$(MODULE_big),$(OBJS)) + endif # with_llvm +-ifeq ($(PORTNAME), emscripten) ++ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) + find . -name "*.o" -exec $(LLVM_NM) --undefined-only {} \; | awk '{print $$2}' | sed '/^$$/d' | sort -u > '$(MODULE_big).undef.txt' + find . -type f \( -name "*.o" -o -name "*.so" \) -exec $(LLVM_NM) --defined-only {} \; | awk '$$2 ~ /^[TDB]$$/ {print $$3}' | sed '/^$$/d' | sort -u > '$(MODULE_big).defs.txt' +- comm -23 '$(MODULE_big).undef.txt' '$(MODULE_big).defs.txt' > '$(emscripten_extension_imports_dir)/$(MODULE_big).imports' +-endif # PORTNAME=emscripten ++ comm -23 '$(MODULE_big).undef.txt' '$(MODULE_big).defs.txt' > '$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULE_big).imports' ++endif # PORTNAME=emscripten wasix-dl + + install: install-lib + endif # MODULE_big +@@ -307,8 +307,8 @@ endif # DOCS + ifneq (,$(PROGRAM)$(SCRIPTS)$(SCRIPTS_built)) + $(MKDIR_P) '$(DESTDIR)$(bindir)' + endif +-ifeq ($(PORTNAME), emscripten) +- $(MKDIR_P) '$(DESTDIR)$(emscripten_extension_imports_dir)' ++ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) ++ $(MKDIR_P) '$(DESTDIR)$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))' + endif + + ifdef MODULE_big +@@ -331,8 +331,8 @@ ifdef MODULES + ifeq ($(with_llvm), yes) + $(foreach mod, $(MODULES), $(call uninstall_llvm_module,$(mod))) + endif # with_llvm +-ifeq ($(PORTNAME), emscripten) +- rm -f '$(DESTDIR)$(emscripten_extension_imports_dir)/$(MODULES).imports' ++ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) ++ rm -f '$(DESTDIR)$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULES).imports' + endif + endif # MODULES + ifdef DOCS +@@ -356,8 +356,8 @@ ifeq ($(with_llvm), yes) + $(call uninstall_llvm_module,$(MODULE_big)) + endif # with_llvm + +-ifeq ($(PORTNAME), emscripten) +- rm -f '$(DESTDIR)$(emscripten_extension_imports_dir)/$(MODULE_big).imports' ++ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) ++ rm -f '$(DESTDIR)$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULE_big).imports' + endif + + uninstall: uninstall-lib +diff --git a/src/template/wasix-dl b/src/template/wasix-dl +new file mode 100644 +index 0000000000..f747e11735 +--- /dev/null ++++ b/src/template/wasix-dl +@@ -0,0 +1,13 @@ ++# src/template/wasix-dl ++ ++# Prefer unnamed POSIX semaphores if available, unless user overrides choice. ++if test x"$PREFERRED_SEMAPHORES" = x"" ; then ++ PREFERRED_SEMAPHORES=UNNAMED_POSIX ++fi ++ ++# Keep the same GNU feature surface as the Emscripten/WASI templates and ++# identify the dynamic-linking WASIX personality inside shared extension code. ++CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE -DPGLITE_WASIX_DL" ++ ++# Side modules must be position-independent WebAssembly objects. ++CFLAGS_SL="-fPIC" diff --git a/assets/wasix-build/pg_config_wasix.sh b/assets/wasix-build/pg_config_wasix.sh new file mode 100755 index 00000000..61d0141e --- /dev/null +++ b/assets/wasix-build/pg_config_wasix.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +BUILD_DIR="${BUILD_DIR:-/work/assets/wasix-build/work/docker-pglite}" +PGSRC="${PGSRC:-/work/assets/checkouts/postgres-pglite}" +PREFIX="${PGLITE_WASIX_PREFIX:-$BUILD_DIR/install}" + +case "${1:-}" in + --pgxs) + echo "$BUILD_DIR/src/makefiles/pgxs.mk" + ;; + --bindir) + echo "$PREFIX/bin" + ;; + --sharedir) + echo "$PREFIX/share" + ;; + --sysconfdir) + echo "$PREFIX/etc" + ;; + --libdir) + echo "$PREFIX/lib" + ;; + --pkglibdir) + echo "$PREFIX/lib/postgresql" + ;; + --includedir | --pkgincludedir) + echo "$PREFIX/include" + ;; + --mandir) + echo "$PREFIX/share/man" + ;; + --docdir) + echo "$PREFIX/share/doc" + ;; + --localedir) + echo "$PREFIX/share/locale" + ;; + --version) + echo "PostgreSQL 17.5-wasix-pglite" + ;; + --configure) + echo "--host=wasm32-wasix --with-template=wasix-dl" + ;; + --cc) + echo "wasixcc" + ;; + --cppflags) + echo "-I$BUILD_DIR/src/include -I$PGSRC/src/include -I$PGSRC/src/include/port/wasix-dl" + ;; + --cflags) + echo "" + ;; + --ldflags | --libs) + echo "" + ;; + *) + echo "unsupported pg_config_wasix.sh option: ${1:-}" >&2 + exit 2 + ;; +esac diff --git a/assets/wasix-build/prepare_patched_source.sh b/assets/wasix-build/prepare_patched_source.sh new file mode 100755 index 00000000..dd69867a --- /dev/null +++ b/assets/wasix-build/prepare_patched_source.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$ROOT/../.." && pwd)" +UPSTREAM_PGSRC="${UPSTREAM_PGSRC:-$REPO_ROOT/assets/checkouts/postgres-pglite}" +PATCHED_PGSRC="${PATCHED_PGSRC:-$ROOT/work/postgres-pglite-wasix-src}" +PATCH_PATH="${PATCH_PATH:-$ROOT/patches/postgres-pglite-wasix-dl.patch}" +POSTGRES_PGLITE_COMMIT="${POSTGRES_PGLITE_COMMIT:-$(git -C "$UPSTREAM_PGSRC" rev-parse HEAD)}" + +PATCH_SHA="$(shasum -a 256 "$PATCH_PATH" | awk '{print $1}')" +HEAD_FILE="$PATCHED_PGSRC/.pglite-oxide-source-head" +PATCH_FILE="$PATCHED_PGSRC/.pglite-oxide-patch-sha256" + +if [ -e "$PATCHED_PGSRC/.git" ] \ + && [ -f "$HEAD_FILE" ] \ + && [ -f "$PATCH_FILE" ] \ + && [ "$(cat "$HEAD_FILE")" = "$POSTGRES_PGLITE_COMMIT" ] \ + && [ "$(cat "$PATCH_FILE")" = "$PATCH_SHA" ]; then + echo "reusing patched postgres-pglite source at $PATCHED_PGSRC" + exit 0 +fi + +git -C "$UPSTREAM_PGSRC" worktree remove --force "$PATCHED_PGSRC" >/dev/null 2>&1 || true +rm -rf "$PATCHED_PGSRC" +git -C "$UPSTREAM_PGSRC" worktree prune +git -C "$UPSTREAM_PGSRC" worktree add --detach "$PATCHED_PGSRC" "$POSTGRES_PGLITE_COMMIT" +git -C "$PATCHED_PGSRC" apply --unidiff-zero --whitespace=nowarn "$PATCH_PATH" + +printf '%s' "$POSTGRES_PGLITE_COMMIT" > "$HEAD_FILE" +printf '%s' "$PATCH_SHA" > "$PATCH_FILE" +echo "prepared patched postgres-pglite source at $PATCHED_PGSRC" diff --git a/assets/wasix-build/profile_flags.sh b/assets/wasix-build/profile_flags.sh new file mode 100644 index 00000000..ed052309 --- /dev/null +++ b/assets/wasix-build/profile_flags.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +pglite_oxide_wasix_profile="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" + +case "$pglite_oxide_wasix_profile" in + debug) + PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--O0 -g3}" + PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" + ;; + release) + PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--O2 -g0}" + PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" + ;; + release-o3) + PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--O3 -g0 -flto=thin}" + PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:--flto=thin}" + ;; + release-os) + PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--Os -g0}" + PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" + ;; + release-oz) + PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--Oz -g0}" + PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" + ;; + *) + echo "unknown PGLITE_OXIDE_BUILD_PROFILE=$pglite_oxide_wasix_profile" >&2 + exit 2 + ;; +esac + +PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" +PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" +PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" +if [ -z "${PGLITE_OXIDE_WASM_OPT_FLAGS:-}" ]; then + case "$pglite_oxide_wasix_profile" in + release*) + PGLITE_OXIDE_WASM_OPT_FLAGS="--converge:--strip-debug:--strip-producers" + ;; + *) + PGLITE_OXIDE_WASM_OPT_FLAGS="" + ;; + esac +elif [ "$PGLITE_OXIDE_WASM_OPT_FLAGS" = "none" ]; then + PGLITE_OXIDE_WASM_OPT_FLAGS="" +fi + +pglite_oxide_reject_asyncify_flag() { + local name="$1" + local value="${!name:-}" + + if [ -z "$value" ] || [ -n "${PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT:-}" ]; then + return + fi + + case "$value" in + *ASYNCIFY*|*asyncify*) + echo "$name contains Asyncify flags; production WASIX artifacts require WebAssembly exceptions. Set PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT=1 only for isolated experiments." >&2 + exit 2 + ;; + esac +} + +for pglite_oxide_flag_var in \ + PGLITE_OXIDE_PROFILE_CFLAGS \ + PGLITE_OXIDE_PROFILE_LDFLAGS \ + PGLITE_OXIDE_WASM_OPT_FLAGS \ + PGLITE_OXIDE_WASIX_COMPILER_FLAGS \ + PGLITE_OXIDE_WASIX_LINKER_FLAGS +do + pglite_oxide_reject_asyncify_flag "$pglite_oxide_flag_var" +done + +pglite_oxide_apply_wasix_profile() { + local phase="${1:-build}" + + export PGLITE_OXIDE_PROFILE_CFLAGS + export PGLITE_OXIDE_PROFILE_LDFLAGS + export WASIXCC_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" + export WASIXCC_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" + export WASIXCC_WASM_OPT_FLAGS="$PGLITE_OXIDE_WASM_OPT_FLAGS" + if [ -n "${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT:-}" ]; then + export WASIXCC_WASM_OPT_SUPPRESS_DEFAULT="$PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT" + fi + if [ -n "${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED:-}" ]; then + export WASIXCC_WASM_OPT_PRESERVE_UNOPTIMIZED="$PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED" + fi + + if [ "$phase" = "configure" ]; then + export WASIXCC_RUN_WASM_OPT="$PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT" + else + export WASIXCC_RUN_WASM_OPT="$PGLITE_OXIDE_WASIX_BUILD_WASM_OPT" + fi +} + +pglite_oxide_wasix_profile_signature() { + printf 'profile=%s\n' "$pglite_oxide_wasix_profile" + printf 'cflags=%s\n' "$PGLITE_OXIDE_PROFILE_CFLAGS" + printf 'ldflags=%s\n' "$PGLITE_OXIDE_PROFILE_LDFLAGS" + printf 'configure_wasm_opt=%s\n' "$PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT" + printf 'build_wasm_opt=%s\n' "$PGLITE_OXIDE_WASIX_BUILD_WASM_OPT" + printf 'wasm_opt_flags=%s\n' "$PGLITE_OXIDE_WASM_OPT_FLAGS" + printf 'wasm_opt_suppress_default=%s\n' "${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT:-}" + printf 'wasm_opt_preserve_unoptimized=%s\n' "${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED:-}" + printf 'compiler_flags=%s\n' "${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" + printf 'linker_flags=%s\n' "${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" + printf 'backend_timing=%s\n' "$PGLITE_OXIDE_WASIX_BACKEND_TIMING" + if [ -f ./assets/wasix-build/configure_wasix_dl.sh ]; then + printf 'configure_wasix_dl_sha256=%s\n' "$(sha256sum ./assets/wasix-build/configure_wasix_dl.sh | awk '{print $1}')" + fi +} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_bridge.c b/assets/wasix-build/wasix_shim/pglite_wasix_bridge.c new file mode 100644 index 00000000..0dd0114e --- /dev/null +++ b/assets/wasix-build/wasix_shim/pglite_wasix_bridge.c @@ -0,0 +1,1103 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#ifndef _DARWIN_C_SOURCE +#define _DARWIN_C_SOURCE +#endif +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef EMSCRIPTEN_KEEPALIVE +#define EMSCRIPTEN_KEEPALIVE __attribute__((used)) +#endif + +#define PGLITE_UID 123 +#define PGLITE_PROTOCOL_FD 1 +#define POSTGRES_MAIN_LONGJMP 100 +#define MAX_ATEXIT_FUNCS 32 +#ifdef PGLITE_WASIX_BACKEND_TIMING +#define PGL_BACKEND_TIMING_MAX 64 +#endif + +volatile int is_pglite_active = 0; +volatile int force_host_error_recovery = 0; +volatile int pglite_wasix_startup_error_capture_active = 0; +volatile sigjmp_buf postgresmain_sigjmp_buf; +volatile bool ignore_till_sync = false; +volatile bool send_ready_for_query = false; + +extern int pg_char_to_encoding_private(const char *name); +extern const char *pg_encoding_to_char_private(int encoding); + +/* + * PGlite's libpq sources intentionally use private encoding symbols in the + * embedded backend build so libpq does not leak a second copy of the encoding + * table into the main module. A standalone WASIX pg_dump links the same static + * libpq archive, whose connection path still expects libpq's public aliases. + * Provide only those aliases here so pg_dump can use the normal static + * libpgcommon archive without also pulling in libpgcommon_shlib. + */ +int __attribute__((weak)) EMSCRIPTEN_KEEPALIVE +pg_char_to_encoding(const char *name) +{ + return pg_char_to_encoding_private(name); +} + +const char __attribute__((weak)) *EMSCRIPTEN_KEEPALIVE +pg_encoding_to_char(int encoding) +{ + return pg_encoding_to_char_private(encoding); +} + +static unsigned char *pgl_wasix_input_buf; +static size_t pgl_wasix_input_len; +static size_t pgl_wasix_input_off; + +static unsigned char *pgl_wasix_output_buf; +static size_t pgl_wasix_output_len_value; +static size_t pgl_wasix_output_cap; +enum +{ + PGL_WASIX_PROTOCOL_BUFFERED = 0, + PGL_WASIX_PROTOCOL_STREAM = 1, + PGL_WASIX_PROTOCOL_HYBRID = 2, +}; +enum +{ + PGL_WASIX_PROTOCOL_COPY_NONE = 0, + PGL_WASIX_PROTOCOL_COPY_IN = 1, + PGL_WASIX_PROTOCOL_COPY_OUT = 2, + PGL_WASIX_PROTOCOL_COPY_BOTH = 3, +}; + +static int pgl_wasix_protocol_transport; +static int pgl_wasix_protocol_copy_state; +static bool pgl_wasix_protocol_stream_requested; +static bool pgl_wasix_protocol_stream_active; +static void (*atexit_funcs[MAX_ATEXIT_FUNCS])(void); +static int atexit_func_count; + +int pgl_set_protocol_transport(int mode); + +int EMSCRIPTEN_KEEPALIVE +pgl_set_protocol_stdio(int enabled) +{ + return pgl_set_protocol_transport(enabled ? PGL_WASIX_PROTOCOL_STREAM + : PGL_WASIX_PROTOCOL_BUFFERED); +} + +int EMSCRIPTEN_KEEPALIVE +pgl_set_protocol_transport(int mode) +{ + if (mode < PGL_WASIX_PROTOCOL_BUFFERED || mode > PGL_WASIX_PROTOCOL_HYBRID) + { + errno = EINVAL; + return -1; + } + + int previous = pgl_wasix_protocol_transport; + pgl_wasix_protocol_transport = mode; + pgl_wasix_protocol_stream_active = mode == PGL_WASIX_PROTOCOL_STREAM; + if (mode != PGL_WASIX_PROTOCOL_HYBRID) + { + pgl_wasix_protocol_copy_state = PGL_WASIX_PROTOCOL_COPY_NONE; + pgl_wasix_protocol_stream_requested = false; + } + return previous; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_protocol_stream_active(void) +{ + return pgl_wasix_protocol_stream_active ? 1 : 0; +} + +void EMSCRIPTEN_KEEPALIVE +pgl_protocol_report_copy_response(int state) +{ + if (state < PGL_WASIX_PROTOCOL_COPY_NONE || + state > PGL_WASIX_PROTOCOL_COPY_BOTH) + { + errno = EINVAL; + return; + } + pgl_wasix_protocol_copy_state = state; + pgl_wasix_protocol_stream_requested = + pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_HYBRID && + state != PGL_WASIX_PROTOCOL_COPY_NONE; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_protocol_copy_state(void) +{ + return pgl_wasix_protocol_copy_state; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_set_force_host_error_recovery(int new_value) +{ + int current = force_host_error_recovery; + force_host_error_recovery = new_value != 0; + return current; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_setPGliteActive(int new_value) +{ + int current = is_pglite_active; + is_pglite_active = new_value; + if (new_value == 0) + { + struct itimerval zero = {{0, 0}, {0, 0}}; + (void) setitimer(ITIMER_REAL, &zero, NULL); + } + return current; +} + +void EMSCRIPTEN_KEEPALIVE +pgl_longjmp(jmp_buf env, int val) +{ + /* + * Some hosts can run nested WebAssembly exception unwinds and can preserve + * PostgreSQL's normal PG_TRY/PG_CATCH behavior. Hosts without that support + * must route every PostgreSQL ERROR longjmp through the existing + * single-user process-exit boundary; Rust then invokes PostgresMainLongJmp() + * to perform the same top-level cleanup and emit the backend ErrorResponse. + */ + if (is_pglite_active && + (force_host_error_recovery || + memcmp(env, (void *) postgresmain_sigjmp_buf, sizeof(jmp_buf)) == 0)) + { + exit(POSTGRES_MAIN_LONGJMP); + } + longjmp(env, val); +} + +void EMSCRIPTEN_KEEPALIVE +pgl_siglongjmp(sigjmp_buf env, int val) +{ + pgl_longjmp(env, val); +} + +#ifdef PGLITE_WASIX_BACKEND_TIMING +static uint64_t pgl_backend_timing_started_us[PGL_BACKEND_TIMING_MAX]; +static uint64_t pgl_backend_timing_elapsed_us_value[PGL_BACKEND_TIMING_MAX]; +static bool pgl_backend_timing_seen[PGL_BACKEND_TIMING_MAX]; + +static uint64_t +pgl_monotonic_us(void) +{ + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0; + return ((uint64_t) ts.tv_sec * 1000000ULL) + ((uint64_t) ts.tv_nsec / 1000ULL); +} + +void EMSCRIPTEN_KEEPALIVE +pgl_backend_timing_reset(void) +{ + memset(pgl_backend_timing_started_us, 0, sizeof(pgl_backend_timing_started_us)); + memset(pgl_backend_timing_elapsed_us_value, 0, sizeof(pgl_backend_timing_elapsed_us_value)); + memset(pgl_backend_timing_seen, 0, sizeof(pgl_backend_timing_seen)); +} + +void EMSCRIPTEN_KEEPALIVE +pgl_backend_timing_start(int id) +{ + if (id <= 0 || id >= PGL_BACKEND_TIMING_MAX) + return; + pgl_backend_timing_started_us[id] = pgl_monotonic_us(); +} + +void EMSCRIPTEN_KEEPALIVE +pgl_backend_timing_end(int id) +{ + if (id <= 0 || id >= PGL_BACKEND_TIMING_MAX) + return; + + uint64_t started = pgl_backend_timing_started_us[id]; + uint64_t ended = pgl_monotonic_us(); + if (started == 0 || ended < started) + return; + + pgl_backend_timing_elapsed_us_value[id] += ended - started; + pgl_backend_timing_seen[id] = true; + pgl_backend_timing_started_us[id] = 0; +} + +int64_t EMSCRIPTEN_KEEPALIVE +pgl_backend_timing_elapsed_us(int id) +{ + if (id <= 0 || id >= PGL_BACKEND_TIMING_MAX || !pgl_backend_timing_seen[id]) + return -1; + return (int64_t) pgl_backend_timing_elapsed_us_value[id]; +} +#endif + +int EMSCRIPTEN_KEEPALIVE +pgl_wasix_input_reset(void) +{ + pgl_wasix_input_len = 0; + pgl_wasix_input_off = 0; + return 0; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_wasix_input_write(const void *buffer, size_t length) +{ + if (length == 0) + return 0; + if (buffer == NULL) + { + errno = EINVAL; + return -1; + } + + if (pgl_wasix_input_off == pgl_wasix_input_len) + { + pgl_wasix_input_len = 0; + pgl_wasix_input_off = 0; + } + + size_t new_len = pgl_wasix_input_len + length; + unsigned char *new_buf = realloc(pgl_wasix_input_buf, new_len); + if (new_buf == NULL) + { + errno = ENOMEM; + return -1; + } + + pgl_wasix_input_buf = new_buf; + memcpy(pgl_wasix_input_buf + pgl_wasix_input_len, buffer, length); + pgl_wasix_input_len = new_len; + return (int) length; +} + +size_t EMSCRIPTEN_KEEPALIVE +pgl_wasix_input_available(void) +{ + if (pgl_wasix_input_off >= pgl_wasix_input_len) + return 0; + return pgl_wasix_input_len - pgl_wasix_input_off; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_wasix_input_peek(void) +{ + if (pgl_wasix_input_off >= pgl_wasix_input_len) + return -1; + return (int) pgl_wasix_input_buf[pgl_wasix_input_off]; +} + +static ssize_t +pgl_wasix_buffer_read(void *buffer, size_t max_length) +{ + if (buffer == NULL || max_length == 0) + return 0; + if (pgl_wasix_input_off >= pgl_wasix_input_len) + return 0; + + size_t available = pgl_wasix_input_len - pgl_wasix_input_off; + size_t to_copy = available < max_length ? available : max_length; + memcpy(buffer, pgl_wasix_input_buf + pgl_wasix_input_off, to_copy); + pgl_wasix_input_off += to_copy; + return (ssize_t) to_copy; +} + +static int +pgl_wasix_flush_output_to_stdio(void) +{ + size_t off = 0; + while (off < pgl_wasix_output_len_value) + { + ssize_t written = write(STDOUT_FILENO, + pgl_wasix_output_buf + off, + pgl_wasix_output_len_value - off); + if (written < 0) + return -1; + if (written == 0) + { + errno = EIO; + return -1; + } + off += (size_t) written; + } + pgl_wasix_output_len_value = 0; + return 0; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_wasix_output_reset(void) +{ + pgl_wasix_output_len_value = 0; + pgl_wasix_protocol_copy_state = PGL_WASIX_PROTOCOL_COPY_NONE; + pgl_wasix_protocol_stream_requested = false; + return 0; +} + +size_t EMSCRIPTEN_KEEPALIVE +pgl_wasix_output_len(void) +{ + return pgl_wasix_output_len_value; +} + +size_t EMSCRIPTEN_KEEPALIVE +pgl_wasix_output_read(void *buffer, size_t max_length) +{ + if (buffer == NULL || max_length == 0 || pgl_wasix_output_len_value == 0) + return 0; + + size_t to_copy = pgl_wasix_output_len_value < max_length + ? pgl_wasix_output_len_value + : max_length; + memcpy(buffer, pgl_wasix_output_buf, to_copy); + return to_copy; +} + +static ssize_t +pgl_wasix_buffer_write(const void *buffer, size_t length) +{ + if (length == 0) + return 0; + if (buffer == NULL) + { + errno = EINVAL; + return -1; + } + + size_t required = pgl_wasix_output_len_value + length; + if (required > pgl_wasix_output_cap) + { + size_t next_cap = pgl_wasix_output_cap ? pgl_wasix_output_cap : 8192; + while (next_cap < required) + next_cap *= 2; + unsigned char *new_buf = realloc(pgl_wasix_output_buf, next_cap); + if (new_buf == NULL) + { + errno = ENOMEM; + return -1; + } + pgl_wasix_output_buf = new_buf; + pgl_wasix_output_cap = next_cap; + } + + memcpy(pgl_wasix_output_buf + pgl_wasix_output_len_value, buffer, length); + pgl_wasix_output_len_value += length; + if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_HYBRID && + pgl_wasix_protocol_stream_requested) + { + if (pgl_wasix_flush_output_to_stdio() != 0) + return -1; + pgl_wasix_protocol_stream_active = true; + pgl_wasix_protocol_stream_requested = false; + } + return (ssize_t) length; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_system(const char *command) +{ + (void) command; + errno = ENOSYS; + return -1; +} + +__attribute__((weak)) void EMSCRIPTEN_KEEPALIVE +pg_free(void *ptr) +{ + free(ptr); +} + +static char * +pgl_locale_file_path(void) +{ + const char *sysconfdir = getenv("PGSYSCONFDIR"); + if (sysconfdir == NULL || sysconfdir[0] == '\0') + { + errno = ENOENT; + return NULL; + } + if (access(sysconfdir, F_OK) != 0) + return NULL; + + const char *name = "/locale"; + size_t len = strlen(sysconfdir) + strlen(name) + 1; + char *path = malloc(len); + if (path == NULL) + return NULL; + + snprintf(path, len, "%s%s", sysconfdir, name); + return path; +} + +static FILE * +pgl_open_locale_pipe(const char *command, const char *mode) +{ + if (command == NULL || mode == NULL || strcmp(command, "locale -a") != 0 || + strcmp(mode, "r") != 0) + { + errno = ENOSYS; + return NULL; + } + + char *path = pgl_locale_file_path(); + if (path == NULL) + { + if (errno == 0) + errno = ENOMEM; + return NULL; + } + + if (access(path, F_OK) != 0) + { + FILE *file = fopen(path, "w"); + if (file != NULL) + { + const char *encoding = getenv("PGCLIENTENCODING"); + if (encoding == NULL || encoding[0] == '\0') + encoding = "UTF8"; + fprintf(file, "C\nC.%s\nPOSIX\n%s\n", encoding, encoding); + fclose(file); + } + } + + FILE *file = fopen(path, mode); + free(path); + return file; +} + +__attribute__((weak)) FILE *EMSCRIPTEN_KEEPALIVE +pgl_popen(const char *command, const char *mode) +{ + return pgl_open_locale_pipe(command, mode); +} + +__attribute__((weak)) int EMSCRIPTEN_KEEPALIVE +pgl_pclose(FILE *file) +{ + if (file == NULL) + { + errno = EINVAL; + return -1; + } + return fclose(file); +} + +uid_t EMSCRIPTEN_KEEPALIVE +pgl_geteuid(void) +{ + return PGLITE_UID; +} + +uid_t EMSCRIPTEN_KEEPALIVE +pgl_getuid(void) +{ + return PGLITE_UID; +} + +struct passwd *EMSCRIPTEN_KEEPALIVE +pgl_getpwuid(uid_t uid) +{ + if (uid != PGLITE_UID) + { + errno = ENOENT; + return NULL; + } + + static struct passwd pw; + static char name[] = "postgres"; + static char passwd[] = "x"; + static char gecos[] = "Static User"; + static char dir[] = "/home/postgres"; + static char shell[] = "/bin/sh"; + + pw.pw_name = name; + pw.pw_passwd = passwd; + pw.pw_uid = uid; + pw.pw_gid = uid; + pw.pw_gecos = gecos; + pw.pw_dir = dir; + pw.pw_shell = shell; + + return &pw; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_atexit(void (*function)(void)) +{ + if (atexit_func_count >= MAX_ATEXIT_FUNCS) + return -1; + atexit_funcs[atexit_func_count++] = function; + return 0; +} + +void EMSCRIPTEN_KEEPALIVE +pgl_run_atexit_funcs(void) +{ + for (int i = atexit_func_count - 1; i >= 0; i--) + { + if (atexit_funcs[i]) + atexit_funcs[i](); + } + atexit_func_count = 0; +} + +static void +pgl_clear_interval_timer(void) +{ + struct itimerval zero = {{0, 0}, {0, 0}}; + (void) setitimer(ITIMER_REAL, &zero, NULL); +} + +void EMSCRIPTEN_KEEPALIVE +pgl_exit(int status) +{ + pgl_clear_interval_timer(); + optind = 1; + if (pglite_wasix_startup_error_capture_active && status != 0) + { + pglite_wasix_startup_error_capture_active = 0; + __builtin_trap(); + } + exit(status); +} + +int EMSCRIPTEN_KEEPALIVE +pgl_munmap(void *addr, size_t length) +{ + if (addr == NULL || length == 0) + { + errno = EINVAL; + return -1; + } + return munmap(addr, length); +} + +int EMSCRIPTEN_KEEPALIVE +pgl_fcntl(int fd, int cmd, ...) +{ + va_list args; + long arg = 0; + + switch (cmd) + { +#ifdef F_GETFL + case F_GETFL: + if (fd == PGLITE_PROTOCOL_FD) + return 0; + return fcntl(fd, cmd); +#endif +#ifdef F_GETFD + case F_GETFD: + if (fd == PGLITE_PROTOCOL_FD) + return 0; + return fcntl(fd, cmd); +#endif +#ifdef F_SETFL + case F_SETFL: + va_start(args, cmd); + arg = va_arg(args, long); + va_end(args); + if (fd == PGLITE_PROTOCOL_FD) + { +#ifdef O_NONBLOCK + if ((arg & ~((long) O_NONBLOCK)) == 0) + return 0; +#else + if (arg == 0) + return 0; +#endif + errno = EINVAL; + return -1; + } + return fcntl(fd, cmd, (int) arg); +#endif +#ifdef F_SETFD + case F_SETFD: + va_start(args, cmd); + arg = va_arg(args, long); + va_end(args); + if (fd == PGLITE_PROTOCOL_FD) + { +#ifdef FD_CLOEXEC + if ((arg & ~((long) FD_CLOEXEC)) == 0) + return 0; +#else + if (arg == 0) + return 0; +#endif + errno = EINVAL; + return -1; + } + return fcntl(fd, cmd, (int) arg); +#endif + default: + errno = EINVAL; + return -1; + } +} + +static int +pgl_write_int_sockopt(void *optval, socklen_t *optlen, int value) +{ + if (optval == NULL || optlen == NULL || *optlen < (socklen_t) sizeof(int)) + { + errno = EINVAL; + return -1; + } + memcpy(optval, &value, sizeof(value)); + *optlen = (socklen_t) sizeof(value); + return 0; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen) +{ + if (fd != PGLITE_PROTOCOL_FD) + return setsockopt(fd, level, optname, optval, optlen); + + if (optval == NULL && optlen != 0) + { + errno = EINVAL; + return -1; + } + + if (level == SOL_SOCKET) + { + switch (optname) + { +#ifdef SO_KEEPALIVE + case SO_KEEPALIVE: +#endif +#ifdef SO_REUSEADDR + case SO_REUSEADDR: +#endif +#ifdef SO_SNDBUF + case SO_SNDBUF: +#endif +#ifdef SO_RCVBUF + case SO_RCVBUF: +#endif +#ifdef SO_NOSIGPIPE + case SO_NOSIGPIPE: +#endif + return 0; + default: + break; + } + } + + if (level == IPPROTO_TCP) + { + switch (optname) + { +#ifdef TCP_NODELAY + case TCP_NODELAY: +#endif +#ifdef TCP_KEEPIDLE + case TCP_KEEPIDLE: +#endif +#ifdef TCP_KEEPINTVL + case TCP_KEEPINTVL: +#endif +#ifdef TCP_KEEPCNT + case TCP_KEEPCNT: +#endif +#ifdef TCP_USER_TIMEOUT + case TCP_USER_TIMEOUT: +#endif + return 0; + default: + break; + } + } + + errno = ENOPROTOOPT; + return -1; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_getsockopt(int fd, int level, int optname, void *optval, socklen_t *optlen) +{ + if (fd != PGLITE_PROTOCOL_FD) + return getsockopt(fd, level, optname, optval, optlen); + + if (level == SOL_SOCKET) + { + switch (optname) + { +#ifdef SO_ERROR + case SO_ERROR: + return pgl_write_int_sockopt(optval, optlen, 0); +#endif +#ifdef SO_TYPE + case SO_TYPE: + return pgl_write_int_sockopt(optval, optlen, SOCK_STREAM); +#endif +#ifdef SO_SNDBUF + case SO_SNDBUF: + return pgl_write_int_sockopt(optval, optlen, 32768); +#endif +#ifdef SO_RCVBUF + case SO_RCVBUF: + return pgl_write_int_sockopt(optval, optlen, 32768); +#endif + default: + break; + } + } + + if (level == IPPROTO_TCP) + { + switch (optname) + { +#ifdef TCP_KEEPIDLE + case TCP_KEEPIDLE: +#endif +#ifdef TCP_KEEPINTVL + case TCP_KEEPINTVL: +#endif +#ifdef TCP_KEEPCNT + case TCP_KEEPCNT: +#endif +#ifdef TCP_USER_TIMEOUT + case TCP_USER_TIMEOUT: +#endif + return pgl_write_int_sockopt(optval, optlen, 0); + default: + break; + } + } + + errno = ENOPROTOOPT; + return -1; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_getsockname(int fd, struct sockaddr *addr, socklen_t *len) +{ + if (fd != PGLITE_PROTOCOL_FD) + return getsockname(fd, addr, len); + + if (addr == NULL || len == NULL || *len < (socklen_t) sizeof(sa_family_t)) + { + errno = EINVAL; + return -1; + } + + memset(addr, 0, *len); + addr->sa_family = AF_UNIX; + *len = (socklen_t) sizeof(sa_family_t); + return 0; +} + +ssize_t EMSCRIPTEN_KEEPALIVE +pgl_recv(int fd, void *buf, size_t n, int flags) +{ + if (fd != PGLITE_PROTOCOL_FD) + return recv(fd, buf, n, flags); + if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_STREAM || + pgl_wasix_protocol_stream_active) + { + (void) flags; + return read(STDIN_FILENO, buf, n); + } + return pgl_wasix_buffer_read(buf, n); +} + +ssize_t EMSCRIPTEN_KEEPALIVE +pgl_send(int fd, const void *buf, size_t n, int flags) +{ + if (fd != PGLITE_PROTOCOL_FD) + return send(fd, buf, n, flags); + if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_STREAM || + pgl_wasix_protocol_stream_active) + { + (void) flags; + return write(STDOUT_FILENO, buf, n); + } + return pgl_wasix_buffer_write(buf, n); +} + +int EMSCRIPTEN_KEEPALIVE +pgl_connect(int socket, const struct sockaddr *address, socklen_t address_len) +{ + if (socket != PGLITE_PROTOCOL_FD) + return connect(socket, address, address_len); + errno = ENOSYS; + return -1; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_poll(struct pollfd fds[], nfds_t nfds, int timeout) +{ + bool has_protocol_fd = false; + int ready = 0; + + for (nfds_t i = 0; i < nfds; i++) + { + if (fds[i].fd == PGLITE_PROTOCOL_FD) + { + has_protocol_fd = true; + break; + } + } + + if (!has_protocol_fd) + return poll(fds, nfds, timeout); + + for (nfds_t i = 0; i < nfds; i++) + { + fds[i].revents = 0; + if (fds[i].fd != PGLITE_PROTOCOL_FD) + { + struct pollfd one = fds[i]; + int rc = poll(&one, 1, 0); + if (rc < 0) + return rc; + fds[i].revents = one.revents; + if (rc > 0) + ready++; + continue; + } + if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_STREAM || + pgl_wasix_protocol_stream_active) + { + struct pollfd one; + int rc; + + one.fd = STDIN_FILENO; + one.events = fds[i].events; + one.revents = 0; + rc = poll(&one, 1, 0); + if (rc < 0) + return rc; + fds[i].revents = one.revents; + if (rc > 0) + ready++; + continue; + } +#ifdef POLLIN + if ((fds[i].events & POLLIN) && + pgl_wasix_input_available() > 0) + fds[i].revents |= POLLIN; +#endif +#ifdef POLLOUT + if (fds[i].events & POLLOUT) + fds[i].revents |= POLLOUT; +#endif + if (fds[i].revents) + ready++; + } + return ready; +} + +typedef struct WasixShmSegment +{ + int shmid; + key_t key; + size_t size; + void *addr; + unsigned long nattch; + struct WasixShmSegment *next; +} WasixShmSegment; + +static WasixShmSegment *wasix_shm_list; +static int wasix_next_shmid = 1; + +static WasixShmSegment * +find_by_key(key_t key) +{ + for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) + { + if (seg->key == key) + return seg; + } + return NULL; +} + +static WasixShmSegment * +find_by_id(int shmid) +{ + for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) + { + if (seg->shmid == shmid) + return seg; + } + return NULL; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_shmget(key_t key, size_t size, int shmflg) +{ + WasixShmSegment *existing = find_by_key(key); + + if (existing) + { + if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) + { + errno = EEXIST; + return -1; + } + return existing->shmid; + } + + if ((shmflg & IPC_CREAT) == 0) + { + errno = ENOENT; + return -1; + } + + size_t alloc_size = size ? size : 1; + long pagesize = sysconf(_SC_PAGESIZE); + if (pagesize > 0) + { + size_t page = (size_t) pagesize; + alloc_size = ((alloc_size + page - 1) / page) * page; + } + + void *addr = calloc(1, alloc_size); + if (!addr) + { + errno = ENOMEM; + return -1; + } + + WasixShmSegment *seg = calloc(1, sizeof(*seg)); + if (!seg) + { + free(addr); + errno = ENOMEM; + return -1; + } + + seg->shmid = wasix_next_shmid++; + seg->key = key; + seg->size = size; + seg->addr = addr; + seg->next = wasix_shm_list; + wasix_shm_list = seg; + + return seg->shmid; +} + +void *EMSCRIPTEN_KEEPALIVE +pgl_shmat(int shmid, const void *shmaddr, int shmflg) +{ + (void) shmaddr; + (void) shmflg; + + WasixShmSegment *seg = find_by_id(shmid); + if (!seg) + { + errno = EINVAL; + return (void *) -1; + } + + seg->nattch++; + return seg->addr; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_shmdt(const void *shmaddr) +{ + for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) + { + if (seg->addr == shmaddr) + { + if (seg->nattch > 0) + seg->nattch--; + return 0; + } + } + + errno = EINVAL; + return -1; +} + +int EMSCRIPTEN_KEEPALIVE +pgl_shmctl(int shmid, int cmd, struct shmid_ds *buf) +{ + WasixShmSegment *prev = NULL; + WasixShmSegment *seg = wasix_shm_list; + + while (seg && seg->shmid != shmid) + { + prev = seg; + seg = seg->next; + } + + if (!seg) + { + errno = EINVAL; + return -1; + } + + switch (cmd) + { + case IPC_RMID: + if (prev) + prev->next = seg->next; + else + wasix_shm_list = seg->next; + free(seg->addr); + free(seg); + return 0; + + case IPC_STAT: + if (!buf) + { + errno = EINVAL; + return -1; + } + memset(buf, 0, sizeof(*buf)); +#if defined(__APPLE__) + buf->shm_perm._key = seg->key; +#else + buf->shm_perm.__key = seg->key; +#endif + buf->shm_segsz = seg->size; + buf->shm_nattch = seg->nattch; + buf->shm_atime = buf->shm_dtime = buf->shm_ctime = time(NULL); + return 0; + + case IPC_SET: + if (!buf) + { + errno = EINVAL; + return -1; + } + seg->size = buf->shm_segsz; + return 0; + + default: + errno = EINVAL; + return -1; + } +} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_bridge_abi_test.c b/assets/wasix-build/wasix_shim/pglite_wasix_bridge_abi_test.c new file mode 100644 index 00000000..742e4bc1 --- /dev/null +++ b/assets/wasix-build/wasix_shim/pglite_wasix_bridge_abi_test.c @@ -0,0 +1,330 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#ifndef _DARWIN_C_SOURCE +#define _DARWIN_C_SOURCE +#endif +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "bridge ABI check failed at %s:%d: %s\n", __FILE__, __LINE__, \ + #condition); \ + return 1; \ + } \ + } while (0) + +FILE *pgl_popen(const char *command, const char *mode); +int pgl_system(const char *command); +int pgl_set_force_host_error_recovery(int new_value); +int pgl_setPGliteActive(int new_value); +int pgl_atexit(void (*function)(void)); +void pgl_run_atexit_funcs(void); +uid_t pgl_geteuid(void); +uid_t pgl_getuid(void); +struct passwd *pgl_getpwuid(uid_t uid); +int pgl_wasix_input_reset(void); +int pgl_wasix_input_write(const void *buffer, size_t length); +size_t pgl_wasix_input_available(void); +int pgl_wasix_output_reset(void); +size_t pgl_wasix_output_len(void); +size_t pgl_wasix_output_read(void *buffer, size_t max_length); +int pgl_fcntl(int fd, int cmd, ...); +int pgl_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen); +int pgl_getsockopt(int fd, int level, int optname, void *optval, socklen_t *optlen); +int pgl_getsockname(int fd, struct sockaddr *addr, socklen_t *len); +int pgl_set_protocol_stdio(int enabled); +int pgl_set_protocol_transport(int mode); +int pgl_protocol_stream_active(void); +void pgl_protocol_report_copy_response(int state); +int pgl_protocol_copy_state(void); +ssize_t pgl_recv(int fd, void *buf, size_t n, int flags); +ssize_t pgl_send(int fd, const void *buf, size_t n, int flags); +int pgl_connect(int socket, const struct sockaddr *address, socklen_t address_len); +int pgl_poll(struct pollfd fds[], nfds_t nfds, int timeout); +int pgl_munmap(void *addr, size_t length); +int pgl_shmget(key_t key, size_t size, int shmflg); +void *pgl_shmat(int shmid, const void *shmaddr, int shmflg); +int pgl_shmdt(const void *shmaddr); +int pgl_shmctl(int shmid, int cmd, struct shmid_ds *buf); + +int +pg_char_to_encoding_private(const char *name) +{ + return strcmp(name, "UTF8") == 0 ? 6 : -1; +} + +const char * +pg_encoding_to_char_private(int encoding) +{ + return encoding == 6 ? "UTF8" : ""; +} + +static int atexit_counter; + +static void +increment_atexit_counter(void) +{ + atexit_counter++; +} + +static int +check_locale_pipe(void) +{ + char temp_template[] = "/tmp/pglite-bridge-abi-XXXXXX"; + char *dir = mkdtemp(temp_template); + CHECK(dir != NULL); + CHECK(setenv("PGSYSCONFDIR", dir, 1) == 0); + CHECK(setenv("PGCLIENTENCODING", "UTF8", 1) == 0); + + errno = 0; + CHECK(pgl_popen("uname -a", "r") == NULL); + CHECK(errno == ENOSYS); + errno = 0; + CHECK(pgl_popen("locale -a", "w") == NULL); + CHECK(errno == ENOSYS); + + FILE *file = pgl_popen("locale -a", "r"); + CHECK(file != NULL); + char contents[128] = {0}; + size_t read_len = fread(contents, 1, sizeof(contents) - 1, file); + CHECK(fclose(file) == 0); + CHECK(read_len > 0); + CHECK(strstr(contents, "C\n") != NULL); + CHECK(strstr(contents, "C.UTF8\n") != NULL); + CHECK(strstr(contents, "POSIX\n") != NULL); + CHECK(unsetenv("PGSYSCONFDIR") == 0); + errno = 0; + CHECK(pgl_popen("locale -a", "r") == NULL); + CHECK(errno == ENOENT); + return 0; +} + +static int +check_identity_and_fail_closed_calls(void) +{ + CHECK(pgl_geteuid() == 123); + CHECK(pgl_getuid() == 123); + struct passwd *pw = pgl_getpwuid(123); + CHECK(pw != NULL); + CHECK(strcmp(pw->pw_name, "postgres") == 0); + CHECK(pw->pw_uid == 123); + errno = 0; + CHECK(pgl_getpwuid(999) == NULL); + CHECK(errno == ENOENT); + + errno = 0; + CHECK(pgl_system("echo unsafe") == -1); + CHECK(errno == ENOSYS); + + CHECK(pgl_set_force_host_error_recovery(1) == 0); + CHECK(pgl_set_force_host_error_recovery(0) == 1); + CHECK(pgl_setPGliteActive(1) == 0); + CHECK(pgl_setPGliteActive(0) == 1); + CHECK(pgl_atexit(increment_atexit_counter) == 0); + CHECK(pgl_atexit(increment_atexit_counter) == 0); + pgl_run_atexit_funcs(); + CHECK(atexit_counter == 2); + pgl_run_atexit_funcs(); + CHECK(atexit_counter == 2); + + errno = 0; + CHECK(pgl_connect(1, NULL, 0) == -1); + CHECK(errno == ENOSYS); + errno = 0; + CHECK(pgl_connect(-1, NULL, 0) == -1); + CHECK(errno == EBADF); + return 0; +} + +static int +check_protocol_socket(void) +{ + char buf[8] = {0}; + const char input[] = "abc"; + const char output[] = "xyz"; + + CHECK(pgl_wasix_input_reset() == 0); + CHECK(pgl_wasix_output_reset() == 0); + CHECK(pgl_recv(1, buf, sizeof(buf), 0) == 0); + CHECK(pgl_wasix_input_write(input, sizeof(input) - 1) == (int) (sizeof(input) - 1)); + CHECK(pgl_wasix_input_available() == sizeof(input) - 1); + CHECK(pgl_recv(1, buf, 2, 0) == 2); + CHECK(memcmp(buf, "ab", 2) == 0); + CHECK(pgl_wasix_input_available() == 1); + + CHECK(pgl_send(1, output, sizeof(output) - 1, 0) == (ssize_t) (sizeof(output) - 1)); + CHECK(pgl_wasix_output_len() == sizeof(output) - 1); + memset(buf, 0, sizeof(buf)); + CHECK(pgl_wasix_output_read(buf, sizeof(buf)) == sizeof(output) - 1); + CHECK(memcmp(buf, output, sizeof(output) - 1) == 0); + + CHECK(pgl_set_protocol_stdio(0) == 0); + CHECK(pgl_protocol_stream_active() == 0); + CHECK(pgl_set_protocol_stdio(1) == 0); + CHECK(pgl_protocol_stream_active() == 1); + CHECK(pgl_set_protocol_stdio(0) == 1); + CHECK(pgl_protocol_stream_active() == 0); + CHECK(pgl_set_protocol_transport(2) == 0); + CHECK(pgl_protocol_stream_active() == 0); + CHECK(pgl_protocol_copy_state() == 0); + pgl_protocol_report_copy_response(1); + CHECK(pgl_protocol_copy_state() == 1); + CHECK(pgl_send(1, output, sizeof(output) - 1, 0) == (ssize_t) (sizeof(output) - 1)); + CHECK(pgl_protocol_stream_active() == 1); + CHECK(pgl_set_protocol_transport(0) == 2); + CHECK(pgl_protocol_stream_active() == 0); + CHECK(pgl_protocol_copy_state() == 0); + CHECK(pgl_set_protocol_transport(2) == 0); + pgl_protocol_report_copy_response(0); + CHECK(pgl_protocol_copy_state() == 0); + CHECK(pgl_set_protocol_transport(0) == 2); + errno = 0; + CHECK(pgl_set_protocol_transport(99) == -1); + CHECK(errno == EINVAL); + +#ifdef ENOTSOCK + errno = 0; + CHECK(pgl_recv(2, buf, sizeof(buf), 0) == -1); + CHECK(errno == ENOTSOCK); + errno = 0; + CHECK(pgl_send(2, output, sizeof(output) - 1, 0) == -1); + CHECK(errno == ENOTSOCK); +#endif + + CHECK(pgl_fcntl(1, F_GETFL) == 0); + CHECK(pgl_fcntl(1, F_SETFL, O_NONBLOCK) == 0); +#ifdef O_APPEND + errno = 0; + CHECK(pgl_fcntl(1, F_SETFL, O_APPEND) == -1); + CHECK(errno == EINVAL); +#endif + + int opt = 1; + CHECK(pgl_setsockopt(1, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt)) == 0); +#ifdef TCP_NODELAY + CHECK(pgl_setsockopt(1, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) == 0); +#endif + errno = 0; + CHECK(pgl_setsockopt(1, SOL_SOCKET, 0x7ffffffe, &opt, sizeof(opt)) == -1); + CHECK(errno == ENOPROTOOPT); + + opt = 0; + socklen_t optlen = sizeof(opt); + CHECK(pgl_getsockopt(1, SOL_SOCKET, SO_TYPE, &opt, &optlen) == 0); + CHECK(opt == SOCK_STREAM); + CHECK(optlen == (socklen_t) sizeof(opt)); + errno = 0; + optlen = sizeof(opt); + CHECK(pgl_getsockopt(1, SOL_SOCKET, 0x7ffffffd, &opt, &optlen) == -1); + CHECK(errno == ENOPROTOOPT); + + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + CHECK(pgl_getsockname(1, (struct sockaddr *) &addr, &addrlen) == 0); + CHECK(addr.ss_family == AF_UNIX); + + CHECK(pgl_wasix_input_reset() == 0); + struct pollfd fds[1] = {{.fd = 1, .events = POLLIN, .revents = 0}}; + CHECK(pgl_poll(fds, 1, 0) == 0); + CHECK(fds[0].revents == 0); + CHECK(pgl_wasix_input_write("q", 1) == 1); + CHECK(pgl_poll(fds, 1, 0) == 1); + CHECK((fds[0].revents & POLLIN) != 0); + + struct pollfd ignored[1] = {{.fd = -1, .events = POLLIN, .revents = 0}}; + CHECK(pgl_poll(ignored, 1, 0) == 0); + struct pollfd mixed[2] = { + {.fd = 1, .events = POLLOUT, .revents = 0}, + {.fd = 99, .events = POLLIN, .revents = 0}, + }; + CHECK(pgl_poll(mixed, 2, 0) == 2); + CHECK((mixed[0].revents & POLLOUT) != 0); +#ifdef POLLNVAL + CHECK((mixed[1].revents & POLLNVAL) != 0); +#endif + return 0; +} + +static int +check_memory_and_shared_memory(void) +{ + errno = 0; + CHECK(pgl_munmap(NULL, 0) == -1); + CHECK(errno == EINVAL); + +#if defined(MAP_ANON) + int anon_flag = MAP_ANON; +#elif defined(MAP_ANONYMOUS) + int anon_flag = MAP_ANONYMOUS; +#else + int anon_flag = 0; +#endif + if (anon_flag != 0) + { + void *mapping = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | anon_flag, -1, 0); + CHECK(mapping != MAP_FAILED); + CHECK(pgl_munmap(mapping, 4096) == 0); + } + + key_t key = 4242; + int shmid = pgl_shmget(key, 64, IPC_CREAT | IPC_EXCL); + CHECK(shmid > 0); + errno = 0; + CHECK(pgl_shmget(key, 64, IPC_CREAT | IPC_EXCL) == -1); + CHECK(errno == EEXIST); + errno = 0; + CHECK(pgl_shmget(key + 1, 64, 0) == -1); + CHECK(errno == ENOENT); + + void *addr = pgl_shmat(shmid, NULL, 0); + CHECK(addr != (void *) -1); + memset(addr, 0x7b, 64); + + struct shmid_ds statbuf; + CHECK(pgl_shmctl(shmid, IPC_STAT, &statbuf) == 0); + CHECK(statbuf.shm_segsz == 64); + CHECK(statbuf.shm_nattch == 1); + CHECK(pgl_shmdt(addr) == 0); + CHECK(pgl_shmctl(shmid, IPC_RMID, NULL) == 0); + errno = 0; + CHECK(pgl_shmat(shmid, NULL, 0) == (void *) -1); + CHECK(errno == EINVAL); + return 0; +} + +int +main(void) +{ + CHECK(check_locale_pipe() == 0); + CHECK(check_identity_and_fail_closed_calls() == 0); + CHECK(check_protocol_socket() == 0); + CHECK(check_memory_and_shared_memory() == 0); + return 0; +} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim.c b/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim.c new file mode 100644 index 00000000..84bbf059 --- /dev/null +++ b/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim.c @@ -0,0 +1,679 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#ifndef _DARWIN_C_SOURCE +#define _DARWIN_C_SOURCE +#endif +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__has_include) +#if __has_include("pg_config.h") +#include "pg_config.h" +#endif +#endif + +#ifndef PG_VERSION +#define PG_VERSION "unknown" +#endif + +#define PGLITE_UID 123 + +extern char **environ; +extern int pg_char_to_encoding_private(const char *name); +extern const char *pg_encoding_to_char_private(int encoding); + +typedef struct LocalePipe +{ + FILE *file; + struct LocalePipe *next; +} LocalePipe; + +typedef struct ChildPipe +{ + FILE *file; + pid_t pid; + struct ChildPipe *next; +} ChildPipe; + +typedef struct CommandSpec +{ + char **argv; + int argc; + char *stdin_path; + char *stdout_path; + bool stderr_to_stdout; +} CommandSpec; + +static LocalePipe *locale_pipes; +static ChildPipe *child_pipes; + +static void +free_command_spec(CommandSpec *spec) +{ + if (spec == NULL) + return; + if (spec->argv) + { + for (int i = 0; i < spec->argc; i++) + free(spec->argv[i]); + free(spec->argv); + } + free(spec->stdin_path); + free(spec->stdout_path); + memset(spec, 0, sizeof(*spec)); +} + +static char * +read_command_token(const char **cursor) +{ + const char *p = *cursor; + while (*p == ' ' || *p == '\t' || *p == '\n') + p++; + if (*p == '\0') + { + *cursor = p; + return NULL; + } + + char quote = 0; + if (*p == '\'' || *p == '"') + quote = *p++; + + size_t capacity = strlen(p) + 1; + char *token = malloc(capacity); + if (token == NULL) + return NULL; + size_t len = 0; + while (*p) + { + if (quote) + { + if (*p == quote) + { + p++; + break; + } + } + else if (*p == ' ' || *p == '\t' || *p == '\n') + { + break; + } + + if (*p == '\\' && p[1] != '\0') + p++; + token[len++] = *p++; + } + token[len] = '\0'; + while (*p == ' ' || *p == '\t' || *p == '\n') + p++; + *cursor = p; + return token; +} + +static bool +append_command_arg(CommandSpec *spec, char *arg) +{ + char **next = realloc(spec->argv, sizeof(char *) * (spec->argc + 2)); + if (next == NULL) + { + free(arg); + return false; + } + spec->argv = next; + spec->argv[spec->argc++] = arg; + spec->argv[spec->argc] = NULL; + return true; +} + +static bool +set_redirect_path(char **slot, char *path) +{ + if (path == NULL || path[0] == '\0') + { + free(path); + errno = EINVAL; + return false; + } + free(*slot); + *slot = path; + return true; +} + +static bool +parse_command(const char *command, CommandSpec *spec) +{ + memset(spec, 0, sizeof(*spec)); + const char *cursor = command; + for (;;) + { + char *token = read_command_token(&cursor); + if (token == NULL) + break; + + if (strcmp(token, "2>&1") == 0) + { + spec->stderr_to_stdout = true; + free(token); + continue; + } + if (strcmp(token, "<") == 0 || strcmp(token, ">") == 0) + { + bool input = token[0] == '<'; + free(token); + char *path = read_command_token(&cursor); + if (!set_redirect_path(input ? &spec->stdin_path : &spec->stdout_path, path)) + goto fail; + continue; + } + if ((token[0] == '<' || token[0] == '>') && token[1] != '\0') + { + bool input = token[0] == '<'; + char *path = strdup(token + 1); + free(token); + if (!set_redirect_path(input ? &spec->stdin_path : &spec->stdout_path, path)) + goto fail; + continue; + } + if (!append_command_arg(spec, token)) + { + errno = ENOMEM; + goto fail; + } + } + if (spec->argc == 0) + { + errno = EINVAL; + goto fail; + } + return true; + +fail: + free_command_spec(spec); + return false; +} + +static const char * +base_name(const char *path) +{ + const char *slash = strrchr(path, '/'); + return slash ? slash + 1 : path; +} + +static bool +is_postgres_command(const CommandSpec *spec) +{ + if (spec->argc == 0 || spec->argv == NULL || spec->argv[0] == NULL) + return false; + const char *name = base_name(spec->argv[0]); + return strcmp(name, "postgres") == 0 || strcmp(name, "pglite") == 0; +} + +static int +open_redirect(const char *path, int flags) +{ + return open(path, flags, 0600); +} + +static bool +add_redirect_action(posix_spawn_file_actions_t *actions, int fd, int target) +{ + if (posix_spawn_file_actions_adddup2(actions, fd, target) != 0) + return false; + if (posix_spawn_file_actions_addclose(actions, fd) != 0) + return false; + return true; +} + +static int +spawn_postgres_command(const CommandSpec *spec, int stdin_fd, int stdout_fd, pid_t *pid) +{ + posix_spawn_file_actions_t actions; + int rc = posix_spawn_file_actions_init(&actions); + if (rc != 0) + { + errno = rc; + return -1; + } + + int opened_stdin = -1; + int opened_stdout = -1; + if ((stdin_fd >= 0 && spec->stdin_path != NULL) || + (stdout_fd >= 0 && spec->stdout_path != NULL)) + { + errno = EINVAL; + goto fail; + } + if (stdin_fd >= 0 && !add_redirect_action(&actions, stdin_fd, STDIN_FILENO)) + goto fail; + if (stdout_fd >= 0 && !add_redirect_action(&actions, stdout_fd, STDOUT_FILENO)) + goto fail; + if (spec->stdin_path != NULL) + { + opened_stdin = open_redirect(spec->stdin_path, O_RDONLY); + if (opened_stdin < 0 || !add_redirect_action(&actions, opened_stdin, STDIN_FILENO)) + goto fail; + } + if (spec->stdout_path != NULL) + { + opened_stdout = open_redirect(spec->stdout_path, O_WRONLY | O_CREAT | O_TRUNC); + if (opened_stdout < 0 || !add_redirect_action(&actions, opened_stdout, STDOUT_FILENO)) + goto fail; + } + if (spec->stderr_to_stdout && + posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO) != 0) + goto fail; + + rc = posix_spawnp(pid, spec->argv[0], &actions, NULL, spec->argv, environ); + posix_spawn_file_actions_destroy(&actions); + if (opened_stdin >= 0) + close(opened_stdin); + if (opened_stdout >= 0) + close(opened_stdout); + if (rc != 0) + { + errno = rc; + return -1; + } + return 0; + +fail: + rc = errno ? errno : EINVAL; + posix_spawn_file_actions_destroy(&actions); + if (opened_stdin >= 0) + close(opened_stdin); + if (opened_stdout >= 0) + close(opened_stdout); + errno = rc; + return -1; +} + +static int +wait_child_status(pid_t pid) +{ + int status = 0; + while (waitpid(pid, &status, 0) < 0) + { + if (errno != EINTR) + return -1; + } + return status; +} + +static bool +remember_child_pipe(FILE *file, pid_t pid) +{ + ChildPipe *pipe = calloc(1, sizeof(*pipe)); + if (pipe == NULL) + return false; + pipe->file = file; + pipe->pid = pid; + pipe->next = child_pipes; + child_pipes = pipe; + return true; +} + +static bool +forget_child_pipe(FILE *file, pid_t *pid) +{ + ChildPipe *previous = NULL; + ChildPipe *pipe = child_pipes; + + while (pipe) + { + if (pipe->file == file) + { + if (previous) + previous->next = pipe->next; + else + child_pipes = pipe->next; + *pid = pipe->pid; + free(pipe); + return true; + } + previous = pipe; + pipe = pipe->next; + } + return false; +} + +static bool +remember_static_pipe(FILE *file) +{ + LocalePipe *pipe = calloc(1, sizeof(*pipe)); + if (pipe == NULL) + return false; + pipe->file = file; + pipe->next = locale_pipes; + locale_pipes = pipe; + return true; +} + +static bool +forget_static_pipe(FILE *file) +{ + LocalePipe *previous = NULL; + LocalePipe *pipe = locale_pipes; + + while (pipe) + { + if (pipe->file == file) + { + if (previous) + previous->next = pipe->next; + else + locale_pipes = pipe->next; + free(pipe); + return true; + } + previous = pipe; + pipe = pipe->next; + } + return false; +} + +static FILE * +open_locale_pipe(const char *command, const char *mode) +{ + if (command == NULL || mode == NULL || strcmp(command, "locale -a") != 0 || + strcmp(mode, "r") != 0) + { + errno = ENOSYS; + return NULL; + } + + FILE *file = tmpfile(); + if (file == NULL) + return NULL; + + const char *encoding = getenv("PGCLIENTENCODING"); + if (encoding == NULL || encoding[0] == '\0') + encoding = "UTF8"; + fprintf(file, "C\nC.%s\nPOSIX\n%s\n", encoding, encoding); + rewind(file); + + if (!remember_static_pipe(file)) + { + fclose(file); + errno = ENOMEM; + return NULL; + } + return file; +} + +static FILE * +open_postgres_read_pipe(const char *command, const char *mode) +{ + if (command == NULL || mode == NULL || strcmp(mode, "r") != 0) + { + errno = ENOSYS; + return NULL; + } + + CommandSpec spec; + if (!parse_command(command, &spec)) + return NULL; + if (!is_postgres_command(&spec)) + { + free_command_spec(&spec); + errno = ENOSYS; + return NULL; + } + + int fds[2]; + if (pipe(fds) != 0) + return NULL; + (void) fcntl(fds[0], F_SETFD, FD_CLOEXEC); + pid_t pid = -1; + if (spawn_postgres_command(&spec, -1, fds[1], &pid) != 0) + { + int saved = errno; + close(fds[0]); + close(fds[1]); + free_command_spec(&spec); + errno = saved; + return NULL; + } + close(fds[1]); + FILE *file = fdopen(fds[0], mode); + if (file == NULL) + { + int saved = errno; + close(fds[0]); + (void) wait_child_status(pid); + free_command_spec(&spec); + errno = saved; + return NULL; + } + if (!remember_child_pipe(file, pid)) + { + fclose(file); + (void) wait_child_status(pid); + free_command_spec(&spec); + errno = ENOMEM; + return NULL; + } + free_command_spec(&spec); + return file; +} + +int +pgl_initdb_system(const char *command) +{ + CommandSpec spec; + if (command == NULL || !parse_command(command, &spec)) + return -1; + if (!is_postgres_command(&spec)) + { + free_command_spec(&spec); + errno = ENOSYS; + return -1; + } + + pid_t pid = -1; + int rc = spawn_postgres_command(&spec, -1, -1, &pid); + free_command_spec(&spec); + if (rc != 0) + return -1; + return wait_child_status(pid); +} + +int +pgl_system(const char *command) +{ + return pgl_initdb_system(command); +} + +FILE * +pgl_initdb_popen(const char *command, const char *mode) +{ + FILE *locale = open_locale_pipe(command, mode); + if (locale != NULL) + return locale; + + if (errno != ENOSYS) + return NULL; + FILE *read_pipe = open_postgres_read_pipe(command, mode); + if (read_pipe != NULL) + return read_pipe; + if (errno != ENOSYS) + return NULL; + if (command == NULL || mode == NULL || strcmp(mode, "w") != 0) + { + errno = ENOSYS; + return NULL; + } + + CommandSpec spec; + if (!parse_command(command, &spec)) + return NULL; + if (!is_postgres_command(&spec)) + { + free_command_spec(&spec); + errno = ENOSYS; + return NULL; + } + + int fds[2]; + if (pipe(fds) != 0) + { + free_command_spec(&spec); + return NULL; + } + (void) fcntl(fds[1], F_SETFD, FD_CLOEXEC); + pid_t pid = -1; + if (spawn_postgres_command(&spec, fds[0], -1, &pid) != 0) + { + int saved = errno; + close(fds[0]); + close(fds[1]); + free_command_spec(&spec); + errno = saved; + return NULL; + } + close(fds[0]); + FILE *file = fdopen(fds[1], mode); + if (file == NULL) + { + int saved = errno; + close(fds[1]); + (void) wait_child_status(pid); + free_command_spec(&spec); + errno = saved; + return NULL; + } + if (!remember_child_pipe(file, pid)) + { + fclose(file); + (void) wait_child_status(pid); + free_command_spec(&spec); + errno = ENOMEM; + return NULL; + } + free_command_spec(&spec); + return file; +} + +FILE * +pgl_popen(const char *command, const char *mode) +{ + return pgl_initdb_popen(command, mode); +} + +int +pgl_initdb_pclose(FILE *file) +{ + if (file == NULL) + { + errno = EINVAL; + return -1; + } + if (forget_static_pipe(file)) + return fclose(file); + + pid_t pid = -1; + if (forget_child_pipe(file, &pid)) + { + int close_rc = fclose(file); + int status = wait_child_status(pid); + if (close_rc != 0) + return -1; + return status; + } + errno = EINVAL; + return -1; +} + +int +pgl_pclose(FILE *file) +{ + return pgl_initdb_pclose(file); +} + +int +__wrap_system(const char *command) +{ + return pgl_initdb_system(command); +} + +FILE * +__wrap_popen(const char *command, const char *mode) +{ + return pgl_initdb_popen(command, mode); +} + +int +__wrap_pclose(FILE *file) +{ + return pgl_initdb_pclose(file); +} + +int +pg_char_to_encoding(const char *name) +{ + return pg_char_to_encoding_private(name); +} + +const char * +pg_encoding_to_char(int encoding) +{ + return pg_encoding_to_char_private(encoding); +} + +uid_t +pgl_geteuid(void) +{ + return PGLITE_UID; +} + +uid_t +pgl_getuid(void) +{ + return PGLITE_UID; +} + +struct passwd * +pgl_getpwuid(uid_t uid) +{ + if (uid != PGLITE_UID) + { + errno = ENOENT; + return NULL; + } + + static struct passwd pw; + static char name[] = "postgres"; + static char passwd[] = "x"; + static char gecos[] = "Static User"; + static char dir[] = "/home/postgres"; + static char shell[] = "/bin/sh"; + + pw.pw_name = name; + pw.pw_passwd = passwd; + pw.pw_uid = uid; + pw.pw_gid = uid; + pw.pw_gecos = gecos; + pw.pw_dir = dir; + pw.pw_shell = shell; + + return &pw; +} + +void +pgl_exit(int status) +{ + exit(status); +} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim_abi_test.c b/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim_abi_test.c new file mode 100644 index 00000000..c3b8fd4c --- /dev/null +++ b/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim_abi_test.c @@ -0,0 +1,174 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#ifndef _DARWIN_C_SOURCE +#define _DARWIN_C_SOURCE +#endif +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "initdb shim ABI check failed at %s:%d: %s\n", __FILE__, __LINE__, \ + #condition); \ + return 1; \ + } \ + } while (0) + +FILE *pgl_initdb_popen(const char *command, const char *mode); +int pgl_initdb_pclose(FILE *file); +uid_t pgl_geteuid(void); +uid_t pgl_getuid(void); +struct passwd *pgl_getpwuid(uid_t uid); + +int +pg_char_to_encoding_private(const char *name) +{ + return strcmp(name, "UTF8") == 0 ? 6 : -1; +} + +const char * +pg_encoding_to_char_private(int encoding) +{ + return encoding == 6 ? "UTF8" : ""; +} + +static int +write_file(const char *path, const char *contents) +{ + FILE *file = fopen(path, "w"); + if (file == NULL) + return -1; + if (fputs(contents, file) < 0) + { + fclose(file); + return -1; + } + return fclose(file); +} + +static int +install_fake_postgres(char *dir) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/postgres", dir); + const char *script = + "#!/bin/sh\n" + "if [ \"$1\" = \"-V\" ] || [ \"$1\" = \"--version\" ]; then\n" + " echo 'postgres (PostgreSQL) fake-from-child'\n" + " exit 0\n" + "fi\n" + "cat > \"$PGLITE_INITDB_STDIN_CAPTURE\"\n"; + CHECK(write_file(path, script) == 0); + CHECK(chmod(path, 0700) == 0); + return 0; +} + +static int +prepend_path(const char *dir) +{ + const char *old_path = getenv("PATH"); + if (old_path == NULL) + old_path = ""; + size_t len = strlen(dir) + 1 + strlen(old_path) + 1; + char *next = malloc(len); + CHECK(next != NULL); + snprintf(next, len, "%s:%s", dir, old_path); + CHECK(setenv("PATH", next, 1) == 0); + free(next); + return 0; +} + +static int +check_locale_and_fail_closed(void) +{ + CHECK(setenv("PGCLIENTENCODING", "UTF8", 1) == 0); + errno = 0; + CHECK(pgl_initdb_popen("uname -a", "r") == NULL); + CHECK(errno == ENOSYS); + errno = 0; + CHECK(pgl_initdb_popen("locale -a", "w") == NULL); + CHECK(errno == ENOSYS); + + FILE *file = pgl_initdb_popen("locale -a", "r"); + CHECK(file != NULL); + char contents[128] = {0}; + size_t read_len = fread(contents, 1, sizeof(contents) - 1, file); + CHECK(pgl_initdb_pclose(file) == 0); + CHECK(read_len > 0); + CHECK(strstr(contents, "C\n") != NULL); + CHECK(strstr(contents, "C.UTF8\n") != NULL); + CHECK(strstr(contents, "POSIX\n") != NULL); + return 0; +} + +static int +check_postgres_read_and_write_pipes(char *dir) +{ + CHECK(install_fake_postgres(dir) == 0); + CHECK(prepend_path(dir) == 0); + + FILE *read_pipe = pgl_initdb_popen("postgres -V 2>&1", "r"); + CHECK(read_pipe != NULL); + char version[128] = {0}; + CHECK(fread(version, 1, sizeof(version) - 1, read_pipe) > 0); + CHECK(pgl_initdb_pclose(read_pipe) == 0); + CHECK(strstr(version, "fake-from-child") != NULL); + + char capture[512]; + snprintf(capture, sizeof(capture), "%s/stdin.txt", dir); + CHECK(setenv("PGLITE_INITDB_STDIN_CAPTURE", capture, 1) == 0); + FILE *write_pipe = pgl_initdb_popen("postgres --boot \"quoted arg\"", "w"); + CHECK(write_pipe != NULL); + CHECK(fputs("bootstrap input\n", write_pipe) >= 0); + CHECK(pgl_initdb_pclose(write_pipe) == 0); + + FILE *captured = fopen(capture, "r"); + CHECK(captured != NULL); + char captured_text[128] = {0}; + CHECK(fread(captured_text, 1, sizeof(captured_text) - 1, captured) > 0); + CHECK(fclose(captured) == 0); + CHECK(strstr(captured_text, "bootstrap input") != NULL); + return 0; +} + +static int +check_identity(void) +{ + CHECK(pgl_geteuid() == 123); + CHECK(pgl_getuid() == 123); + struct passwd *pw = pgl_getpwuid(123); + CHECK(pw != NULL); + CHECK(strcmp(pw->pw_name, "postgres") == 0); + errno = 0; + CHECK(pgl_getpwuid(999) == NULL); + CHECK(errno == ENOENT); + return 0; +} + +int +main(void) +{ + char temp_template[] = "/tmp/pglite-initdb-shim-XXXXXX"; + char *dir = mkdtemp(temp_template); + CHECK(dir != NULL); + CHECK(check_locale_and_fail_closed() == 0); + CHECK(check_postgres_read_and_write_pipes(dir) == 0); + CHECK(check_identity() == 0); + return 0; +} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_shim.c b/assets/wasix-build/wasix_shim/pglite_wasix_shim.c new file mode 100644 index 00000000..45549d1f --- /dev/null +++ b/assets/wasix-build/wasix_shim/pglite_wasix_shim.c @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include +#include +#include + +typedef struct WasixShmSegment +{ + int shmid; + key_t key; + size_t size; + void *addr; + unsigned long nattch; + struct WasixShmSegment *next; +} WasixShmSegment; + +static WasixShmSegment *wasix_shm_list; +static int wasix_next_shmid = 1; + +static WasixShmSegment * +find_by_key(key_t key) +{ + for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) + { + if (seg->key == key) + return seg; + } + return NULL; +} + +static WasixShmSegment * +find_by_id(int shmid) +{ + for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) + { + if (seg->shmid == shmid) + return seg; + } + return NULL; +} + +int +shmget(key_t key, size_t size, int shmflg) +{ + WasixShmSegment *existing = find_by_key(key); + + if (existing) + { + if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) + { + errno = EEXIST; + return -1; + } + return existing->shmid; + } + + if ((shmflg & IPC_CREAT) == 0) + { + errno = ENOENT; + return -1; + } + + size_t alloc_size = size ? size : 1; + long pagesize = sysconf(_SC_PAGESIZE); + if (pagesize > 0) + { + size_t page = (size_t) pagesize; + alloc_size = ((alloc_size + page - 1) / page) * page; + } + + void *addr = calloc(1, alloc_size); + if (!addr) + { + errno = ENOMEM; + return -1; + } + + WasixShmSegment *seg = calloc(1, sizeof(*seg)); + if (!seg) + { + free(addr); + errno = ENOMEM; + return -1; + } + + seg->shmid = wasix_next_shmid++; + seg->key = key; + seg->size = size; + seg->addr = addr; + seg->next = wasix_shm_list; + wasix_shm_list = seg; + + return seg->shmid; +} + +void * +shmat(int shmid, const void *shmaddr, int shmflg) +{ + (void) shmaddr; + (void) shmflg; + + WasixShmSegment *seg = find_by_id(shmid); + if (!seg) + { + errno = EINVAL; + return (void *) -1; + } + + seg->nattch++; + return seg->addr; +} + +int +shmdt(const void *shmaddr) +{ + for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) + { + if (seg->addr == shmaddr) + { + if (seg->nattch > 0) + seg->nattch--; + return 0; + } + } + + errno = EINVAL; + return -1; +} + +int +shmctl(int shmid, int cmd, struct shmid_ds *buf) +{ + WasixShmSegment *prev = NULL; + WasixShmSegment *seg = wasix_shm_list; + + while (seg && seg->shmid != shmid) + { + prev = seg; + seg = seg->next; + } + + if (!seg) + { + errno = EINVAL; + return -1; + } + + switch (cmd) + { + case IPC_RMID: + if (prev) + prev->next = seg->next; + else + wasix_shm_list = seg->next; + free(seg->addr); + free(seg); + return 0; + + case IPC_STAT: + if (!buf) + { + errno = EINVAL; + return -1; + } + memset(buf, 0, sizeof(*buf)); + buf->shm_perm.__key = seg->key; + buf->shm_segsz = seg->size; + buf->shm_nattch = seg->nattch; + buf->shm_atime = buf->shm_dtime = buf->shm_ctime = time(NULL); + return 0; + + case IPC_SET: + if (!buf) + { + errno = EINVAL; + return -1; + } + seg->size = buf->shm_segsz; + return 0; + + default: + errno = EINVAL; + return -1; + } +} diff --git a/crates/aot/aarch64-apple-darwin/Cargo.toml b/crates/aot/aarch64-apple-darwin/Cargo.toml new file mode 100644 index 00000000..00fc22ed --- /dev/null +++ b/crates/aot/aarch64-apple-darwin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pglite-oxide-aot-aarch64-apple-darwin" +version = "0.3.0" +edition = "2024" +rust-version = "1.92" +description = "Internal Wasmer AOT artifacts for pglite-oxide on aarch64-apple-darwin" +repository = "https://github.com/f0rr0/pglite-oxide" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = true +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" diff --git a/crates/aot/aarch64-apple-darwin/README.md b/crates/aot/aarch64-apple-darwin/README.md new file mode 100644 index 00000000..9c39ab0d --- /dev/null +++ b/crates/aot/aarch64-apple-darwin/README.md @@ -0,0 +1,4 @@ +# pglite-oxide-aot-aarch64-apple-darwin + +Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. +Do not depend on this crate directly. diff --git a/crates/aot/aarch64-apple-darwin/build.rs b/crates/aot/aarch64-apple-darwin/build.rs new file mode 100644 index 00000000..f50c6f15 --- /dev/null +++ b/crates/aot/aarch64-apple-darwin/build.rs @@ -0,0 +1,174 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("pglite-oxide-aot-") + .expect("AOT crate name starts with pglite-oxide-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + emit_manifest_probe(&repo_root.join("target/pglite-oxide/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + None +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.2\",\"wasmer-wasix-version\":\"0.702.0-alpha.2\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "pglite" => "runtime:pglite".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} diff --git a/crates/aot/aarch64-apple-darwin/src/lib.rs b/crates/aot/aarch64-apple-darwin/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/crates/aot/aarch64-apple-darwin/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml b/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml new file mode 100644 index 00000000..8862072e --- /dev/null +++ b/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" +version = "0.3.0" +edition = "2024" +rust-version = "1.92" +description = "Internal Wasmer AOT artifacts for pglite-oxide on aarch64-unknown-linux-gnu" +repository = "https://github.com/f0rr0/pglite-oxide" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = true +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" diff --git a/crates/aot/aarch64-unknown-linux-gnu/README.md b/crates/aot/aarch64-unknown-linux-gnu/README.md new file mode 100644 index 00000000..034067d4 --- /dev/null +++ b/crates/aot/aarch64-unknown-linux-gnu/README.md @@ -0,0 +1,4 @@ +# pglite-oxide-aot-aarch64-unknown-linux-gnu + +Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. +Do not depend on this crate directly. diff --git a/crates/aot/aarch64-unknown-linux-gnu/build.rs b/crates/aot/aarch64-unknown-linux-gnu/build.rs new file mode 100644 index 00000000..f50c6f15 --- /dev/null +++ b/crates/aot/aarch64-unknown-linux-gnu/build.rs @@ -0,0 +1,174 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("pglite-oxide-aot-") + .expect("AOT crate name starts with pglite-oxide-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + emit_manifest_probe(&repo_root.join("target/pglite-oxide/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + None +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.2\",\"wasmer-wasix-version\":\"0.702.0-alpha.2\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "pglite" => "runtime:pglite".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} diff --git a/crates/aot/aarch64-unknown-linux-gnu/src/lib.rs b/crates/aot/aarch64-unknown-linux-gnu/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/crates/aot/aarch64-unknown-linux-gnu/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/crates/aot/x86_64-pc-windows-msvc/Cargo.toml b/crates/aot/x86_64-pc-windows-msvc/Cargo.toml new file mode 100644 index 00000000..a1ec2813 --- /dev/null +++ b/crates/aot/x86_64-pc-windows-msvc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pglite-oxide-aot-x86_64-pc-windows-msvc" +version = "0.3.0" +edition = "2024" +rust-version = "1.92" +description = "Internal Wasmer AOT artifacts for pglite-oxide on x86_64-pc-windows-msvc" +repository = "https://github.com/f0rr0/pglite-oxide" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = true +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" diff --git a/crates/aot/x86_64-pc-windows-msvc/README.md b/crates/aot/x86_64-pc-windows-msvc/README.md new file mode 100644 index 00000000..e0d849f5 --- /dev/null +++ b/crates/aot/x86_64-pc-windows-msvc/README.md @@ -0,0 +1,4 @@ +# pglite-oxide-aot-x86_64-pc-windows-msvc + +Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. +Do not depend on this crate directly. diff --git a/crates/aot/x86_64-pc-windows-msvc/build.rs b/crates/aot/x86_64-pc-windows-msvc/build.rs new file mode 100644 index 00000000..f50c6f15 --- /dev/null +++ b/crates/aot/x86_64-pc-windows-msvc/build.rs @@ -0,0 +1,174 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("pglite-oxide-aot-") + .expect("AOT crate name starts with pglite-oxide-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + emit_manifest_probe(&repo_root.join("target/pglite-oxide/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + None +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.2\",\"wasmer-wasix-version\":\"0.702.0-alpha.2\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "pglite" => "runtime:pglite".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} diff --git a/crates/aot/x86_64-pc-windows-msvc/src/lib.rs b/crates/aot/x86_64-pc-windows-msvc/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/crates/aot/x86_64-pc-windows-msvc/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml b/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml new file mode 100644 index 00000000..5c1586ef --- /dev/null +++ b/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" +version = "0.3.0" +edition = "2024" +rust-version = "1.92" +description = "Internal Wasmer AOT artifacts for pglite-oxide on x86_64-unknown-linux-gnu" +repository = "https://github.com/f0rr0/pglite-oxide" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = true +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" diff --git a/crates/aot/x86_64-unknown-linux-gnu/README.md b/crates/aot/x86_64-unknown-linux-gnu/README.md new file mode 100644 index 00000000..bf4ba9a8 --- /dev/null +++ b/crates/aot/x86_64-unknown-linux-gnu/README.md @@ -0,0 +1,4 @@ +# pglite-oxide-aot-x86_64-unknown-linux-gnu + +Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. +Do not depend on this crate directly. diff --git a/crates/aot/x86_64-unknown-linux-gnu/build.rs b/crates/aot/x86_64-unknown-linux-gnu/build.rs new file mode 100644 index 00000000..f50c6f15 --- /dev/null +++ b/crates/aot/x86_64-unknown-linux-gnu/build.rs @@ -0,0 +1,174 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("pglite-oxide-aot-") + .expect("AOT crate name starts with pglite-oxide-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + emit_manifest_probe(&repo_root.join("target/pglite-oxide/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + None +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.2\",\"wasmer-wasix-version\":\"0.702.0-alpha.2\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "pglite" => "runtime:pglite".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} diff --git a/crates/aot/x86_64-unknown-linux-gnu/src/lib.rs b/crates/aot/x86_64-unknown-linux-gnu/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/crates/aot/x86_64-unknown-linux-gnu/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 00000000..b8cd2c63 --- /dev/null +++ b/crates/assets/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pglite-oxide-assets" +version = "0.3.0" +edition = "2024" +rust-version = "1.92" +description = "Internal PGlite runtime and extension assets for pglite-oxide" +repository = "https://github.com/f0rr0/pglite-oxide" +homepage = "https://github.com/f0rr0/pglite-oxide" +documentation = "https://docs.rs/pglite-oxide-assets" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = true +include = [ + "Cargo.toml", + "build.rs", + "README.md", + "src/**", + "payload/**", +] + +[lib] +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/crates/assets/README.md b/crates/assets/README.md new file mode 100644 index 00000000..9fe9be2c --- /dev/null +++ b/crates/assets/README.md @@ -0,0 +1,6 @@ +# pglite-oxide-assets + +Internal runtime assets for `pglite-oxide`. + +Do not depend on this crate directly. It is published so Cargo can resolve the +default `pglite-oxide` feature set from crates.io. diff --git a/crates/assets/build.rs b/crates/assets/build.rs new file mode 100644 index 00000000..843bf0e1 --- /dev/null +++ b/crates/assets/build.rs @@ -0,0 +1,192 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_ASSETS_DIR"); + emit_expected_asset_inputs(); + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")); + let out = out_dir.join("generated_assets.rs"); + + if let Some(asset_dir) = find_asset_dir() { + emit_rerun_directives(&asset_dir); + write_generated_assets(&out, &asset_dir); + } else { + write_source_only_assets(&out); + } +} + +fn emit_expected_asset_inputs() { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_ASSETS_DIR") { + emit_manifest_probe(&PathBuf::from(path)); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + if let Some(repo_root) = manifest_dir.parent().and_then(Path::parent) { + emit_manifest_probe(&repo_root.join("target/pglite-oxide/assets")); + } + emit_manifest_probe(&manifest_dir.join("payload")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_asset_dir() -> Option { + if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_ASSETS_DIR") { + let path = PathBuf::from(path); + if path.join("manifest.json").is_file() { + return Some(path); + } + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf); + if let Some(repo_root) = repo_root { + let target_assets = repo_root.join("target/pglite-oxide/assets"); + if target_assets.join("manifest.json").is_file() { + return Some(target_assets); + } + } + + let package_payload = manifest_dir.join("payload"); + if package_payload.join("manifest.json").is_file() { + return Some(package_payload); + } + + None +} + +fn emit_rerun_directives(asset_dir: &Path) { + println!("cargo:rerun-if-changed={}", asset_dir.display()); + visit_files(asset_dir, &mut |path| { + println!("cargo:rerun-if-changed={}", path.display()); + }); +} + +fn visit_files(path: &Path, f: &mut impl FnMut(&Path)) { + let Ok(entries) = fs::read_dir(path) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + visit_files(&path, f); + } else if path.is_file() { + f(&path); + } + } +} + +fn write_generated_assets(out: &Path, asset_dir: &Path) { + let manifest = asset_dir.join("manifest.json"); + let runtime = asset_dir.join("pglite.wasix.tar.zst"); + let pgdata_archive = asset_dir.join("prepopulated/pgdata-template.tar.zst"); + let pgdata_manifest = asset_dir.join("prepopulated/pgdata-template.json"); + let pg_dump = asset_dir.join("bin/pg_dump.wasix.wasm"); + let initdb = asset_dir.join("bin/initdb.wasix.wasm"); + + for required in [&manifest, &runtime, &pg_dump, &initdb] { + assert!( + required.is_file(), + "generated asset directory {} is missing required file {}", + asset_dir.display(), + required.display() + ); + } + assert!( + pgdata_archive.is_file() && pgdata_manifest.is_file(), + "generated asset directory {} is missing the required PGDATA template; expected both {} and {}", + asset_dir.display(), + pgdata_archive.display(), + pgdata_manifest.display() + ); + + let pgdata_archive_body = optional_include_bytes_body(&pgdata_archive); + let pgdata_manifest_body = optional_include_bytes_body(&pgdata_manifest); + + let mut extension_cases = String::new(); + let extension_dir = asset_dir.join("extensions"); + if extension_dir.is_dir() { + let mut archives = fs::read_dir(&extension_dir) + .expect("read generated extension directory") + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + archives.sort(); + for archive in archives { + let Some(file_name) = archive.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(sql_name) = file_name.strip_suffix(".tar.zst") else { + continue; + }; + extension_cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + sql_name, + rust_string_literal(&archive) + )); + } + } + extension_cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const HAS_EMBEDDED_ASSETS: bool = true;\n\ + pub const MANIFEST_JSON: &str = include_str!({manifest});\n\ + pub fn runtime_archive() -> Option<&'static [u8]> {{ Some(include_bytes!({runtime})) }}\n\ + pub fn pgdata_template_archive() -> Option<&'static [u8]> {{ {pgdata_archive_body} }}\n\ + pub fn pgdata_template_manifest() -> Option<&'static [u8]> {{ {pgdata_manifest_body} }}\n\ + pub fn pg_dump_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({pg_dump})) }}\n\ + pub fn initdb_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({initdb})) }}\n\ + #[rustfmt::skip]\n\ + pub fn extension_archive(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {extension_cases} }}\n\ + }}\n", + manifest = rust_string_literal(&manifest), + runtime = rust_string_literal(&runtime), + pgdata_archive_body = pgdata_archive_body, + pgdata_manifest_body = pgdata_manifest_body, + pg_dump = rust_string_literal(&pg_dump), + initdb = rust_string_literal(&initdb), + ); + fs::write(out, text).expect("write generated asset include module"); +} + +fn write_source_only_assets(out: &Path) { + let text = r##"pub const HAS_EMBEDDED_ASSETS: bool = false; +pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"pg-dump":null,"extensions":[],"sources":[]}"#; +pub fn runtime_archive() -> Option<&'static [u8]> { None } +pub fn pgdata_template_archive() -> Option<&'static [u8]> { None } +pub fn pgdata_template_manifest() -> Option<&'static [u8]> { None } +pub fn pg_dump_wasm() -> Option<&'static [u8]> { None } +pub fn initdb_wasm() -> Option<&'static [u8]> { None } +pub fn extension_archive(_name: &str) -> Option<&'static [u8]> { None } +"##; + fs::write(out, text).expect("write source-only asset include module"); +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn optional_include_bytes_body(path: &Path) -> String { + if path.is_file() { + format!("Some(include_bytes!({}))", rust_string_literal(path)) + } else { + "None".to_owned() + } +} diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs new file mode 100644 index 00000000..48e49e89 --- /dev/null +++ b/crates/assets/src/lib.rs @@ -0,0 +1,248 @@ +#![deny(unsafe_code)] + +use serde::{Deserialize, Serialize}; + +include!(concat!(env!("OUT_DIR"), "/generated_assets.rs")); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct AssetManifest { + pub format_version: u32, + pub runtime: RuntimeAsset, + #[serde(default)] + pub runtime_support: Vec, + #[serde(default)] + pub pg_dump: Option, + #[serde(default)] + pub initdb: Option, + #[serde(default)] + pub pgdata_template: Option, + #[serde(default)] + pub extensions: Vec, + #[serde(default)] + pub sources: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct RuntimeAsset { + pub archive: String, + pub sha256: String, + #[serde(default)] + pub module_sha256: String, + pub postgres_version: String, + pub runtime_kind: String, + #[serde(default)] + pub link: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct BinaryAsset { + pub name: String, + pub path: String, + pub sha256: String, + #[serde(default)] + pub module_sha256: String, + #[serde(default)] + pub native_module: Option, + pub size: u64, + #[serde(default)] + pub link: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct PgDataTemplateAsset { + pub archive: String, + pub manifest: String, + pub sha256: String, + pub size: u64, + pub runtime_module_sha256: String, + pub initdb_module_sha256: String, + pub source_pins_sha256: String, + pub postgres_version: String, + pub catalog_version: String, + pub init_profile: String, + pub wasmer_version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct ExtensionAsset { + pub name: String, + pub sql_name: String, + #[serde(default)] + pub source_kind: String, + pub archive: String, + pub sha256: String, + #[serde(default)] + pub module_sha256: String, + pub size: u64, + #[serde(default)] + pub stable: bool, + #[serde(default)] + pub control_files: Vec, + #[serde(default)] + pub dependencies: Vec, + #[serde(default)] + pub native_dependencies: Vec, + #[serde(default)] + pub load_order: Vec, + #[serde(default)] + pub lifecycle: Option, + #[serde(default)] + pub extension_imports: Vec, + #[serde(default)] + pub core_exports_required: Vec, + #[serde(default)] + pub unresolved_imports: Vec, + #[serde(default)] + pub installed_files: Vec, + #[serde(default)] + pub smoke_status: Option, + #[serde(default)] + pub link: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct ExtensionLifecycle { + pub create_extension: bool, + #[serde(default)] + pub create_schema: Option, + #[serde(default)] + pub load_sql: Vec, + #[serde(default)] + pub post_create_sql: Vec, + #[serde(default)] + pub startup_config: Vec, + #[serde(default)] + pub preload_required: bool, + #[serde(default)] + pub restart_required: bool, + #[serde(default)] + pub shared_memory_required: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct ExtensionSmokeStatus { + pub promoted: bool, + pub direct: String, + pub server: String, + pub restart: String, + pub dump_restore: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct WasmLinkMetadata { + pub has_dylink0: bool, + #[serde(default)] + pub dylink_needed: Vec, + #[serde(default)] + pub dylink_runtime_paths: Vec, + #[serde(default)] + pub dylink_memory: Option, + #[serde(default)] + pub dylink_imports: Vec, + #[serde(default)] + pub dylink_exports: Vec, + #[serde(default)] + pub imports: Vec, + #[serde(default)] + pub exports: Vec, + #[serde(default)] + pub memories: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct WasmDylinkMemory { + pub memory_size: u32, + pub memory_alignment: u32, + pub table_size: u32, + pub table_alignment: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct WasmDylinkSymbol { + pub module: Option, + pub name: String, + pub flags: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct WasmImport { + pub module: String, + pub name: String, + pub kind: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct WasmExport { + pub name: String, + pub kind: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct WasmMemory { + pub initial_pages: u64, + pub maximum_pages: Option, + pub memory64: bool, + pub shared: bool, + pub page_size_log2: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct SourcePin { + pub name: String, + pub url: String, + pub branch: String, + pub commit: String, +} + +pub fn manifest() -> Result { + serde_json::from_str(MANIFEST_JSON) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn manifest_parses_and_lists_promoted_extensions() { + let manifest = manifest().expect("asset manifest should parse"); + if !HAS_EMBEDDED_ASSETS { + assert_eq!(manifest.runtime.runtime_kind, "source-only-template"); + assert!(manifest.extensions.is_empty()); + return; + } + assert_eq!(manifest.runtime.postgres_version, "17.5"); + assert_eq!(manifest.runtime.runtime_kind, "wasix-dynamic-main"); + assert!( + manifest + .extensions + .iter() + .any(|extension| extension.sql_name == "vector" && extension.stable) + ); + assert!( + manifest + .extensions + .iter() + .any(|extension| extension.sql_name == "pg_trgm" && extension.stable) + ); + assert!( + manifest + .extensions + .iter() + .any(|extension| extension.sql_name == "hstore" && extension.stable) + ); + } +} diff --git a/deny.toml b/deny.toml index b3d589d5..89695623 100644 --- a/deny.toml +++ b/deny.toml @@ -1,6 +1,13 @@ [advisories] yanked = "warn" -ignore = [] +ignore = [ + # Wasmer/WASIX 7.2 alpha currently pulls bincode through virtual-net and + # wasmer-journal. Keep this explicit until Wasmer removes or replaces it. + "RUSTSEC-2025-0141", + # Wasmer 7.2 alpha currently pulls paste. Keep this explicit until upstream + # moves to a maintained macro helper or removes the dependency. + "RUSTSEC-2024-0436", +] [licenses] allow = [ @@ -9,7 +16,11 @@ allow = [ "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", + "BSL-1.0", + "CDLA-Permissive-2.0", + "ISC", "MIT", + "MIT-0", "MPL-2.0", "PostgreSQL", "Unicode-3.0", @@ -19,7 +30,7 @@ allow = [ confidence-threshold = 0.8 [bans] -# Wasmtime and dev-only PostgreSQL client compatibility tests pull a few +# Wasmer and dev-only PostgreSQL client compatibility tests pull a few # legitimate duplicate transitive versions. Keep this gate focused on actionable # supply-chain failures. multiple-versions = "allow" diff --git a/docs/ASSETS.md b/docs/ASSETS.md index 7005ca3d..cd001194 100644 --- a/docs/ASSETS.md +++ b/docs/ASSETS.md @@ -1,34 +1,163 @@ -# Runtime Assets - -The crate embeds a WASI PGlite runtime archive, not the Emscripten `pglite.wasm` -published in the JavaScript package. - -Current source: - -- Runtime artifact branch: `electric-sql/pglite-build` `gh-pages` -- Runtime artifact commit: `4c78ee29513799a51d4e1f75008cf9c3f00b11e9` -- Full artifact set on that branch includes the WASI runtime archive, - `pglite.wasm`, `pglite.js`, `pglite.cjs`, `pglite.html`, `bin/pg_dump.wasm`, - and extension archives. -- The crate packages a recompressed `assets/pglite-wasi.tar.zst` archive and a - bundled PGDATA template to keep the published crate under crates.io's 10 MiB - package limit while avoiding first-run `initdb`. - -Current metadata: - -- PostgreSQL runtime: `17.5` -- Upstream branch family: `electric-sql/postgres-pglite` `REL_17_5-pglite` -- JS package version checked for this asset set: `@electric-sql/pglite@0.4.4` -- Runtime archive SHA-256: `f6f90bf571c7f5bc925ff22f94233893c2c740466da2ad23bcd4e8e1ea8c498a` -- Packaged `pglite.wasi` SHA-256: `ad423e536096ede1870f4802e7dbe4b49599c6e3bafc45aa9a4ddeeae2c5f4f8` -- PGDATA template archive SHA-256: `63e398b3cd4fec134d06539f064018fc2aa7fef75b9c331472f9b2d7385b913d` - -Update checklist: - -1. Check `electric-sql/pglite-build` `gh-pages` and `npm view - @electric-sql/pglite version` for the latest published runtime artifacts. -2. Replace or regenerate `assets/pglite-wasi.tar.zst`. -3. Regenerate `assets/prepopulated/pgdata-template.tar.zst` and - `assets/prepopulated/pgdata-template.json`. -4. Update `[package.metadata.pglite-oxide.assets]` in `Cargo.toml`. -5. Run `scripts/validate.sh ci` and `scripts/validate.sh release`. +# Maintainer Asset Notes + +This page is maintainer documentation for packaged runtime assets, generated +payloads, and release provenance. It is not end-user product documentation. +Application users should start with `README.md`, `docs/USAGE.md`, and +`docs/RUNTIME.md`. + +`pglite-oxide` ships the database runtime as package-managed assets. Most users +do not need to download Postgres, run Docker, install LLVM, or configure a +runtime path. + +## What Ships + +With default features, the crate includes: + +- the portable PGlite/Postgres WASIX runtime tree; +- a prepopulated PGDATA template for faster temporary databases; +- bundled extension archives for supported SQL extensions; +- the packaged `initdb` module used by asset CI and explicit fresh-initdb paths; +- the packaged `pg_dump` module used by the public dump API and CLI; +- a target-specific Wasmer AOT pack when the current host target is supported. + +The internal asset crates exist only because crates.io packages dependencies as +separate crates. Application code should depend on `pglite-oxide`, not on +`pglite-oxide-assets` or `pglite-oxide-aot-*` directly. + +## Feature Flags + +Default install: + +```toml +pglite-oxide = "0.4" +``` + +Default features include the packaged runtime/AOT assets and bundled extension +APIs: + +```toml +pglite-oxide = { version = "0.4", default-features = false, features = ["bundled"] } +``` + +The `bundled` feature keeps the package-managed PGlite/Postgres runtime and the +current platform's AOT crate, but leaves the public extension API disabled. +This is the "embedded Postgres without extension helpers" mode. + +Size-sensitive builds can opt out of packaged assets entirely: + +```toml +pglite-oxide = { version = "0.4", default-features = false } +``` + +When bundled assets are disabled, normal database opens do not have packaged +runtime/AOT assets available. This mode is intended for specialized maintainer +and custom-runtime workflows. + +## Cache Behavior + +Runtime files are expanded into a cache and then composed with a small writable +per-root skeleton by default. Temporary and template-backed databases use a +cached PGDATA template as a lower filesystem and materialize files into the +database root only when PostgreSQL opens them for mutation. + +The runtime tree keeps both `/bin/pglite` and `/bin/postgres`. They are the same +backend module; the `postgres` path exists so upstream `initdb` can discover and +spawn the backend through PostgreSQL's normal `find_other_exec()` path. + +The cache is content-addressed by the asset manifest and artifact hashes. If an +asset hash does not match the manifest, startup fails instead of using a mixed +or corrupted runtime. + +## Extension Assets + +Extensions are demand-driven. An extension archive is installed into the +database root only when the builder requests it or `enable_extension` is called: + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + +db.enable_extension(extensions::PG_TRGM)?; +# Ok::<_, Box>(()) +``` + +Archive extraction rejects parent traversal, absolute paths, symlinks, +hardlinks, device nodes, and unsupported entry types. + +## Provenance + +Asset provenance is recorded in `assets/sources.toml`, the committed asset +input fingerprint, and the generated asset/AOT manifests produced by the Assets +workflow. Generated manifests record source pins, runtime hashes, `initdb` +hashes, PGDATA template hashes, extension archive hashes, target information, +and Wasmer engine identity. + +The public repository tracks source-controlled inputs and crate skeletons. It +does not track upstream source checkouts, generated PGDATA templates, portable +WASIX blobs, or native AOT binaries. +Maintainer source trees are fetched on demand into ignored +`assets/checkouts/**` directories: + +```sh +cargo run -p xtask -- assets fetch +``` + +Normal development and source-free validation do not clone upstream repositories +or run Docker. The source-free gate is: + +```sh +cargo run -p xtask -- assets verify-committed +``` + +It verifies source pins, source/build input fingerprints, extension +metadata/constants when generated manifests are installed, AOT crate templates, +and the absence of committed PGDATA template, portable WASIX, or native AOT +blobs. + +Release assets are built with the `release-o3` profile by default: WASIX C code +uses `-O3 -g0 -flto=thin`, links with `-flto=thin`, and Binaryen runs the +wasixcc default optimization plus `--converge`, `--strip-debug`, and +`--strip-producers`. + +Generated runtime hashes in package metadata are refreshed in the release +staging workspace. They are not a committed source-of-truth value in normal +development; `assets/sources.toml` and `assets/generated/asset-inputs.sha256` +are the small committed provenance files. + +The `Assets` workflow mirrors the release topology: one Linux/Docker job builds +portable WASIX modules from `assets/wasix-build` into +`target/pglite-oxide/assets`, then native matrix jobs generate and package +target-specific Wasmer AOT crates into `target/pglite-oxide/aot/`. +Artifacts are uploaded with checksums, manifests, and the committed asset-input +fingerprint. + +Manual `Assets` runs use the same producer path. Maintainers may select one +native target for focused validation, but the workflow still rebuilds portable +WASIX assets, generates AOT artifacts, runs the runtime gate, stages the release +workspace, package-checks the target crate, and uploads the canonical release +artifact shape. + +Native AOT generation intentionally installs Wasmer's LLVM 22.1.x custom build +only inside the Assets workflow or a maintainer's explicit local artifact +build. Normal contributors and end users never need LLVM; they use committed +Rust sources plus downloaded or released AOT payloads. + +The normal CI runtime matrix downloads the latest compatible Assets workflow +bundle, verifies that the downloaded fingerprint matches the current source +inputs, installs the payloads into ignored generated paths, and runs runtime +tests. Any change to source pins, WASIX patches, extension catalogs, build +scripts, or AOT crate templates is treated as asset-producing and must pass the +full `Assets` workflow. Release validation downloads the exact-SHA portable and +AOT bundles, stages them into a clean release workspace, validates package +contents, and only then publishes. + +After an intentional asset-source change and regenerated artifacts, refresh the +committed input fingerprint: + +```sh +cargo run -p xtask -- assets input-fingerprint --write +``` diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 58d6cf25..694e0f1e 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,27 +1,61 @@ -# Development +# Maintainer Development Guide + +This page is maintainer documentation for repository validation, generated +artifacts, and local release workflows. It is not end-user product +documentation. Run the local gates before opening a PR: ```sh -scripts/validate.sh ci -scripts/validate.sh release -cargo deny check +scripts/bootstrap-tools.sh +scripts/validate.sh dev +scripts/validate.sh workflows +scripts/validate.sh supply-chain ``` +The validation entrypoint is split by maintainer workflow: + +- `scripts/validate.sh repo`: file hygiene and formatting; +- `scripts/validate.sh artifacts`: source-controlled asset input verification + plus AOT crate template checks; +- `scripts/validate.sh lint`: dependency invariants and clippy; +- `scripts/validate.sh test`: source-only no-default-features checks, + doctests, and test compilation without requiring generated runtime assets; +- `scripts/validate.sh workflows`: local `actionlint` and `zizmor` checks using + the same zizmor config and severity/persona as CI; +- `scripts/validate.sh runtime`: hard-requires portable assets plus host AOT, + installs them into ignored paths, and runs the real runtime tests; +- `scripts/validate.sh runtime-smoke`: the runtime smoke subset; +- `scripts/validate.sh examples`: Tauri/Rust/frontend example checks; +- `scripts/validate.sh package`: package all published crates and enforce + crates.io size limits; +- `scripts/validate.sh feature-powerset`: cargo-hack feature combination checks; +- `scripts/validate.sh semver`: cargo-semver-checks public API compatibility; +- `scripts/validate.sh supply-chain`: cargo-deny dependency policy checks; +- `scripts/validate.sh ci`: full local CI parity lane; +- `scripts/validate.sh dev-ci`: fast contributor lane for repo, lint, source + tests, and examples; +- `scripts/validate.sh release`: release-workspace package checks plus publish + dry-runs for internal crates after CI-generated AOT artifacts have been + downloaded. + The hook split is intentionally small: - pre-commit: file hygiene and formatting -- pre-push: whitespace diff check, `cargo clippy --all-targets`, and - `cargo test --all-targets` -- CI/release: the hook checks plus no-default build, doctests, Tauri example, - frontend build, workflow linting, feature powerset, public API compatibility, - crate packaging, publish dry-run, and supply-chain policy +- pre-push: whitespace diff check, `scripts/validate.sh lint`, and + `scripts/validate.sh test` +- CI/release: path-aware combinations of the same validation modes, workflow + linting, feature powerset, public API compatibility, crate packaging, + native AOT runtime tests, release-plz dry-run/publish, and supply-chain + policy -Install local hooks and the supply-chain gate when needed: +Install local hooks and pinned CLI tools when needed. The bootstrap installs +`cargo-binstall` first and uses binary installs for Rust tools before falling +back to source builds. ```sh +scripts/bootstrap-tools.sh scripts/install-hooks.sh -cargo install cargo-deny --locked ``` `tests/runtime_smoke.rs` starts the real WASM backend and is intentionally @@ -31,10 +65,123 @@ slower than the protocol unit tests. The repository includes maintenance commands: -- `pglite-dump` expands the bundled runtime archive for inspection. +- `pglite-dump` is the logical dump CLI entry point. - `pglite-proxy` exposes a local PostgreSQL socket backed by the embedded runtime. -- `cargo run --example build_pgdata_template` regenerates the bundled - prepopulated PGDATA template. +- `xtask assets template` generates the architecture-independent PGDATA + template from the split WASIX `initdb` module. Portable WASIX, PGDATA + templates, and native AOT payloads remain generated-only. + +Asset and source checks: + +```sh +cargo run -p xtask -- assets verify-committed +cargo run -p xtask -- assets fetch +cargo run -p xtask -- assets check --strict-local +cargo run -p xtask -- assets check --strict-generated +cargo run -p xtask --features template-runner -- assets template +cargo run -p xtask -- assets source-spine --check-patch-applies +cargo run -p xtask -- assets audit-upstream --strict +cargo run -p xtask -- assets input-fingerprint --write +cargo run -p xtask -- package-size --enforce +``` + +## Local Runtime Development + +Local development has three supported modes. + +Fast contributor mode does not require Docker, upstream source checkouts, or +generated native AOT payloads. Use it for ordinary Rust, docs, tests, examples, +and workflow edits: + +```sh +scripts/validate.sh dev-ci +cargo check --workspace --all-targets +cargo test --workspace --no-default-features +``` + +For the shortest source-only path, use: + +```sh +scripts/validate.sh dev +``` + +Host-platform artifact mode is for runtime work on the current machine. It +builds or packages only the current host target, leaves all generated payloads +in ignored paths, and then runs the real runtime tests: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets fetch +cargo run -p xtask --features aot-serializer -- assets build-host +scripts/validate.sh runtime +``` + +Local AOT generation requires the Wasmer LLVM 22.1.x build for the +maintainer-only serializer. That build includes the LLVM target set Wasmer's +LLVM backend expects, including LoongArch and WebAssembly. Set +`LLVM_SYS_221_PREFIX` to an extracted +`wasmerio/llvm-custom-builds` 22.x archive, or use downloaded-artifact mode to +avoid local LLVM setup. + +When the portable WASIX assets are already current and only the host AOT crate +needs to be refreshed, skip the source/Docker build and generate host AOT from +the existing generated portable assets: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets aot --target-triple "$host" +cargo run -p xtask -- assets package-aot --target-triple "$host" +scripts/validate.sh runtime +``` + +Downloaded-artifact mode is the intended way to test a CI-produced runtime +locally without rebuilding Postgres/WASIX. Download the successful Assets +workflow artifacts for the exact commit and install the host target payloads +into the same ignored generated locations used by the local build path: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets download --sha --target-triple "$host" +scripts/validate.sh runtime +``` + +For Rust-only work where the asset inputs have not changed, the same command +can install the latest compatible `main` bundle after verifying the +asset-input fingerprint: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets download --latest-compatible --target-triple "$host" +scripts/validate.sh runtime +``` + +Release validation can download every supported target from the exact Assets +workflow SHA: + +```sh +cargo run -p xtask -- assets download --sha --all-targets +scripts/validate.sh release +``` + +Developers should not be expected to build every target locally. Local runtime +work validates the host target; the Assets workflow is the authority for the +full macOS, Linux, and Windows AOT matrix. + +Contributors do not need upstream source checkouts for normal Rust, docs, +examples, or package validation. Maintainers fetch sources only when rebuilding +the portable WASIX runtime, extensions, `initdb`, `pg_dump`, or the generated +PGDATA template. Portable WASIX artifacts, generated PGDATA templates, and +native AOT artifacts are generated under `target/pglite-oxide/**` locally or by +CI; they are not committed to git. + +Rust-only PRs download the latest compatible Assets workflow bundle, verify its +asset-input fingerprint, install it into ignored generated paths, and run the +runtime test suite on every supported host target. Asset-producing PRs run the +heavier `Assets` workflow instead: that workflow rebuilds portable WASIX from +pinned sources, generates native AOT for every target, runs smoke tests, and +uploads release artifacts. Release process details are tracked in [RELEASE.md](RELEASE.md). +Completed implementation work is summarized in [DONE.md](DONE.md), and the +implementation backlog is tracked in [TODO.md](TODO.md). diff --git a/docs/DONE.md b/docs/DONE.md new file mode 100644 index 00000000..31ee0b09 --- /dev/null +++ b/docs/DONE.md @@ -0,0 +1,928 @@ +# Done (Maintainers) + +This is the single status document for implementation work already completed. +It is maintainer-facing and intentionally separate from the end-user docs. + +## Runtime Direction + +The repository now has one production direction: WASIX dynamic linking plus +headless Wasmer loading of CI-produced LLVM AOT artifacts. + +Removed or excluded from the production path: + +- Wasmtime/static-WASI runtime path; +- Emscripten/JavaScript glue runtime path; +- user-side Docker, LLVM, Cranelift, or local Postgres compilation; +- duplicated runtime layouts and host-side timezone/path rewrite shims; +- historical spike workspaces from the tracked repository. + +Production build inputs now live under `assets/`. + +## Workspace And Asset Crates + +Implemented: + +- root `pglite-oxide` crate remains the public crate; +- `pglite-oxide-assets` is the published runtime asset crate skeleton; +- source-only target AOT crate templates exist under `crates/aot/*`; +- `xtask` owns source checks, build orchestration, packaging, manifest checks, + package sizing, upstream audits, and source-spine validation; +- upstream checkouts are no longer tracked; maintainers fetch pinned sources on + demand into ignored `assets/checkouts`; +- source pins live in `assets/sources.toml`; +- root packages exclude upstream checkouts from published crates. +- `xtask assets verify-committed` validates source-controlled asset inputs, + source pins, package metadata, AOT crate templates, and generated extension + coherence when generated manifests are installed, without local upstream + checkouts; + +Generated release asset set: + +- portable PGlite WASIX runtime archive; +- `pg_dump.wasix.wasm`; +- deterministic `.tar.zst` archives for the 37 requested extension build + candidates. All 37 packaged extensions are stable public constants after + direct, server, restart, and lifecycle materialization gates; +- prepopulated PGDATA template archive; +- native Wasmer LLVM AOT artifacts. + +These artifacts are generated locally under `target/pglite-oxide/**` or by the +Assets workflow and are consumed by release staging without being committed to +git. + +## Source And Build Spine + +Implemented: + +- active source baseline switched to `electric-sql/postgres-pglite` + `REL_17_5-pglite` at `01792c31a62b7045eb22e93d7dad022bb64b1184`, matching + the audited `@electric-sql/pglite` 0.4.5 source/artifact pair; +- `pglite-build` `portable` is pinned as build-script provenance; +- maintained WASIX build files live under `assets/wasix-build`; +- `xtask assets build --execute` can produce the main runtime, support modules, + requested contrib/PGXS extension side modules, SQL-only extension payloads, + and `pg_dump` for the local target; +- `xtask assets package` emits deterministic archives, generated manifests, and + crate assets; +- `xtask assets aot` regenerates local Wasmer LLVM AOT artifacts; +- `xtask assets check --strict-generated` validates generated metadata; +- `xtask assets source-spine --check-patch-applies` validates the maintained + source patch and C ABI harness; +- `xtask assets audit-upstream --strict` records upstream fix decisions; +- required upstream fixes from `REL_17_5-pglite` are now the active source + spine rather than comparison material. The WASIX patch keeps dynamic-main and + side-module support, C startup timers, and explicit exports for the stable + branch lifecycle while reusing upstream `pgl_startPGlite`, + `pgl_setPGliteActive`, `ProcessStartupPacket`, `PostgresMainLoopOnce`, and + `PostgresMainLongJmp`; +- source-spine review conclusion: upstream PGlite's libc/host adaptations are + purposeful for wasm hosts, not arbitrary shortcuts. `pglitec.c` supplies + stable `postgres` identity, explicit process-active state, manual top-level + longjmp recovery, socket callbacks, shared-memory emulation, and explicit + atexit replay because browser/Emscripten cannot provide normal Postgres + child processes, sockets, Unix users, SysV shared memory, or a native process + lifecycle. The WASIX bridge keeps the same architectural contracts where + Wasmer still needs host assistance, but uses Rust-owned input/output buffers + instead of Emscripten callback pointers because the Rust host does not have + Emscripten's JS table callback mechanism; +- WASIX-specific deviation from upstream PGlite: top-level Postgres longjmp + detection uses `jmp_buf` pointer identity instead of upstream's buffer-content + `memcmp`. The memcmp test is acceptable in the Emscripten artifact it was + written for, but under Wasmer/WASIX it misclassified nested PostgreSQL + `PG_TRY` handlers and skipped normal portal cleanup. Pointer identity keeps + the host escape hatch scoped to the single exported top-level recovery buffer; +- WASIX-specific PostgreSQL fix: active portal abort cleanup is owned in + `AtAbort_Portals` for `PGLITE_WASIX_DL`, not in Rust. This keeps simple-query + and COPY error recovery at the PostgreSQL portal lifecycle boundary and avoids + fabricating cleanup behavior in the wire proxy; +- stable branch behavior note: startup `ParameterStatus` messages may be emitted + on raw protocol paths before `ReadyForQuery`. Tests now allow those legal + PostgreSQL messages instead of assuming the older minimal message sequence; +- new roots are now created through the packaged PGDATA template path. The old + embedded-backend `pgl_initdb` path was removed; explicit fresh-initdb paths + now use the bundled split WASIX `initdb` command and remain outside the + default fast path; +- the old builder-branch `pglite-wasm/*` runtime wrapper is no longer the + production patch target. It remains historical/reference material only; +- `xtask package-size --enforce` passes locally for the root, asset, and macOS + arm64 AOT crates. + +Parity verified against upstream PGlite stable source and TypeScript host: + +- startup/initdb: upstream TypeScript creates a cluster with `initdb.wasm`, + dumps PGDATA, loads that tarball into the main runtime, calls + `_pgl_setPGliteActive(1)`, runs `callMain([...startParams, -D, PGDATA, + PGDATABASE])`, expects exit `99`, then calls `_pgl_startPGlite()`. + pglite-oxide matches the main-runtime lifecycle from `_pgl_setPGliteActive` + onward and deliberately consumes a packaged PGDATA template instead of + exposing split runtime `initdb` yet. That is an explicit product gap, not a + hidden fallback; +- startup packet: upstream calls `_pgl_getMyProcPort()`, + `_ProcessStartupPacket(...)`, `_pgl_sendConnData()`, and `_pgl_pq_flush()`. + pglite-oxide uses the same C exports. Server connections now open the + embedded backend against the startup packet database, apply client startup + options on the C side, and apply non-`postgres` users through PostgreSQL + `SET ROLE` semantics, matching PGlite's single-process identity model; +- query loop: upstream feeds the whole frontend message buffer, repeatedly calls + `_PostgresMainLoopOnce()` while frontend bytes or libpq buffered data remain, + catches status `100`, calls `_PostgresMainLongJmp()`, then always calls + `_PostgresSendReadyForQueryIfNecessary()` and `_pgl_pq_flush()`. The Rust host + now follows that control flow with Rust-owned input/output buffers instead of + Emscripten callback pointers; +- close: upstream clears active state, sends protocol terminate, and replays + `_pgl_run_atexit_funcs()`. The Rust host clears active state and replays + atexit on shutdown, while tests cover clean restart, root locking, and stale + runtime-state cleanup; +- host ABI: upstream `pglitec.c` emulates sockets, identity, shared memory, + `system`/`popen`, timers, longjmp, and atexit because browser/Emscripten does + not provide normal process or OS services. pglite-oxide keeps the same + categories only where WASIX still needs host assistance, and the ABI harness + tests stable identity, fail-closed `system`, protocol fd bridging, shared + memory, atexit replay, mmap, and libpq encoding aliases; +- justified deviations: WASIX longjmp detection uses pointer identity instead of + upstream's `jmp_buf` content `memcmp`; simple-query/COPY portal abort cleanup + is owned in PostgreSQL `AtAbort_Portals`; startup `ParameterStatus` messages + are accepted as legal protocol output; split WASIX `initdb` is now the owned + template-generation path instead of resurrecting the old builder wrapper. + +## Runtime Behavior + +Implemented: + +- runtime loads verified headless Wasmer AOT artifacts; +- AOT artifacts record source module hash, Wasmer version, and engine identity; +- runtime verifies asset and archive hashes before use; +- unsupported targets return a clear missing-AOT-artifact error instead of + compiling locally; +- `Pglite::preload()` and `Pglite::preload_extensions(...)` exist; +- `Pglite::preload()` now warms the persistent runtime cache, headless Wasmer + engine, main AOT module, shared WASIX runtime, and runtime side modules; +- `Pglite::preload_extensions(...)` warms requested extension artifacts and + side-module cache entries generically; +- direct, persistent, app-id, proxy, server, and temporary roots now share the + `RootPlan`/`prepare_root` root-preparation pipeline; +- direct API, server API, proxy CLI, raw protocol API, and direct `pg_dump` now + share `BackendSession` for WASIX instance creation, backend start, startup + packet handling, protocol transport, shutdown, restart, and atexit replay; +- roots can install immutable runtime files from a persistent runtime cache and + install the embedded PGDATA template without running initdb on the default + startup path; +- mutable PGDATA template files are copied or archive-installed, never + hardlinked; immutable runtime files hardlink from cache when possible; +- persistent roots use lock files to prevent concurrent direct/server opens; +- runtime and extension archive extraction rejects unsafe paths, symlinks, + hardlinks, device nodes, and unsupported archive entry types; +- runtime uses canonical Postgres paths: + `/bin`, `/lib/postgresql`, `/share/postgresql/extension`, and + `/share/postgresql/timezonesets`. + +## Public API Surface + +Implemented: + +- `PgliteBuilder::extension`; +- `PgliteBuilder::extensions`; +- `PgliteBuilder::username`; +- `PgliteBuilder::database`; +- `PgliteBuilder::debug_level`; +- `PgliteBuilder::relaxed_durability`; +- `PgliteBuilder::startup_arg`; +- `PgliteBuilder::startup_args`; +- `PgliteBuilder::load_data_dir_archive`; +- `Pglite::enable_extension`; +- `Pglite::preload`; +- `Pglite::preload_extensions`; +- `Pglite::dump_data_dir`; +- `Pglite::dump_data_dir_with_format`; +- `Pglite::try_clone`; +- physical PGDATA archives now apply Wasmer overlay whiteouts, so files deleted + from the lower template are not resurrected by dump/load/clone; +- physical PGDATA archives are written from a materialized effective PGDATA view + instead of directly mixing lower-template and upper-overlay entries in the tar + writer; +- physical PGDATA archive/clone now checkpoints, quiesces the backend, + materializes the archive, and restarts the same backend session; docs state + this is a same-runtime/same-version physical import/export path, not a + cross-version backup protocol; +- `Pglite::exec_protocol_raw`; +- `Pglite::exec_protocol_raw_stream`; +- `Pglite::dump_sql`; +- `Pglite::dump_bytes`; +- `PgliteServerBuilder::extension`; +- `PgliteServerBuilder::extensions`; +- `PgliteServerBuilder::username`; +- `PgliteServerBuilder::database`; +- `PgliteServerBuilder::debug_level`; +- `PgliteServerBuilder::relaxed_durability`; +- `PgliteServerBuilder::startup_arg`; +- `PgliteServerBuilder::startup_args`; +- `PgliteServer::database_url`; +- `PgliteServer::dump_sql`; +- `PgliteServer::dump_bytes`; +- `PgDumpOptions`; +- 37 public extension constants plus `extensions::ALL`, covering the smoke-gated + packaged PGlite/Postgres catalog: `amcheck`, `auto_explain`, `bloom`, + `age`, `btree_gin`, `btree_gist`, `citext`, `cube`, `dict_int`, `dict_xsyn`, + `earthdistance`, `file_fdw`, `fuzzystrmatch`, `hstore`, `intarray`, `isn`, + `lo`, `ltree`, `pageinspect`, `pg_buffercache`, `pg_freespacemap`, + `pg_hashids`, `pg_ivm`, `pg_surgery`, `pg_textsearch`, `pg_trgm`, + `pg_uuidv7`, `pg_visibility`, `pg_walinspect`, SQL-only `pgtap`, `seg`, + `tablefunc`, `tcn`, `tsm_system_rows`, `tsm_system_time`, `unaccent`, and + `vector`. + +`pglite-dump` no longer exposes the old archive-unpack behavior. It is now a +real logical dump CLI backed by the packaged WASIX `pg_dump` module. + +`relaxed_durability` is a startup-profile flag rather than a hidden mutation of +`PostgresConfig`; explicit user `postgres_config` values win and +`relaxed_durability(true).relaxed_durability(false)` returns to the normal +profile. + +## Protocol And Server Correctness + +Implemented coverage: + +- direct Rust API open/init/query; +- persistence, close/reopen, stale runtime-state cleanup, interrupted PGDATA + cleanup, and root-lock conflicts; +- SQLx and `tokio-postgres` local-server connections; +- SSLRequest no-SSL response; +- CancelRequest safe close; +- backend-open failures no longer map every non-`template1` startup failure to + SQLSTATE `3D000`. PostgreSQL/C now owns startup identity and database errors: + the WASIX backend captures `InitPostgres` startup `ErrorResponse` bytes, the + proxy forwards them directly, and runtime/filesystem failures before + PostgreSQL can speak protocol remain synthesized `XX000`; +- Parse, Bind, and Execute error recovery; +- SQLSTATE preservation for syntax, missing relation, invalid typed parameter, + wrong parameter count, and extension-originated errors; +- extended-query `ReadyForQuery` synchronization; +- successful pipelined extended queries; +- mixed success/error/success pipelined queries; +- explicit prepared-statement reuse; +- transaction error recovery through rollback; +- client disconnect during an extended-query exchange; +- partial TCP reads and pipelined simple queries; +- server-mode `COPY FROM STDIN` now streams through the backend-owned protocol + pump instead of Rust SQL-text detection or proxy-fabricated COPY state. Normal + SQLx/tokio-postgres traffic uses the buffered raw-protocol path; when + PostgreSQL emits a real `CopyInResponse`, `CopyOutResponse`, or + `CopyBothResponse`, the WASIX bridge flushes buffered backend output to the + attached socket continuation and lets PostgreSQL continue on the socket. Raw + wire coverage includes simple COPY, extended-protocol COPY, CSV `WITH (...)` + COPY, binary COPY, `CopyData`, `CopyDone`, `CopyFail`, Unix-socket COPY + parity, and post-COPY connection reuse. +- continuation bytes are borrowed in the proxy read loop and materialized only + after the C bridge reports active streaming COPY; +- direct raw protocol streaming is routed through the shared `BackendSession` + framed sender instead of a separate client-only transport path; +- Rust-owned guest bridge allocations are scoped through `pg_free`/`free`, and + debug builds now have a direct raw-protocol stress test proving repeated + bridge round trips keep allocation/free counters balanced; +- direct LISTEN/UNLISTEN quotes channel identifiers and dispatches notifications + by the exact backend channel name, including case-sensitive and quoted names. +- a larger PostgreSQL regression subset now ports the relevant PGlite test + surface for datatypes, DDL, transactions/savepoints, planner/index behavior, + and direct `/dev/blob` CSV COPY. The datatype coverage also found and fixed a + direct-client multidimensional array parser bug, with unit coverage for + nested arrays, quoted values, and unquoted NULL handling. + +## Independent P0 Architecture Review + +The P0 review was re-run against the current Rust host, WASIX bridge, source +patch, and regression tests. No current P0 architecture blockers remain in the +reviewed surface. The completed P0 items were moved out of the backlog; future +major protocol, backup, runtime, or source-spine changes should get a new +review entry here instead of leaving completed checklists in `TODO.md`. + +Verified ownership boundaries: + +- Rust owns hosting, root preparation, caches, process lifecycle, direct/server + API shape, and typed fallbacks for host/runtime failures before PostgreSQL can + speak wire protocol; +- PostgreSQL/C owns SQLSTATEs, startup identity/database errors, query protocol + state, COPY state, portal cleanup, and longjmp recovery boundaries; +- the WASIX bridge owns only the host ABI that Wasmer/WASIX cannot provide as a + normal OS process boundary: protocol fd transport, locale/identity shims, + single-process shared memory, fail-closed process calls, and explicit + allocation/free ownership. + +Review conclusions: + +- guest-memory ownership is scoped through `GuestAllocator`, `pg_free`/`free`, + and debug allocation/free counters; +- detached protocol stdio fails closed rather than silently accepting bytes; +- COPY state is reported by PostgreSQL through + `pgl_protocol_report_copy_response`; the proxy no longer parses SQL text, + fabricates COPY state, scans whole backend buffers, or eagerly copies + continuation bytes for ordinary traffic; +- direct raw protocol streaming and direct `pg_dump` use the shared + `BackendSession` transport instead of a separate clone/server path; +- startup role/database failures are PostgreSQL-owned: WASIX backend open + captures `InitPostgres` `ErrorResponse` bytes, the proxy forwards those bytes, + and Rust no longer probes `pg_database` or string-guesses `3D000`; +- direct API, server API, proxy CLI, raw protocol, physical archive/clone, and + direct `pg_dump` share `RootPlan`/`prepare_root` and `BackendSession` + lifecycle paths; +- side-module cache seeding is keyed by artifact name, source module hash, + Wasmer version, Wasmer-WASIX version, and engine identity; +- AOT startup keeps full SHA verification behind + `PGLITE_OXIDE_AOT_VERIFY=full` while default loading uses metadata receipts + and mmap/native deserialization; +- PGDATA physical archive/clone materializes the effective overlay view with + whiteouts, quiesces/restarts the backend, and is documented as + same-runtime/same-version physical transfer rather than a WAL-aware backup; +- public API parity additions were reviewed: `fresh_temporary()` stayed out, + raw protocol streaming is real, physical clone/export has honest semantics, + startup args remain advanced, and listener channel names are identifier + quoted. + +Residual work from this review is intentionally not P0 architecture debt: +target-matrix CI, broader extension generation, additional PostgreSQL +regression subsets, release performance gates, and future split-WASIX `initdb` +support remain tracked in `TODO.md`. + +## Extensions And `pg_dump` + +Implemented coverage: + +- `vector` direct API load, `CREATE EXTENSION`, insert, distance query, and + pgvector type cases; +- `vector` through `PgliteServer` and SQLx; +- SQLx recovery after vector-originated errors; +- demand-driven extension install and idempotent `enable_extension`; +- installed extension side modules are seeded into the headless Wasmer cache on + reopen; +- `pg_trgm` direct API and SQLx server smoke coverage; +- `hstore` direct API, persistence/reopen, and SQLx server smoke coverage; +- PGlite extension tests were ported into a generic promotion gate for direct + API, server API, restart, and lifecycle materialization. The gate now covers + every packaged candidate. AGE now uses its upstream 32-bit `SIZEOF_DATUM=4` + SQL generation path, passes direct/server/restart/lifecycle gates, and is + exposed as `extensions::AGE`; +- extension discovery now merges PGlite docs/REPL exports, PGlite package + exports, PostgreSQL contrib metadata, `postgres-pglite` `other_extensions` + pins, PGlite tests, and the packaged asset manifest into + `assets/generated/extensions.catalog.json`; +- `xtask assets fetch` now clones/fetches every pinned source from + `assets/sources.toml` into ignored `assets/checkouts/**` directories, + including the external extension sources for pgtap, pg_ivm, pg_uuidv7, + pg_hashids, AGE, PostGIS, and pg_textsearch; +- extension build intent now lives in `assets/extensions.promoted.toml` instead + of being inferred from already-packaged artifacts. The generated catalog + separates requested, packaged, stable, and publicly promoted state; +- extension smoke evidence now lives in `assets/extensions.smoke.toml`; + generated public constants require requested + packaged + stable + direct, + server, and restart smoke status recorded as passed; +- `xtask extensions build-plan --write` generates + `assets/generated/extensions.build-plan.json`, + `assets/generated/contrib-build.tsv`, and + `assets/generated/pgxs-build.tsv`; `xtask assets check --strict-generated` + fails if those generated files drift; +- the WASIX extension build spine now uses generic contrib and PGXS build + scripts driven by the generated build plans, replacing the previous + `pg_trgm`-only and `pgvector`-only Docker scripts; +- the generated catalog now requires every discovered SQL extension to be + either requested for build or explicitly blocked with a concrete reason. The + current catalog discovers 40 SQL extensions, requests/packages 37, and blocks + only `pgcrypto`, PostGIS, and `uuid-ossp` on missing pinned native dependency + stacks; +- native side-module names are generated from control-file `module_pathname` + and PGXS Makefile metadata instead of assuming `.so`. This covers + cases such as `intarray` using `_int.so` and SQL-only extensions such as + `pgtap`; +- both generated build plans now support native and SQL-only extensions. The + local WASIX build produced all requested contrib and PGXS extension payloads, + generated local macOS arm64 AOT artifacts for all requested native modules, + and packaged all requested extension archives into `pglite-oxide-assets`; +- contrib packaging now carries extension-owned tsearch rule files into + `share/postgresql/tsearch_data`, matching PGlite behavior for `dict_xsyn` and + `unaccent`; +- generated extension constants are emitted only for extensions that are + requested, packaged, stable, and direct/server/restart smoke-passed; generated + asset includes carry all packaged candidates so private promotion tests can + exercise candidates before they become public API; +- manifest metadata records extension source kind, control files, + dependencies, lifecycle, imports, required core exports, unresolved imports, + installed files, load order, and smoke status; +- the `wasix-dl` export list is generated from the runtime exports plus + runtime-support/extension side-module imports, rather than being a + hand-maintained export allowlist; +- extension archive hash mismatch rejection; +- public WASIX `pg_dump` runner loads through the AOT manifest, connects to + `PgliteServer`, dumps plain SQL, restores into fresh `Pglite`, and verifies + schema/data; +- direct `Pglite::dump_sql` no longer uses a temporary physical clone, public + `PgliteServer`, or OS loopback TCP; it runs the standalone WASIX `pg_dump` + against an in-process Wasmer virtual TCP connection whose host side is routed + through the same direct raw-protocol backend; +- direct `Pglite::dump_sql` rejects database/user options that would imply a + different backend than the already-open direct session; callers needing that + use the server `pg_dump` path; +- the direct `pg_dump` transport keeps `pg_dump`/libpq stock and owns the only + required semantic adapter in Rust: a first-write-readiness normalization for + Wasmer's in-memory `TcpSocketHalf` so libpq's connect-time and first-write + polls remain level-triggered; +- public `pg_dump` coverage includes indexes, views, sequences, + `--schema-only`, `--quote-all-identifiers`, source-server reuse after dump, + and vector extension dump/restore; +- `PgDumpOptions` rejects passthrough flags that conflict with the typed + output/connection contract instead of letting callers override the internal + output file, format, host, port, username, database, or job count. + +## WASIX C Boundary Ownership + +The remaining C-side differences are owned as WASIX portability and host ABI, +not hidden generic stubs: + +- `pg_proto.c` manually coordinates `ReadyForQuery` for the current + call/return protocol loop and is covered by SQLx, `tokio-postgres`, and raw + wire-protocol tests; +- `pg_main.c` drives initdb boot/single-user phases inside one embedded process, + with named helpers for boot, stdin restoration, and single-user replay; +- `pgl_os.h` emulates only the expected initdb boot/single `popen()` commands + under `PGLITE_WASIX_DL` and fails closed otherwise; +- `pgl_stubs.h` is gated to `PGLITE_WASIX_DL`, and future removals are driven by + link-symbol analysis; +- `pglite_wasix_bridge.c` owns locale command emulation, stable `postgres` + uid/passwd identity, protocol socket buffers, fail-closed `system()`, selected + fd/socket delegation to WASIX libc, and single-process SysV shared memory. + +The source-spine guard checks for removed spike smells: debug-only `#pragma` +markers, diagnostic `popen`, broad socket fake-success behavior, layout +mirroring, timezone rewrites, and generic stub logging. + +## Validation Already Run + +The following local gates passed before this consolidation: + +```sh +cargo fmt --check +cargo check -p pglite-oxide --all-targets +cargo check -p pglite-oxide --no-default-features --all-targets +cargo run -p xtask -- assets check --strict-generated +cargo run -p xtask -- assets source-spine --check-patch-applies +cargo run -p xtask -- assets audit-upstream --strict +cargo run -p xtask -- package-size --enforce +cargo test --test client_compat +cargo test --test runtime_smoke +cargo test --test extensions_smoke +``` + +The public `pg_dump` round-trip tests and asset/AOT hash-mismatch tests also +passed locally. + +## Cold-Start Performance Work + +Implemented: + +- internal phase timing via `capture_phase_timings`; +- `cargo run -p xtask -- perf cold` emits structured JSON with explicit + `cacheStateBefore`, `processStateBefore`, `rootState`, `queryState`, and + `workload` fields, so first-install bootstrap, process warmup, new-root first + query, and client/server first query are no longer conflated; +- `cargo run -p xtask -- perf cold --reset-cache` removes the pglite-oxide cache + before measuring, making runtime extraction, AOT materialization, PGDATA + template install, and extension-template creation visible in the first + operation that pays each cost; +- process-wide headless Wasmer engine cache; +- process-wide AOT `Module` cache keyed by artifact hash; +- AOT manifests now include raw artifact SHA256/size metadata; the default + startup path uses an atomic cache receipt and file metadata instead of scanning + the raw AOT file; +- bundled runtime, extension, PGDATA-template, and AOT content hashes are kept + off the default startup path and are only scanned with + `PGLITE_OXIDE_AOT_VERIFY=full`; +- process-wide shared Tokio runtime, WASIX runtime, and `SharedCache`; +- side-module seeding is reused by artifact name, module hash, Wasmer version, + Wasmer-WASIX version, and engine identity; +- phase timing now propagates into the server listener thread, so + `PgliteServer` cold runs report root preparation, listener bind/spawn, + proxy backend open, client connect, first query, and shutdown phases instead + of a single opaque total; +- server accept loops now use blocking `accept()` plus an explicit wake + connection during shutdown, removing the previous nonblocking accept plus + 10ms sleep polling jitter; +- fresh proxy backend initialization no longer runs the post-client + `ROLLBACK`/`DISCARD ALL` cleanup path. Fresh startup applies default GUCs + directly; full reset remains in place after client disconnects; +- persistent runtime asset cache under the platform cache directory; +- runtime-cache repair removes mutable scratch state and restores required + support files before the cache is used as a shared overlay source; +- per-root runtime scratch directories are reset during root preparation; +- `password` is copied as per-root mutable support data instead of hardlinked + from the shared runtime cache; +- PGDATA template manifests are parsed without archive hashing on the default + path; +- the parsed generated asset manifest is cached process-wide, avoiding repeated + 1.4 MB JSON parses during AOT, extension, and PGDATA template checks; +- an eager PGDATA template overlay is implemented as the mainline template + path: the cached initialized template is mounted as lower `/base`, the + per-instance upper starts almost empty, and individual template files are + copied into the upper only before mutating opens; +- the eager PGDATA overlay is passed as a runner-level WASIX mount. Nested + mounts placed inside the supplied `WasiFsRoot` were not sufficient because + `WasiRunner::prepare_webc_env` rebuilds the final mount tree from the root + `/` filesystem plus runner-owned mounts; +- direct `Pglite::open` no longer performs a separate session-setup round trip + and no longer folds session defaults into array discovery SQL. The Rust WASIX + host now calls the real C `ProcessStartupPacket` export from + `backend_startup.c`; C `pgl_sendConnData()` applies the direct-session + defaults before connection data is sent, so `BeginReportingGUCOptions` + observes `TimeZone=UTC` and `search_path=public`; +- `PgliteBuilder::postgres_config`, `PgliteServerBuilder::postgres_config`, + and `pglite-proxy --postgres-config name=value` now pass user startup GUCs + through PostgreSQL's normal `-c name=value` argv handling. User settings are + appended after the default profile, so they override defaults without + special-casing individual GUCs such as `synchronous_commit`; +- server-mode client startup `options=-c ...` is now applied on the C side after + `ProcessStartupPacket` parses the packet and before `pgl_sendConnData()` + emits `AuthenticationOk` and `ParameterStatus`, preserving PostgreSQL's + startup-option timing for supported single-backend clients; +- extension-enabled PGDATA template caches include the startup-GUC entries in + their manifest and cache key, so a template created under one backend config + is not reused for another config; +- direct scalar open/query paths no longer scan `pg_type` for array metadata. + Built-in PostgreSQL array OIDs are registered statically in the Rust direct + client, and runtime-created enum/domain/composite arrays are discovered + lazily from parameter/result OIDs or through explicit + `refresh_array_types()` calls; +- the old `pgl_stubs.h` `ProcessStartupPacket` placeholder has been removed + from the maintained WASIX patch. Startup packet parsing now lives in + PostgreSQL's `backend_startup.c`, and the host no longer calls a separate + Rust-side default-GUC helper; +- focused tests cover process AOT cache reuse, extension preload reuse, + cross-instance state isolation, mutable PGDATA clone safety, eager PGDATA + lower-file visibility, direct runtime smoke, vector direct/server smoke, and + proxy smoke. + +Previous local debug `xtask perf cold` run after explicit preload: + +- explicit preload: about 605ms; +- temporary first query: about 553ms; +- warm temporary first query: about 547ms; +- representative extension-backed first query after extension preload: about + 646ms; +- server plus first `tokio-postgres` query: about 543ms. + +In that run bundled archive/module SHA scans were absent from the default path. +The remaining visible costs were main Wasmer deserialization at about 447ms and +temporary filesystem setup at about 321ms, mostly runtime clone plus PGDATA +template clone. + +Latest local debug `cargo run -p xtask -- perf cold` run after the shared +root-preparation work: + +- explicit preload: about 640ms; +- temporary first query: about 404ms; +- warm temporary first query: about 386ms; +- representative extension-backed first query after extension preload: about + 504ms; +- server plus first `tokio-postgres` query: about 371ms. + +That run removed the full immutable runtime clone from temporary opens. The +same prepared runtime-layout machinery now feeds direct, persistent, app-id, +proxy, and server roots as well. Per-root runtime setup was about 30ms and +`wasix.mountfs_overlay_construct` was under 1ms at that point. The dominant +remaining setup cost was PGDATA template clone/install at about 187-190ms, +followed by +backend start around 44-48ms and Wasmer instance creation around 30-36ms. + +Latest local debug `cargo run -p xtask -- perf cold` run after the eager PGDATA +overlay and parsed-manifest cache: + +- explicit preload: about 601ms; +- temporary first query: about 191ms; +- warm temporary first query: about 144ms; +- representative extension-backed first query after extension preload: about + 257ms; +- server plus first `tokio-postgres` query: about 123ms. + +In that run `pgdata.overlay_prepare` was about 0.4-0.5ms, down from the +previous 187-190ms template clone/install cost. The visible per-open costs are now +Wasmer instance creation around 30-37ms and PostgreSQL backend start around +49-52ms. Main-module AOT deserialization remains the dominant explicit preload +cost at about 506ms on this local debug profile. + +Historical local debug run after removing the separate direct session-setup +round trip, before lazy/generated array metadata: + +- explicit preload: about 535ms; +- temporary first query: about 230ms; +- warm temporary first query: about 133ms; +- representative extension-backed first query after extension preload: about + 254ms; +- server plus first `tokio-postgres` query: about 118ms. + +The warm direct `pglite.open` phase dropped to about 112ms. At that point the +remaining direct-open client-side cost was the array catalog scan, about 30ms +for the warm catalog query and less than 1ms for Rust-side parser/serializer +registration. Scalar paths no longer pay that scan after lazy/generated array +metadata. + +Latest local release work: + +- asset release builds now default to `release-o3`, which compiles WASIX C + modules with `-O3 -g0 -flto=thin` and links with `-flto=thin`; +- release profiles run wasixcc's default Binaryen optimization plus + `--converge`, `--strip-debug`, and `--strip-producers`; +- the current exact PGlite speed-suite run favors `release-o3 + converge/strip` + plus ThinLTO for SQL workload parity. The package-size gate still passes + locally with the macOS arm64 AOT crate at about 7.2MiB compressed and the + asset crate at about 5.6MiB compressed. Earlier startup-only runs favored + `release-os` over `release-oz`, and adding a project `-msimd128` flag was + redundant because the WASIX EH+PIC sysroot already invokes clang with SIMD, + relaxed SIMD, and extended const enabled; +- Wasmer LLVM AOT codegen experiments selected the mainline serializer profile: + nonvolatile memory operations plus a readonly funcref table. Nonvolatile + memory operations improved the exact PGlite server SQLx speed suite by about + 9% geomean and won all 18 cases, but Wasmer marks that optimization as not + fully WebAssembly-spec compliant. Adding readonly funcref on top was about + 1.4% faster geomean than nonvolatile-only and improved indexed updates, but + regressed CREATE INDEX and DROP TABLE cases. The risk is now explicit release + profile surface and must be covered by the correctness matrix. The macOS + arm64 packaged AOT artifacts were regenerated with this profile; +- exact PGlite speed-suite comparison now has its own harness and diagnostic + path. The latest ThinLTO `release-o3` direct run on macOS arm64 measured test + 9 at about 569ms, test 10 at about 724ms, test 11 at about 98ms, and test 14 + at about 77ms. Against the locally audited npm NodeFS reference, the direct + suite is about 1.22x faster geomean, with 16/18 wins but not a 10x-class + result under identical SQL/Postgres semantics; +- selected speed-case diagnostics show that host filesystem work is not the + remaining dominant cost on the heavy SQL cases. Test 10, for example, was + about 748ms total with about 21ms in traced filesystem work and about 743ms + inside PostgreSQL/AOT dispatch. This points the next investigation at + symbolized AOT/Postgres executor profiling, not more Rust result parsing or + root-layout tuning; +- prepared indexed-update benchmarking now compares SQLx sequential prepared + updates, tokio-postgres sequential prepared updates over TCP and Unix + sockets, tokio-postgres pipelined prepared updates over TCP and Unix sockets, + and native Postgres equivalents using the exact PGlite Test 9/10 values. + Deferring extended-protocol `Sync` flush only within bytes already read from + one socket read reduced PgliteServer TCP pipelined prepared updates from about + `612.835ms -> 399.921ms` for numeric indexed updates and + `640.691ms -> 416.837ms` for text indexed updates. Unix-socket PgliteServer + was faster again at about 374/397ms, so transport still matters for + sequential prepared execution and modestly for pipelined execution. The exact + simple-query server speed suite stayed in the same range after the change: + Test 9 about 583ms and Test 10 about 740ms locally. A larger 256KiB proxy + read buffer was tested and rejected because it regressed the same pipelined + prepared workload to about 545/562ms; +- the native Postgres benchmark helper now attempts graceful termination before + falling back to `Child::kill()`, because SIGKILL can leak SysV shared-memory + IDs on macOS. `perf prepared-updates --skip-native` exists for Pglite-only + runs when local native Postgres IPC state is unhealthy; +- `perf prepared-updates --gate` now emits protocol counters and fails if + ordinary prepared traffic activates the backend-owned streaming continuation + or if pipelined prepared traffic stops batching. The timing thresholds are + intentionally a local regression smoke gate until stable CI runner baselines + exist; +- phase timing guards are hot-path no-ops when no recorder is active, so + diagnostic spans do not call `Instant::now()` in normal runtime traffic; +- PostgreSQL spinlocks are enabled in the WASIX build. The earlier + `--disable-spinlocks` fallback is gone, and the source-spine guard rejects it + if it returns. This is a correctness/architecture baseline because wasixcc + exposes the required atomic operations; local single-backend speed numbers are + mixed enough that it should not be treated as a standalone benchmark win; +- the shared runtime overlay and eager PGDATA overlay are now mainline runtime + behavior, with the old full-local runtime and full-template clone paths kept + only as internal build/staging machinery where still required; +- local release `cargo run -p xtask -- perf cold` with no env overrides showed + warmed preload around 18ms, temporary first query around 100ms, warm temporary + first query around 83ms, representative extension-backed first query around + 148ms after extension preload, and server first query around 77ms; +- that run predated lazy/generated array metadata and showed direct open + dominated by backend startup around 33-40ms plus the old array catalog scan + around 24-33ms. Scalar paths no longer pay that catalog scan; new release + numbers should replace this historical baseline; +- after adding deeper preload instrumentation, local release runs showed + explicit preload between about 15ms and 56ms depending on OS cache warmth. + The first uncached visible run spent about 37ms in main AOT mmap + deserialization and about 10ms in runtime cache setup; repeated warmed runs + spent about 10ms in main AOT deserialization for both mmap and file modes; +- Wasmer AOT loading now uses the native mmapped-file deserializer as the only + production path; the old file deserializer runtime switch was removed; +- after promoting the mainline AOT and filesystem paths, local release + `cargo run --release -p xtask -- perf cold` showed primary visible latencies + around 36ms for preload, 55ms for a new temporary direct first query, 45ms + for a second new temporary direct first query, 47ms for server SQLx first + query, and 57ms for server SQLx vector first query; +- the same mainline artifact profile measured exact PGlite server speed-suite + Test 9 at about 587ms, Test 10 at about 730ms, Test 11 at about 91ms, Test 14 + at about 71ms, and 18-test geomean around 76ms locally. Prepared-update + server probes measured TCP pipelined prepared updates around 395/414ms and + Unix pipelined prepared updates around 366/392ms for the numeric/text indexed + workloads; +- after static built-in arrays and lazy runtime array discovery, local release + `cargo run --release -p xtask -- perf cold` showed explicit preload about + 52ms, temporary first query about 88ms, warm temporary first query about 79ms, + representative extension-backed first query about 131ms after extension + preload, and server first query about 75ms. Scalar direct paths did not emit + the `pglite.array_type_catalog_query` phase; +- after server-thread timing and accept-loop cleanup, local release + `cargo run --release -p xtask -- perf cold` showed explicit preload about + 19-22ms, temporary first query about 86-89ms, warm temporary first query about + 77ms, representative extension-backed first query about 132-140ms after + extension preload, tokio-postgres server first query about 68ms, and SQLx + server first query about 68-70ms. The server path now shows `server.start` + around 52-54ms, + `proxy.backend_open` around 44-46ms, `postgres.backend_start` around 35-37ms, + tokio-postgres connect around 0.6ms/query around 5.5ms, and SQLx connect + around 2.1ms/query around 6.0ms; +- `xtask perf cold` includes the extension-enabled SQLx server path, now named + `process_warm_new_temp_server_sqlx_vector_first_query`, which starts + `PgliteServer` with a requested bundled extension and measures a first + extension-backed SQLx query for a new temporary server root. This keeps + server-mode extension install/load, `CREATE EXTENSION`, client connect, and + first extension query visible as one product-shaped path. The first local + release run measured about 175ms total, dominated by `proxy.extension_enable` + around 107ms; SQLx connect and the + first vector query were both sub-millisecond on that run; +- cold perf reporting now breaks out preload runtime cache setup, AOT install, + mmap/file deserialization, WASIX runtime construction, instance creation, + startup-packet/default-GUC work, client protocol round trips, extension side + module seeding, and public `pg_dump` runner phases; +- instrumented WASIX runtime artifacts can export C-side backend startup timers + via `pgl_backend_timing_elapsed_us`, and the Rust host records them as + `postgres.backend.c.*` phases when the export is present. Production WASIX + artifacts keep `PGLITE_OXIDE_WASIX_BACKEND_TIMING=0`, so the C timing macros + compile away and the export is absent. Local release instrumented runs show + backend startup split mainly between `postgres.backend.c.shared_memory` around + 11-12ms and `postgres.backend.c.init_postgres` around 19-21ms, inside + `postgres.backend.c.async_single_user_main` around 33-36ms; +- C-side timers now reach inside `InitPostgres`: `StartupXLOG`, + relcache/catcache initialization, transaction snapshot, session-user setup, + database lookup/recheck/path validation, `CheckMyDatabase`, startup option + processing, session initialization, and session preload libraries are reported + as individual `postgres.backend.c.*` phases; +- the C timing ABI has additional instrumented-only IDs for + `InitializeMaxBackends`, `CreateSharedMemoryAndSemaphores`, `InitProcess`, + `RelationCacheInitializePhase3`, and `initialize_acl`, so the two remaining + startup hotspots can be subdivided without adding production clock reads; +- a generic extension-set PGDATA template cache now builds templates through + normal `CREATE EXTENSION`, runs `CHECKPOINT`, then closes the embedded backend + through the runtime `pgl_shutdown` export before caching the template. The + cache is keyed by the base runtime/template manifest plus sorted extension + archive identities and is mounted as the lower PGDATA template for direct and + server temporary roots; +- direct and server extension paths skip redundant `CREATE EXTENSION` when the + requested extension set is already present in the cached template, while still + installing/preloading side-module assets into each instance root; +- extension-template cache keys were bumped to version 2 after adding clean + backend shutdown, so older templates that left `pg_control` in a + recovery-heavy state are ignored; +- current local release timings with the clean generic extension template cache + show extension-template lookup/overlay under 1ms, extension archive install + around 5ms, and extension-enabled `StartupXLOG` around 3-4ms instead of the + previous roughly 350ms recovery path. In the steady cached run, the direct + vector first-query path for a new temporary root was about 82-93ms and the + SQLx vector first-query path for a new temporary server root was about + 74-78ms; +- pure MountFS runtime composition now keeps core runtime assets in the shared + cached lower runtime and materializes only mutable state plus requested + extension assets in the per-root upper layer. Runtime and extension smoke + tests assert that core binaries/catalog files are not copied into the upper + root and unrelated extensions are not installed. Local release comparison + showed per-root runtime setup dropping from roughly 7ms to about 0.6-0.9ms, + the SQLx first-query path for a new temporary server root around 55ms, and + the SQLx vector first-query path for a new temporary server root around 66ms + after cache cleanup; +- cold perf operations now report `primaryLatencyPhase` and + `primaryLatencyMicros` so user-visible latency is separated from teardown. + The deeper local release run showed direct first-query totals were previously + inflated by a Rust-side host directory sync during query finish; +- direct `Pglite` no longer calls host directory `sync_all` after every + non-transaction query. PostgreSQL's WAL/fsync path owns durability, and the + server path already avoided this extra host sync. In the local release run, + direct visible latency dropped from about 68ms to about 53ms for the first + new temporary root and to about 45ms for the second new temporary root; +- direct and server protocol timing now splits startup packet handling, + protocol input/output, guest `PostgresMainLoopOnce`, direct parse/describe, + direct execute, and direct result finish. The remaining first-query protocol + cost is mostly PostgreSQL main-loop work for the parse/describe or prepared + extended-query batch, not Rust parsing or buffer copies; +- `cargo run -p xtask -- perf warm` now measures true warm behavior separately + from first-open work: repeated direct scalar queries, direct transaction + batches, direct extension-backed queries, SQLx repeated queries over one + connection, SQLx repeated connect-query-close cycles, SQLx extension-backed + repeated queries, and tokio-postgres repeated queries. It reports total and + per-iteration average phases while keeping open/shutdown phases as context; +- `cargo run --release -p xtask -- perf bench` now provides a product-style + benchmark harness similar to PGlite's published benchmark families. It runs + trimmed-average CRUD round-trip benchmarks and a generated SQLite + speedtest-style suite through both the direct Rust API and `PgliteServer` + with a long-lived SQLx connection. The speed suite is generated locally + instead of vendoring PGlite's multi-megabyte generated SQL files, and supports + `--suite`, `--mode`, `--iterations`, and `--scale` for local and CI runs; +- May 1, 2026 local release parity/timing run after pinning + `REL_17_5-pglite@01792c31` recorded raw JSON under `target/perf/`: + `cold-release-latest.json`, `warm-release-latest.json`, and + `bench-release-latest.json`; +- that cold release run used existing caches and production artifacts, so C-side + backend timers were absent by design. Primary visible latencies were: + preload 28.8ms, first direct temporary query 41.1ms, second direct temporary + query 30.0ms, vector preload 8.4ms, first direct vector query 36.8ms, + first tokio-postgres server query 31.4ms, first SQLx server query 31.9ms, + first SQLx server vector query 36.9ms, and first SQLx vector query on an + existing persistent root 25.8ms; +- dominant cold phases in that run were production runtime/AOT preload + (`aot.deserialize.mmap` 16.3ms), Wasmer instance creation for new roots + (about 5.3-10.2ms), backend start for template roots (about 18-24ms), and + first protocol dispatch/query work (about 4.4-6.1ms). Per-root runtime setup + stayed below the 1ms reporting threshold for scalar temporary roots; +- warm release run with 100 query iterations and 20 connect iterations showed: + direct scalar repeated query average 0.024ms, direct transaction batch average + 0.022ms, direct vector repeated query average 0.025ms, SQLx single-connection + query average 0.054ms, SQLx vector single-connection query average 0.058ms, + tokio-postgres single-connection query average 0.175ms, and SQLx + connect-query-close average 18.565ms; +- product-style benchmark run with `--suite all --mode all --iterations 100 + --scale 1` showed RTT trimmed averages from about 0.031-0.101ms for direct + CRUD cases and about 0.055-0.130ms for SQLx server CRUD cases. The generated + speed suite remained dominated by indexed updates: direct 25k indexed update + 4.390s, direct 25k text indexed update 8.024s, SQLx server 25k indexed update + 4.350s, and SQLx server 25k text indexed update 8.057s; +- follow-up parity work found that the WASIX host was starting single-user + Postgres with `shared_buffers=400kB`, while `@electric-sql/pglite@0.4.5` + reports `shared_buffers=128MB`. The fix moved the intended buffer GUCs into + the Rust startup arguments (`shared_buffers=128MB`, `wal_buffers=4MB`, + `min_wal_size=80MB`). The exact PGlite speed-source rerun now records local + all-suite direct timings around 570ms for Test 9, 732ms for Test 10, 106ms for + Test 11, and 86ms for Test 14; SQLx server timings were about 593ms, 726ms, + 102ms, and 83ms for the same tests. + `perf diagnose-buffer-cache` verifies zero Postgres shared read blocks for the + table-copy hotspots after setup, matching PGlite's effective buffer behavior; +- `xtask assets check` now guards production WASIX inputs for mandatory + WebAssembly exception and dynamic-linking flags and rejects Asyncify markers + in production configure scripts; +- production profile scripts reject Asyncify flag injection by default; the + explicit `PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT=1` override is reserved for + local snapshot/journaling experiments; +- final package sizes stayed under crates.io's 10 MB compressed limit: + `pglite-oxide` about 7.15 MB, `pglite-oxide-assets` about 4.87 MB, and + `pglite-oxide-aot-aarch64-apple-darwin` about 5.62 MB; +- `cargo test --release --workspace --all-targets`, + `cargo check --workspace --no-default-features --all-targets`, + `cargo run -p xtask -- assets check --strict-generated`, and + `cargo run -p xtask -- package-size --limit 10000000` passed against the + regenerated artifacts. + +## CI/CD And Release Workflow + +- validation now uses a DRY `scripts/validate.sh` entrypoint with explicit + modes for repository hygiene, linting, tests, examples, package checks, and + release dry-runs; +- CI classifies changed paths through `scripts/ci-scope.sh` so docs-only, + CI-only, test-only, package-affecting, and asset-affecting PRs can run the + right checks without forcing every maintainer change through release work; +- release intent checks now focus on published package surfaces + (`Cargo.toml`, `build.rs`, `src/**`, and `crates/**`) instead of forcing + docs, tests, examples, xtask-only, or source-build-script maintenance to use + release-producing PR titles; +- the manual Release workflow keeps the three maintainer operations: + `prepare-release-pr`, `publish-dry-run`, and `publish`, with job-scoped + permissions and Trusted Publishing through `id-token: write`; +- release-plz remains the release owner with one root changelog, one version + group, exact internal dependency versions, internal asset/AOT changes folded + into the root release notes, and bare SemVer tags for the user-facing root + release; +- the Assets workflow now uses production build inputs under + `assets/wasix-build`, the `release-o3` profile, one Linux/Docker portable + WASIX build job, and native AOT matrix jobs for macOS, Linux, and Windows; +- the portable WASIX build in the Assets workflow is now the artifact producer: + it builds generated runtime assets under `target/pglite-oxide/assets`, uploads + them with provenance, and feeds native AOT matrix jobs; +- normal CI now has a Rust-only native AOT runtime matrix that downloads the + latest compatible Assets workflow bundle, verifies the asset-input + fingerprint, installs generated artifacts into ignored paths, and runs the + runtime test suite on macOS arm/x64, Linux arm/x64, and Windows x64; +- asset and AOT crates are source-only in git; release jobs download generated + portable and AOT workflow artifacts for the exact SHA, stage them into crate + skeletons, package-check that generated workspace, and publish with + release-plz dirty-publish support; +- dependency invariant checks now block Wasmtime/static-WASI regressions and + backend compiler crates such as LLVM/Cranelift/Singlepass from entering the + normal user dependency tree; +- the public dependency graph now uses Cargo target-specific dependencies for + AOT packs, so a normal `pglite-oxide` install resolves the target-independent + `pglite-oxide-assets` crate plus only the current platform's + `pglite-oxide-aot-*` crate; +- source-only `scripts/validate.sh test` no longer pretends runtime coverage + happened when AOT artifacts are absent. `scripts/validate.sh runtime` is now + the hard runtime gate and requires portable assets plus the host AOT pack; +- `.github/scripts/download-aot-artifacts.sh` is a thin wrapper over + `xtask assets download`; exact-SHA, latest-compatible, host-target, and + all-target artifact downloads share one implementation; +- AOT serialization is now owned by a maintainer-only `xtask` feature. The + normal runtime tree keeps headless Wasmer loading, while + `xtask --features aot-serializer` is the only path that enables Wasmer LLVM; +- the Assets workflow now probes the LLVM AOT serializer before full AOT + generation, validates generated portable assets before AOT work, smokes the + target runtime before packaging/upload, and fails on empty/missing AOT + manifests instead of uploading placeholder crates; +- `wasmer-wasix` is now explicitly feature-minimized for the runtime path + (`sys-minimal`, `sys-poll`, `host-vnet`, and `time`). The root dependency gate + rejects Wasmtime, backend compiler crates, Cranelift/Singlepass, LLVM, and + broad HTTP/TLS stacks such as `reqwest`, `hyper`, and `rustls`; +- normal CI cache writes are limited to `main` while PRs still restore existing + Rust caches. Release and AOT-heavy jobs opt into cache writes explicitly. diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md new file mode 100644 index 00000000..d36cd272 --- /dev/null +++ b/docs/EXTENSIONS.md @@ -0,0 +1,164 @@ +# Extensions + +Bundled SQL extensions are enabled explicitly. The runtime installs only the +extension assets each database asks for. + +The public extension API is available through the default feature set. If you +disable default features, enable `extensions`; it currently implies `bundled` +because extension constants are backed by packaged, smoke-tested extension +payloads. + +## Enable Extensions At Open Time + +The builder path is the easiest option and resolves bundled extension +dependencies before the database opens. + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .extension(extensions::PG_TRGM) + .open()?; + db.close()?; + Ok(()) +} +``` + +You can also add multiple extensions at once: + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::builder() + .temporary() + .extensions([extensions::HSTORE, extensions::LTREE, extensions::UNACCENT]) + .open()?; + db.close()?; + Ok(()) +} +``` + +## Enable Extensions After Open + +Use `enable_extension(...)` when you want to install an extension into an +already-open direct database: + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + db.enable_extension(extensions::VECTOR)?; + db.close()?; + Ok(()) +} +``` + +For dependency-heavy extensions, prefer the builder path. Builder requests are +resolved as a set before open, while `enable_extension(...)` installs the +extension you name into the current root. + +## Preload Extension Artifacts + +Use `preload_extensions(...)` when an extension-backed first query sits on a hot +path: + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +fn main() -> Result<(), Box> { + Pglite::preload_extensions([extensions::VECTOR, extensions::PG_TRGM])?; + Ok(()) +} +``` + +## Server And CLI Usage + +Extensions work in server mode too: + +```rust,no_run +use pglite_oxide::{extensions, PgliteServer}; + +fn main() -> Result<(), Box> { + let server = PgliteServer::builder() + .temporary() + .extension(extensions::VECTOR) + .start()?; + server.shutdown()?; + Ok(()) +} +``` + +The proxy CLI accepts SQL extension names: + +```sh +pglite-proxy --temporary --extension vector --extension pg_trgm --print-uri +``` + +## Available Bundled Extensions + +Current public constants: + +- `extensions::AGE` +- `extensions::AMCHECK` +- `extensions::AUTO_EXPLAIN` +- `extensions::BLOOM` +- `extensions::BTREE_GIN` +- `extensions::BTREE_GIST` +- `extensions::CITEXT` +- `extensions::CUBE` +- `extensions::DICT_INT` +- `extensions::DICT_XSYN` +- `extensions::EARTHDISTANCE` +- `extensions::FILE_FDW` +- `extensions::FUZZYSTRMATCH` +- `extensions::HSTORE` +- `extensions::INTARRAY` +- `extensions::ISN` +- `extensions::LO` +- `extensions::LTREE` +- `extensions::PAGEINSPECT` +- `extensions::PG_BUFFERCACHE` +- `extensions::PG_FREESPACEMAP` +- `extensions::PG_HASHIDS` +- `extensions::PG_IVM` +- `extensions::PG_SURGERY` +- `extensions::PG_TEXTSEARCH` +- `extensions::PG_TRGM` +- `extensions::PG_UUIDV7` +- `extensions::PG_VISIBILITY` +- `extensions::PG_WALINSPECT` +- `extensions::PGTAP` +- `extensions::SEG` +- `extensions::TABLEFUNC` +- `extensions::TCN` +- `extensions::TSM_SYSTEM_ROWS` +- `extensions::TSM_SYSTEM_TIME` +- `extensions::UNACCENT` +- `extensions::VECTOR` + +`extensions::ALL` is the slice of all currently public bundled extensions. +`extensions::by_sql_name(...)` resolves a bundled extension constant from its SQL +name, for example `"vector"` or `"pg_trgm"`. + +## Not Currently Available + +The generated extension catalog currently tracks additional candidates that are +not part of the bundled public surface: + +- `pgcrypto` +- `uuid-ossp` +- `postgis` + +They are not in `extensions::ALL` and do not have public constants in the +current asset set. + +## Safety And Install Behavior + +Bundled extension archives are installed into the database root before their SQL +setup runs. Archive extraction is path-safe and validated against the packaged +asset manifest. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md new file mode 100644 index 00000000..84cacd38 --- /dev/null +++ b/docs/PERFORMANCE.md @@ -0,0 +1,109 @@ +# Performance + +`pglite-oxide` is built to stay close to native Postgres while keeping the +database embedded in the Rust process. + +This page tracks the repo benchmark matrix. The main comparison uses SQLx on +each wire-protocol path: + +- native Postgres with SQLx; +- `pglite-oxide + SQLx`; +- vanilla `@electric-sql/pglite` persisted with NodeFS and reached through + `@electric-sql/pglite-socket`, then measured with SQLx. + +## Snapshot + +Snapshot run: `20260507T113000Z` + +Environment: + +- OS: `macOS 26.4.1 (Darwin 25.4.0 arm64)` +- CPU: `Apple M1 Pro` +- RAM: `16 GB` +- Logical cores: `10` +- Node: `v24.13.0` +- Node packages: `@electric-sql/pglite@0.4.5`, + `@electric-sql/pglite-socket@0.1.5` +- Native Postgres: `18.3 (Homebrew)` +- Oxide Wasmer: `7.2.0-alpha.2` +- Oxide Wasmer WASIX: `0.702.0-alpha.2` +- RTT iterations: `100` +- Speed source: exact upstream SQL from + `assets/checkouts/pglite/packages/benchmark/src` + +Every mode was run serially. + +## Representative Operations + +Lower is better. + +| Operation | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx | +|---|---:|---:|---:| +| 25,000 INSERTs in one transaction | 132.36 ms | 149.54 ms | 257.02 ms | +| 25,000 INSERTs in one statement | 46.14 ms | 59.39 ms | 117.19 ms | +| 25,000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | +| 5,000 indexed SELECTs | 81.39 ms | 125.31 ms | 203.05 ms | +| 25,000 indexed UPDATEs | 351.05 ms | 578.96 ms | 720.63 ms | + +## Full Operation Table + +| ID | Test | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx | +|---|---|---:|---:|---:| +| 1 | Test 1: 1000 INSERTs | 9.13 ms | 19.76 ms | 15.66 ms | +| 2 | Test 2: 25000 INSERTs in a transaction | 132.36 ms | 149.54 ms | 257.02 ms | +| 2.1 | Test 2.1: 25000 INSERTs in single statement | 46.14 ms | 59.39 ms | 117.19 ms | +| 3 | Test 3: 25000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | +| 3.1 | Test 3.1: 25000 INSERTs into an indexed table in single statement | 66.41 ms | 95.12 ms | 93.88 ms | +| 4 | Test 4: 100 SELECTs without an index | 107.63 ms | 162.89 ms | 242.03 ms | +| 5 | Test 5: 100 SELECTs on a string comparison | 305.38 ms | 338.01 ms | 434.63 ms | +| 6 | Test 6: Creating indexes | 9.94 ms | 13.08 ms | 17.12 ms | +| 7 | Test 7: 5000 SELECTs with an index | 81.39 ms | 125.31 ms | 203.05 ms | +| 8 | Test 8: 1000 UPDATEs without an index | 47.91 ms | 74.42 ms | 103.66 ms | +| 9 | Test 9: 25000 UPDATEs with an index | 351.05 ms | 578.96 ms | 720.63 ms | +| 10 | Test 10: 25000 text UPDATEs with an index | 471.74 ms | 712.38 ms | 858.95 ms | +| 11 | Test 11: INSERTs from a SELECT | 65.64 ms | 97.43 ms | 112.87 ms | +| 12 | Test 12: DELETE without an index | 7.54 ms | 9.74 ms | 11.69 ms | +| 13 | Test 13: DELETE with an index | 9.31 ms | 26.58 ms | 27.7 ms | +| 14 | Test 14: A big INSERT after a big DELETE | 53 ms | 71.6 ms | 87.72 ms | +| 15 | Test 15: A big DELETE followed by 12000 small INSERTs | 58.98 ms | 74.49 ms | 112.18 ms | +| 16 | Test 16: DROP TABLE | 3.43 ms | 10.17 ms | 6.74 ms | + +## Reproduce + +Run the serial matrix: + +```sh +scripts/perf/run_bench_matrix.sh +``` + +That command runs: + +1. `pglite-oxide + SQLx` RTT + speed benchmarks +2. native Postgres + SQLx RTT + speed benchmarks +3. vanilla PGlite + SQLx RTT + speed benchmarks +4. a markdown comparison report + +Outputs land under `target/perf/`: + +- `bench-oxide-.json` +- `bench-native-postgres-sqlx-.json` +- `bench-pglite-nodefs-sqlx-.json` +- `bench-pglite-nodefs-sqlx-ready-.json` +- `bench-comparison-.md` + +Override the native Postgres binaries when needed: + +```sh +PGLITE_OXIDE_NATIVE_POSTGRES=/path/to/postgres \ +PGLITE_OXIDE_NATIVE_INITDB=/path/to/initdb \ +scripts/perf/run_bench_matrix.sh +``` + +## Reading The Matrix + +- `pglite-oxide + SQLx` is the product-style path for apps that connect through + standard Postgres clients. +- `vanilla PGlite + SQLx` keeps upstream PGlite on NodeFS, but uses the same Rust + SQLx client path as the other wire-protocol rows. +- These are machine-local numbers. Re-run the matrix before quoting them in a + release note or public comparison. diff --git a/docs/PERFORMANCE_INTERNAL.md b/docs/PERFORMANCE_INTERNAL.md new file mode 100644 index 00000000..705b8742 --- /dev/null +++ b/docs/PERFORMANCE_INTERNAL.md @@ -0,0 +1,309 @@ +# Performance Internals + +This page is maintainer documentation for performance tuning, measurement +harnesses, and release profiling. Public benchmark results now live in +`docs/PERFORMANCE.md`. + +`pglite-oxide` is optimized for test setup and local-app startup. The runtime +avoids user-side compilation: supported targets load packaged Wasmer AOT +artifacts and reuse cached runtime files. + +## Fast Startup Practices + +For test suites: + +- use `Pglite::temporary()` or `PgliteServer::temporary_tcp()`; +- reuse the process when possible so the template and module caches stay warm; +- keep Postgres client pools at one connection; +- call `Pglite::preload()` once before a visible UI path or a large test group; +- call `Pglite::preload_extensions([...])` when extension setup is on the hot + path. + +Example: + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +fn main() -> Result<(), Box> { + Pglite::preload_extensions([extensions::VECTOR])?; + + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + + db.exec("CREATE TABLE items (embedding vector(3))", None)?; + Ok(()) +} +``` + +## Cache Layers + +The runtime uses several cache layers: + +- a process cache for loaded modules; +- a persistent AOT artifact cache; +- a runtime asset cache for immutable files; +- an extension asset cache; +- a template PGDATA cache for roots that use template initialization; +- an eager PGDATA template overlay that avoids cloning the whole initialized + template before first query. + +The older full-local path hardlinks immutable files into database roots when +the filesystem supports it, then falls back to copying when linking is +unavailable. The default path avoids that per-root immutable-file population. + +## Default Filesystem Fast Path + +By default, database roots use Wasmer filesystem composition. Fresh roots use a +pure mount-composition layout: immutable runtime files are served from the +shared cached lower runtime, while the per-root upper layer contains only +mutable state, device/tmp files, and the extension assets explicitly requested +for that root. The same prepared layout is used by direct databases, persistent +paths, app-id paths, fresh and cached temporary databases, proxy roots, and +local server mode. + +The eager PGDATA template overlay is also enabled by default. It mounts the +cached initialized template as the lower `/base` filesystem and starts each +database with a tiny per-instance upper directory. When PostgreSQL opens a +template-backed file for mutation, the runtime copies that one file into the +upper directory before opening it. + +This is intentionally not a pre-provisioned pool: each database root is still +created on demand and owns its mutable files. In local release runs, runtime +composition is now about 0.6-0.9ms, down from roughly 7ms for the previous +per-root asset population. PGDATA setup is under 1ms. The remaining direct +first-query costs are mostly PostgreSQL backend startup, Wasmer instance +creation, and the protocol roundtrip for the query itself. + +Direct `Pglite::open` no longer runs a separate session-setup query. Direct +session defaults are applied during startup before connection data is sent, not +through SQL. The regenerated WASIX runtime owns this as a required +`pgl_apply_default_gucs` bridge helper. + +Direct `Pglite` no longer forces a host directory `sync_all` after every +non-transaction query. PostgreSQL's own WAL/fsync behavior owns durability; the +extra Rust-side directory sync was expensive and weaker than file-level database +fsyncs. This matches the server path, which did not pay that cost. + +Direct `Pglite` also no longer scans `pg_type` on scalar open/query paths. +Built-in PostgreSQL array OIDs are registered statically in the Rust direct +client. Runtime-created enum/domain/composite arrays are discovered lazily when +they appear in direct API parameters or result metadata, or explicitly through +`Pglite::refresh_array_types()`. + +The WASIX startup arguments explicitly preserve PGlite's effective buffer +profile: `shared_buffers=128MB`, `wal_buffers=4MB`, and `min_wal_size=80MB`. +This matters for PGlite benchmark parity. Without those GUCs, single-user +startup fell back to a tiny `shared_buffers=400kB`, causing table-copy and +indexed-update workloads to reread relation pages from the host filesystem. + +Detailed C-side backend startup timers are an instrumented-build diagnostic, not +production runtime surface. Build WASIX assets with +`PGLITE_OXIDE_WASIX_BACKEND_TIMING=1` when investigating `shared_memory`, +`InitPostgres`, or relcache work. Production WASIX artifacts leave that flag off, +so timing macros compile away and the `pgl_backend_timing_elapsed_us` export is +absent. + +Nested child mounts inside a supplied `WasiFsRoot` were tested first. They were +rejected because Wasmer's `WasiRunner::prepare_webc_env` rebuilds the final +mount tree from the supplied `/` filesystem plus runner-owned mounts, so child +mounts must be passed as runner mounts or represented inside the root +filesystem itself. + +## Release Asset Profile + +The default asset release profile is `release-o3`: WASIX C modules are compiled +with `-O3 -g0 -flto=thin`, linked with `-flto=thin`, then Binaryen runs with the +wasixcc default optimization level plus `--converge`, `--strip-debug`, and +`--strip-producers`. This is the current SQL-workload profile: local PGlite +benchmark parity runs showed broad speed wins over non-LTO O3 and `release-os`, +and package-size checks still stayed comfortably under crates.io limits. + +Available profile knobs: + +- `PGLITE_OXIDE_BUILD_PROFILE=release-o3` is the default release asset profile; +- `release`, `release-o3`, `release-os`, and `release-oz` remain available for + comparison builds. `release-o3` is the performance profile and includes + ThinLTO by default; +- set `PGLITE_OXIDE_WASM_OPT_FLAGS=none` to disable the release-profile + Binaryen converge/strip extras for local build iteration; +- set `PGLITE_OXIDE_WASM_OPT_FLAGS=''` to override the + release-profile Binaryen extras. + +The WASIX toolchain already enables the relevant Wasm feature baseline for this +EH+PIC sysroot, including SIMD, relaxed SIMD, and extended const. Adding an +extra `-msimd128` did not change the generated AOT artifact sizes in the local +release experiment, so it is not carried as a project-specific flag. + +Wasmer LLVM AOT is generated with the selected mainline codegen profile: +nonvolatile memory operations and a readonly funcref table. Local exact PGlite +speed-suite measurements showed nonvolatile memory operations improving the +server SQLx suite by about 9% geomean. Adding the readonly funcref table on top +was about 1.4% faster geomean than nonvolatile-only and improved the indexed +update cases (`557.152ms -> 534.737ms` and `695.663ms -> 681.778ms`), while +regressing CREATE INDEX and DROP TABLE cases. Wasmer documents nonvolatile +memory operations as faster but not fully WebAssembly-spec compliant; this is a +conscious mainline runtime-profile decision for the packaged single-process +Postgres runtime and must stay covered by the correctness matrix. + +WebAssembly exceptions are mandatory for production artifacts. The Postgres +runtime depends on exception/longjmp recovery across the main module and side +modules, so there is no supported non-EH fallback and no opt-out flag. Asyncify +is not part of production builds; it may only be used in an isolated +snapshot/journaling experiment if a specific restore design proves it needs +that control-flow model. The build scripts reject Asyncify flags by default; +`PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT=1` is reserved for local experiment +branches only. + +WASIX dynamic linking is also mandatory. The main module is built as a +dynamic-main module, extension/tool modules are PIC side modules, and all +runtime, extension, and `pg_dump` artifacts must come from the same configured +source tree. + +## Native Deserialization + +The runtime loads Wasmer AOT artifacts through Wasmer's native mmapped-file +deserializer. This keeps the startup path off the old read-the-whole-native- +artifact path and does not reintroduce full artifact hashing. There is no +runtime opt-out for the older file deserializer. + +## Strict Verification + +By default, startup avoids content-hashing bundled assets. Cached Wasmer AOT +artifacts use fast receipt verification: the runtime checks the cache receipt +and file metadata, then lets Wasmer deserialize the cached native artifact. If +deserialization fails, the cache entry is deleted, rebuilt once from the bundled +artifact, and retried. + +Set `PGLITE_OXIDE_AOT_VERIFY=full` to force full SHA-256 verification of cached +AOT files, bundled runtime archives, bundled extension archives, PGDATA template +archives, and runtime/template module matches. This is useful for debugging +cache corruption or CI integrity checks, but it adds cold-start latency and is +not the default. + +## Snapshot And Journal Work + +Wasmer 7.2 exposes WASIX journal/process snapshot APIs, and `StoreSnapshot` +captures store globals. That is not enough by itself to ship an instant restore +path for Postgres: a promoted design must prove correctness for PGDATA state, +mount state, file descriptors, direct protocol state, server mode, extensions, +and `pg_dump`. This remains a first-class performance track, but it must beat +the current template/overlay path while passing the same runtime and extension +suite before it becomes default. + +## Measuring Locally + +The smoke benchmark prints preload and open timings: + +```sh +cargo test --test performance_smoke -- --nocapture +``` + +To measure the current cold-start path: + +```sh +cargo run -p xtask -- perf cold +``` + +This runs operations sequentially in one process. Each operation reports +`cacheStateBefore`, `processStateBefore`, `rootState`, `queryState`, and +`workload`, so a "first query" is explicitly the first query for that +operation's newly opened root/server, not necessarily a cold cache or cold +process. Each operation also reports `primaryLatencyPhase` and +`primaryLatencyMicros`; this is the user-visible latency target for that +operation and excludes cleanup/teardown where appropriate. + +To include first-install cache bootstrap costs in the first measured preload: + +```sh +cargo run -p xtask -- perf cold --reset-cache +``` + +To measure true warm behavior after startup, use the warm harness: + +```sh +cargo run -p xtask -- perf warm +``` + +It keeps databases/servers alive and measures repeated direct queries, +transactions, SQLx/tokio-postgres queries, repeated SQLx connections, and +extension-backed queries separately from open and shutdown phases. Use +`--iterations N` and `--connections N` for shorter local probes. + +To run product-style SQL benchmarks similar to PGlite's published benchmark +families: + +```sh +cargo run --release -p xtask -- perf bench +``` + +This emits JSON with two benchmark suites: + +- `rtt`: PGlite-style CRUD round-trip microbenchmarks. Each query runs many + times, the lowest and highest 10% are discarded when enough samples exist, + and the trimmed average is reported. +- `speed`: a generated SQLite speedtest-style SQL suite with large insert, + select, update, index, delete, and drop workloads. + +The RTT suite can run through the direct Rust API, through `PgliteServer` with a +single long-lived SQLx connection, and through `PgliteServer` with a raw +`tokio-postgres` simple-query-protocol connection. The raw `tokio-postgres` +mode is there to separate proxy/wire overhead from SQLx client overhead: + +```sh +cargo run --release -p xtask -- perf bench --suite rtt --mode server-sqlx +cargo run --release -p xtask -- perf bench --suite rtt --mode server-tokio-postgres-simple +cargo run --release -p xtask -- perf bench --suite speed --mode direct --scale 0.05 +cargo run --release -p xtask -- perf bench --suite speed --speed-source pglite +``` + +The speed suite is generated locally instead of vendoring PGlite's generated +multi-megabyte SQL files. Use `--scale` for quick local probes and `--scale 1` +for the full default shape. Use `--speed-source pglite` when you need exact +parity with the SQL files checked out under +`assets/checkouts/pglite/packages/benchmark/src`; this mode requires +`--scale 1`. + +To compare simple-query indexed updates against parameterized prepared updates +and client pipelining: + +```sh +cargo run --release -p xtask -- perf prepared-updates +cargo run --release -p xtask -- perf prepared-updates --skip-native +cargo run --release -p xtask -- perf prepared-updates --skip-native --gate +``` + +This parses the exact update values from PGlite benchmark Tests 9 and 10, uses +the same indexed-table setup, and measures SQLx sequential prepared execution, +tokio-postgres sequential prepared execution, tokio-postgres pipelined prepared +execution over TCP and Unix sockets, and the same tokio-postgres modes against +native Postgres. Use `--skip-native` when local native Postgres IPC state is not +healthy or when only PgliteServer modes are needed. This is a server/protocol +benchmark; it does not replace the exact PGlite simple-query suite. + +`--gate` is a local regression smoke gate, not a final CI performance oracle. +It checks the transport shape that caused the COPY/prepared-update regression: +non-COPY prepared traffic must not activate the backend-owned streaming +continuation, pipelined prepared traffic must remain batched, SQLx and +sequential tokio-postgres must stay below 5s per 25k updates, and pipelined +tokio-postgres must stay below 1.5s per 25k updates. The command emits per-run +protocol counters so failures show whether the problem is batching, protocol +pump activation, or backend execution. + +For focused investigation of indexed update hotspots, run: + +```sh +cargo run --release -p xtask -- perf diagnose-indexed-update +cargo run --release -p xtask -- perf diagnose-buffer-cache +``` + +This opens fresh temporary databases, runs setup outside the measured section, +then compares exact PGlite Test 9/10 SQL against controlled variants: lookup +index only, unlogged table, text update after numeric update, vacuumed variants, +and one set-based update. The buffer-cache diagnostic runs +`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` for the remaining table-copy hotspots +and reports the effective Postgres memory GUCs plus host filesystem trace data. + +Treat these numbers as machine-local diagnostics. CI performance gates and +release targets depend on the runner, host filesystem, and cache state. diff --git a/docs/PG_DUMP.md b/docs/PG_DUMP.md new file mode 100644 index 00000000..2e90660b --- /dev/null +++ b/docs/PG_DUMP.md @@ -0,0 +1,155 @@ +# Dump, Restore, And Upgrade + +`pglite-oxide` ships a bundled WASIX `pg_dump` path behind the `extensions` +feature, which is enabled by default. Use it for portable SQL exports, +restores, and version-to-version upgrades. + +## Choose The Right Export Format + +Use logical dumps when you need: + +- a portable SQL export; +- an upgrade path between `pglite-oxide` releases; +- a way to move data between different roots safely. + +Use physical data-dir archives when you need: + +- a same-version clone; +- a same-runtime restore into another `pglite-oxide` root; +- a fast local snapshot of the current cluster state. + +Physical archives are not a cross-version upgrade path. + +## Direct API + +Dump an already-open `Pglite` database to SQL: + +```rust,no_run +use pglite_oxide::{PgDumpOptions, Pglite}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + db.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let sql = db.dump_sql(PgDumpOptions::new())?; + assert!(sql.contains("INSERT INTO")); + + db.close()?; + Ok(()) +} +``` + +Get UTF-8 bytes instead: + +```rust,no_run +use pglite_oxide::{PgDumpOptions, Pglite}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + let bytes = db.dump_bytes(PgDumpOptions::new())?; + assert!(!bytes.is_empty()); + db.close()?; + Ok(()) +} +``` + +Direct dumps run against the already-open embedded backend. If you need to dump +as a different user or from a different database, start a `PgliteServer` and +use the server dump path instead. + +## Server API + +Dump through a local Postgres endpoint when another part of your workflow +already uses `PgliteServer`: + +```rust,no_run +use pglite_oxide::{PgDumpOptions, PgliteServer}; + +fn main() -> Result<(), Box> { + let server = PgliteServer::temporary_tcp()?; + let sql = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + assert!(!sql.is_empty()); + server.shutdown()?; + Ok(()) +} +``` + +`PgliteServer::dump_sql(...)` currently requires a TCP endpoint. + +## `PgDumpOptions` + +`PgDumpOptions` controls the managed parts of the dump command: + +```rust,no_run +use pglite_oxide::PgDumpOptions; + +let options = PgDumpOptions::new() + .username("postgres") + .database("template1") + .args(["--schema-only", "--quote-all-identifiers"]); +``` + +Useful passthrough flags include dump-shaping options such as: + +- `--schema-only` +- `--quote-all-identifiers` +- `-n ` +- `-t ` + +Managed connection and output flags are reserved by the API. Do not pass +`--file`, `--format`, `--host`, `--port`, `--username`, `--dbname`, or `--jobs` +through `arg(...)` or `args(...)`. + +## CLI + +Dump a persistent root: + +```sh +pglite-dump --root ./.pglite +``` + +Pass through normal `pg_dump` shaping flags after `--`: + +```sh +pglite-dump --root ./.pglite -- --schema-only +pglite-dump --root ./.pglite -- --quote-all-identifiers +``` + +## Restore + +Restore a logical dump by executing the SQL against a new database: + +```rust,no_run +use pglite_oxide::{PgDumpOptions, Pglite}; + +fn main() -> Result<(), Box> { + let mut source = Pglite::temporary()?; + source.exec("CREATE TABLE items(value TEXT)", None)?; + source.exec("INSERT INTO items VALUES ('alpha')", None)?; + let dump_sql = source.dump_sql(PgDumpOptions::new())?; + + let mut restored = Pglite::temporary()?; + restored.exec(&dump_sql, None)?; + + source.close()?; + restored.close()?; + Ok(()) +} +``` + +For same-version root copies, prefer `dump_data_dir()` / +`load_data_dir_archive(...)` or `try_clone()`. + +## Upgrade Guidance + +Use logical dump and restore when upgrading between `pglite-oxide` versions or +changing packaged runtime assets: + +1. Open the old database with the old crate/runtime. +2. Create a logical dump with `dump_sql(...)` or `pglite-dump`. +3. Open a fresh database with the new crate/runtime. +4. Execute the dump SQL into the new database. + +Do not treat physical data-dir archives as a general upgrade mechanism. They are +for the same runtime family and database format. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 04b1e52f..7b0507fb 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,4 +1,45 @@ -# Release Process +# Release Process (Maintainers) + +This page is maintainer documentation for versioning, release CI, and crates.io +publishing. It is not part of the end-user documentation path. + +Release automation is workspace-aware. `release-plz` owns version bumps for the +root crate, `pglite-oxide-assets`, and every `pglite-oxide-aot-*` crate. +Feature PRs should not edit package versions directly. + +The root crate is the only user-facing release and changelog. Asset and AOT +crate changes are included in the root `CHANGELOG.md`, while those internal +crates do not create separate GitHub releases or tags. The public Git tag stays +the bare SemVer version, for example `0.4.0`, because the internal crates are +implementation details. + +Before publishing, CI packages every published crate and enforces crates.io's +10 MB compressed `.crate` limit. + +Releases use source-controlled inputs plus CI-generated portable WASIX and AOT +artifacts. The git repo intentionally does not commit portable runtime blobs or +native AOT binaries. The release workflow first verifies source pins, asset +input fingerprints, extension metadata, and crate templates; then it requires a +successful `Assets` workflow for the same SHA, downloads the generated portable +and AOT artifacts, stages them into a clean release workspace, package-checks +that workspace, and only then runs release-plz. + +The release source of truth is the exact Assets workflow output for the release +SHA. Portable WASIX artifacts are built from pinned sources, native AOT +artifacts are generated from that portable asset set, and release validation +checks package contents, target triples, Wasmer versions, runtime hashes, and +crate sizes before publishing. The release workflow runs source/lint/example +checks before artifact download, then reruns the Rust test gate after artifact +installation so the release host executes against materialized native artifacts +instead of compile-only tests. + +Normal CI and release CI split responsibilities deliberately. Rust-only changes +download the latest compatible Assets workflow bundle, verify its asset-input +fingerprint, and run native AOT runtime tests on the supported host matrix. +Asset-producing changes run the `Assets` workflow, which rebuilds portable +WASIX from pinned sources before generating and smoking the target AOT packs. +The release workflow refuses to publish unless the generated portable and AOT +artifacts for the exact release SHA are downloaded, staged, and package-checked. `pglite-oxide` publishes source crates to crates.io with release-plz. The CLI binaries in this repository are maintenance helpers, so the release path @@ -7,16 +48,21 @@ user-facing binary to distribute. ## One-time setup -- Ensure the crate owner has crates.io publish rights for `pglite-oxide`. -- The crate already exists on crates.io. Future releases use crates.io Trusted - Publishing. Configure `f0rr0/pglite-oxide`, workflow - `.github/workflows/release.yml`, and environment `crates-io` in the crates.io - trusted publisher settings. +- Ensure the crate owner has crates.io publish rights for `pglite-oxide`, + `pglite-oxide-assets`, and every `pglite-oxide-aot-*` crate. +- Configure crates.io Trusted Publishing for every published crate. Use + repository `f0rr0/pglite-oxide`, workflow `.github/workflows/release.yml`, + and environment `crates-io`. - Do not configure `CARGO_REGISTRY_TOKEN`; the release workflow relies on the GitHub OIDC token granted by `id-token: write`. - Repository Actions settings must allow GitHub Actions to create pull requests. -- The `Release` workflow needs `contents: write`, `pull-requests: write`, and - `id-token: write`; these are already declared in the workflow. +- The `Release` workflow uses job-scoped permissions. The release-PR job needs + `contents: write` and `pull-requests: write`; the publish job needs + `contents: write` and `id-token: write`. +- If release PRs should run normal PR CI automatically, configure a + `RELEASE_PLZ_TOKEN` secret backed by a GitHub App or maintainer bot token. + Without it, release-plz falls back to `GITHUB_TOKEN`; GitHub does not trigger + normal PR workflows from PRs opened by that token. - Do not set `package.publish = ["crates-io"]`; crates.io is Cargo's default registry, and release-plz treats `package.publish` entries as named alternate registries. @@ -33,16 +79,32 @@ release-affecting package files must use one of these PR title types: - `revert:` for reverted release-affecting changes - any type with `!` for breaking changes -Docs, CI, issue-template, and other repository-only changes may use non-release -types such as `docs:`, `ci:`, `chore:`, `style:`, or `test:`. The CI release -intent check treats these paths as release-affecting: `Cargo.toml`, `Cargo.lock`, -`build.rs`, `src/**`, `assets/**`, `examples/**`, and `benches/**`. +Docs, CI, issue-template, tests, examples, xtask-only maintenance, source +checkout scripts, and other repository-only changes may use non-release types +such as `docs:`, `ci:`, `chore:`, `style:`, or `test:`. The CI release intent +check treats these paths as release-affecting: `Cargo.toml`, `build.rs`, +`src/**`, and `crates/**`. Package version bumps are release-plz owned. Feature and fix PRs may change -package code, dependencies, and assets, but they must not change the root -`Cargo.toml` package version. The version bump and matching `CHANGELOG.md` +package code, dependencies, and generated assets, but they must not change +workspace package versions. The version bump and matching `CHANGELOG.md` section must come from a `release-plz-*` PR titled `chore(release): ...`. +## Maintainer paths + +- Docs-only and repository-only PRs run the lightweight repository hygiene and + workflow checks. They do not need a release title or changelog entry. +- Test-only PRs run Rust checks but do not need a release title unless they also + change published package code. +- Runtime, API, generated asset, and AOT crate changes are release-affecting. + Use a release-producing PR title such as `fix:`, `feat:`, `perf:`, or + `refactor:`. +- Source-spine and asset-build script changes are not automatically + release-affecting until they change generated package contents under + `src/**` or `crates/**`, but CI treats them as asset-producing changes and + requires committed artifact verification plus the `Assets` workflow when they + affect release artifacts. + ## Releasing from main 1. Merge release-worthy work to `main`. @@ -50,8 +112,27 @@ section must come from a `release-plz-*` PR titled `chore(release): ...`. `prepare-release-pr`. 3. Review and merge the release-plz PR. It updates `Cargo.toml`, `Cargo.lock`, and `CHANGELOG.md`. -4. Run `Release` from `main` with `publish-dry-run`. -5. If the dry run passes, run `Release` again with `publish`. +4. Wait for the `Assets` workflow on `main` to pass for the release commit. +5. Run `Release` from `main` with `publish-dry-run`. +6. If the dry run passes, run `Release` again with `publish`. + +For portable asset-source changes, regenerate and verify the generated artifact +set before merging: + +```sh +cargo run -p xtask -- assets fetch +cargo run -p xtask --features aot-serializer -- assets build-host +cargo run -p xtask -- assets verify-committed +``` + +Portable WASIX and native AOT artifacts are not committed. They are produced by +the `Assets` workflow matrix and downloaded by +`.github/scripts/download-aot-artifacts.sh` during dry-run and publish jobs. The +architecture-independent PGDATA template is also generated by that workflow +from the split WASIX `initdb` module and is not checked in. +`xtask release stage` materializes those generated payloads into crate skeletons +inside `target/pglite-oxide/release/workspace`; packaging and publish dry-runs +run from that staged workspace. The manual publish job uses `release_always = true` because the workflow is not triggered on every merge; it only runs when a maintainer explicitly selects a @@ -67,7 +148,10 @@ and the `[Unreleased]` compare link must start at that version. If this check fails, run `prepare-release-pr` and merge the generated release-plz PR before publishing. -release-plz publishes unpublished package versions to crates.io, creates the bare -SemVer tag such as `0.3.0`, and creates the GitHub release from the generated -changelog. Bare SemVer tags intentionally match the existing `0.1.0` and -`0.2.0` tags. +release-plz publishes unpublished package versions to crates.io, creates the +bare SemVer tag such as `0.4.0`, and creates the GitHub release from the +generated changelog. The root crate depends on internal crates with exact +versions. Plain Cargo cannot fully dry-run the root crate before those exact +internal versions exist in the registry, so validation dry-runs every internal +crate, enforces package sizes, attempts the root checks, and leaves final +workspace publish ordering to release-plz. diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index 5cee0684..fa1b499f 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -1,87 +1,109 @@ -# Runtime and Performance Notes +# Runtime Guide -`pglite-oxide` runs the upstream PGlite WASI runtime inside Wasmtime. It does -not link a native Postgres library. +`pglite-oxide` embeds a PostgreSQL-compatible runtime in the current Rust +process. The direct API talks to that backend directly, and `PgliteServer` +exposes the same backend through a local Postgres connection string. -## WASI Layout +## Choose A Mode -The embedded backend uses the same shared-memory CMA protocol as upstream -PGlite. The host preopens: +Use `Pglite` when your Rust code owns the database calls: -- `/tmp` as the runtime root -- `/tmp/pglite/base` as the Postgres data directory -- `/home` for runtime home files -- `/dev` for small device shims such as `urandom` +- direct function and method calls; +- no socket listener; +- best fit for tests, commands, jobs, and Tauri state. -Opening an existing cluster still invokes PGlite's `initdb` export because the -WASM runtime uses that entry point for in-memory backend setup too. Existing data -is preserved; the full cluster creation work is avoided once `PG_VERSION` -exists. +Use `PgliteServer` when a library expects a PostgreSQL URI: -## Startup Cost +- SQLx, Diesel, SeaORM, `tokio-postgres`, or cross-language clients; +- local TCP or Unix socket listener; +- compatibility layer for existing Postgres clients. -The first instance in a process can take a while because Wasmtime prepares the -large PGlite WASM module and starts the embedded backend. +Both modes still use one embedded backend. -`pglite-oxide` reduces startup cost in four ways: +## Persistence Modes -- compiled modules are cached in process, so additional `Pglite` instances avoid - the same compile work -- compiled modules are serialized to a `.cwasm` cache keyed by the PGlite WASM - SHA-256, Wasmtime major version, target, and config id -- fresh clusters are created from a bundled prepopulated PGDATA template before - the backend session starts -- `Pglite::temporary()` clones a process-local template cluster, so later - temporary databases copy a prepared filesystem +Direct and server builders expose the same root choices: -Use `Pglite::builder().fresh_temporary().open()?` only when a test specifically -needs fresh cluster initialization. +- `path(...)` for a persistent database under an explicit directory; +- `app(...)` or `app_id(...)` for a persistent database under app data; +- `temporary()` for a fast cached temporary database; +- `fresh_temporary()` for an explicit fresh-cluster path. -## Persistent Compile Cache +Choose `temporary()` for most tests. Choose `fresh_temporary()` only when you +need a brand-new cluster and are willing to pay its slower startup path. -The crate keeps Wasmtime's persistent cache feature enabled and also writes a -crate-owned `.cwasm` cache under the platform cache directory. Cache writes are -best effort; if the cache cannot be read or written, the module is compiled -normally and the app continues. +## Operational Limits -For fast local test loops in a downstream workspace, add the same profile -override used by this repository. Wasmtime's debug cache otherwise keys entries -by the rebuilt test binary mtime, which defeats reuse after ordinary edits: +The current runtime model is single-backend: -```toml -[profile.dev.package.wasmtime-internal-cache] -debug-assertions = false -``` +- one `Pglite` instance owns one embedded backend; +- one `PgliteServer` exposes one embedded backend; +- downstream client pools should use one connection; +- server mode is for local compatibility, not a multi-user Postgres replacement. -For larger suites, prefer reusing one `Pglite` instance per test when isolation -allows it, and use `fresh_temporary` only for initialization-specific coverage. +Generated server URLs include `sslmode=disable`. `CancelRequest` and normal +startup packets are supported, but there is still one backend behind the server. -## Socket Server Limits +## Root Locking And Lifecycle -`PgliteServer` is deliberately blocking and handles one frontend connection at a -time against a single embedded backend. It refuses SSL/GSS negotiation requests -with the standard PostgreSQL `N` response; connection URIs generated by the -crate include `sslmode=disable`. +Persistent roots are locked while open. A second direct or server open against +the same root fails instead of sharing one data directory unsafely. -Set SQLx, `tokio-postgres`, Diesel, or framework pools to one connection. +Close database clients before calling `PgliteServer::shutdown()`. The current +server thread waits for active client work to finish before exiting. -## Proxy Utility +If you need a same-version physical clone, use `dump_data_dir()` / +`load_data_dir_archive(...)` or `try_clone()`. For portable exports and +upgrades, use logical dumps through `pg_dump`. -Expose a persistent database over TCP: +## Startup And Preload -```sh -cargo run --bin pglite-proxy -- --root ./.pglite --tcp 127.0.0.1:5432 -psql 'postgresql://postgres@127.0.0.1:5432/template1?sslmode=disable' -``` +The crate exposes two preload hooks: -On Unix systems, the default proxy mode is `/tmp/.s.PGSQL.5432`: +```rust,no_run +use pglite_oxide::{extensions, Pglite}; -```sh -cargo run --bin pglite-proxy -PGPASSWORD=postgres psql 'postgresql://postgres@/template1?host=/tmp' +fn main() -> Result<(), Box> { + Pglite::preload()?; + Pglite::preload_extensions([extensions::VECTOR])?; + Ok(()) +} ``` -## Runtime Assets +Call them before a visible startup path when you want to warm the packaged +runtime and bundled extension artifacts. + +Startup configuration belongs on the builders: + +- `postgres_config(...)` for PostgreSQL GUCs; +- `username(...)` and `database(...)` for the session target; +- `relaxed_durability(true)` for cacheable local workloads; +- `startup_arg(...)` only for advanced cases. + +## Supported Targets + +Default builds include packaged runtime assets and host artifacts for: + +- macOS arm64; +- Linux x64; +- Linux arm64; +- Windows x64. + +Unsupported host targets fail with a missing-artifact error instead of trying +to compile PostgreSQL locally. + +Browser, worker, and mobile topics from upstream PGlite docs do not apply to +this crate. `pglite-oxide` is a Rust crate for local embedded and desktop/server +workloads. + +## What Server Mode Is For + +Reach for `PgliteServer` when you need client-library compatibility: + +- SQLx migrations and query APIs; +- ORMs that expect a PostgreSQL URI; +- test fixtures for Python, Go, or Node clients; +- local tools that already speak the Postgres wire protocol. -Runtime asset provenance is tracked in [ASSETS.md](ASSETS.md). The crate bundles -the PGlite runtime files needed to start the embedded backend. +Reach for `Pglite` when you control the Rust call site. It avoids the extra +socket layer and keeps the API surface smaller. diff --git a/docs/TAURI.md b/docs/TAURI.md index ade1c01d..97e6f19f 100644 --- a/docs/TAURI.md +++ b/docs/TAURI.md @@ -1,22 +1,23 @@ # Tauri Usage -Use `pglite-oxide` from Rust state, not from the webview. The main value is a -sidecar-free local Postgres runtime that your commands, background tasks, and -Rust libraries can share. +Use `pglite-oxide` from Rust state, not from the webview. The crate's main value +in Tauri is a sidecar-free local Postgres runtime that commands, background +tasks, and Rust libraries can share. -See `examples/tauri-sqlx-vanilla` for a Tauri v2 vanilla app that stores the -runtime in managed Rust state, connects SQLx with `max_connections(1)`, and -returns startup/query profile data as JSON. +See the +[Tauri SQLx example](https://github.com/f0rr0/pglite-oxide/blob/main/examples/tauri-sqlx-vanilla/README.md) +for a Tauri v2 app that keeps the database in Rust state and exposes a small +SQLx-backed profile command to the frontend. -## Direct Embedded API +## Direct Rust State -Use `Pglite` when your Rust code owns the database calls: +Use `Pglite` when your Tauri commands own the database calls: ```rust,no_run use pglite_oxide::Pglite; use serde_json::json; -use tauri::State; use std::sync::Mutex; +use tauri::State; struct Db(Mutex); @@ -38,37 +39,42 @@ Open the database under your app data directory during setup: ```rust,no_run use pglite_oxide::Pglite; -let db = Pglite::builder() - .app("com", "example", "desktop-app") - .open()?; +fn main() -> Result<(), Box> { + let mut db = Pglite::builder() + .app("com", "example", "desktop-app") + .open()?; + db.close()?; + Ok(()) +} ``` ## Existing Postgres Clients -Use `PgliteServer` when another crate expects a PostgreSQL URL: +Use `PgliteServer` when another Rust library expects a PostgreSQL URL: ```rust,no_run use pglite_oxide::PgliteServer; -let server = PgliteServer::builder() - .path("./.pglite") - .start()?; +fn main() -> Result<(), Box> { + let server = PgliteServer::builder() + .path("./.pglite") + .start()?; + + let database_url = server.database_url(); + println!("{database_url}"); -let database_url = server.connection_uri(); + server.shutdown()?; + Ok(()) +} ``` -Configure SQLx, `tokio-postgres`, Diesel, or a framework pool with one -connection. The current runtime is a single embedded backend, not a multi-user -Postgres server. - -## Practical Limits - -- Keep database access serialized unless you are only using one client - connection. -- Prefer `Pglite` over `PgliteServer` when you do not need a PostgreSQL URL. -- Use `Pglite::temporary()` or `PgliteServer::temporary_tcp()` for tests; both - use the template-cluster cache by default. -- Fresh app databases use the bundled PGDATA template by default; there is no - Tauri-specific startup configuration required. -- Mobile targets need separate validation. The current crate targets desktop - Rust with Wasmtime. +This is the right fit for SQLx or other client libraries that already speak the +Postgres wire protocol. + +## Operational Guidance + +- Keep database access serialized around one backend. +- Configure SQLx and other pools with one connection. +- Prefer `Pglite` over `PgliteServer` when you do not need a PostgreSQL URI. +- Use `temporary()` or `temporary_tcp()` for tests. +- Use `fresh_temporary()` only when you need fresh-cluster semantics. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 00000000..a6364dbb --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,136 @@ +# Testing With pglite-oxide + +`pglite-oxide` is intended for tests that need real Postgres semantics without +Docker. + +## Direct Rust Tests + +Use `Pglite::temporary()` when the code under test can call the direct Rust API: + +```rust,no_run +use pglite_oxide::Pglite; + +#[test] +fn stores_rows() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + + db.exec("CREATE TABLE items (id int primary key, name text)", None)?; + db.exec("INSERT INTO items VALUES (1, 'alpha')", None)?; + + let rows = db.query("SELECT name FROM items WHERE id = 1", &[], None)?; + assert_eq!(rows.rows[0].get("name").unwrap(), "alpha"); + + db.close()?; + Ok(()) +} +``` + +Use `fresh_temporary()` only when the test must validate fresh-cluster +initialization behavior: + +```rust,no_run +use pglite_oxide::Pglite; + +#[test] +fn fresh_cluster_path() -> Result<(), Box> { + let mut db = Pglite::builder().fresh_temporary().open()?; + db.close()?; + Ok(()) +} +``` + +## Server Tests + +Use `PgliteServer` when the application already talks to Postgres through a +client library: + +```rust,no_run +use pglite_oxide::PgliteServer; +use sqlx::{Connection, Row}; + +#[tokio::test] +async fn sqlx_query() -> Result<(), Box> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; + + let row = sqlx::query("SELECT $1::int4 + 1 AS n") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("n")?, 42); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} +``` + +Keep client pools at one connection. + +## Extension Tests + +Enable bundled extensions through the builder: + +```rust,no_run +use pglite_oxide::{extensions, Pglite}; + +#[test] +fn vector_query() -> Result<(), Box> { + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + + db.exec("CREATE TABLE items (embedding vector(3))", None)?; + db.exec("INSERT INTO items VALUES ('[1,2,3]')", None)?; + db.exec("SELECT embedding <-> '[1,2,4]' FROM items", None)?; + + db.close()?; + Ok(()) +} +``` + +When an extension has bundled dependencies, prefer the builder path over +post-open `enable_extension(...)`. + +## Snapshot And Fixture Setup + +Use physical data-dir archives or `try_clone()` when a test suite needs a +pre-populated same-version fixture: + +```rust,no_run +use pglite_oxide::Pglite; + +#[test] +fn clone_fixture() -> Result<(), Box> { + let mut seed = Pglite::temporary()?; + seed.exec("CREATE TABLE items(value TEXT)", None)?; + seed.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let mut clone = seed.try_clone()?; + clone.exec("SELECT * FROM items", None)?; + + clone.close()?; + seed.close()?; + Ok(()) +} +``` + +Use logical dumps, not physical archives, when you need a portable export. + +## Cross-Language Tests + +Use `pglite-proxy` when the test process lives outside Rust: + +```sh +pglite-proxy --temporary --tcp 127.0.0.1:0 --print-uri +``` + +Pass the printed URI to Python `psycopg`, Go `pgx`, Node `pg`, or another +standard Postgres client. + +## COPY And Raw Protocol Tests + +Direct `Pglite` supports `/dev/blob` for `COPY TO` and `COPY FROM`. Server mode +supports ordinary client-driven `COPY FROM STDIN` and other standard wire +protocol flows through the local Postgres endpoint. diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 00000000..e00d9c16 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,517 @@ +# To Do (Maintainers) + +This is the single implementation backlog for `pglite-oxide`. It is +maintainer-facing and intentionally separate from the user-facing docs. + +This file should contain only unfinished architecture, implementation, release, +and research work. + +## Product Target + +`pglite-oxide` should provide embedded Postgres for Rust tests and local apps: + +- no Docker, local LLVM, Cranelift, or Postgres build step for users; +- direct Rust API for embedded use; +- local server mode for SQLx, `tokio-postgres`, Diesel, SeaORM, Python, Go, + Node, and any other Postgres client; +- bundled pgvector and common SQL extensions; +- `pg_dump` support driven by the same packaged runtime. + +The production runtime target is PGlite/Postgres built as WASIX dynamic-linking +modules, precompiled with Wasmer LLVM AOT in CI, then loaded through headless +Wasmer in applications. + +## Release Blockers + +These are the top-level blockers before calling the WASIX/Wasmer path +production ready: + +1. Generate and validate asset/AOT packs across the supported target matrix: + macOS arm64/x64, Linux x64/arm64, and Windows x64. +2. Enforce cold-start and warm-path release gates in CI after collecting + release-mode baselines on GitHub-hosted runners. +3. Finish the remaining extension dependency stacks: pinned WASIX + OpenSSL/libcrypto for `pgcrypto`, pinned WASIX OSSP UUID/libuuid for + `uuid-ossp`, and the pinned PostGIS geospatial stack. +4. Harden public `pg_dump` API/CLI across target platforms and add release-mode + performance gates. +5. Harden CI, release-plz, package-size gates, Trusted Publishing, and + dependency invariants for all internal asset and AOT crates. +6. Validate the split WASIX `initdb` artifact end to end in the Assets workflow: + generated template determinism, fresh direct/server temporary roots, + interrupted-initdb cleanup, and package-size impact. + +## Bucket 1: Performance And Runtime Architecture + +Performance work is product work, not optional research. If a work item affects +both cold and warm behavior, track it under cold. + +### Current State + +- Default runtime uses headless Wasmer and packaged AOT artifacts. +- Runtime assets use pure mount composition; immutable runtime files stay in the + shared cache and per-root upper layers contain mutable state and requested + extension assets. +- PGDATA uses the eager template overlay; local PGDATA setup is under 1ms. +- Direct scalar paths no longer scan `pg_type` for arrays on open/query. +- Direct query paths no longer force Rust-side directory `sync_all`. +- Direct API, server API, proxy CLI, raw protocol, and `pg_dump` share + `BackendSession`. +- Current local release runs show visible first-query paths mostly in the + tens-of-ms range, dominated by PostgreSQL backend startup, Wasmer instance + creation, and first protocol round trips. The latest public benchmark + snapshot lives in [PERFORMANCE.md](PERFORMANCE.md). Maintainer tuning details + live in [PERFORMANCE_INTERNAL.md](PERFORMANCE_INTERNAL.md); historical + rollout notes stay in [DONE.md](DONE.md). + +### Release Gates + +- temporary database first `SELECT 1` under 500ms on GitHub Ubuntu; +- persistent database first `SELECT 1` under 500ms on GitHub Ubuntu; +- `PgliteServer` start plus first SQLx query under 500ms on GitHub Ubuntu; +- temporary database with requested extensions plus first extension-backed + query under 500ms on GitHub Ubuntu; +- public `pg_dump` startup plus first dump row under 500ms on GitHub Ubuntu; +- warm temporary direct first query under 100ms on GitHub Ubuntu; +- warm temporary server plus first SQLx query under 100ms on GitHub Ubuntu; +- warm temporary server plus first extension-backed SQLx query under 125ms on + GitHub Ubuntu; +- no more than 15% regression after stable baselines. + +### Cold Path Work + +- Turn the local `xtask perf cold`, `perf warm`, speed-suite, and prepared + update baselines into CI checks on representative runners. +- Keep the source/runtime invariants in the performance matrix: + `postgres-pglite` `REL_17_5-pglite` at + `01792c31a62b7045eb22e93d7dad022bb64b1184`, Wasmer/WebAssembly exceptions, + WASIX dynamic linking, spinlock-enabled WASIX build, and PGlite buffer + profile (`shared_buffers=128MB`, `wal_buffers=4MB`, `min_wal_size=80MB`). +- Reduce explicit `Pglite::preload()` latency further by profiling runtime + cache setup, mmap/native deserialization, and Wasmer native artifact loading. +- Keep Wasmer native mmap deserialization as the only production AOT loading + path. Do not reintroduce full content hashing on default startup. +- Cross-platform validate the default pure mount composition and eager PGDATA + overlay across direct, persistent, app-id, temporary, proxy, and server roots. +- Validate lazy/generated direct-client array metadata across built-in arrays, + enum arrays, domain arrays, composite arrays, explicit + `refresh_array_types`, transactions, row-mode results, and caller-supplied + parser/serializer overrides. +- Deepen C-side timers inside remaining backend-open costs: + shared-memory initialization, relcache/catcache work, database lookup, + `CheckMyDatabase`, and session initialization. Keep these timers gated out of + production artifacts unless explicitly enabled. +- Keep an explicit regression guard so extension-template opens do not fall + back into slow `StartupXLOG` recovery. +- Extend server perf visibility to proxy CLI runs, TCP, Unix sockets, + tokio-postgres, SQLx, scalar roots, and extension-enabled roots. +- Remove or mount-compose the remaining per-root requested-extension asset + materialization cost. This must stay generic by requested extension set, not + special-cased to vector. +- Add an init-profile dimension to extension-set PGDATA template cache keys + once init options become configurable. +- Evaluate catalog/syscache warmup during template creation: `SELECT 1`, + representative prepared/extended query, extension type I/O/query smoke, + SQLx/tokio-postgres startup flow, and representative extension setup. +- Run temporary durability experiments (`fsync=off`, `synchronous_commit=off`, + reduced WAL work) for temporary roots only. Persistent databases stay + conservative unless separately proven safe. +- Ensure side-module AOT artifact identity is tied to main runtime identity. +- Add Linux perf/perfmap support for symbolized AOT profiling so remaining time + can be attributed to executor, btree, heap, WAL, memory, or Wasmer-generated + code. +- Validate ThinLTO build time, package-size budget, and performance on every + supported CI target. +- Validate the current Wasmer LLVM codegen profile across the full target and + correctness matrix: nonvolatile memory operations plus readonly funcref table. +- Benchmark pgvector insert/query/distance workloads with the default WASIX + toolchain feature baseline, including SIMD/relaxed-SIMD behavior. +- Inspect tail calls, extended const expressions, and wide arithmetic use so + feature usage is consistent across target artifacts. + +### Warm/Steady-State Work + +- Reduce PostgreSQL backend startup without changing semantics, especially + `shared_memory`, `InitPostgres`, `relcache_phase3`, database/session setup, + and PGlite-specific startup work. +- Evaluate safe relcache/catcache/syscache warmup only if it is normal + Postgres-compatible state and cannot cache broken process-global state. +- Keep warm opens free of AOT decompression, full hashing, and asset extraction. +- Test whether any supported Wasmer engine/runtime reuse can reduce instance + creation without leaking Store, WASI env, fd, mount, protocol, or database + state. +- Use `perf warm` for long-lived `PgliteServer` baselines: repeated + connections, repeated SQLx/tokio-postgres prepared queries, transaction + batches, extension-backed queries, reconnect after client disconnect, and + idle-to-next-query latency. +- Keep `perf prepared-updates --skip-native --gate` in the local regression + path while CI baselines settle. +- Extend extended-protocol batching only through protocol-correct reductions in + host/backend crossings and buffer copies. Do not add sleep-based coalescing. +- Add direct warm benchmarks for prepared query reuse and first unknown runtime + array type discovery. +- Add warm public `pg_dump` benchmarks that measure startup separately from + dump volume. +- Evaluate context switching, experimental async APIs, CPU idle/backoff, and + opt-in backend pools only if correctness and state reset semantics are + stronger than user expectations. +- Defer threaded/multi-backend execution until the single-backend path is stable + and atomics/shared memory do not break dynamic linking or Postgres + process-global assumptions. + +### Runtime Experiments + +Experiments are real work. Each must report timing, correctness, state +isolation, artifact size impact, and implementation risk. + +- WASIX journaling, Wasmer `StoreSnapshot`, or InstaBoot-style restore: + re-enter only with a small upstream repro or a fixed journal layer. The last + local spike passed `SELECT 1` from an instance-created restore but did not + skip Postgres startup; backend-ready/protocol-ready snapshots were too slow + and failed fd seek replay. +- Cranelift: evaluate direct `SELECT 1`, SQL error recovery, representative + extension create/query, server SQLx smoke, compile speed, and cross-platform + exception/dynamic-linking behavior. +- Singlepass: evaluate only after the same longjmp/error and extension suite + passes. +- Asyncify: keep out of production unless a specific snapshot or journaling path + proves a need on an experiment branch. +- Alternative engines such as V8 or JavaScriptCore: evaluate only for mobile or + special embedded targets, checking WASIX, dynamic linking, filesystem, + exceptions, and headless/AOT implications. +- Native CPU tuning: evaluate only if artifacts remain portable or target packs + are split intentionally. + +## Bucket 2: CI, CD, Release, And Workspace Hygiene + +### CI/CD Target Model + +- Validate the source-controlled-inputs model on GitHub: normal CI must keep + using only source templates plus downloaded compatible Assets workflow + bundles, and asset-producing changes must remain the only PR path that + fetches upstream sources or runs Docker. +- Harden `xtask release publish`: the local command should stage and validate + exactly what the Release workflow publishes, then either invoke release-plz in + a Trusted Publishing environment or fail before any partial publish is + possible. +- Keep `xtask release stage` and `scripts/validate.sh release` as the only + packaging path for generated portable/AOT crate contents. Any future release + check must run against the staged workspace, not ad hoc copied artifacts. +- Split packaged runtime payloads from extension payloads after the `bundled` + feature model lands. Today `bundled` gives users an embedded-runtime install + mode without the public extension API, but the single `pglite-oxide-assets` + crate still carries extension archives and the target AOT pack can carry + extension AOT artifacts. A future crate split should make `bundled` + runtime-only at download/package-size level and keep extension archives plus + extension AOT artifacts behind `extensions`. +- Keep the local development split into three modes: fast assetless contributor + checks, host-platform artifact-backed runtime work, and downloaded CI + artifact testing. Developers validate their host platform locally; CI remains + responsible for the full target matrix. +- Ensure new asset-producing inputs update the committed asset-input + fingerprint and are covered by source-free `assets verify-committed`, + generated-asset validation, package-size checks, and runtime smoke tests. +- Release CI must publish only artifacts generated and tested for the exact + release SHA, with package checks performed against the same staged crate + contents that are published. + +### Target Matrix + +First-class targets: + +- `aarch64-apple-darwin`; +- `x86_64-unknown-linux-gnu`; +- `aarch64-unknown-linux-gnu`; +- `x86_64-pc-windows-msvc`. + +Experimental targets: + +- Linux musl; +- Android; +- iOS through V8/JSC/interpreter paths if feasible; +- RISC-V after Wasmer target support matures. + +### Asset And AOT CI + +- Validate the first full `Assets` workflow run after the portable-WASIX plus + native-AOT CI split lands. +- Keep dynamic-link closure checks, manifest validation, package-size checks, + and smoke tests coupled to asset release orchestration. +- Package strategy order: + 1. raw AOT artifact if the crate stays under crates.io's compressed limit; + 2. `.zst` compressed artifact with one-time expansion into the persistent + cache; + 3. deterministic split AOT/asset packs if compression is still too large. + +### Workspace Hygiene + +- Keep root `pglite-oxide` as the public crate. +- Keep asset/AOT crates internal implementation details with exact internal + dependency versions. +- Keep the source-free asset workflow honest as new asset-producing inputs are + added: every new input must be covered by `assets verify-committed`, the + input fingerprint, or an explicit asset CI gate. +- Keep one active source root: configured `postgres-pglite` + `REL_17_5-pglite` pinned to the audited commit. +- Keep user-facing docs free of implementation backlog/status notes. +- Keep [DONE.md](DONE.md) as the only completed-work/status document and this + file as the only implementation backlog. + +### Normal CI + +- Validate the first path-aware CI run for docs-only, CI-only, test-only, and + package-affecting PRs. +- Validate the first Rust-only native-AOT runtime matrix run across macOS + arm/x64, Linux arm/x64, and Windows x64. +- Keep doctests, no-default-features checks, feature powerset, dependency + invariants, package checks, supply-chain + checks, and example checks routed through the DRY validation script. +- keep the minimal `wasmer` and `wasmer-wasix` feature sets while retaining + filesystem mounts, WASIX env/args, networking required by `pg_dump`, and + dynamic linking; +- macOS multi-module LLVM exception gate: main module plus at least two side + modules, SQL error recovery after each load, and normal parallel test + scheduling on macOS arm64 and x64; +- keep actionlint, cargo-deny, and GitHub Actions security audit green. + +### Release And Publishing + +- Configure Trusted Publishing for every published crate. +- Complete first-publish/bootstrap verification for internal asset/AOT crates on + crates.io. +- Validate that release-plz publishes internal asset/AOT crates before the root + crate when exact internal dependency versions require that order. +- Keep release PRs using a GitHub App or bot token if maintainers want normal PR + CI to run automatically on release-plz branches. + +### Reproducibility + +- Record Wasmer crate version, Wasmer CLI/tool version, wasixcc/toolchain + version, WASIX libc/EH-PIC sysroot identity, LLVM version, postgres-pglite + commit, pglite-build commit, extension repository commits, Docker image + digest, and build profile in manifests. +- Use Wasmer reproducible-build controls such as `WASMER_REPRODUCIBLE_BUILD=1` + where applicable. +- Add deterministic two-build comparisons for identical source pins. +- Make asset and AOT crate hashes stable enough for audit and cache + invalidation. + +## Bucket 3: Source, Build Spine, And Asset Provenance + +The active source baseline is `electric-sql/postgres-pglite` +`REL_17_5-pglite` at `01792c31a62b7045eb22e93d7dad022bb64b1184`, matching the +`@electric-sql/pglite` 0.4.5 source/artifact pair. The historical +`REL_17_5_WASM-pglite-builder` branch remains reference material for extension +and `pg_dump` packaging ideas, not the production source spine. +`electric-sql/pglite-build` `portable` remains pinned as build-script +provenance, not as a second runtime source root. + +### Source-Spine Work + +- Keep stable branch lifecycle/protocol exports: + `_start`/single-user startup, `pgl_setPGliteActive`, `pgl_startPGlite`, + `ProcessStartupPacket`, `PostgresMainLoopOnce`, and `PostgresMainLongJmp`. +- Critique and document each PGlite adaptation before copying it. Keep only host + ABI adaptations still necessary under Wasmer/WASIX. +- Keep `pglite-build` and the builder branch as reference inputs for extension + symbol discovery and packaging, without reintroducing the old `pglite-wasm/*` + wrapper as production runtime code. +- Keep the generated `wasix-dl` export list wired to side-module import + discovery and extend negative tests as more extension packs are added. +- Replace catalog-driven extension packaging with install-delta packaging before + promoting extensions that scatter files outside the standard `.so`, + `.control`, and extension SQL layout. Reuse upstream `pack_extension.py` + concepts without using non-deterministic archive writing. +- Keep manifest fields for extension imports and core exports current so + dynamic-link failures are diagnosed before startup. +- Add negative fixtures proving wrong-core side modules and unresolved imports + fail during validation, before runtime startup. +- Verify `vector`, at least one contrib extension, one PGXS extension, and + `pg_dump` are always built from the same configured tree before release. + +### Upstream Audit + +- Keep `xtask assets audit-upstream --strict` as the source of truth for newer + upstream `postgres-pglite` fixes, marking each item as included, replaced by + WASIX architecture, optional, or pending. +- Keep the WASIX longjmp bridge intentionally narrower than upstream + Emscripten's `jmp_buf` content comparison: pointer identity against exported + top-level `postgresmain_sigjmp_buf`. +- Keep active-portal abort cleanup in PostgreSQL-owned code for + `PGLITE_WASIX_DL`; do not reintroduce Rust-side synthetic `Sync` or portal + cleanup without a failing upstream regression test. +- Keep startup identity/database handling owned by PostgreSQL startup code; Rust + should synthesize only runtime/host failures that occur before PostgreSQL can + emit wire output. +- Audit, cherry-pick, or explicitly reject remaining upstream/runtime items: + background-worker disable semantics, artifact cache fixes, data-directory + locking deltas, upstream `postgresConfig` parity beyond the Rust startup-GUC + API, and `pgoutput` symbol exports. +- Decide whether proxy/frontend startup should eventually stop fabricating + startup responses in Rust and converge further toward upstream + `interactive_one`/`ProcessStartupPacket` lifecycle for every client + connection. +- Keep future config changes flowing through the Rust startup-GUC API, a pinned + upstream `postgresConfig` surface, or a documented initdb-time config model. + +### Canonical Assets + +- Keep timezone data generated by `zic` inside the pinned build image from + PostgreSQL `tzdata.zi`, never from a maintainer host. +- Generate PGDATA with the desired timezone instead of patching extracted config + text. +- Keep runtime prefix files packaged from the pinned configured tree, including + timezone files, extension SQL/control files, and installed support libraries. +- Keep asset manifests tied to source commits, Docker image digest, Wasmer + version, engine identity, source module hashes, import/export sets, archive + hashes, and package sizes. + +## Bucket 4: Runtime Correctness And Protocol + +- Continue expanding PostgreSQL regression coverage beyond the current PGlite + parity subset into less common planner, catalog, lock, utility-command, and + wait/socket behavior. +- Add broader raw wire-protocol and fuzz coverage around extended query + sequencing. +- Keep export guards requiring `PostgresMainLongJmp`, + `PostgresSendReadyForQueryIfNecessary`, `pgl_pq_flush`, and WASIX + input/output symbols. +- If a future Wasmer version resumes the C `sigsetjmp` boundary directly, keep + the explicit recovery export as a tested no-op fallback until tests prove it + can be removed. +- Keep the guard that treats missing `ParseComplete` as an error on successful + Parse paths. +- Keep the production patch free of `pglite-wasm/*`; future frontend/initdb + stubs must be justified by link-symbol analysis against the stable branch. +- Add a C/link audit for the split-initdb child-process shim and keep it + fail-closed to locale discovery plus upstream initdb's `postgres` boot/check + commands. +- Keep interrupted-PGDATA and root-locking tests as the owned coverage for + failed opens. Do not add a fake child-process kill model unless the runtime + grows a real child-process boundary. +- Harden backend-side COPY error coverage beyond the current suite. +- Investigate returning from COPY streaming continuation to buffered mode after + COPY if it can be proven correct for SQLx, tokio-postgres, raw TCP, Unix + sockets, `CopyFail`, and post-COPY reuse. +- Keep direct raw protocol streaming and direct `pg_dump` on the shared + `BackendSession` path; do not reintroduce clone/server indirection. +- Reject asset mixing through negative tests: wrong runtime, wrong side module, + wrong AOT identity, wrong extension archive, and stale manifest. + +## Bucket 5: Extensions + +Extension catalog generation discovers 40 SQL extensions from PGlite docs/REPL +exports, PostgreSQL contrib, `postgres-pglite/pglite/other_extensions`, pinned +external repositories, and the packaged asset manifest. The current build plan +requests and packages 37 extensions. All 37 packaged extensions have passed +direct, server, restart, and lifecycle materialization gates and are public +constants. `pgcrypto`, `uuid-ossp`, and PostGIS remain explicitly blocked until +their native dependency stacks are pinned and smoke-tested for WASIX. + +### Remaining Promotion Order + +1. Add pinned WASIX OpenSSL/libcrypto sysroot and promote `pgcrypto`. +2. Add pinned WASIX OSSP UUID/libuuid sysroot and promote `uuid-ossp`. +3. Add pinned WASIX geospatial dependency stack and install-delta packaging for + PostGIS. + +### Extension Rules And Hardening + +- Generate public constants only after direct, server, restart, and lifecycle + smoke gates pass for the current asset set. +- Keep PGlite `live` out of SQL extension constants until there is a + Rust-native live-query API. +- Keep `extensions::ALL` limited to extensions passing for the current asset + set. +- Keep every discovered SQL extension either build-requested or blocked with a + concrete reason. +- Verify that manifest metadata remains sufficient for preload/config/shared + memory/restart/dependency/load-order needs; add fields only where the current + dependency, load-order, and lifecycle metadata are insufficient. +- Prove preload-required extensions such as `pg_stat_statements` apply + `shared_preload_libraries` before backend startup before exposing them. +- Make extension dependency errors fail at manifest/build-plan generation time + where possible. +- Add extension load-order and missing-native-dependency failure tests. +- Add preload/startup-config extension tests before exposing extensions that + require postmaster-time configuration. +- Add lifecycle negative tests for missing side modules, wrong core runtime, + missing SQL/control files, repeated enable, reopen after install, and missing + requested archives. +- Keep generated native-module metadata authoritative: SQL extension names and + native side-module names can differ, and some extensions are SQL-only. +- Replace remaining PGXS build assumptions with extension-specific build + metadata where external modules require extra flags, generated headers, + install hooks, generated SQL, or multiple side modules. +- Add automation that updates `assets/extensions.smoke.toml` from reviewed smoke + suite output instead of requiring maintainers to edit it by hand. + +## Bucket 6: `pg_dump` + +- Keep public dump/restore tests for direct `Pglite`, `PgliteServer`, vector, + indexes, views, sequences, `--schema-only`, and quoted identifiers. +- Keep direct `Pglite::dump_sql` no-clone, no-public-server, and no-OS-loopback: + stock WASIX `pg_dump`/libpq should route through Wasmer virtual networking and + host-side `exec_protocol_raw`. +- Do not add a pglite-oxide-specific `pg_dump` callback ABI unless stock libpq + over virtual networking fails a concrete correctness or performance gate. +- Keep rejecting passthrough flags that conflict with the typed API's managed + output file, output format, host, port, username, database, and job count. +- Add release-mode performance and cross-platform CI for `PgDumpOptions`, + `Pglite::dump_sql`, `Pglite::dump_bytes`, `PgliteServer::dump_sql`, + `PgliteServer::dump_bytes`, and the real `pglite-dump` CLI. + +## Bucket 7: Examples, Docs, And Ecosystem Tests + +- Add examples and CI for SQLx, `tokio-postgres`, rstest, Diesel, SeaORM, + Tauri, pgvector local RAG, Python/psycopg, Go/pgx, and Node `pg`. +- Add Python, Go, and Node proxy examples that verify SQLSTATE preservation and + recovery behavior through ordinary client libraries. +- Keep README first screen focused on embedded Postgres, tests, local apps, + pgvector/common extensions, no Docker, and any Postgres client through local + server mode. +- Keep user-facing docs free of internal status notes; implementation notes stay + in this file or [DONE.md](DONE.md). + +Required release test categories: + +- direct `SELECT 1`, persistence, restart, temporary template cache, and root + locks; +- SQLx and `tokio-postgres` server connections; +- SSLRequest, CancelRequest, Parse/Bind/Execute error recovery, and pipelined + extended queries; +- vector create/insert/query/distance through direct API and server mode; +- generated extension smoke suite; +- unsafe archive rejection and canonical path validation; +- manifest SHA validation and AOT source-module identity verification; +- unsupported target errors; +- macOS multi-module exception recovery; +- public dump/restore; +- Python, Go, and Node proxy tests; +- package size checks and publish dry-runs. + +## Experiment And Decision Policy + +Every runtime-affecting experiment must end in one repo-visible state: + +- `promoted`: implementation is on the production path; +- `blocked`: evidence and blocker are documented; +- `rejected`: reason and alternative are documented. + +Do not leave runtime-affecting experiments as loose notes. + +## Reference Material To Recheck + +- Wasmer 7 announcement and runtime feature docs; +- Wasmer 7.2 alpha release notes; +- Wasmer WASIX dynamic-linking docs; +- Wasmer WordPress/WebAssembly case study; +- Wasmer InstaBoot documentation; +- Wasmer macOS multi-module LLVM exception issue; +- Wasmer embedded/iOS tracking issue; +- Wasmer Rust API docs; +- PGlite extension docs and extension-development docs; +- `postgres-pglite` `REL_17_5-pglite` and historical + `REL_17_5_WASM-pglite-builder` reference branch; +- `pglite-build` `portable`; +- PGlite data-directory locking, startup config, and `pgoutput` upstream PRs. diff --git a/docs/USAGE.md b/docs/USAGE.md index e56de773..77b20663 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -1,11 +1,40 @@ # Usage Guide -`pglite-oxide` has two public entry points: +`pglite-oxide` has two primary entry points: -- `Pglite` for direct embedded calls from Rust -- `PgliteServer` for crates that need a PostgreSQL connection URI +- `Pglite` for direct embedded queries from Rust; +- `PgliteServer` for libraries that need a PostgreSQL connection URI. -Prefer `Pglite` unless you specifically need a Postgres wire-protocol client. +Prefer `Pglite` unless you specifically need a Postgres client connection. + +## Install Modes + +Most projects should use the default install: + +```toml +pglite-oxide = "0.4" +``` + +Default features include the packaged embedded Postgres runtime, the +target-specific Wasmer AOT artifact for the current platform, and the public +bundled-extension API. + +If you want embedded Postgres without the extension API, keep the packaged +runtime and turn off defaults explicitly: + +```toml +pglite-oxide = { version = "0.4", default-features = false, features = ["bundled"] } +``` + +Use the minimal mode only when you intentionally do not want packaged runtime +assets in the dependency graph: + +```toml +pglite-oxide = { version = "0.4", default-features = false } +``` + +Minimal mode is for specialized integrations and maintainer workflows. Normal +database opens need the packaged runtime/AOT assets provided by `bundled`. ## Opening Databases @@ -14,7 +43,7 @@ Persistent database under an explicit path: ```rust,no_run use pglite_oxide::Pglite; -fn main() -> anyhow::Result<()> { +fn main() -> Result<(), Box> { let mut db = Pglite::open("./.pglite")?; db.close()?; Ok(()) @@ -26,7 +55,7 @@ Persistent database under the platform app-data directory: ```rust,no_run use pglite_oxide::Pglite; -fn main() -> anyhow::Result<()> { +fn main() -> Result<(), Box> { let mut db = Pglite::builder() .app("com", "example", "desktop-app") .open()?; @@ -35,41 +64,99 @@ fn main() -> anyhow::Result<()> { } ``` -Temporary database for tests: +Fast temporary database for tests: ```rust,no_run use pglite_oxide::Pglite; -fn main() -> anyhow::Result<()> { +fn main() -> Result<(), Box> { let mut db = Pglite::temporary()?; db.close()?; Ok(()) } ``` -`Pglite::temporary()` uses the process-local template cluster cache by default. -Use `Pglite::builder().fresh_temporary().open()?` only when a test needs to -exercise fresh cluster initialization. +Explicit fresh-cluster temporary database: + +```rust,no_run +use pglite_oxide::Pglite; + +fn main() -> Result<(), Box> { + let mut db = Pglite::builder().fresh_temporary().open()?; + db.close()?; + Ok(()) +} +``` + +`temporary()` uses the cached template path. `fresh_temporary()` disables that +cache and runs the packaged `initdb` path instead. Use it when a test needs a +brand-new cluster, not for the common fast path. + +The direct builder also exposes: + +- `path(...)`, `app(...)`, and `app_id(...)` for persistent roots; +- `temporary()`, `template_cache(bool)`, and `fresh_temporary()` for ephemeral + roots; +- `load_data_dir_archive(...)` for restoring a physical data-dir archive before + open. + +## Startup Configuration -Fresh persistent databases use the bundled PGDATA template by default, so app -code does not need to opt into the fast startup path. +Use builder methods for startup-time database settings: + +```rust,no_run +use pglite_oxide::Pglite; + +fn main() -> Result<(), Box> { + let mut db = Pglite::builder() + .temporary() + .postgres_config("synchronous_commit", "off") + .postgres_config("work_mem", "8MB") + .username("postgres") + .database("template1") + .relaxed_durability(true) + .open()?; + db.close()?; + Ok(()) +} +``` + +Relevant direct and server builder methods: + +- `postgres_config(name, value)` and `postgres_configs(...)`; +- `username(...)` and `database(...)`; +- `debug_level(level)` with PostgreSQL levels `0..=5`; +- `relaxed_durability(true)` for cacheable local workloads; +- `startup_arg(...)` and `startup_args(...)` for advanced PostgreSQL arguments. + +Use `postgres_config` for ordinary GUCs. It follows PostgreSQL's normal +`-c name=value` startup behavior, and explicit values override the default +startup profile. + +For `PgliteServer`, the same startup methods are available on +`PgliteServer::builder()`. The `pglite-proxy` CLI exposes startup GUCs with +`--postgres-config NAME=VALUE`. ## Queries -`exec` runs SQL without parameters. `query` runs the extended protocol with JSON -parameters. +`exec` runs SQL without parameters. `query` runs the extended protocol with +JSON parameters. ```rust,no_run use pglite_oxide::Pglite; use serde_json::json; -fn main() -> anyhow::Result<()> { - let mut db = Pglite::open("./.pglite")?; +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; - db.exec("CREATE TABLE IF NOT EXISTS items(value TEXT)", None)?; - db.query("INSERT INTO items(value) VALUES ($1)", &[json!("alpha")], None)?; + db.exec("CREATE TABLE items(id INT PRIMARY KEY, value TEXT)", None)?; + db.query( + "INSERT INTO items(id, value) VALUES ($1, $2)", + &[json!(1), json!("alpha")], + None, + )?; - let result = db.query("SELECT value FROM items", &[], None)?; + let result = db.query("SELECT value FROM items WHERE id = $1", &[json!(1)], None)?; println!("{:?}", result.rows); db.close()?; @@ -77,35 +164,47 @@ fn main() -> anyhow::Result<()> { } ``` -Values are passed as `serde_json::Value`. Default parsers and serializers cover -common Postgres types including integers, floats, booleans, JSON/JSONB, bytea, -dates/timestamps, UUIDs, and arrays discovered from `pg_type`. +Parameters are `serde_json::Value`. Default parsers and serializers cover +common Postgres scalar types, JSON, bytea, UUIDs, timestamps, and built-in +arrays. + +When you add runtime-created array types such as arrays of enums, domains, or +composites, `pglite-oxide` usually discovers them lazily. If you want to refresh +that state explicitly, call `refresh_array_types()`. ## Query Options -`QueryOptions` controls result parsing and protocol behavior. +`QueryOptions` controls result parsing and protocol behavior: + +- `row_mode` switches between object rows and positional arrays; +- `parsers` and `serializers` override type handling for specific OIDs; +- `blob` attaches bytes to `/dev/blob` for `COPY FROM`; +- `param_types` pins parameter OIDs for cases where PostgreSQL cannot infer + them cleanly; +- `on_notice` handles backend notices on a query-by-query basis. + +Example: ```rust,no_run use pglite_oxide::{Pglite, QueryOptions, RowMode}; -fn main() -> anyhow::Result<()> { - let mut db = Pglite::open("./.pglite")?; +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; let options = QueryOptions { row_mode: Some(RowMode::Array), ..QueryOptions::default() }; - let rows = db.query("SELECT 1, 2", &[], Some(&options))?; - println!("{:?}", rows.rows); + let result = db.query("SELECT 1, 2", &[], Some(&options))?; + println!("{:?}", result.rows); db.close()?; Ok(()) } ``` -For `COPY ... FROM '/dev/blob'`, set `QueryOptions::blob` to the bytes exposed -through the guest `/dev/blob`. For `COPY ... TO '/dev/blob'`, read the returned -`Results::blob`. +Use `describe_query(...)` when you need parameter and result type metadata +without executing the query. ## Transactions @@ -115,8 +214,9 @@ Use `transaction` when several direct calls should commit or roll back together. use pglite_oxide::Pglite; use serde_json::json; -fn main() -> anyhow::Result<()> { - let mut db = Pglite::open("./.pglite")?; +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; db.transaction(|tx| { tx.query("INSERT INTO items(value) VALUES ($1)", &[json!("alpha")], None)?; @@ -129,17 +229,79 @@ fn main() -> anyhow::Result<()> { } ``` +The `Transaction` handle also exposes `exec`, `query`, `refresh_array_types`, +`commit`, and `rollback`. + +## Notifications + +Use `listen` when you want channel-specific `LISTEN/NOTIFY` callbacks, and +`on_notification` when you want to observe every notification. + +```rust,no_run +use pglite_oxide::Pglite; + +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + + let specific = db.listen("events", |payload| { + println!("events payload: {payload}"); + })?; + let global = db.on_notification(|channel, payload| { + println!("{channel}: {payload}"); + }); + + db.exec("NOTIFY events, 'hello'", None)?; + + db.unlisten(specific)?; + db.off_notification(global); + db.close()?; + Ok(()) +} +``` + +`unlisten_channel(...)` removes all listeners for a specific channel. + +## `/dev/blob` and COPY + +Direct `Pglite` can send and receive bytes through the virtual `/dev/blob` +device. + +```rust,no_run +use pglite_oxide::{Pglite, QueryOptions}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + + let import = QueryOptions { + blob: Some(b"alpha\nbeta\n".to_vec()), + ..QueryOptions::default() + }; + db.exec("COPY items FROM '/dev/blob'", Some(&import))?; + + let exported = db.exec("COPY items TO '/dev/blob'", None)?; + let blob = exported[0].blob.clone().expect("COPY TO blob"); + println!("{}", String::from_utf8(blob)?); + + db.close()?; + Ok(()) +} +``` + +If you already use a standard Postgres client, `PgliteServer` also supports +client-driven `COPY FROM STDIN` through the normal wire protocol. + ## SQL Helpers -`format_query` asks Postgres to quote parameter values. `QueryTemplate` helps -build SQL while keeping identifiers and values separate. +`format_query` asks Postgres to quote parameter values. `QueryTemplate` and +`quote_identifier` help build SQL while keeping identifiers and values separate. ```rust,no_run use pglite_oxide::{Pglite, QueryTemplate, format_query, quote_identifier}; use serde_json::json; -fn main() -> anyhow::Result<()> { - let mut db = Pglite::open("./.pglite")?; +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; let sql = format_query(&mut db, "SELECT $1::int", &[json!(42)])?; assert_eq!(sql, "SELECT '42'::int"); @@ -159,19 +321,18 @@ fn main() -> anyhow::Result<()> { } ``` -## PostgreSQL Clients +## Server Mode -Use `PgliteServer` when another crate expects a PostgreSQL URL. The server owns -one embedded backend, so configure downstream pools with one connection. +Use `PgliteServer` when another crate expects a PostgreSQL URL. ```rust,no_run use pglite_oxide::PgliteServer; use sqlx::{Connection, Row}; #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<(), Box> { let server = PgliteServer::temporary_tcp()?; - let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; let row = sqlx::query("SELECT $1::int4 + 1 AS answer") .bind(41_i32) @@ -185,18 +346,104 @@ async fn main() -> anyhow::Result<()> { } ``` -For app persistence, use: +`PgliteServer::builder()` supports: + +- `path(...)`, `temporary()`, and `fresh_temporary()`; +- `tcp(...)`, and on Unix hosts `unix(...)`; +- the same startup configuration methods as `PgliteBuilder`; +- bundled extensions with `extension(...)` and `extensions(...)`. + +Use `connection_uri()` or `database_url()` to hand a URI to a client library. +Generated URLs include `sslmode=disable`. + +Server mode still exposes one embedded backend. Configure SQLx, Diesel, +SeaORM, `tokio-postgres`, and framework pools with one connection. + +## Raw Protocol + +`exec_protocol` is the safest low-level wire-protocol entry point. It returns +parsed backend messages and still handles notices and notifications. ```rust,no_run -use pglite_oxide::PgliteServer; +use pglite_oxide::{ExecProtocolOptions, Pglite}; -fn main() -> anyhow::Result<()> { - let server = PgliteServer::builder() - .path("./.pglite") - .start()?; - server.shutdown()?; +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + let mut query = vec![b'Q']; + query.extend_from_slice(&13_i32.to_be_bytes()); + query.extend_from_slice(b"SELECT 1\0"); + + let result = db.exec_protocol(&query, ExecProtocolOptions::default())?; + assert!(!result.messages.is_empty()); + + db.close()?; Ok(()) } ``` -Connection URIs generated by the crate include `sslmode=disable`. +Use `exec_protocol_raw(...)` when you need raw bytes, and +`exec_protocol_raw_stream(...)` when you want to forward backend bytes as they +arrive. + +## Physical Data-Dir Archives + +Use physical archives for same-version restore and fast cloning. They are not a +cross-version backup protocol. + +```rust,no_run +use pglite_oxide::Pglite; + +fn main() -> Result<(), Box> { + let mut source = Pglite::temporary()?; + source.exec("CREATE TABLE items(value TEXT)", None)?; + source.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let archive = source.dump_data_dir()?; + + let mut restored = Pglite::builder() + .temporary() + .load_data_dir_archive(archive) + .open()?; + let mut cloned = restored.try_clone()?; + + cloned.exec("SELECT * FROM items", None)?; + + restored.close()?; + cloned.close()?; + source.close()?; + Ok(()) +} +``` + +Use `dump_data_dir_with_format(...)` when you want an explicit +`DataDirArchiveFormat::Tar` or `DataDirArchiveFormat::TarGz`. + +## Logical Dumps + +With the default feature set, both direct and server APIs expose logical dumps +through `PgDumpOptions`. + +```rust,no_run +use pglite_oxide::{PgDumpOptions, Pglite}; + +fn main() -> Result<(), Box> { + let mut db = Pglite::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + db.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let sql = db.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + println!("{sql}"); + + db.close()?; + Ok(()) +} +``` + +CLI: + +```sh +pglite-dump --root ./.pglite +pglite-dump --root ./.pglite -- --schema-only +``` + +See [PG_DUMP.md](PG_DUMP.md) for dump/restore and upgrade guidance. diff --git a/docs/assets/pglite-oxide.png b/docs/assets/pglite-oxide.png new file mode 100644 index 00000000..f77687b8 Binary files /dev/null and b/docs/assets/pglite-oxide.png differ diff --git a/examples/build_pgdata_template.rs b/examples/build_pgdata_template.rs index 147898f5..87fe75de 100644 --- a/examples/build_pgdata_template.rs +++ b/examples/build_pgdata_template.rs @@ -5,10 +5,9 @@ use anyhow::Result; use pglite_oxide::build_pgdata_template; fn main() -> Result<()> { - let output_dir = env::args_os() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/prepopulated")); + let output_dir = env::args_os().nth(1).map(PathBuf::from).unwrap_or_else(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/pglite-oxide/assets/prepopulated") + }); let template = build_pgdata_template(&output_dir)?; println!("archive: {}", template.archive_path.display()); diff --git a/examples/tauri-sqlx-vanilla/README.md b/examples/tauri-sqlx-vanilla/README.md index c86247e8..d3dca54c 100644 --- a/examples/tauri-sqlx-vanilla/README.md +++ b/examples/tauri-sqlx-vanilla/README.md @@ -1,6 +1,7 @@ -# pglite SQLx Tauri profile +# pglite-oxide Tauri SQLx example -This is a vanilla TypeScript Tauri v2 app that exercises `pglite-oxide` through a real `sqlx::PgPool`. It uses the crate defaults: bundled PGDATA template, compiled Wasmtime module cache, quiet WASI stdio, and the preferred local proxy. +This is a Tauri v2 example that keeps `pglite-oxide` in Rust state and talks to +it through a real one-connection `sqlx::PgPool`. ## Run the desktop app @@ -9,7 +10,8 @@ npm install npm run tauri dev ``` -The window paints first. The pglite runtime, preferred local proxy, SQLx pool, schema setup, and query profile run only when the profile command is invoked. +The app opens first and runs the database profile only when the profile command +is invoked from the UI. ## Run the headless profiler @@ -18,20 +20,12 @@ cd src-tauri cargo run --release --bin profile_queries -- --fresh --rows 10000 --json-out /tmp/pglite-profile-release.json ``` -Use `--fresh` to remove the profile data directory before the run. Omit it to measure a warm start with an existing cluster. +Use `--fresh` to remove the profile data directory before the run. Omit it to +measure a warm start with an existing cluster. -The profiler uses the optimized default path. Flags: +## What it demonstrates -- `--rows `: control seed size. -- `--json-out `: write the full report as JSON. - -## What is measured - -- Runtime archive install/reuse. -- Wasmtime module load, compile, or compiled-cache reuse. -- PostgreSQL cluster creation, bundled template install, or reuse. -- Preferred proxy startup: Unix socket on macOS/Linux when possible, TCP fallback otherwise. -- SQLx pool connection, including the first backend wire-protocol handshake. -- Schema creation, seeding, indexing, and real SQLx query timings. - -The SQLx pool intentionally uses `max_connections(1)` because the embedded pglite runtime is single-process and proxy access is serialized. +- storing the database in managed Rust state; +- using `PgliteServer` to hand SQLx a PostgreSQL URI; +- configuring the SQLx pool with `max_connections(1)`; +- creating schema, seeding rows, and profiling real SQL queries. diff --git a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index 95382a76..a6fcbcc7 100644 --- a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -4,11 +4,11 @@ version = 4 [[package]] name = "addr2line" -version = "0.26.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", ] [[package]] @@ -47,12 +47,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -62,6 +56,12 @@ dependencies = [ "libc", ] +[[package]] +name = "any_ascii" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" + [[package]] name = "anyhow" version = "1.0.102" @@ -69,10 +69,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arbitrary" -version = "1.4.2" +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-broadcast" @@ -125,7 +131,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.4", + "rustix", "slab", "windows-sys 0.61.2", ] @@ -156,7 +162,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.4", + "rustix", ] [[package]] @@ -182,7 +188,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.4", + "rustix", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -249,6 +255,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link 0.2.1", +] + [[package]] name = "base64" version = "0.21.7" @@ -261,6 +282,46 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -291,6 +352,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -300,6 +375,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -343,13 +427,54 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bus" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" dependencies = [ - "allocator-api2", + "crossbeam-channel", + "num_cpus", + "parking_lot_core", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -373,6 +498,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +dependencies = [ + "serde_core", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -407,84 +541,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "cap-fs-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "cap-net-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" -dependencies = [ - "cap-primitives", - "cap-std", - "rustix 1.1.4", - "smallvec", -] - -[[package]] -name = "cap-primitives" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix 1.1.4", - "rustix-linux-procfs", - "windows-sys 0.59.0", - "winx", -] - -[[package]] -name = "cap-rand" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" -dependencies = [ - "ambient-authority", - "rand 0.8.6", -] - -[[package]] -name = "cap-std" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", -] - -[[package]] -name = "cap-time-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" -dependencies = [ - "ambient-authority", - "cap-primitives", - "iana-time-zone", - "once_cell", - "rustix 1.1.4", - "winx", -] - [[package]] name = "cargo-platform" version = "0.1.9" @@ -536,6 +592,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfb" version = "0.7.3" @@ -563,6 +628,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -576,12 +652,50 @@ dependencies = [ ] [[package]] -name = "cobs" -version = "0.3.0" +name = "ciborium" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ - "thiserror 2.0.18", + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", ] [[package]] @@ -603,11 +717,43 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" -version = "0.4.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cooked-waker" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" [[package]] name = "cookie" @@ -659,6 +805,19 @@ dependencies = [ "libc", ] +[[package]] +name = "corosensei" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + [[package]] name = "cpp_demangle" version = "0.4.5" @@ -678,147 +837,14 @@ dependencies = [ ] [[package]] -name = "cranelift-assembler-x64" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb5bdd1af46714e3224a017fabbbd57f70df4e840eb5ad6a7429dc456119d6" -dependencies = [ - "cranelift-assembler-x64-meta", -] - -[[package]] -name = "cranelift-assembler-x64-meta" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a819599186e1b1a1f88d464e06045696afc7aa3e0cc018aa0b2999cb63d1d088" -dependencies = [ - "cranelift-srcgen", -] - -[[package]] -name = "cranelift-bforest" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e2c152d488e03c87b913bc2ed3414416eb1e0d66d61b49af60bf456a9665c7" -dependencies = [ - "cranelift-entity", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-bitset" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6559d4fbc253d1396e1f6beeae57fa88a244f02aaf0cde2a735afd3492d9b2e" -dependencies = [ - "serde", - "serde_derive", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d9315d98d6e0a64454d4c83be2ee0e8055c3f80c3b2d7bcad7079f281a06ff" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.16.1", - "libm", - "log", - "pulley-interpreter", - "regalloc2", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon 0.13.5", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89c00a88081c55e3087c45bebc77e0cc973de2d7b44ef6a943c7122647b89f5" -dependencies = [ - "cranelift-assembler-x64-meta", - "cranelift-codegen-shared", - "cranelift-srcgen", - "heck 0.5.0", - "pulley-interpreter", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f77c497a1eb6273482aa1ac3b23cb8563ff04edb39ed5dfcfd28c8deff8f5" - -[[package]] -name = "cranelift-control" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498dc1f17a6910c88316d49c7176d8fa97cf10c30859c32a266040449317f963" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2acba797f6a46042ce82aaf7680d0c3567fe2001e238db9df649fd104a2727f" -dependencies = [ - "cranelift-bitset", - "serde", - "serde_derive", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-frontend" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dca3df1d107d98d88f159ad1d5eaa2d5cdb678b3d5bcfadc6fc83d8ebb448ea" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon 0.13.5", -] - -[[package]] -name = "cranelift-isle" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f62dd18116d88bed649871feceda79dad7b59cc685ea8998c2b3e64d0e689602" - -[[package]] -name = "cranelift-native" -version = "0.131.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f843b80360d7fdf61a6124642af7597f6d55724cf521210c34af8a1c66daca6e" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "cranelift-codegen", "libc", - "target-lexicon 0.13.5", ] -[[package]] -name = "cranelift-srcgen" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090ee5de58c6f17eb5e3a5ae8cf1695c7efea04ec4dd0ecba6a5b996c9bad7dc" - [[package]] name = "crc" version = "3.4.0" @@ -886,6 +912,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -897,20 +929,12 @@ dependencies = [ ] [[package]] -name = "cssparser" -version = "0.29.6" +name = "crypto-common" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", + "hybrid-array", ] [[package]] @@ -938,12 +962,38 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -952,8 +1002,35 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -969,17 +1046,114 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "deranged" version = "0.5.8" @@ -991,15 +1165,33 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "0.99.20" +name = "derive_builder" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ - "convert_case", + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", "proc-macro2", "quote", - "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", "syn 2.0.117", ] @@ -1018,10 +1210,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", "rustc_version", "syn 2.0.117", + "unicode-xid", ] [[package]] @@ -1030,28 +1224,29 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] [[package]] -name = "directories" -version = "6.0.0" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "dirs-sys", + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", ] [[package]] -name = "directories-next" -version = "2.0.0" +name = "directories" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] @@ -1071,21 +1266,10 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.2", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "dispatch2" version = "0.3.1" @@ -1132,6 +1316,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -1139,12 +1332,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", - "cssparser 0.36.0", + "cssparser", "foldhash 0.2.0", - "html5ever 0.38.0", + "html5ever", "precomputed-hash", - "selectors 0.36.1", - "tendril 0.5.0", + "selectors", + "tendril", ] [[package]] @@ -1177,6 +1370,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -1219,31 +1427,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] -name = "embedded-io" -version = "0.4.0" +name = "encode_unicode" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] -name = "embedded-io" -version = "0.6.1" +name = "endi" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "enum-iterator" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" dependencies = [ - "cfg-if", + "enum-iterator-derive", ] [[package]] -name = "endi" -version = "1.1.1" +name = "enum-iterator-derive" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "enumflags2" @@ -1256,11 +1469,32 @@ dependencies = [ ] [[package]] -name = "enumflags2_derive" -version = "0.7.12" +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" dependencies = [ + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -1331,17 +1565,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.59.0", -] - [[package]] name = "fdeflate" version = "0.3.7" @@ -1378,6 +1601,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1442,27 +1671,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs-set-times" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" -dependencies = [ - "io-lifetimes", - "rustix 1.1.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.32" @@ -1471,6 +1679,7 @@ checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1563,6 +1772,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1573,15 +1783,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gdk" version = "0.18.2" @@ -1691,17 +1892,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1710,7 +1900,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1720,9 +1910,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1732,12 +1924,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "gimli" version = "0.33.0" @@ -1812,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.0", + "proc-macro-crate 2.0.2", "proc-macro-error", "proc-macro2", "quote", @@ -1835,6 +2036,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1898,12 +2112,38 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1920,11 +2160,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", - "serde", - "serde_core", -] [[package]] name = "hashbrown" @@ -1944,6 +2179,25 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.4.1" @@ -1983,7 +2237,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1995,18 +2249,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - [[package]] name = "html5ever" version = "0.38.0" @@ -2014,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.38.0", + "markup5ever", ] [[package]] @@ -2056,6 +2298,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -2130,7 +2381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -2240,14 +2491,30 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2281,27 +2548,34 @@ dependencies = [ ] [[package]] -name = "io-extras" -version = "0.18.4" +name = "insta" +version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ - "io-lifetimes", - "windows-sys 0.59.0", + "console", + "once_cell", + "regex", + "serde", + "similar", + "tempfile", ] -[[package]] -name = "io-lifetimes" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" - [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "iprange" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" +dependencies = [ + "ipnet", +] + [[package]] name = "iri-string" version = "0.7.12" @@ -2331,6 +2605,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2425,9 +2708,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -2468,18 +2751,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser 0.29.6", - "html5ever 0.29.1", - "indexmap 2.14.0", - "selectors 0.24.0", -] - [[package]] name = "leb128" version = "0.2.6" @@ -2492,6 +2763,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -2512,7 +2792,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2522,6 +2802,15 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2533,10 +2822,14 @@ dependencies = [ ] [[package]] -name = "libm" -version = "0.2.16" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] [[package]] name = "libredox" @@ -2547,14 +2840,29 @@ dependencies = [ "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.4", + "redox_syscall 0.7.5", ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] [[package]] name = "linux-raw-sys" @@ -2568,6 +2876,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -2584,10 +2898,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "mac" -version = "0.1.1" +name = "lz4_flex" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" +dependencies = [ + "twox-hash", +] [[package]] name = "mach2" @@ -2599,19 +2916,28 @@ dependencies = [ ] [[package]] -name = "markup5ever" -version = "0.14.1" +name = "mach2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", + "thiserror 2.0.18", + "zerocopy", + "zerocopy-derive", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "markup5ever" version = "0.38.0" @@ -2619,33 +2945,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "tendril 0.5.0", + "tendril", "web_atoms", ] -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - [[package]] name = "md-5" version = "0.10.6" @@ -2653,7 +2956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2663,12 +2966,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "memfd" -version = "0.6.5" +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ - "rustix 1.1.4", + "libc", ] [[package]] @@ -2686,6 +2998,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2703,15 +3021,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "log", + "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "msvc-demangler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" +dependencies = [ + "bitflags 2.11.1", + "itoa", +] + [[package]] name = "muda" -version = "0.17.2" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" dependencies = [ "crossbeam-channel", "dpi", @@ -2722,10 +3057,30 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2743,12 +3098,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2765,10 +3114,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "nodrop" -version = "0.1.14" +name = "nom" +version = "5.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] [[package]] name = "num-conv" @@ -2785,6 +3148,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -2830,6 +3203,27 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -2854,6 +3248,38 @@ dependencies = [ "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -2911,8 +3337,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.11.1", + "block2", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", "objc2-foundation", ] @@ -2930,6 +3375,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.39.1" @@ -2937,9 +3391,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", + "flate2", "hashbrown 0.17.0", "indexmap 2.14.0", "memchr", + "ruzstd", ] [[package]] @@ -3030,6 +3486,18 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3042,45 +3510,72 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + [[package]] name = "pglite-oxide" version = "0.3.0" dependencies = [ "anyhow", + "async-trait", "directories", + "dunce", + "filetime", "flate2", - "getrandom 0.4.2", "hex", + "pglite-oxide-aot-aarch64-apple-darwin", + "pglite-oxide-aot-aarch64-unknown-linux-gnu", + "pglite-oxide-aot-x86_64-pc-windows-msvc", + "pglite-oxide-aot-x86_64-unknown-linux-gnu", + "pglite-oxide-assets", "regex", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tar", "tempfile", + "tokio", "tracing", - "wasmtime", - "wasmtime-wasi", + "wasmer", + "wasmer-config", + "wasmer-types", + "wasmer-wasix", + "webc", "zstd", ] [[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] +name = "pglite-oxide-aot-aarch64-apple-darwin" +version = "0.3.0" [[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-aot-x86_64-pc-windows-msvc" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" +version = "0.3.0" + +[[package]] +name = "pglite-oxide-assets" +version = "0.3.0" dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", + "serde", + "serde_json", ] [[package]] @@ -3104,26 +3599,6 @@ dependencies = [ "serde", ] -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.13.1" @@ -3134,26 +3609,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.6", -] - [[package]] name = "phf_generator" version = "0.11.3" @@ -3174,20 +3629,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "phf_macros" version = "0.11.3" @@ -3216,38 +3657,40 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.8.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] -name = "phf_shared" -version = "0.11.3" +name = "pin-project" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ - "siphasher 1.0.2", + "pin-project-internal", ] [[package]] -name = "phf_shared" -version = "0.13.1" +name = "pin-project-internal" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ - "siphasher 1.0.2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3256,6 +3699,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.5" @@ -3281,9 +3730,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", @@ -3305,6 +3754,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3315,22 +3777,10 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -3383,11 +3833,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ - "toml_edit 0.20.7", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] @@ -3424,10 +3875,26 @@ dependencies = [ ] [[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "proc-macro2" @@ -3439,33 +3906,41 @@ dependencies = [ ] [[package]] -name = "pulley-interpreter" -version = "44.0.0" +name = "ptr_meta" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df866b7fd522992ccc6682e58b2741cc7972b163b661db24c4328f4c914cb09d" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" dependencies = [ - "cranelift-bitset", - "log", - "pulley-macros", - "wasmtime-internal-core", + "ptr_meta_derive", ] [[package]] -name = "pulley-macros" -version = "44.0.0" +name = "ptr_meta_derive" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7dfa8354acc622b3857e1bb1a4e4315d3bc1a44ad31d5653c3e87c0da9306d7" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" dependencies = [ "memchr", ] @@ -3492,17 +3967,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand" -version = "0.7.3" +name = "rancor" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", + "ptr_meta", ] [[package]] @@ -3517,13 +3987,24 @@ dependencies = [ ] [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "rand" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -3537,12 +4018,13 @@ dependencies = [ ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "getrandom 0.1.16", + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -3555,22 +4037,25 @@ dependencies = [ ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "rand_core 0.5.1", + "getrandom 0.3.4", ] [[package]] -name = "rand_pcg" -version = "0.2.1" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "raw-window-handle" @@ -3609,24 +4094,13 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ "bitflags 2.11.1", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -3658,20 +4132,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "regalloc2" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.17.0", - "log", - "rustc-hash", - "smallvec", -] - [[package]] name = "regex" version = "1.12.3" @@ -3701,11 +4161,38 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -3749,6 +4236,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.0", + "indexmap 2.14.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -3770,19 +4287,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.4" @@ -3792,25 +4296,15 @@ dependencies = [ "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] -[[package]] -name = "rustix-linux-procfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" -dependencies = [ - "once_cell", - "rustix 1.1.4", -] - [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring", @@ -3846,6 +4340,44 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty_pool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" +dependencies = [ + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", +] + +[[package]] +name = "ruzstd" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "saffron" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" +dependencies = [ + "chrono", + "nom 5.1.3", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3863,7 +4395,7 @@ checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", - "schemars_derive", + "schemars_derive 0.8.22", "serde", "serde_json", "url", @@ -3889,9 +4421,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", + "indexmap 2.14.0", "ref-cast", + "schemars_derive 1.2.1", "serde", "serde_json", + "url", ] [[package]] @@ -3907,28 +4442,22 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "schemars_derive" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] [[package]] -name = "selectors" -version = "0.24.0" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser 0.29.6", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc 0.2.0", - "smallvec", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "selectors" @@ -3937,18 +4466,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ "bitflags 2.11.1", - "cssparser 0.36.0", - "derive_more 2.1.1", + "cssparser", + "derive_more", "log", "new_debug_unreachable", "phf 0.13.1", - "phf_codegen 0.13.1", + "phf_codegen", "precomputed-hash", "rustc-hash", - "servo_arc 0.4.3", + "servo_arc", "smallvec", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.28" @@ -3981,6 +4516,17 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4056,9 +4602,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" dependencies = [ "base64 0.22.1", "chrono", @@ -4075,16 +4621,29 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -4109,32 +4668,43 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] [[package]] -name = "servo_arc" -version = "0.4.3" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "stable_deref_trait", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", ] [[package]] @@ -4160,16 +4730,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "siphasher" -version = "0.3.11" +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -4186,6 +4762,20 @@ dependencies = [ "serde", ] +[[package]] +name = "smoltcp" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f73d40463bba65efc9adc6370b56df76d563cc46e2482bba58351b4afb7535e" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "managed", +] + [[package]] name = "socket2" version = "0.6.3" @@ -4281,7 +4871,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "tokio", @@ -4319,7 +4909,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-postgres", "syn 2.0.117", @@ -4355,7 +4945,7 @@ dependencies = [ "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -4370,19 +4960,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -4395,18 +4972,6 @@ dependencies = [ "precomputed-hash", ] -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - [[package]] name = "string_cache_codegen" version = "0.6.1" @@ -4453,6 +5018,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "symbolic-common" +version = "12.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332615d90111d8eeaf86a84dc9bbe9f65d0d8c5cf11b4caccedc37754eb0dcfd" +dependencies = [ + "debugid", + "memmap2 0.9.10", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "12.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912017718eb4d21930546245af9a3475c9dccf15675a5c215664e76621afc471" +dependencies = [ + "cpp_demangle", + "msvc-demangler", + "rustc-demangle", + "symbolic-common", +] + [[package]] name = "syn" version = "1.0.109" @@ -4504,37 +5093,22 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.23", + "toml 0.8.2", "version-compare", ] -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags 2.11.1", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.59.0", - "winx", -] - [[package]] name = "tao" -version = "0.34.8" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" dependencies = [ "bitflags 2.11.1", "block2", "core-foundation", "core-graphics", "crossbeam-channel", + "dbus", "dispatch2", "dlopen2", "dpi", @@ -4545,13 +5119,14 @@ dependencies = [ "libc", "log", "ndk", - "ndk-context", "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", @@ -4598,9 +5173,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" dependencies = [ "anyhow", "bytes", @@ -4649,9 +5224,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" dependencies = [ "anyhow", "cargo_toml", @@ -4665,28 +5240,27 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.117", "tauri-utils", "thiserror 2.0.18", @@ -4698,9 +5272,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4712,9 +5286,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" dependencies = [ "anyhow", "glob", @@ -4723,15 +5297,14 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -4751,9 +5324,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" dependencies = [ "cookie", "dpi", @@ -4776,9 +5349,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" dependencies = [ "gtk", "http", @@ -4818,24 +5391,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever 0.29.1", "http", "infer", "json-patch", - "kuchikiki", "log", "memchr", "phf 0.11.3", + "plist", "proc-macro2", "quote", "regex", @@ -4847,7 +5420,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4856,13 +5429,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", ] [[package]] @@ -4874,38 +5447,37 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] [[package]] name = "tendril" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" dependencies = [ - "futf", - "mac", + "new_debug_unreachable", "utf-8", ] [[package]] -name = "tendril" -version = "0.5.0" +name = "terminal_size" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "new_debug_unreachable", - "utf-8", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "termios" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ - "winapi-util", + "libc", ] [[package]] @@ -5006,9 +5578,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -5039,6 +5611,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -5056,14 +5629,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] @@ -5098,9 +5671,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] @@ -5130,32 +5703,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.14.0", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.11", + "toml_datetime 0.6.3", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow 0.7.15", + "toml_datetime 0.6.3", + "winnow 0.5.40", ] [[package]] @@ -5264,9 +5826,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", "dirs", @@ -5278,10 +5840,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5290,6 +5852,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typeid" version = "1.0.3" @@ -5354,6 +5922,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -5393,12 +5967,24 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -5412,6 +5998,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -5460,6 +6052,85 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtual-fs" +version = "0.702.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8091f35d7e5531288dbfccd8ec8c6942c510e04147e4f191d4ed1086d3b5722" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "dashmap", + "derive_more", + "dunce", + "futures", + "getrandom 0.4.2", + "indexmap 2.14.0", + "pin-project-lite", + "replace_with", + "shared-buffer", + "slab", + "thiserror 2.0.18", + "tokio", + "tracing", + "virtual-mio", + "wasmer-package", + "webc", +] + +[[package]] +name = "virtual-mio" +version = "0.702.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db61f3409d96c79cb68ea15458d6ee1ad415235efc9896f0a878dc7d2832640c" +dependencies = [ + "async-trait", + "bytes", + "futures", + "mio", + "parking", + "serde", + "socket2", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "virtual-net" +version = "0.702.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17529a707ebafa6a085170700ea1055e3785a10026dc04c2d5eb98576df26647" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "futures-util", + "ipnet", + "iprange", + "libc", + "mio", + "pin-project-lite", + "rkyv", + "serde", + "smoltcp", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "virtual-mio", +] + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "vswhom" version = "0.1.0" @@ -5480,6 +6151,78 @@ dependencies = [ "libc", ] +[[package]] +name = "wai-bindgen-gen-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa3dc41b510811122b3088197234c27e08fcad63ef936306dd8e11e2803876c" +dependencies = [ + "anyhow", + "wai-parser", +] + +[[package]] +name = "wai-bindgen-gen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc05e8380515c4337c40ef03b2ff233e391315b178a320de8640703d522efe" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", +] + +[[package]] +name = "wai-bindgen-gen-rust-wasm" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f35ce5e74086fac87f3a7bd50f643f00fe3559adb75c88521ecaa01c8a6199" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", +] + +[[package]] +name = "wai-bindgen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5601c6f448c063e83a5e931b8fefcdf7e01ada424ad42372c948d2e3d67741" +dependencies = [ + "bitflags 1.3.2", + "wai-bindgen-rust-impl", +] + +[[package]] +name = "wai-bindgen-rust-impl" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeb5c1170246de8425a3e123e7ef260dc05ba2b522a1d369fe2315376efea4" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wai-parser" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd0acb6d70885ea0c343749019ba74f015f64a9d30542e66db69b49b7e28186" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -5499,12 +6242,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -5537,9 +6274,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -5550,9 +6287,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -5560,9 +6297,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5570,9 +6307,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -5583,9 +6320,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -5602,12 +6339,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.246.2" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ "leb128fmt", - "wasmparser 0.246.2", + "wasmparser 0.247.0", ] [[package]] @@ -5636,343 +6373,346 @@ dependencies = [ ] [[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - -[[package]] -name = "wasmparser" -version = "0.246.2" +name = "wasmer" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +checksum = "4605aab3837fdddf33ecafec6d90aa012d99dc9201570deed0d6b32c6459d36e" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.16.1", + "bindgen", + "bytes", + "cfg-if", + "cmake", + "corosensei", + "dashmap", + "derive_more", + "futures", "indexmap 2.14.0", - "semver", + "js-sys", + "more-asserts", + "paste", "serde", + "serde-wasm-bindgen", + "shared-buffer", + "symbolic-demangle", + "tar", + "target-lexicon 0.13.5", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "windows-sys 0.61.2", ] [[package]] -name = "wasmprinter" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e41f7493ba994b8a779430a4c25ff550fd5a40d291693af43a6ef48688f00e3" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.246.2", -] - -[[package]] -name = "wasmtime" -version = "44.0.0" +name = "wasmer-compiler" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca3f777dfb4db45915f95eeb25cac7f2eeb268797a27e5eb78b072618135c7f" +checksum = "2ef403f60d7977ff8571575a083edae36b8c3cf56b326ce20c315fcc812751d6" dependencies = [ - "addr2line", - "async-trait", - "bitflags 2.11.1", - "bumpalo", - "cc", + "backtrace", + "bytes", "cfg-if", - "encoding_rs", + "crossbeam-channel", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", "libc", - "log", - "mach2", - "memfd", - "object", - "once_cell", - "postcard", - "pulley-interpreter", + "macho-unwind-info", + "memmap2 0.9.10", + "more-asserts", + "object 0.39.1", + "rangemap", "rayon", - "rustix 1.1.4", - "semver", - "serde", - "serde_derive", + "region", + "rkyv", + "self_cell", + "shared-buffer", "smallvec", "target-lexicon 0.13.5", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-cache", - "wasmtime-internal-component-macro", - "wasmtime-internal-component-util", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", - "wasmtime-internal-fiber", - "wasmtime-internal-jit-debug", - "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", - "wasmtime-internal-winch", + "tempfile", + "thiserror 2.0.18", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.247.0", + "which", "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-environ" -version = "44.0.0" +name = "wasmer-config" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5ca1af838cec374931242d07af5d354aedf63f297f95b3625ac863e516ef67" +checksum = "18d113f780598913bda441a38471810cefe2cda54a621aad77d65d82fd60a703" dependencies = [ "anyhow", - "cpp_demangle", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-entity", - "gimli", - "hashbrown 0.16.1", + "bytesize", + "ciborium", + "derive_builder", + "hex", "indexmap 2.14.0", - "log", - "object", - "postcard", - "rustc-demangle", + "saffron", + "schemars 1.2.1", "semver", "serde", - "serde_derive", - "sha2", - "smallvec", - "target-lexicon 0.13.5", - "wasm-encoder 0.246.2", - "wasmparser 0.246.2", - "wasmprinter", - "wasmtime-internal-component-util", - "wasmtime-internal-core", -] - -[[package]] -name = "wasmtime-internal-cache" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2004f7c86ebeb116550655377cdf16dbf7b03ae5aa6b4b1c1458cfa23aaa306" -dependencies = [ - "base64 0.22.1", - "directories-next", - "log", - "postcard", - "rustix 1.1.4", - "serde", - "serde_derive", - "sha2", - "toml 0.9.12+spec-1.1.0", - "wasmtime-environ", - "windows-sys 0.61.2", - "zstd", -] - -[[package]] -name = "wasmtime-internal-component-macro" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b31927f7b613d8fe019609744e226f6458d8aa5e6289e92fbbc60e521cd026" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasmtime-internal-component-util", - "wasmtime-internal-wit-bindgen", - "wit-parser 0.246.2", -] - -[[package]] -name = "wasmtime-internal-component-util" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc29e3478928b93979831ba02a997ce7f707c673ce47180d643091cf4fa4f561" - -[[package]] -name = "wasmtime-internal-core" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816a61a75275c6be435131fc625a4f5956daf24d9f9f59443e81cbef228929b3" -dependencies = [ - "hashbrown 0.16.1", - "libm", - "serde", -] - -[[package]] -name = "wasmtime-internal-cranelift" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ceb5e079877e7e4565c1e2d86d9db889175d55f7ca0001315576d08c71e634" -dependencies = [ - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli", - "itertools", - "log", - "object", - "pulley-interpreter", - "smallvec", - "target-lexicon 0.13.5", - "thiserror 2.0.18", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", -] - -[[package]] -name = "wasmtime-internal-fiber" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18f8bb05d25e0d4cca7278147c9f9e2f26f66886ef754b562bf729128f1e537" -dependencies = [ - "cc", - "cfg-if", - "libc", - "rustix 1.1.4", - "wasmtime-environ", - "wasmtime-internal-versioned-export-macros", - "windows-sys 0.61.2", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", ] [[package]] -name = "wasmtime-internal-jit-debug" -version = "44.0.0" +name = "wasmer-derive" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357f1070b31154ee463937b477ca0b2962bf450b40fc59799bef2f656b15da73" +checksum = "9e83cb1ef2f745694abfd55eec5f95a1898e011143dc7adf1efb90edc746f473" dependencies = [ - "cc", - "wasmtime-internal-versioned-export-macros", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "44.0.0" +name = "wasmer-journal" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd683a94490bf755d016a09697b0955602c50106b1ded97d16983ab2ded9fed" +checksum = "1fddba7b9a7a5fb75e09e9ed8a7d12cb711d98f5d1eefa8702759063451772e6" dependencies = [ - "cfg-if", - "libc", - "wasmtime-internal-core", - "windows-sys 0.61.2", + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "lz4_flex", + "num_enum", + "rkyv", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "virtual-fs", + "virtual-net", + "wasmer", + "wasmer-config", + "wasmer-wasix-types", ] [[package]] -name = "wasmtime-internal-unwinder" -version = "44.0.0" +name = "wasmer-package" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4471746ce113c3c1862ce2c0674acb35399a4b3ed3ef4531dc087f333c74f064" +checksum = "45f8d4543ba2aae126a39ffa0b3856c6794c112ac3b4458772df2309123414ca" dependencies = [ + "anyhow", + "bytes", "cfg-if", - "cranelift-codegen", - "log", - "object", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-internal-versioned-export-macros" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6af582ec18b674bf7a17775d6fbfbddfcc143f0edbd89c9c1778239c8aa92ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "ciborium", + "flate2", + "ignore", + "insta", + "libc", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "shared-buffer", + "tar", + "tempfile", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "wasmer-config", + "wasmer-types", + "webc", ] [[package]] -name = "wasmtime-internal-winch" -version = "44.0.0" +name = "wasmer-types" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d31be8916bb60ea756d2f0ae1f634d9258442aa71e773c893e2f4cead30501b5" +checksum = "573557fe88edc93098d66f4eaad1b58ddcd5454be22dae2ed91677a7c96d4057" dependencies = [ - "cranelift-codegen", - "gimli", - "log", - "object", + "bytecheck", + "crc32fast", + "enum-iterator", + "enumset", + "getrandom 0.4.2", + "hex", + "indexmap 2.14.0", + "itertools 0.14.0", + "more-asserts", + "rkyv", + "serde", + "sha2 0.11.0", "target-lexicon 0.13.5", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-cranelift", - "winch-codegen", + "thiserror 2.0.18", + "wasmparser 0.247.0", ] [[package]] -name = "wasmtime-internal-wit-bindgen" -version = "44.0.0" +name = "wasmer-vm" +version = "7.2.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2150e63d502ab2d64754e5abe8eb737ae674b7dd4ad53144fd16bbeceaf4a19" +checksum = "e0511df85bc7bad5feb66ba6da79afd37e03bb1aae95f5a352aeda8e2babe724" dependencies = [ - "anyhow", - "bitflags 2.11.1", - "heck 0.5.0", + "backtrace", + "bytesize", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.33.0", "indexmap 2.14.0", - "wit-parser 0.246.2", + "itertools 0.14.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror 2.0.18", + "wasmer-types", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-wasi" -version = "44.0.0" +name = "wasmer-wasix" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f5109b4fd619b9796b9c9901de59d83e3575cd1226c1a36d1901371f43db28" +checksum = "4a5dfefe1640080a492a940b279f0e661354450884218abb8921417159b90f4a" dependencies = [ + "anyhow", "async-trait", - "bitflags 2.11.1", + "base64 0.22.1", + "bincode", + "blake3", + "bus", + "bytecheck", "bytes", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", + "cfg-if", + "cooked-waker", + "crossbeam-channel", + "dashmap", + "derive_more", + "flate2", + "fnv", "futures", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", - "system-interface", + "getrandom 0.3.4", + "getrandom 0.4.2", + "heapless", + "hex", + "http", + "libc", + "linked_hash_set", + "lz4_flex", + "num_enum", + "once_cell", + "petgraph", + "pin-project", + "pin-utils", + "rand 0.10.1", + "rkyv", + "rusty_pool", + "semver", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "shared-buffer", + "tempfile", + "terminal_size", + "termios", "thiserror 2.0.18", "tokio", + "tokio-stream", + "toml 1.1.2+spec-1.1.0", "tracing", "url", - "wasmtime", - "wasmtime-wasi-io", - "wiggle", + "urlencoding", + "virtual-fs", + "virtual-mio", + "virtual-net", + "waker-fn", + "wasm-encoder 0.247.0", + "wasmer", + "wasmer-config", + "wasmer-journal", + "wasmer-package", + "wasmer-types", + "wasmer-wasix-types", + "wasmparser 0.247.0", + "webc", + "weezl", "windows-sys 0.61.2", + "xxhash-rust", + "zstd", ] [[package]] -name = "wasmtime-wasi-io" -version = "44.0.0" +name = "wasmer-wasix-types" +version = "0.702.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ebe14c586e98d2fdc32c76ca0005ef28348e98ed737e776d378b3b0cc2afd0" +checksum = "dd4bfe23433cbf2c8e4d1157fa96edead70dc8f87f716720dc15613bded3880b" dependencies = [ - "async-trait", - "bytes", - "futures", + "anyhow", + "bitflags 2.11.1", + "byteorder", + "cfg-if", + "num_enum", + "serde", + "time", "tracing", - "wasmtime", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", + "wai-bindgen-gen-rust-wasm", + "wai-bindgen-rust", + "wai-parser", + "wasmer", + "wasmer-derive", + "wasmer-types", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", ] [[package]] -name = "wast" -version = "35.0.2" +name = "wasmparser" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" dependencies = [ - "leb128", + "bitflags 2.11.1", + "indexmap 2.14.0", ] [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -5985,9 +6725,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webc" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b8b523112384e8c1caf77cffdd371ca7e0013779f081e6beafc537f5b5325d" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "cfg-if", + "ciborium", + "document-features", + "ignore", + "indexmap 2.14.0", + "leb128", + "lexical-sort", + "libc", + "once_cell", + "path-clean", + "rand 0.9.4", + "serde", + "serde_json", + "sha2 0.10.9", + "shared-buffer", + "thiserror 2.0.18", + "url", ] [[package]] @@ -6089,53 +6857,28 @@ dependencies = [ ] [[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - -[[package]] -name = "wiggle" -version = "44.0.0" +name = "weezl" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cff414ef7dce0cc1cf8a033ff80d3f38e3987c37e3efeec7926ecb5ffaaae6" -dependencies = [ - "bitflags 2.11.1", - "thiserror 2.0.18", - "tracing", - "wasmtime", - "wasmtime-environ", - "wiggle-macro", -] +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] -name = "wiggle-generate" -version = "44.0.0" +name = "which" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf9dc7272b151a9616a2699e7f94ea1d4ae253b47b63a79fbc8f38e2cca5fa6" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasmtime-environ", - "witx", + "libc", ] [[package]] -name = "wiggle-macro" -version = "44.0.0" +name = "whoami" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa7c29fcf738630cba4e35f1805da5e42dde20ee9809ee9202b0648ae671602f" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "wiggle-generate", + "libredox", + "wasite", ] [[package]] @@ -6169,25 +6912,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "44.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9339858ad222412200fd8b1af9e270712201aaec440c7618991443af3446481f" -dependencies = [ - "cranelift-assembler-x64", - "cranelift-codegen", - "gimli", - "regalloc2", - "smallvec", - "target-lexicon 0.13.5", - "thiserror 2.0.18", - "wasmparser 0.246.2", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", -] - [[package]] name = "window-vibrancy" version = "0.6.0" @@ -6378,15 +7102,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -6435,30 +7150,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.1.0" @@ -6495,12 +7193,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6519,12 +7211,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6543,24 +7229,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6579,12 +7253,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6603,12 +7271,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6627,12 +7289,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6651,12 +7307,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.5.40" @@ -6671,9 +7321,6 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] [[package]] name = "winnow" @@ -6694,16 +7341,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winx" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" -dependencies = [ - "bitflags 2.11.1", - "windows-sys 0.59.0", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -6727,7 +7364,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser 0.244.0", + "wit-parser", ] [[package]] @@ -6777,7 +7414,7 @@ dependencies = [ "wasm-encoder 0.244.0", "wasm-metadata", "wasmparser 0.244.0", - "wit-parser 0.244.0", + "wit-parser", ] [[package]] @@ -6798,37 +7435,6 @@ dependencies = [ "wasmparser 0.244.0", ] -[[package]] -name = "wit-parser" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" -dependencies = [ - "anyhow", - "hashbrown 0.16.1", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.246.2", -] - -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror 1.0.69", - "wast", -] - [[package]] name = "writeable" version = "0.6.3" @@ -6837,9 +7443,9 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.54.4" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2", @@ -6865,7 +7471,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -6907,9 +7513,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.2" @@ -6935,9 +7547,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", "async-executor", @@ -6955,14 +7567,14 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.4", + "rustix", "serde", "serde_repr", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", + "winnow 1.0.2", "zbus_macros", "zbus_names", "zvariant", @@ -6970,9 +7582,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", @@ -6985,12 +7597,12 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 0.7.15", + "winnow 1.0.2", "zvariant", ] @@ -7110,23 +7722,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.15", + "winnow 1.0.2", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", @@ -7137,13 +7749,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" dependencies = [ "proc-macro2", "quote", "serde", "syn 2.0.117", - "winnow 0.7.15", + "winnow 1.0.2", ] diff --git a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml b/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml index 7c7a59fe..40a271da 100644 --- a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml +++ b/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml @@ -6,6 +6,8 @@ authors = ["pglite-oxide contributors"] edition = "2021" publish = false +[workspace] + [lib] name = "tauri_sqlx_vanilla_lib" crate-type = ["staticlib", "cdylib", "rlib"] diff --git a/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index a6242f8f..8c58ab99 100644 --- a/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -110,7 +110,7 @@ impl DatabaseHarness { .await?; let preload_paths = paths.clone(); - time_blocking(&mut startup, "load/compile wasmtime module", move || { + time_blocking(&mut startup, "load Wasmer AOT module", move || { preload_runtime_module(&preload_paths) }) .await?; diff --git a/prek.toml b/prek.toml index 1abe2ed8..a6017c7c 100644 --- a/prek.toml +++ b/prek.toml @@ -36,6 +36,6 @@ hooks = [ { id = "cargo-fmt", name = "cargo fmt", language = "system", entry = "cargo fmt --check", pass_filenames = false, files = "\\.(rs|toml)$", stages = ["pre-commit"] }, { id = "tauri-cargo-fmt", name = "Tauri cargo fmt", language = "system", entry = "cargo fmt --manifest-path examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --check", pass_filenames = false, files = "^examples/tauri-sqlx-vanilla/src-tauri/.*\\.(rs|toml)$", stages = ["pre-commit"] }, { id = "git-diff-check", name = "git diff --check", language = "system", entry = "git diff --check", pass_filenames = false, always_run = true, stages = ["pre-push"] }, - { id = "cargo-clippy", name = "cargo clippy", language = "system", entry = "cargo clippy --all-targets --locked -- -D warnings", pass_filenames = false, always_run = true, stages = ["pre-push"] }, - { id = "cargo-test", name = "cargo test --all-targets", language = "system", entry = "cargo test --all-targets --locked -- --nocapture", pass_filenames = false, always_run = true, stages = ["pre-push"] }, + { id = "pre-push-fmt", name = "pre-push cargo fmt", language = "system", entry = "cargo fmt --all --check", pass_filenames = false, always_run = true, stages = ["pre-push"] }, + { id = "pre-push-check", name = "pre-push cargo check", language = "system", entry = "cargo check --workspace --locked", pass_filenames = false, always_run = true, stages = ["pre-push"] }, ] diff --git a/release-plz.toml b/release-plz.toml index 5a9e87f3..8aef01f4 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -1,14 +1,16 @@ [workspace] +allow_dirty = true changelog_update = true -dependencies_update = false +dependencies_update = true features_always_increment_minor = false git_release_enable = true -git_release_name = "{{ version }}" -git_tag_name = "{{ version }}" +git_release_name = "{{ package }} v{{ version }}" +git_tag_name = "{{ package }}-v{{ version }}" pr_branch_prefix = "release-plz-" pr_labels = ["release"] pr_name = "chore(release): {{ version }}" publish = true +publish_allow_dirty = true publish_timeout = "30m" release_always = true release_commits = '^((feat|fix|perf|refactor|revert)(\([a-z0-9][a-z0-9._/-]*\))?(!)?|[a-z]+(\([a-z0-9][a-z0-9._/-]*\))?!): .+' @@ -32,3 +34,57 @@ commit_parsers = [ { message = "^chore", skip = true }, { message = "^style", skip = true }, ] + +[[package]] +name = "pglite-oxide" +version_group = "pglite-oxide" +changelog_path = "CHANGELOG.md" +changelog_include = [ + "pglite-oxide-assets", + "pglite-oxide-aot-aarch64-apple-darwin", + "pglite-oxide-aot-x86_64-unknown-linux-gnu", + "pglite-oxide-aot-aarch64-unknown-linux-gnu", + "pglite-oxide-aot-x86_64-pc-windows-msvc", +] +git_release_enable = true +git_tag_name = "{{ version }}" + +[[package]] +name = "pglite-oxide-assets" +version_group = "pglite-oxide" +changelog_update = false +git_release_enable = false +git_tag_enable = false +semver_check = false + +[[package]] +name = "pglite-oxide-aot-aarch64-apple-darwin" +version_group = "pglite-oxide" +changelog_update = false +git_release_enable = false +git_tag_enable = false +semver_check = false + +[[package]] +name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" +version_group = "pglite-oxide" +changelog_update = false +git_release_enable = false +git_tag_enable = false +semver_check = false + +[[package]] +name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" +version_group = "pglite-oxide" +changelog_update = false +git_release_enable = false +git_tag_enable = false +semver_check = false + +[[package]] +name = "pglite-oxide-aot-x86_64-pc-windows-msvc" +version_group = "pglite-oxide" +changelog_update = false +git_release_enable = false +git_tag_enable = false +semver_check = false diff --git a/scripts/bootstrap-tools.sh b/scripts/bootstrap-tools.sh new file mode 100755 index 00000000..6c8fcca6 --- /dev/null +++ b/scripts/bootstrap-tools.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +set -euo pipefail + +PREK_VERSION="${PREK_VERSION:-0.3.10}" +CARGO_BINSTALL_VERSION="${CARGO_BINSTALL_VERSION:-1.19.1}" +CARGO_DENY_VERSION="${CARGO_DENY_VERSION:-0.19.4}" +CARGO_HACK_VERSION="${CARGO_HACK_VERSION:-0.6.44}" +CARGO_SEMVER_CHECKS_VERSION="${CARGO_SEMVER_CHECKS_VERSION:-0.47.0}" +ZIZMOR_VERSION="${ZIZMOR_VERSION:-1.24.1}" +ACTIONLINT_VERSION="${ACTIONLINT_VERSION:-1.7.12}" + +cargo_bin_dir="${CARGO_HOME:-$HOME/.cargo}/bin" +mkdir -p "$cargo_bin_dir" +PATH="$cargo_bin_dir:$PATH" +export PATH + +has_command() { + command -v "$1" >/dev/null 2>&1 +} + +installed_tool_version() { + binary="$1" + case "$binary" in + cargo-hack) cargo hack --version 2>/dev/null || true ;; + cargo-semver-checks) cargo semver-checks --version 2>/dev/null || true ;; + *) "$binary" --version 2>/dev/null || true ;; + esac +} + +version_output_matches() { + output="$1" + version="$2" + escaped_version="$(printf '%s' "$version" | sed 's/[][\\.^$*+?{}|()]/\\&/g')" + printf '%s\n' "$output" | grep -Eq "(^|[^0-9.])${escaped_version}([^0-9.]|$)" +} + +require_pinned_version() { + binary="$1" + version="$2" + output="$3" + if ! version_output_matches "$output" "$version"; then + cat >&2 </dev/null + return + fi + echo "cargo-binstall could not install $package@$version from a binary; falling back to cargo install" >&2 + fi + cargo install "$package" --version "$version" --locked + installed_pinned_tool_version "$binary" "$version" >/dev/null +} + +install_cargo_binstall() { + if has_command cargo-binstall; then + output="$(cargo-binstall -V 2>/dev/null || true)" + require_pinned_version cargo-binstall "$CARGO_BINSTALL_VERSION" "$output" + echo "cargo-binstall already installed: $output" + return + fi + + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + case "$os:$arch" in + darwin:arm64) asset="cargo-binstall-aarch64-apple-darwin.zip"; extract=zip ;; + darwin:x86_64) asset="cargo-binstall-x86_64-apple-darwin.zip"; extract=zip ;; + linux:aarch64 | linux:arm64) asset="cargo-binstall-aarch64-unknown-linux-musl.tgz"; extract=tgz ;; + linux:x86_64) asset="cargo-binstall-x86_64-unknown-linux-musl.tgz"; extract=tgz ;; + *) + echo "unsupported cargo-binstall platform: $os/$arch" >&2 + echo "falling back to source-built cargo-installed tools" >&2 + return 0 + ;; + esac + + tmp="$(mktemp -d)" + archive="$tmp/$asset" + url="https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/${asset}" + curl -L --fail --retry 3 --output "$archive" "$url" + case "$extract" in + zip) + python3 - "$archive" "$tmp" <<'PY' +import sys +import zipfile + +with zipfile.ZipFile(sys.argv[1]) as archive: + archive.extractall(sys.argv[2]) +PY + ;; + tgz) + tar -xzf "$archive" -C "$tmp" + ;; + esac + binstall_bin="$(find "$tmp" -type f -name cargo-binstall | head -n 1)" + if [ -z "$binstall_bin" ]; then + echo "cargo-binstall archive did not contain a cargo-binstall binary" >&2 + find "$tmp" -maxdepth 3 -type f -print >&2 + return 1 + fi + install "$binstall_bin" "$cargo_bin_dir/cargo-binstall" + rm -rf "$tmp" + output="$(cargo-binstall -V 2>/dev/null || true)" + require_pinned_version cargo-binstall "$CARGO_BINSTALL_VERSION" "$output" +} + +install_actionlint() { + if has_command actionlint; then + output="$(actionlint -version 2>/dev/null || true)" + require_pinned_version actionlint "$ACTIONLINT_VERSION" "$output" + echo "actionlint already installed: $output" + return + fi + + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + case "$os:$arch" in + darwin:arm64) asset_os=darwin; asset_arch=arm64 ;; + darwin:x86_64) asset_os=darwin; asset_arch=amd64 ;; + linux:aarch64 | linux:arm64) asset_os=linux; asset_arch=arm64 ;; + linux:x86_64) asset_os=linux; asset_arch=amd64 ;; + *) + echo "unsupported actionlint platform: $os/$arch" >&2 + echo "install actionlint manually from https://github.com/rhysd/actionlint/releases" >&2 + return 1 + ;; + esac + + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + archive="$tmp/actionlint.tar.gz" + curl -L --fail --retry 3 \ + --output "$archive" \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${asset_os}_${asset_arch}.tar.gz" + tar -xzf "$archive" -C "$tmp" + install "$tmp/actionlint" "$cargo_bin_dir/actionlint" + output="$(actionlint -version 2>/dev/null || true)" + require_pinned_version actionlint "$ACTIONLINT_VERSION" "$output" +} + +install_cargo_binstall +install_cargo_tool prek prek "$PREK_VERSION" +install_cargo_tool cargo-deny cargo-deny "$CARGO_DENY_VERSION" +install_cargo_tool cargo-hack cargo-hack "$CARGO_HACK_VERSION" +install_cargo_tool cargo-semver-checks cargo-semver-checks "$CARGO_SEMVER_CHECKS_VERSION" +install_cargo_tool zizmor zizmor "$ZIZMOR_VERSION" +install_actionlint + +echo +echo "Tool bootstrap complete. Ensure $cargo_bin_dir is on PATH." diff --git a/scripts/check-crate-size.sh b/scripts/check-crate-size.sh index f89e9388..b0e1b051 100755 --- a/scripts/check-crate-size.sh +++ b/scripts/check-crate-size.sh @@ -3,27 +3,32 @@ set -eu mode="${1:---warn}" limit_bytes="${CRATES_IO_SIZE_LIMIT_BYTES:-10485760}" -crate_file="$(find target/package -maxdepth 1 -name 'pglite-oxide-*.crate' -type f 2>/dev/null | sort | tail -n 1 || true)" +crate_files="$(find target/package -maxdepth 1 -name '*.crate' -type f 2>/dev/null | sort || true)" -if [ -z "$crate_file" ]; then +if [ -z "$crate_files" ]; then echo "No packaged crate found under target/package; run cargo package first." >&2 exit 1 fi -size_bytes="$(wc -c < "$crate_file" | tr -d ' ')" -size_mib="$(awk "BEGIN { printf \"%.2f\", $size_bytes / 1048576 }")" limit_mib="$(awk "BEGIN { printf \"%.2f\", $limit_bytes / 1048576 }")" +failed=0 -if [ "$size_bytes" -le "$limit_bytes" ]; then - echo "crate size ok: $crate_file is ${size_mib}MiB <= ${limit_mib}MiB" - exit 0 -fi +for crate_file in $crate_files; do + size_bytes="$(wc -c < "$crate_file" | tr -d ' ')" + size_mib="$(awk "BEGIN { printf \"%.2f\", $size_bytes / 1048576 }")" + + if [ "$size_bytes" -le "$limit_bytes" ]; then + echo "crate size ok: $crate_file is ${size_mib}MiB <= ${limit_mib}MiB" + continue + fi -message="crate size warning: $crate_file is ${size_mib}MiB > ${limit_mib}MiB" -if [ "$mode" = "--enforce" ]; then + message="crate size warning: $crate_file is ${size_mib}MiB > ${limit_mib}MiB" echo "$message" >&2 + failed=1 +done + +if [ "$mode" = "--enforce" ] && [ "$failed" -ne 0 ]; then exit 1 fi -echo "$message" >&2 exit 0 diff --git a/scripts/check-dependency-invariants.sh b/scripts/check-dependency-invariants.sh new file mode 100755 index 00000000..5c36a64e --- /dev/null +++ b/scripts/check-dependency-invariants.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$root" + +blocked='wasm''time|wasm''time-wasi|wasmer-compiler-(llvm|cranelift|singlepass)|llvm-sys|cranelift-|singlepass' + +if cargo tree -p pglite-oxide --features extensions --locked | rg -n "$blocked"; then + cat >&2 <<'MSG' +blocked runtime dependency found in the normal user dependency tree. + +The production path must stay on headless Wasmer AOT loading. Backend compiler +crates such as LLVM, Cranelift, Singlepass, and Wasmtime must not enter the +normal user build. +MSG + exit 1 +fi + +if cargo tree -p xtask --features aot-serializer --locked | rg -n 'wasmer-compiler-(cranelift|singlepass)|cranelift-|singlepass|wasm''time'; then + cat >&2 <<'MSG' +blocked maintainer serializer dependency found. + +The AOT serializer may use Wasmer LLVM only. Cranelift, Singlepass, and Wasmtime +belong in isolated maintainer experiments, not in release/AOT tooling. +MSG + exit 1 +fi + +echo "dependency invariants ok" diff --git a/scripts/ci-scope.sh b/scripts/ci-scope.sh new file mode 100755 index 00000000..936925af --- /dev/null +++ b/scripts/ci-scope.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +base_ref="${1:-}" +head_ref="${2:-HEAD}" + +root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$root" + +all_true=false +if [[ -z "$base_ref" ]] || ! git rev-parse --verify -q "$base_ref^{commit}" >/dev/null; then + all_true=true +fi +if ! git rev-parse --verify -q "$head_ref^{commit}" >/dev/null; then + all_true=true +fi + +if [[ "$all_true" == true ]]; then + changed_files="*" +else + changed_files="$(git diff --name-only "$base_ref...$head_ref" --)" +fi + +repo=false +rust=false +examples=false +package=false +assets=false +ci=false +docs=false + +set_all_true() { + repo=true + rust=true + examples=true + package=true + assets=true + ci=true + docs=true +} + +if [[ "$changed_files" == "*" ]]; then + set_all_true +else + while IFS= read -r file; do + [[ -z "$file" ]] && continue + + case "$file" in + .github/workflows/* | .github/scripts/* | .github/actions/* | .github/zizmor.yml | scripts/* | prek.toml | deny.toml | clippy.toml | rust-toolchain.toml) + repo=true + ci=true + ;; + .github/*) + repo=true + docs=true + ;; + README.md | CHANGELOG.md | docs/*) + repo=true + docs=true + ;; + esac + + case "$file" in + Cargo.toml | build.rs | crates/*/Cargo.toml | crates/aot/*/Cargo.toml) + repo=true + rust=true + package=true + ;; + Cargo.lock | src/* | tests/*) + repo=true + rust=true + ;; + xtask/*) + repo=true + rust=true + assets=true + ;; + esac + + case "$file" in + assets/* | crates/assets/* | crates/aot/*) + repo=true + rust=true + assets=true + ;; + esac + + case "$file" in + examples/*) + repo=true + examples=true + ;; + esac + done <<< "$changed_files" +fi + +if [[ "$assets" == true ]]; then + rust=true + package=true +fi + +docs_only=false +if [[ "$docs" == true && "$rust" == false && "$examples" == false && "$package" == false && "$assets" == false && "$ci" == false ]]; then + docs_only=true +fi + +emit() { + local key="$1" + local value="$2" + printf '%s=%s\n' "$key" "$value" + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + printf '%s=%s\n' "$key" "$value" >> "$GITHUB_OUTPUT" + fi +} + +emit repo "$repo" +emit rust "$rust" +emit examples "$examples" +emit package "$package" +emit assets "$assets" +emit ci "$ci" +emit docs "$docs" +emit docs_only "$docs_only" diff --git a/scripts/perf/build_bench_matrix.mjs b/scripts/perf/build_bench_matrix.mjs new file mode 100644 index 00000000..e97e7538 --- /dev/null +++ b/scripts/perf/build_bench_matrix.mjs @@ -0,0 +1,239 @@ +import fs from 'node:fs/promises' +import process from 'node:process' + +function parseArgs(argv) { + const args = {} + for (let index = 0; index < argv.length; index += 1) { + const key = argv[index] + if (!key.startsWith('--')) { + continue + } + const value = argv[index + 1] + if (value && !value.startsWith('--')) { + args[key] = value + index += 1 + } else { + args[key] = 'true' + } + } + return args +} + +function requireArg(args, key) { + const value = args[key] + if (!value) { + throw new Error(`${key} is required`) + } + return value +} + +function sum(values) { + return values.reduce((total, value) => total + value, 0) +} + +function mean(values) { + return sum(values) / values.length +} + +function round(value, decimals = 2) { + return Number(value.toFixed(decimals)) +} + +function formatMicros(value) { + return `${round(value)} us` +} + +function formatMillis(value) { + return `${round(value)} ms` +} + +function formatMillisFromMicros(value) { + if (value === null || value === undefined) { + return '-' + } + return formatMillis(value / 1000) +} + +function formatSecondsFromMicros(value) { + return `${round(value / 1_000_000, 3)} s` +} + +function formatRatio(numerator, denominator) { + if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) { + return '-' + } + return `${round(numerator / denominator, 2)}x` +} + +function readJson(jsonPath) { + return fs.readFile(jsonPath, 'utf8').then((text) => JSON.parse(text)) +} + +function collectRun(report, suite, mode) { + const run = report.runs.find((entry) => entry.suite === suite && entry.mode === mode) + if (!run) { + throw new Error(`missing ${suite}/${mode} run`) + } + return run +} + +function rttAverageMicros(run) { + return mean(run.tests.map((test) => test.averageMicros ?? test.trimmedAverageMicros)) +} + +function speedTotalMicros(run) { + return sum(run.tests.map((test) => test.elapsedMicros)) +} + +function indexTestsById(run) { + return new Map(run.tests.map((test) => [test.id, test])) +} + +async function main() { + const args = parseArgs(process.argv.slice(2)) + const output = requireArg(args, '--output') + const oxidePath = requireArg(args, '--oxide') + const nativePath = requireArg(args, '--native') + const nodePath = requireArg(args, '--node') + const nodeServerPath = requireArg(args, '--node-server') + const runId = requireArg(args, '--run-id') + const nativeVersion = requireArg(args, '--native-version') + const machineOs = requireArg(args, '--machine-os') + const machineCpu = requireArg(args, '--machine-cpu') + const machineRam = requireArg(args, '--machine-ram') + const machineCores = requireArg(args, '--machine-cores') + + const [oxide, native, node, nodeServer] = await Promise.all([ + readJson(oxidePath), + readJson(nativePath), + readJson(nodePath), + readJson(nodeServerPath), + ]) + + const oxideRttSqlx = collectRun(oxide, 'rtt', 'server_sqlx') + const oxideSpeedSqlx = collectRun(oxide, 'speed', 'server_sqlx') + const nativeRttSqlx = collectRun(native, 'rtt', 'native_postgres_sqlx') + const nativeSpeedSqlx = collectRun(native, 'speed', 'native_postgres_sqlx') + const nodeRttNodefsSqlx = collectRun(node, 'rtt', 'pglite_nodefs_sqlx') + const nodeSpeedNodefsSqlx = collectRun(node, 'speed', 'pglite_nodefs_sqlx') + + const headlineModes = [ + { + label: 'native pg + SQLx', + rttRun: nativeRttSqlx, + speedRun: nativeSpeedSqlx, + openMicros: nativeRttSqlx.openMicros, + connectMicros: nativeRttSqlx.connectMicros, + setupMicros: nativeRttSqlx.setupMicros, + }, + { + label: 'pglite-oxide + SQLx', + rttRun: oxideRttSqlx, + speedRun: oxideSpeedSqlx, + openMicros: oxideRttSqlx.openMicros, + connectMicros: oxideRttSqlx.connectMicros, + setupMicros: oxideRttSqlx.setupMicros, + }, + { + label: 'vanilla PGlite + SQLx', + rttRun: nodeRttNodefsSqlx, + speedRun: nodeSpeedNodefsSqlx, + openMicros: nodeRttNodefsSqlx.openMicros, + connectMicros: nodeRttNodefsSqlx.connectMicros, + setupMicros: nodeRttNodefsSqlx.setupMicros, + }, + ] + + const speedMaps = { + oxideSqlx: indexTestsById(oxideSpeedSqlx), + nativeSqlx: indexTestsById(nativeSpeedSqlx), + nodeNodefsSqlx: indexTestsById(nodeSpeedNodefsSqlx), + } + + const lines = [] + lines.push(`# Benchmark Matrix ${runId}`) + lines.push('') + lines.push('Machine-local comparison for the current checkout. Each mode runs serially, never in parallel, so no benchmark shares CPU, disk, or memory pressure with another run.') + lines.push('') + lines.push('## Environment') + lines.push('') + lines.push(`- OS: \`${machineOs}\``) + lines.push(`- CPU: \`${machineCpu}\``) + lines.push(`- RAM: \`${machineRam}\``) + lines.push(`- Logical cores: \`${machineCores}\``) + lines.push(`- Node: \`${nodeServer.node}\``) + lines.push( + `- npm packages: \`${nodeServer.package}@${nodeServer.version}\`, \`${nodeServer.socketPackage}@${nodeServer.socketVersion}\``, + ) + lines.push(`- Native Postgres: \`${nativeVersion}\``) + lines.push(`- Oxide Wasmer: \`${oxide.wasmerVersion}\``) + lines.push(`- Oxide Wasmer WASIX: \`${oxide.wasmerWasixVersion}\``) + lines.push(`- RTT iterations: \`${oxide.rttIterations}\``) + lines.push(`- Speed source: exact upstream SQL from \`assets/checkouts/pglite/packages/benchmark/src\``) + lines.push('') + lines.push('## Headline') + lines.push('') + lines.push('| Metric | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx |') + lines.push('|---|---:|---:|---:|') + + lines.push( + `| Open | ${formatMillisFromMicros(headlineModes[0].openMicros)} | ${formatMillisFromMicros(headlineModes[1].openMicros)} | ${formatMillisFromMicros(headlineModes[2].openMicros)} |`, + ) + + lines.push( + `| Connect | ${formatMillisFromMicros(headlineModes[0].connectMicros)} | ${formatMillisFromMicros(headlineModes[1].connectMicros)} | ${formatMillisFromMicros(headlineModes[2].connectMicros)} |`, + ) + + const rttMetrics = headlineModes.map((mode) => ({ + label: mode.label, + value: rttAverageMicros(mode.rttRun), + })) + lines.push( + `| RTT mean | ${formatMicros(rttMetrics[0].value)} | ${formatMicros(rttMetrics[1].value)} | ${formatMicros(rttMetrics[2].value)} |`, + ) + + const speedMetrics = headlineModes.map((mode) => ({ + label: mode.label, + value: speedTotalMicros(mode.speedRun), + })) + lines.push( + `| Speed total | ${formatSecondsFromMicros(speedMetrics[0].value)} | ${formatSecondsFromMicros(speedMetrics[1].value)} | ${formatSecondsFromMicros(speedMetrics[2].value)} |`, + ) + + lines.push('') + lines.push('## Relative view') + lines.push('') + lines.push(`- pglite-oxide + SQLx RTT vs vanilla PGlite + SQLx: ${formatRatio(rttAverageMicros(oxideRttSqlx), rttAverageMicros(nodeRttNodefsSqlx))}`) + lines.push(`- pglite-oxide + SQLx RTT vs native pg + SQLx: ${formatRatio(rttAverageMicros(oxideRttSqlx), rttAverageMicros(nativeRttSqlx))}`) + lines.push(`- pglite-oxide + SQLx speed total vs vanilla PGlite + SQLx: ${formatRatio(speedTotalMicros(oxideSpeedSqlx), speedTotalMicros(nodeSpeedNodefsSqlx))}`) + lines.push(`- pglite-oxide + SQLx speed total vs native pg + SQLx: ${formatRatio(speedTotalMicros(oxideSpeedSqlx), speedTotalMicros(nativeSpeedSqlx))}`) + lines.push('') + lines.push('## Speed Suite') + lines.push('') + lines.push('| ID | Test | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx |') + lines.push('|---|---|---:|---:|---:|') + + for (const test of oxideSpeedSqlx.tests) { + const oxideSqlx = speedMaps.oxideSqlx.get(test.id).elapsedMicros + const nativeSqlx = speedMaps.nativeSqlx.get(test.id).elapsedMicros + const nodeNodefsSqlx = speedMaps.nodeNodefsSqlx.get(test.id).elapsedMicros + lines.push( + `| ${test.id} | ${test.label} | ${formatMillis(nativeSqlx / 1000)} | ${formatMillis(oxideSqlx / 1000)} | ${formatMillis(nodeNodefsSqlx / 1000)} |`, + ) + } + + lines.push('') + lines.push('## Notes') + lines.push('') + lines.push('- This matrix is meant for local reproducibility, not universal absolute claims. Different CPUs, filesystems, Node versions, and native Postgres builds will move the numbers.') + lines.push('- The serial runner intentionally avoids parallel execution so disk caches, CPU scheduling, and memory pressure stay isolated by mode.') + lines.push('- The SQLx-to-SQLx comparison to focus on in product docs is `native pg + SQLx` vs `pglite-oxide + SQLx` vs `vanilla PGlite + SQLx`.') + lines.push('') + + await fs.writeFile(output, `${lines.join('\n')}\n`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/perf/node-bench/package-lock.json b/scripts/perf/node-bench/package-lock.json new file mode 100644 index 00000000..7b3b8512 --- /dev/null +++ b/scripts/perf/node-bench/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "pglite-oxide-node-bench", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pglite-oxide-node-bench", + "dependencies": { + "@electric-sql/pglite": "0.4.5", + "@electric-sql/pglite-socket": "0.1.5" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.5.tgz", + "integrity": "sha512-aGG2zGEyZzGWKy8P+9ZoNUV0jxt1+hgbeTf+bVAYyxVZZLXg3/9aFlfLxb08AYZVAfAkQlQIysmWjhc5hwDG8g==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.5.tgz", + "integrity": "sha512-/RAye+3EPKfO9nY4tljzxXmkT7yIpFDm0L3F+c28b+Z6uxPOjy/Zz/QEHYHXcrfuUC88/a9S72EO0+3E0j97wQ==", + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.5" + } + } + } +} diff --git a/scripts/perf/node-bench/package.json b/scripts/perf/node-bench/package.json new file mode 100644 index 00000000..9cffc3a9 --- /dev/null +++ b/scripts/perf/node-bench/package.json @@ -0,0 +1,9 @@ +{ + "name": "pglite-oxide-node-bench", + "private": true, + "type": "module", + "dependencies": { + "@electric-sql/pglite": "0.4.5", + "@electric-sql/pglite-socket": "0.1.5" + } +} diff --git a/scripts/perf/node-bench/start_nodefs_socket.mjs b/scripts/perf/node-bench/start_nodefs_socket.mjs new file mode 100644 index 00000000..0b1db3e6 --- /dev/null +++ b/scripts/perf/node-bench/start_nodefs_socket.mjs @@ -0,0 +1,115 @@ +import { performance } from 'node:perf_hooks' +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { PGlite } from '@electric-sql/pglite' +import { PGLiteSocketServer } from '@electric-sql/pglite-socket' + +function parseArgs(argv) { + const args = {} + for (let index = 0; index < argv.length; index += 1) { + const key = argv[index] + if (!key.startsWith('--')) { + continue + } + const value = argv[index + 1] + if (value && !value.startsWith('--')) { + args[key] = value + index += 1 + } else { + args[key] = 'true' + } + } + return args +} + +function requireArg(args, key) { + const value = args[key] + if (!value) { + throw new Error(`${key} is required`) + } + return value +} + +function nowMicros() { + return Math.round(performance.now() * 1000) +} + +function elapsedMicros(startMicros) { + return nowMicros() - startMicros +} + +async function main() { + const args = parseArgs(process.argv.slice(2)) + const readyPath = requireArg(args, '--ready') + const runId = requireArg(args, '--run-id') + + const scriptDir = path.dirname(fileURLToPath(import.meta.url)) + const repoRoot = path.resolve(scriptDir, '../../..') + const dataDir = + args['--data-dir'] ?? + path.join(repoRoot, 'target/perf/node-bench/runtime', runId, 'pglite_nodefs_sqlx') + + await fs.mkdir(path.dirname(readyPath), { recursive: true }) + await fs.rm(readyPath, { force: true }) + await fs.rm(dataDir, { recursive: true, force: true }) + await fs.mkdir(dataDir, { recursive: true }) + + const openStarted = nowMicros() + const db = new PGlite(dataDir) + await db.waitReady + const server = new PGLiteSocketServer({ + db, + host: '127.0.0.1', + port: 0, + maxConnections: 1, + }) + await server.start() + const openMicros = elapsedMicros(openStarted) + + const [host, port] = server.getServerConn().split(':') + const databaseUrl = `postgresql://postgres:postgres@${host}:${port}/postgres?sslmode=disable` + const ready = { + databaseUrl, + host, + port: Number(port), + dataDir, + openMicros, + node: process.version, + package: '@electric-sql/pglite', + version: '0.4.5', + socketPackage: '@electric-sql/pglite-socket', + socketVersion: '0.1.5', + } + await fs.writeFile(readyPath, `${JSON.stringify(ready, null, 2)}\n`) + console.log(`PGlite NodeFS socket ready at ${host}:${port}`) + + let shuttingDown = false + const stop = async () => { + if (shuttingDown) { + return + } + shuttingDown = true + await server.stop() + await db.close() + } + + await new Promise((resolve) => { + for (const signal of ['SIGINT', 'SIGTERM']) { + process.once(signal, () => { + resolve() + }) + } + process.once('disconnect', () => { + resolve() + }) + }) + await stop() +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/perf/run_bench_matrix.sh b/scripts/perf/run_bench_matrix.sh new file mode 100755 index 00000000..e40f0428 --- /dev/null +++ b/scripts/perf/run_bench_matrix.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +TARGET_DIR="$REPO_ROOT/target/perf" +NODE_BENCH_DIR="$SCRIPT_DIR/node-bench" + +RUN_ID="${1:-$(date -u +%Y%m%dT%H%M%SZ)}" + +POSTGRES_BIN="${PGLITE_OXIDE_NATIVE_POSTGRES:-/opt/homebrew/opt/postgresql@18/bin/postgres}" +INITDB_BIN="${PGLITE_OXIDE_NATIVE_INITDB:-/opt/homebrew/opt/postgresql@18/bin/initdb}" + +if [[ ! -x "$POSTGRES_BIN" ]]; then + POSTGRES_BIN="$(command -v postgres)" +fi + +if [[ ! -x "$INITDB_BIN" ]]; then + INITDB_BIN="$(command -v initdb)" +fi + +mkdir -p "$TARGET_DIR" + +if [[ ! -d "$NODE_BENCH_DIR/node_modules" ]]; then + ( + cd "$NODE_BENCH_DIR" + npm install --no-fund --no-audit + ) +fi + +OXIDE_JSON="$TARGET_DIR/bench-oxide-$RUN_ID.json" +NATIVE_JSON="$TARGET_DIR/bench-native-postgres-sqlx-$RUN_ID.json" +NODE_JSON="$TARGET_DIR/bench-pglite-nodefs-sqlx-$RUN_ID.json" +NODE_READY_JSON="$TARGET_DIR/bench-pglite-nodefs-sqlx-ready-$RUN_ID.json" +NODE_LOG="$TARGET_DIR/bench-pglite-nodefs-sqlx-$RUN_ID.log" +REPORT_MD="$TARGET_DIR/bench-comparison-$RUN_ID.md" + +NATIVE_VERSION="$("$POSTGRES_BIN" --version | sed 's/^postgres (PostgreSQL) //')" +OS_LABEL="$(uname -smr)" +if command -v sw_vers >/dev/null 2>&1; then + OS_LABEL="$(sw_vers -productName) $(sw_vers -productVersion) (${OS_LABEL})" +fi +CPU_LABEL="$(sysctl -n machdep.cpu.brand_string 2>/dev/null || uname -m)" +RAM_LABEL="$( + python3 - <<'PY' +import os +try: + mem = int(os.popen('sysctl -n hw.memsize').read().strip()) + print(f"{mem/1024/1024/1024:.0f} GB") +except Exception: + print("unknown") +PY +)" +CORES_LABEL="$(sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN || echo unknown)" + +echo "Running oxide benchmark suite..." +cargo run --release -p xtask -- perf bench \ + --suite all \ + --mode server-sqlx \ + --iterations 100 \ + --speed-source pglite \ + > "$OXIDE_JSON" + +echo "Running native Postgres SQLx benchmark suite..." +cargo run --release -p xtask -- perf native-postgres \ + --suite all \ + --iterations 100 \ + --speed-source pglite \ + --client sqlx \ + --postgres-bin "$POSTGRES_BIN" \ + --initdb-bin "$INITDB_BIN" \ + > "$NATIVE_JSON" + +echo "Starting PGlite NodeFS socket server..." +node "$NODE_BENCH_DIR/start_nodefs_socket.mjs" \ + --ready "$NODE_READY_JSON" \ + --run-id "$RUN_ID" \ + > "$NODE_LOG" 2>&1 & +NODE_PID="$!" +cleanup_node_server() { + if kill -0 "$NODE_PID" >/dev/null 2>&1; then + kill "$NODE_PID" >/dev/null 2>&1 || true + wait "$NODE_PID" >/dev/null 2>&1 || true + fi +} +trap cleanup_node_server EXIT + +for _ in $(seq 1 300); do + if [[ -s "$NODE_READY_JSON" ]]; then + break + fi + if ! kill -0 "$NODE_PID" >/dev/null 2>&1; then + cat "$NODE_LOG" >&2 || true + echo "PGlite NodeFS socket server exited before becoming ready" >&2 + exit 1 + fi + sleep 0.1 +done + +if [[ ! -s "$NODE_READY_JSON" ]]; then + cat "$NODE_LOG" >&2 || true + echo "Timed out waiting for PGlite NodeFS socket server" >&2 + exit 1 +fi + +NODE_DATABASE_URL="$(node -e "const fs=require('fs'); console.log(JSON.parse(fs.readFileSync(process.argv[1], 'utf8')).databaseUrl)" "$NODE_READY_JSON")" +NODE_OPEN_MICROS="$(node -e "const fs=require('fs'); console.log(JSON.parse(fs.readFileSync(process.argv[1], 'utf8')).openMicros)" "$NODE_READY_JSON")" + +echo "Running PGlite NodeFS SQLx benchmark suite..." +cargo run --release -p xtask -- perf pglite-nodefs-sqlx \ + --suite all \ + --iterations 100 \ + --speed-source pglite \ + --database-url "$NODE_DATABASE_URL" \ + --open-micros "$NODE_OPEN_MICROS" \ + > "$NODE_JSON" + +cleanup_node_server +trap - EXIT + +echo "Building comparison markdown..." +node "$SCRIPT_DIR/build_bench_matrix.mjs" \ + --output "$REPORT_MD" \ + --oxide "$OXIDE_JSON" \ + --native "$NATIVE_JSON" \ + --node "$NODE_JSON" \ + --node-server "$NODE_READY_JSON" \ + --run-id "$RUN_ID" \ + --native-version "$NATIVE_VERSION" \ + --machine-os "$OS_LABEL" \ + --machine-cpu "$CPU_LABEL" \ + --machine-ram "$RAM_LABEL" \ + --machine-cores "$CORES_LABEL" + +echo "$REPORT_MD" diff --git a/scripts/validate.sh b/scripts/validate.sh index d0028110..c0dc80cc 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -2,9 +2,32 @@ set -eu mode="${1:-pre-push}" +shift || true + root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" cd "$root" +cargo_bin="${CARGO_HOME:-$HOME/.cargo}/bin" +if [ -d "$cargo_bin" ]; then + PATH="$cargo_bin:$PATH" + export PATH +fi + +allow_dirty=0 +for arg in "$@"; do + case "$arg" in + --allow-dirty) + allow_dirty=1 + ;; + --*) + echo "unknown flag for $mode: $arg" >&2 + exit 2 + ;; + *) + ;; + esac +done + run() { printf '\n==> %s\n' "$*" "$@" @@ -13,10 +36,37 @@ run() { require() { if ! command -v "$1" >/dev/null 2>&1; then echo "missing required command: $1" >&2 + echo "run scripts/bootstrap-tools.sh to install the pinned local toolchain" >&2 exit 1 fi } +run_xtask() { + if [ -n "${PGLITE_OXIDE_XTASK:-}" ]; then + xtask="$PGLITE_OXIDE_XTASK" + if command -v cygpath >/dev/null 2>&1; then + xtask="$(cygpath -u "$xtask" 2>/dev/null || printf '%s\n' "$xtask")" + fi + run "$xtask" "$@" + else + require cargo + run cargo run -p xtask -- "$@" + fi +} + +xtask_output() { + if [ -n "${PGLITE_OXIDE_XTASK:-}" ]; then + xtask="$PGLITE_OXIDE_XTASK" + if command -v cygpath >/dev/null 2>&1; then + xtask="$(cygpath -u "$xtask" 2>/dev/null || printf '%s\n' "$xtask")" + fi + "$xtask" "$@" + else + require cargo + cargo run --quiet -p xtask -- "$@" + fi +} + run_prek() { require prek stage="${1:?run_prek requires a stage}" @@ -24,14 +74,267 @@ run_prek() { run prek run --all-files --stage "$stage" "$@" } +run_prek_tracked_files() { + require prek + stage="${1:?run_prek_tracked_files requires a stage}" + printf '\n==> prek run --tracked-files --stage %s\n' "$stage" + git ls-files | + while IFS= read -r file; do + [ -e "$file" ] && printf '%s\0' "$file" + done | + xargs -0 prek run --stage "$stage" --files +} + +cargo_publish_args() { + if [ "$allow_dirty" -eq 1 ]; then + printf '%s\n' --allow-dirty + fi +} + +cargo_package_args() { + if [ "$allow_dirty" -eq 1 ]; then + printf '%s\n' --allow-dirty + fi +} + clean_package_artifacts() { - rm -f target/package/pglite-oxide-*.crate + rm -f target/package/*.crate +} + +internal_packages() { + xtask_output assets internal-packages +} + +aot_targets() { + xtask_output assets aot-targets +} + +host_aot_manifest() { + host="$1" + if [ -f "target/pglite-oxide/aot/$host/manifest.json" ]; then + printf '%s\n' "target/pglite-oxide/aot/$host/manifest.json" + elif [ -f "crates/aot/$host/artifacts/manifest.json" ]; then + printf '%s\n' "crates/aot/$host/artifacts/manifest.json" + else + return 1 + fi +} + +run_root_publish_dry_run() { + tmp="$(mktemp)" + if cargo publish -p pglite-oxide --dry-run --locked $(cargo_publish_args) >"$tmp" 2>&1; then + cat "$tmp" + rm -f "$tmp" + return 0 + fi + + status=$? + if grep -Eq 'no matching package named `pglite-oxide-(assets|aot-[^`]+)` found' "$tmp"; then + cat >&2 <<'MSG' +warning: root crate publish dry-run could not resolve exact internal crate +versions from crates.io. + +This is expected for same-release internal asset/AOT versions. release-plz owns +the actual publish order; this validation dry-runs every internal crate before +release-plz publish/dry-run is invoked. +MSG + rm -f "$tmp" + return 0 + fi + + cat "$tmp" >&2 + rm -f "$tmp" + return "$status" +} + +validate_repo() { + require prek + run prek validate-config prek.toml + run_prek_tracked_files pre-commit +} + +validate_artifacts() { + run_xtask assets verify-committed +} + +validate_workflows() { + require actionlint + require zizmor + run actionlint + run zizmor --config .github/zizmor.yml --min-severity medium --persona auditor .github/workflows .github/actions +} + +validate_lint() { + require cargo + run scripts/check-dependency-invariants.sh + run cargo clippy --workspace --all-targets --locked -- -D warnings +} + +validate_tests() { + require cargo + run cargo check --workspace --locked + run cargo check --workspace --no-default-features --all-targets --locked + run cargo test --doc --workspace --locked + run cargo test --workspace --all-targets --locked --no-run +} + +validate_dev() { + validate_repo + validate_artifacts + validate_lint + validate_tests +} + +require_host_runtime_artifacts() { + require cargo + host="$(rustc -vV | awk '/^host:/{print $2}')" + if ! host_aot_manifest "$host" >/dev/null 2>&1; then + cat >&2 < --target-triple $host + cargo run -p xtask -- assets download --latest-compatible --target-triple $host + cargo run -p xtask -- assets install-local --target-triple $host +MSG + exit 1 + fi + if [ ! -f "target/pglite-oxide/assets/manifest.json" ]; then + cat >&2 < --target-triple $host + cargo run -p xtask -- assets download --latest-compatible --target-triple $host + cargo run -p xtask -- assets install-local --target-triple $host +MSG + exit 1 + fi + run_xtask assets install-local --target-triple "$host" + export PGLITE_OXIDE_GENERATED_ASSETS_DIR="$root/target/pglite-oxide/assets" + export PGLITE_OXIDE_GENERATED_AOT_DIR="$root/target/pglite-oxide/aot" +} + +validate_runtime_smoke() { + require_host_runtime_artifacts + export RUST_BACKTRACE="${RUST_BACKTRACE:-full}" + run cargo test -p pglite-oxide --locked \ + --test runtime_smoke \ + --test proxy_smoke \ + --test cli_smoke \ + --test performance_smoke \ + --test extensions_smoke \ + --test postgres_regression \ + -- --nocapture + run cargo test -p pglite-oxide --locked --lib pg_dump -- --nocapture +} + +validate_runtime() { + require_host_runtime_artifacts + run cargo test --workspace --all-targets --locked +} + +validate_examples() { + require cargo + require npm + run cargo check --manifest-path examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --locked + run npm --prefix examples/tauri-sqlx-vanilla ci + run npm --prefix examples/tauri-sqlx-vanilla run build +} + +validate_package() { + require cargo + clean_package_artifacts + run cargo package --workspace --exclude xtask --locked --no-verify $(cargo_package_args) + run scripts/check-crate-size.sh --enforce +} + +validate_feature_powerset() { + require cargo-hack + run cargo hack check --workspace --feature-powerset --no-dev-deps --exclude-features aot-serializer,template-runner +} + +validate_semver() { + require cargo-semver-checks + run cargo semver-checks check-release --package pglite-oxide --manifest-path Cargo.toml +} + +validate_supply_chain() { + require cargo-deny + run cargo deny check +} + +require_release_aot_artifacts() { + for target in $(aot_targets); do + manifest="$(host_aot_manifest "$target" 2>/dev/null || true)" + if [ -z "$manifest" ]; then + manifest="target/pglite-oxide/aot/$target/manifest.json" + fi + if [ ! -f "$manifest" ]; then + echo "missing release AOT artifacts for $target; download them from the successful Assets workflow before release validation" >&2 + exit 1 + fi + python3 - "$manifest" <<'PY' +import json +import sys +path = sys.argv[1] +with open(path, encoding="utf-8") as f: + manifest = json.load(f) +if not manifest.get("artifacts"): + raise SystemExit(f"{path} does not contain generated AOT artifacts") +PY + done +} + +require_release_portable_assets() { + if [ ! -f "target/pglite-oxide/assets/manifest.json" ]; then + echo "missing release portable assets; download or build Assets workflow outputs before release validation" >&2 + exit 1 + fi +} + +validate_release_aot_artifacts() { + for target in $(aot_targets); do + run_xtask assets check-aot --target-triple "$target" + done +} + +validate_release() { + require cargo + if [ "${PGLITE_OXIDE_RELEASE_STAGED:-0}" != "1" ]; then + require_release_portable_assets + require_release_aot_artifacts + run_xtask release stage + ( + cd target/pglite-oxide/release/workspace + PGLITE_OXIDE_RELEASE_STAGED=1 scripts/validate.sh release --allow-dirty + ) + return 0 + fi + require_release_portable_assets + require_release_aot_artifacts + run_xtask assets check --strict-generated + validate_release_aot_artifacts + validate_package + for package in $(internal_packages); do + run cargo publish -p "$package" --dry-run --locked $(cargo_publish_args) + done + printf '\n==> cargo publish -p pglite-oxide --dry-run --locked\n' + run_root_publish_dry_run } case "$mode" in commit-msg) require prek - run prek run --stage commit-msg --commit-msg-filename "${2:?commit-msg mode requires a message file}" + run prek run --stage commit-msg --commit-msg-filename "${1:?commit-msg mode requires a message file}" ;; pre-commit) @@ -42,38 +345,101 @@ case "$mode" in run_prek pre-push ;; + repo) + validate_repo + ;; + + artifacts) + validate_artifacts + ;; + + lint) + validate_lint + ;; + + test) + validate_tests + ;; + + workflows) + validate_workflows + ;; + + dev) + validate_dev + ;; + + runtime) + validate_runtime + ;; + + runtime-smoke) + validate_runtime_smoke + ;; + + examples) + validate_examples + ;; + + package) + validate_package + ;; + + feature-powerset) + validate_feature_powerset + ;; + + semver) + validate_semver + ;; + + supply-chain) + validate_supply_chain + ;; + + dev-ci) + validate_dev + validate_examples + ;; + ci) - require cargo - require npm - require prek - run prek validate-config prek.toml - run scripts/validate.sh pre-commit - run scripts/validate.sh pre-push - run cargo check --no-default-features --all-targets --locked - run cargo test --doc --locked - run cargo check --manifest-path examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --locked - run npm --prefix examples/tauri-sqlx-vanilla ci - run npm --prefix examples/tauri-sqlx-vanilla run build + validate_dev + validate_workflows + validate_examples + validate_package + validate_feature_powerset + validate_semver + validate_supply_chain ;; release) - require cargo - clean_package_artifacts - run cargo package --locked --no-verify - run scripts/check-crate-size.sh --enforce - run cargo publish --dry-run --locked + validate_release ;; *) cat >&2 <<'MSG' -usage: scripts/validate.sh +usage: scripts/validate.sh [--allow-dirty] modes: commit-msg validate a Conventional Commit message with prek pre-commit run all pre-commit prek hooks pre-push run all pre-push prek hooks - ci full source, test, lint, docs, and example checks - release crates.io publish dry-run and strict package size + repo repository hygiene and formatting + workflows actionlint and zizmor GitHub Actions checks + lint dependency invariants and clippy + test source-only checks, doctests, and test compilation + dev repo, source-only asset checks, lint, and tests/compile gate + runtime require host generated assets and run runtime tests + runtime-smoke require host generated assets and run runtime smoke tests only + examples Tauri/Rust/frontend example checks + package package all published crates and enforce size limits + feature-powerset cargo-hack feature combination checks + semver cargo-semver-checks public API compatibility + supply-chain cargo-deny dependency checks + dev-ci repo, artifacts, lint, test, and examples + ci full local CI parity lane + release package generated release workspace and publish-dry-run internals + artifacts verify source-controlled asset inputs and AOT crate templates MSG exit 2 ;; diff --git a/src/bin/pglite_dump.rs b/src/bin/pglite_dump.rs index d7f42c37..243c50a4 100644 --- a/src/bin/pglite_dump.rs +++ b/src/bin/pglite_dump.rs @@ -1,58 +1,63 @@ -use std::fs::{self, File}; -use std::path::{Component, Path, PathBuf}; - -use anyhow::{Context, Result, bail}; -use tar::Archive; -use zstd::stream::read::Decoder as ZstdDecoder; - -fn runtime_tar_path() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite-wasi.tar.zst") +use anyhow::Result; +#[cfg(feature = "extensions")] +use pglite_oxide::{PgDumpOptions, PgliteServer}; +#[cfg(feature = "extensions")] +use std::env; +#[cfg(feature = "extensions")] +use std::path::PathBuf; + +#[cfg(feature = "extensions")] +#[derive(Debug)] +struct Args { + root: PathBuf, + passthrough: Vec, } -fn unpack_tar_archive(dest_root: &Path) -> Result<()> { - let tar_path = runtime_tar_path(); - let file = - File::open(&tar_path).with_context(|| format!("open archive {}", tar_path.display()))?; - let decoder = ZstdDecoder::new(file) - .with_context(|| format!("decode zstd archive {}", tar_path.display()))?; - let mut archive = Archive::new(decoder); - - for entry in archive.entries().context("read archive entries")? { - let mut entry = entry.context("read archive entry")?; - let path = entry - .path() - .context("read archive entry path")? - .into_owned(); - let relative = path.strip_prefix("tmp").unwrap_or(&path); - let dest = archive_destination(dest_root, relative)?; - - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create directory {}", parent.display()))?; - } - - entry - .unpack(&dest) - .with_context(|| format!("unpack {}", dest.display()))?; +fn main() -> Result<()> { + #[cfg(not(feature = "extensions"))] + { + anyhow::bail!("pglite-dump requires the `extensions` feature"); + } + #[cfg(feature = "extensions")] + { + let Args { root, passthrough } = parse_args()?; + let server = PgliteServer::builder().path(root).start()?; + let sql = server.dump_sql(PgDumpOptions::new().args(passthrough))?; + print!("{sql}"); + server.shutdown()?; + Ok(()) } - - Ok(()) } -fn archive_destination(root: &Path, archive_path: &Path) -> Result { - let mut dest = root.to_path_buf(); - for component in archive_path.components() { - match component { - Component::CurDir => {} - Component::Normal(part) => dest.push(part), - _ => bail!("unsafe archive path {}", archive_path.display()), +#[cfg(feature = "extensions")] +fn parse_args() -> Result { + let mut root = PathBuf::from("./.pglite"); + let mut passthrough = Vec::new(); + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--root" => { + root = PathBuf::from( + args.next() + .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?, + ); + } + "--help" | "-h" => { + print_usage(); + std::process::exit(0); + } + "--" => { + passthrough.extend(args); + break; + } + other => passthrough.push(other.to_string()), } } - Ok(dest) + Ok(Args { root, passthrough }) } -fn main() -> Result<()> { - let mut args = std::env::args().skip(1); - let dest = args.next().unwrap_or_else(|| "./pglite-fs".to_string()); - unpack_tar_archive(&PathBuf::from(dest)) +#[cfg(feature = "extensions")] +fn print_usage() { + eprintln!("Usage: pglite-dump --root PATH -- [pg_dump args]"); + eprintln!("Example: pglite-dump --root ./.pglite -- --schema-only"); } diff --git a/src/bin/pglite_proxy.rs b/src/bin/pglite_proxy.rs index 9c91252b..8f9d9511 100644 --- a/src/bin/pglite_proxy.rs +++ b/src/bin/pglite_proxy.rs @@ -1,66 +1,120 @@ -use anyhow::{Result, bail}; -use pglite_oxide::PgliteProxy; +use anyhow::{Context, Result, bail}; +use pglite_oxide::PgliteServer; +#[cfg(feature = "extensions")] +use pglite_oxide::extensions; use std::env; +use std::net::SocketAddr; use std::path::PathBuf; #[derive(Debug)] enum Bind { - Tcp(String), + Tcp(SocketAddr), #[cfg(unix)] Unix(PathBuf), } #[derive(Debug)] struct Args { - root: PathBuf, + root: Option, + temporary: bool, bind: Bind, + print_uri: bool, + postgres_config: Vec<(String, String)>, + extensions: Vec, } fn main() -> Result<()> { let args = parse_args()?; - let proxy = PgliteProxy::new(args.root); + let mut builder = if args.temporary { + PgliteServer::builder().temporary() + } else if let Some(root) = args.root { + PgliteServer::builder().path(root) + } else { + PgliteServer::builder().path("./.pglite") + }; - match args.bind { - Bind::Tcp(addr) => { - eprintln!("listening on tcp: {addr}"); - proxy.serve_tcp(addr) - } + builder = match args.bind { + Bind::Tcp(addr) => builder.tcp(addr), #[cfg(unix)] - Bind::Unix(path) => { - eprintln!("listening on unix socket: {}", path.display()); - eprintln!("connection string: postgresql://postgres@/template1?host=/tmp"); - proxy.serve_unix(path) + Bind::Unix(path) => builder.unix(path), + }; + builder = builder.postgres_configs(args.postgres_config); + + #[cfg(feature = "extensions")] + { + for name in &args.extensions { + let extension = extensions::by_sql_name(name) + .ok_or_else(|| anyhow::anyhow!("unknown bundled extension: {name}"))?; + builder = builder.extension(extension); } } + #[cfg(not(feature = "extensions"))] + if !args.extensions.is_empty() { + bail!("this pglite-proxy build was compiled without bundled extension support"); + } + + let server = builder.start()?; + if args.print_uri { + println!("{}", server.database_url()); + } else { + eprintln!("listening: {}", server.database_url()); + } + + loop { + std::thread::park(); + } } fn parse_args() -> Result { - let mut root = PathBuf::from("./.pglite"); - #[cfg(unix)] - let mut bind = Bind::Unix(PathBuf::from("/tmp/.s.PGSQL.5432")); - #[cfg(not(unix))] - let mut bind = Bind::Tcp("127.0.0.1:5432".to_string()); + let mut root = None; + let mut temporary = false; + let mut print_uri = false; + let mut postgres_config = Vec::new(); + let mut extensions = Vec::new(); + let mut bind = Bind::Tcp("127.0.0.1:5432".parse().expect("valid default TCP addr")); let mut args = env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { + "--temporary" => temporary = true, "--root" => { let value = args .next() .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?; - root = PathBuf::from(value); + root = Some(PathBuf::from(value)); + temporary = false; } "--tcp" => { let value = args.next().unwrap_or_else(|| "127.0.0.1:5432".to_string()); - bind = Bind::Tcp(value); + bind = Bind::Tcp( + value + .parse() + .with_context(|| format!("parse TCP bind address {value}"))?, + ); } #[cfg(unix)] - "--uds" => { + "--unix" | "--uds" => { let value = args .next() .unwrap_or_else(|| "/tmp/.s.PGSQL.5432".to_string()); bind = Bind::Unix(PathBuf::from(value)); } + "--print-uri" => print_uri = true, + "--postgres-config" => { + let value = args + .next() + .ok_or_else(|| anyhow::anyhow!("--postgres-config requires name=value"))?; + let (name, value) = value + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("--postgres-config requires name=value"))?; + postgres_config.push((name.to_owned(), value.to_owned())); + } + "--extension" => { + let value = args + .next() + .ok_or_else(|| anyhow::anyhow!("--extension requires a name"))?; + extensions.push(value); + } "--help" | "-h" => { print_usage(); std::process::exit(0); @@ -69,13 +123,27 @@ fn parse_args() -> Result { } } - Ok(Args { root, bind }) + Ok(Args { + root, + temporary, + bind, + print_uri, + postgres_config, + extensions, + }) } fn print_usage() { - eprintln!("Usage: pglite-proxy [--root PATH] [--tcp ADDR | --uds PATH]"); - eprintln!(" --root PATH Runtime and cluster root. Default: ./.pglite"); - eprintln!(" --tcp ADDR Listen on TCP. Default address: 127.0.0.1:5432"); + eprintln!( + "Usage: pglite-proxy [--temporary | --root PATH] [--tcp ADDR | --unix PATH] [--print-uri] [--postgres-config NAME=VALUE] [--extension NAME]" + ); + eprintln!(" --temporary Use an ephemeral database removed on exit"); + eprintln!(" --root PATH Runtime and cluster root. Default: ./.pglite"); + eprintln!(" --tcp ADDR Listen on TCP. Use 127.0.0.1:0 for a random port"); #[cfg(unix)] - eprintln!(" --uds PATH Listen on Unix socket. Default: /tmp/.s.PGSQL.5432"); + eprintln!(" --unix PATH Listen on a Unix socket path"); + eprintln!(" --print-uri Print the PostgreSQL connection URI to stdout"); + eprintln!(" --postgres-config NAME=VALUE"); + eprintln!(" Set a PostgreSQL startup GUC on the embedded backend"); + eprintln!(" --extension NAME Enable a bundled extension that passed the smoke suite"); } diff --git a/src/lib.rs b/src/lib.rs index fd06a6a2..3a3c750b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,19 +4,27 @@ mod pglite; mod protocol; +#[cfg(feature = "extensions")] +pub use pglite::extensions; + +#[cfg(feature = "extensions")] +pub use pglite::PgDumpOptions; pub use pglite::{ - DataTransferContainer, DescribeQueryParam, DescribeQueryResult, DescribeResultField, FieldInfo, - GlobalListenerHandle, ListenerHandle, NoticeCallback, ParserMap, Pglite, PgliteBuilder, - PgliteError, PgliteServer, PgliteServerBuilder, QueryOptions, QueryTemplate, Results, RowMode, - Serializer, SerializerMap, TemplatedQuery, Transaction, TypeParser, format_query, - quote_identifier, + DataDirArchiveFormat, DataTransferContainer, DescribeQueryParam, DescribeQueryResult, + DescribeResultField, ExecProtocolOptions, ExecProtocolResult, FieldInfo, GlobalListenerHandle, + ListenerHandle, NoticeCallback, ParserMap, Pglite, PgliteBuilder, PgliteError, PgliteServer, + PgliteServerBuilder, PostgresConfig, QueryOptions, QueryTemplate, Results, RowMode, Serializer, + SerializerMap, TemplatedQuery, Transaction, TypeParser, format_query, quote_identifier, }; -pub use protocol::messages::{DatabaseError, NoticeMessage}; +pub use protocol::messages::{BackendMessage, DatabaseError, NoticeMessage}; #[doc(hidden)] pub use pglite::{ - DebugLevel, InstallOptions, InstallOutcome, MountInfo, PgDataTemplate, PgDataTemplateManifest, - PglitePaths, PgliteProxy, build_pgdata_template, ensure_cluster, install_and_init, - install_and_init_in, install_default, install_extension_archive, install_extension_bytes, - install_into, install_with_options, preload_runtime_module, + DebugLevel, FsTraceSnapshot, InstallOptions, InstallOutcome, MountInfo, PgDataTemplate, + PgDataTemplateManifest, PglitePaths, PgliteProxy, PhaseTiming, ProtocolStatsSnapshot, + build_pgdata_template, capture_phase_timings, disable_protocol_stats, ensure_cluster, + fs_trace_snapshot, install_and_init, install_and_init_in, install_default, + install_extension_archive, install_extension_bytes, install_into, install_with_options, + measure_phase, preload_runtime_module, protocol_stats_snapshot, record_phase_timing, + reset_fs_trace, reset_protocol_stats, }; diff --git a/src/pglite/aot.rs b/src/pglite/aot.rs new file mode 100644 index 00000000..e4b2fb01 --- /dev/null +++ b/src/pglite/aot.rs @@ -0,0 +1,688 @@ +use std::collections::HashMap; +use std::fs; +use std::io::{Cursor, Read}; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use anyhow::{Context, Result, bail, ensure}; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use wasmer::sys::{EngineBuilder, Features, NativeEngineExt}; +use wasmer::{Engine, Module}; +use zstd::stream::read::Decoder as ZstdDecoder; + +#[cfg(feature = "extensions")] +use super::extensions::Extension; +use super::timing; + +const RUNTIME_ARTIFACT: &str = "runtime:pglite"; +const EXPECTED_AOT_ENGINE: &str = "llvm-opta"; +const EXPECTED_WASMER_VERSION: &str = "7.2.0-alpha.2"; +const EXPECTED_WASMER_WASIX_VERSION: &str = "0.702.0-alpha.2"; +const AOT_ENGINE_ID: &str = concat!( + "engine=", + "llvm-opta", + ";wasmer=", + "7.2.0-alpha.2", + ";wasmer-wasix=", + "0.702.0-alpha.2", + ";cpu=generic-baseline" +); +const ZSTD_MAGIC: &[u8] = &[0x28, 0xb5, 0x2f, 0xfd]; +const CACHE_RECEIPT_FORMAT_VERSION: u32 = 1; +static AOT_INSTALL_LOCK: OnceLock> = OnceLock::new(); +static HEADLESS_ENGINE: OnceLock = OnceLock::new(); +static INSTALLED_ARTIFACTS: OnceLock>> = OnceLock::new(); +static MODULE_CACHE: OnceLock>> = OnceLock::new(); + +#[derive(Debug, Clone)] +struct InstalledArtifact { + path: PathBuf, + sha256: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AotVerifyMode { + Fast, + Full, +} + +pub(crate) fn headless_engine() -> Engine { + HEADLESS_ENGINE + .get_or_init(|| { + let _phase = timing::phase("wasmer.headless_engine"); + let mut features = Features::new(); + features.exceptions(true); + EngineBuilder::headless() + .set_features(Some(features)) + .engine() + .into() + }) + .clone() +} + +pub(crate) fn load_runtime_module() -> Result<(Engine, Module)> { + let engine = headless_engine(); + let module = load_artifact_module(&engine, RUNTIME_ARTIFACT)?; + Ok((engine, module)) +} + +pub(crate) fn engine_identity() -> &'static str { + AOT_ENGINE_ID +} + +pub(crate) fn preload_runtime_artifact() -> Result<()> { + let _ = load_runtime_module()?; + Ok(()) +} + +#[cfg(feature = "extensions")] +pub(crate) fn preload_extension_artifact(extension: Extension) -> Result<()> { + let engine = headless_engine(); + let _ = load_extension_module(&engine, extension)?; + Ok(()) +} + +#[cfg(feature = "extensions")] +pub(crate) fn load_extension_module( + engine: &Engine, + extension: Extension, +) -> Result> { + let Some(aot_name) = extension.aot_name() else { + return Ok(None); + }; + load_artifact_module(engine, aot_name).map(Some) +} + +pub(crate) fn load_artifact_module(engine: &Engine, artifact_name: &str) -> Result { + let artifact = install_artifact(artifact_name)?; + let cache_key = format!("{artifact_name}:{}", artifact.sha256); + let module_cache = MODULE_CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let mut modules = module_cache.lock().expect("AOT module cache poisoned"); + if let Some(module) = modules.get(&cache_key) { + return Ok(module.clone()); + } + + let module = match deserialize_headless(engine, &artifact.path) { + Ok(module) => module, + Err(err) if aot_verify_mode()? == AotVerifyMode::Fast => { + let _phase = timing::phase("aot.rebuild_after_deserialize_failure"); + forget_installed_artifact(artifact_name); + remove_cached_artifact(&artifact.path)?; + let artifact = rebuild_artifact(artifact_name).with_context(|| { + format!("rebuild AOT artifact '{artifact_name}' after deserialize failure") + })?; + deserialize_headless(engine, &artifact.path).with_context(|| { + format!( + "deserialize rebuilt Wasmer AOT artifact '{}' after initial failure: {err:#}", + artifact.path.display() + ) + })? + } + Err(err) => return Err(err), + }; + modules.insert(cache_key, module.clone()); + Ok(module) +} + +#[cfg(feature = "extensions")] +pub(crate) fn load_pg_dump_module(engine: &Engine) -> Result { + load_artifact_module(engine, "tool:pg_dump") +} + +#[cfg(feature = "extensions")] +#[allow(dead_code)] +pub(crate) fn load_initdb_module(engine: &Engine) -> Result { + load_artifact_module(engine, "tool:initdb") +} + +fn install_artifact(name: &str) -> Result { + if let Some(artifact) = installed_artifact(name) { + return Ok(artifact); + } + + let _guard = AOT_INSTALL_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("AOT install lock poisoned"); + if let Some(artifact) = installed_artifact(name) { + return Ok(artifact); + } + + let manifest_artifact = { + let _phase = timing::phase("aot.manifest_validation"); + target_manifest_artifact(name)? + }; + let verify_mode = aot_verify_mode()?; + + if let Some(artifact) = cached_raw_artifact(name, &manifest_artifact, verify_mode)? { + remember_installed_artifact(name, artifact.clone()); + return Ok(artifact); + } + + let artifact = materialize_artifact(name, &manifest_artifact, verify_mode)?; + remember_installed_artifact(name, artifact.clone()); + Ok(artifact) +} + +fn rebuild_artifact(name: &str) -> Result { + let _guard = AOT_INSTALL_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("AOT install lock poisoned"); + forget_installed_artifact(name); + let manifest_artifact = { + let _phase = timing::phase("aot.manifest_validation"); + target_manifest_artifact(name)? + }; + let artifact = materialize_artifact(name, &manifest_artifact, aot_verify_mode()?)?; + remember_installed_artifact(name, artifact.clone()); + Ok(artifact) +} + +fn materialize_artifact( + name: &str, + manifest_artifact: &AotManifestArtifact, + verify_mode: AotVerifyMode, +) -> Result { + let _phase = timing::phase("aot.materialize"); + let raw = artifact_raw_bytes(name, manifest_artifact, verify_mode)?; + let hash = expected_raw_hash(name, manifest_artifact, &raw, verify_mode)?; + let cache_path = cache_path(name, &hash)?; + remove_cached_artifact(&cache_path)?; + + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create AOT cache directory {}", parent.display()))?; + } + let tmp_path = + cache_path.with_extension(format!("bin.{}.{}.tmp", std::process::id(), tmp_suffix())); + fs::write(&tmp_path, raw) + .with_context(|| format!("write AOT artifact {}", tmp_path.display()))?; + if let Err(err) = fs::rename(&tmp_path, &cache_path) { + remove_file_if_exists(&tmp_path).ok(); + return Err(err).with_context(|| { + format!( + "promote AOT artifact {} -> {}", + tmp_path.display(), + cache_path.display() + ) + }); + } + + write_cache_receipt(name, manifest_artifact, &cache_path, &hash)?; + Ok(InstalledArtifact { + path: cache_path, + sha256: hash, + }) +} + +fn cached_raw_artifact( + name: &str, + manifest_artifact: &AotManifestArtifact, + verify_mode: AotVerifyMode, +) -> Result> { + let Some(raw_sha256) = manifest_artifact.raw_sha256.as_deref() else { + return Ok(None); + }; + let cache_path = cache_path(name, raw_sha256)?; + if !cache_path.exists() { + return Ok(None); + } + + match verify_mode { + AotVerifyMode::Fast => { + let _phase = timing::phase("aot.cache_receipt_verify"); + if !cache_receipt_matches(name, manifest_artifact, &cache_path, raw_sha256)? { + remove_cached_artifact(&cache_path)?; + return Ok(None); + } + } + AotVerifyMode::Full => { + let _phase = timing::phase("aot.raw_cache_verify"); + let (actual, actual_size) = sha256_file_with_len(&cache_path)?; + if !actual.eq_ignore_ascii_case(raw_sha256) { + remove_cached_artifact(&cache_path)?; + return Ok(None); + } + if let Some(raw_size) = manifest_artifact.raw_size { + ensure!( + actual_size == raw_size, + "cached AOT artifact '{name}' raw size mismatch: manifest={raw_size} actual={}", + actual_size + ); + } + } + } + Ok(Some(InstalledArtifact { + path: cache_path, + sha256: raw_sha256.to_owned(), + })) +} + +fn installed_artifact(name: &str) -> Option { + INSTALLED_ARTIFACTS + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .expect("installed AOT artifact cache poisoned") + .get(name) + .filter(|artifact| artifact.path.exists()) + .cloned() +} + +fn remember_installed_artifact(name: &str, artifact: InstalledArtifact) { + INSTALLED_ARTIFACTS + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .expect("installed AOT artifact cache poisoned") + .insert(name.to_string(), artifact); +} + +fn forget_installed_artifact(name: &str) { + INSTALLED_ARTIFACTS + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .expect("installed AOT artifact cache poisoned") + .remove(name); +} + +fn remove_cached_artifact(path: &Path) -> Result<()> { + remove_file_if_exists(path)?; + remove_file_if_exists(&receipt_path(path)) +} + +fn remove_file_if_exists(path: &Path) -> Result<()> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(err).with_context(|| format!("remove {}", path.display())), + } +} + +fn tmp_suffix() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or_default() +} + +fn artifact_raw_bytes( + name: &str, + manifest_artifact: &AotManifestArtifact, + verify_mode: AotVerifyMode, +) -> Result> { + let Some(bytes) = target_artifact_bytes(name) else { + bail!( + "no Wasmer LLVM AOT artifact named '{name}' is available for target {}; rebuild assets or disable this unsupported target", + target_triple() + ); + }; + if verify_mode == AotVerifyMode::Full { + validate_compressed_artifact_manifest(name, manifest_artifact, bytes)?; + } + + if bytes.starts_with(ZSTD_MAGIC) { + let _phase = timing::phase("aot.decompress"); + let mut decoder = ZstdDecoder::new(Cursor::new(bytes)) + .with_context(|| format!("decode compressed AOT artifact '{name}'"))?; + let mut raw = Vec::new(); + decoder + .read_to_end(&mut raw) + .with_context(|| format!("decompress AOT artifact '{name}'"))?; + ensure!( + !raw.is_empty(), + "AOT artifact '{name}' decompressed to zero bytes" + ); + Ok(raw) + } else { + Ok(bytes.to_vec()) + } +} + +fn expected_raw_hash( + name: &str, + manifest_artifact: &AotManifestArtifact, + raw: &[u8], + verify_mode: AotVerifyMode, +) -> Result { + if let Some(raw_size) = manifest_artifact.raw_size { + ensure!( + raw.len() as u64 == raw_size, + "AOT artifact '{name}' raw size mismatch: manifest={raw_size} actual={}", + raw.len() + ); + } + + let Some(raw_sha256) = &manifest_artifact.raw_sha256 else { + ensure!( + verify_mode == AotVerifyMode::Full, + "AOT artifact '{name}' is missing raw-sha256 metadata; rebuild assets or set PGLITE_OXIDE_AOT_VERIFY=full for strict hash-derived cache keys" + ); + return Ok(sha256_hex(raw)); + }; + if verify_mode == AotVerifyMode::Full { + let actual = sha256_hex(raw); + ensure!( + actual.eq_ignore_ascii_case(raw_sha256), + "AOT artifact '{name}' raw hash mismatch: manifest={raw_sha256} actual={actual}" + ); + } + Ok(raw_sha256.clone()) +} + +fn target_manifest_artifact(name: &str) -> Result { + let manifest = target_aot_manifest()?; + ensure!( + manifest.target_triple == target_triple(), + "AOT manifest target mismatch: manifest={} actual={}", + manifest.target_triple, + target_triple() + ); + ensure!( + manifest.engine == EXPECTED_AOT_ENGINE, + "AOT manifest engine mismatch: manifest={} expected={EXPECTED_AOT_ENGINE}", + manifest.engine + ); + ensure!( + manifest.wasmer_version == EXPECTED_WASMER_VERSION, + "AOT manifest Wasmer version mismatch: manifest={} expected={EXPECTED_WASMER_VERSION}", + manifest.wasmer_version + ); + ensure!( + manifest.wasmer_wasix_version == EXPECTED_WASMER_WASIX_VERSION, + "AOT manifest wasmer-wasix version mismatch: manifest={} expected={EXPECTED_WASMER_WASIX_VERSION}", + manifest.wasmer_wasix_version + ); + + let artifact = manifest + .artifacts + .into_iter() + .find(|artifact| artifact.name == name) + .ok_or_else(|| anyhow::anyhow!("AOT manifest does not list artifact '{name}'"))?; + #[cfg(feature = "bundled")] + { + let expected_module = super::assets::expected_module_sha256(name)?; + ensure!( + expected_module.eq_ignore_ascii_case(&artifact.module_sha256), + "AOT artifact '{name}' source module hash mismatch: manifest={} assets={expected_module}", + artifact.module_sha256 + ); + } + + Ok(artifact) +} + +fn validate_compressed_artifact_manifest( + name: &str, + artifact: &AotManifestArtifact, + bytes: &[u8], +) -> Result<()> { + let actual_hash = sha256_hex(bytes); + ensure!( + actual_hash.eq_ignore_ascii_case(&artifact.sha256), + "AOT artifact '{name}' hash mismatch: manifest={} actual={actual_hash}", + artifact.sha256 + ); + Ok(()) +} + +fn target_aot_manifest() -> Result { + let Some(json) = target_aot_manifest_json() else { + bail!( + "no Wasmer LLVM AOT manifest is available for target {}; rebuild assets or disable this unsupported target", + target_triple() + ); + }; + serde_json::from_str(json).context("parse bundled AOT manifest") +} + +fn cache_path(name: &str, hash: &str) -> Result { + let safe_name = name.replace([':', '/', '\\'], "-"); + let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") + .context("could not resolve pglite-oxide cache directory")?; + Ok(dirs + .cache_dir() + .join("wasmer-aot") + .join(target_triple()) + .join(format!("{safe_name}-{hash}.bin"))) +} + +fn receipt_path(cache_path: &Path) -> PathBuf { + cache_path.with_extension("receipt.json") +} + +fn cache_receipt_matches( + name: &str, + manifest_artifact: &AotManifestArtifact, + cache_path: &Path, + raw_sha256: &str, +) -> Result { + let Some(raw_size) = manifest_artifact.raw_size else { + return Ok(false); + }; + let metadata = match fs::metadata(cache_path) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("stat {}", cache_path.display())), + }; + if metadata.len() != raw_size { + return Ok(false); + } + + let receipt_path = receipt_path(cache_path); + let receipt = match fs::read(&receipt_path) { + Ok(bytes) => match serde_json::from_slice::(&bytes) { + Ok(receipt) => receipt, + Err(_) => return Ok(false), + }, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("read {}", receipt_path.display())), + }; + + Ok(receipt.format_version == CACHE_RECEIPT_FORMAT_VERSION + && receipt.artifact_name == name + && receipt.target_triple == target_triple() + && receipt.engine == EXPECTED_AOT_ENGINE + && receipt.wasmer_version == EXPECTED_WASMER_VERSION + && receipt.wasmer_wasix_version == EXPECTED_WASMER_WASIX_VERSION + && receipt.raw_sha256.eq_ignore_ascii_case(raw_sha256) + && receipt.raw_size == raw_size + && receipt + .compressed_sha256 + .eq_ignore_ascii_case(&manifest_artifact.sha256) + && receipt + .module_sha256 + .eq_ignore_ascii_case(&manifest_artifact.module_sha256)) +} + +fn write_cache_receipt( + name: &str, + manifest_artifact: &AotManifestArtifact, + cache_path: &Path, + raw_sha256: &str, +) -> Result<()> { + let Some(raw_size) = manifest_artifact.raw_size else { + return Ok(()); + }; + let receipt = AotCacheReceipt { + format_version: CACHE_RECEIPT_FORMAT_VERSION, + artifact_name: name.to_owned(), + target_triple: target_triple().to_owned(), + engine: EXPECTED_AOT_ENGINE.to_owned(), + wasmer_version: EXPECTED_WASMER_VERSION.to_owned(), + wasmer_wasix_version: EXPECTED_WASMER_WASIX_VERSION.to_owned(), + raw_sha256: raw_sha256.to_owned(), + raw_size, + compressed_sha256: manifest_artifact.sha256.clone(), + module_sha256: manifest_artifact.module_sha256.clone(), + }; + + let path = receipt_path(cache_path); + let tmp_path = path.with_extension(format!( + "receipt.{}.{}.tmp", + std::process::id(), + tmp_suffix() + )); + let bytes = serde_json::to_vec(&receipt).context("serialize AOT cache receipt")?; + fs::write(&tmp_path, bytes).with_context(|| format!("write {}", tmp_path.display()))?; + if let Err(err) = fs::rename(&tmp_path, &path) { + remove_file_if_exists(&tmp_path).ok(); + return Err(err).with_context(|| format!("promote AOT cache receipt {}", path.display())); + } + Ok(()) +} + +fn sha256_file_with_len(path: &Path) -> Result<(String, u64)> { + let mut file = fs::File::open(path).with_context(|| format!("open {}", path.display()))?; + let mut hasher = Sha256::new(); + let mut len = 0u64; + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file + .read(&mut buffer) + .with_context(|| format!("read {}", path.display()))?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + len += read as u64; + } + Ok((format!("{:x}", hasher.finalize()), len)) +} + +fn aot_verify_mode() -> Result { + let Some(value) = std::env::var_os("PGLITE_OXIDE_AOT_VERIFY") else { + return Ok(AotVerifyMode::Fast); + }; + let value = value.to_string_lossy().to_ascii_lowercase(); + match value.as_str() { + "" | "fast" | "metadata" | "receipt" | "0" | "false" | "off" => Ok(AotVerifyMode::Fast), + "full" | "sha" | "sha256" | "strict" | "1" | "true" | "on" => Ok(AotVerifyMode::Full), + other => bail!("unsupported PGLITE_OXIDE_AOT_VERIFY={other}; use `fast` or `full`"), + } +} + +#[allow(unsafe_code)] +fn deserialize_headless(engine: &Engine, path: &Path) -> Result { + let _phase = timing::phase("aot.deserialize"); + deserialize_headless_mmap(engine, path) +} + +#[allow(unsafe_code)] +fn deserialize_headless_mmap(engine: &Engine, path: &Path) -> Result { + let _phase = timing::phase("aot.deserialize.mmap"); + // SAFETY: same artifact ownership and cache-key constraints as the file + // deserializer below. This path avoids reading the complete native artifact + // into a Rust Vec before Wasmer deserializes it. + unsafe { + engine + .deserialize_from_mmapped_file(path) + .with_context(|| format!("mmap-deserialize Wasmer AOT artifact {}", path.display())) + } +} + +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("{:x}", hasher.finalize()) +} + +fn target_triple() -> &'static str { + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + return "aarch64-apple-darwin"; + } + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + { + return "x86_64-unknown-linux-gnu"; + } + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] + { + return "aarch64-unknown-linux-gnu"; + } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + { + return "x86_64-pc-windows-msvc"; + } + #[allow(unreachable_code)] + "unsupported" +} + +fn target_artifact_bytes(_name: &str) -> Option<&'static [u8]> { + #[cfg(all(feature = "bundled", target_os = "macos", target_arch = "aarch64"))] + { + return pglite_oxide_aot_aarch64_apple_darwin::artifact_bytes(_name); + } + #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "x86_64"))] + { + return pglite_oxide_aot_x86_64_unknown_linux_gnu::artifact_bytes(_name); + } + #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "aarch64"))] + { + return pglite_oxide_aot_aarch64_unknown_linux_gnu::artifact_bytes(_name); + } + #[cfg(all(feature = "bundled", target_os = "windows", target_arch = "x86_64"))] + { + return pglite_oxide_aot_x86_64_pc_windows_msvc::artifact_bytes(_name); + } + #[allow(unreachable_code)] + None +} + +fn target_aot_manifest_json() -> Option<&'static str> { + #[cfg(all(feature = "bundled", target_os = "macos", target_arch = "aarch64"))] + { + return Some(pglite_oxide_aot_aarch64_apple_darwin::MANIFEST_JSON); + } + #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "x86_64"))] + { + return Some(pglite_oxide_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON); + } + #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "aarch64"))] + { + return Some(pglite_oxide_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON); + } + #[cfg(all(feature = "bundled", target_os = "windows", target_arch = "x86_64"))] + { + return Some(pglite_oxide_aot_x86_64_pc_windows_msvc::MANIFEST_JSON); + } + #[allow(unreachable_code)] + None +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct AotManifest { + target_triple: String, + engine: String, + wasmer_version: String, + wasmer_wasix_version: String, + artifacts: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct AotManifestArtifact { + name: String, + sha256: String, + #[allow(dead_code)] + module_sha256: String, + raw_sha256: Option, + raw_size: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +struct AotCacheReceipt { + format_version: u32, + artifact_name: String, + target_triple: String, + engine: String, + wasmer_version: String, + wasmer_wasix_version: String, + raw_sha256: String, + raw_size: u64, + compressed_sha256: String, + module_sha256: String, +} diff --git a/src/pglite/assets.rs b/src/pglite/assets.rs new file mode 100644 index 00000000..bfe0ffee --- /dev/null +++ b/src/pglite/assets.rs @@ -0,0 +1,145 @@ +#[cfg(feature = "bundled")] +use anyhow::{Context, Result, anyhow}; +#[cfg(feature = "bundled")] +use std::sync::{Arc, OnceLock}; + +#[cfg(feature = "bundled")] +static ASSET_MANIFEST: OnceLock< + std::result::Result, String>, +> = OnceLock::new(); + +#[cfg(feature = "bundled")] +fn asset_manifest() -> Result> { + ASSET_MANIFEST + .get_or_init(|| { + pglite_oxide_assets::manifest() + .map(Arc::new) + .map_err(|err| err.to_string()) + }) + .clone() + .map_err(|message| anyhow!(message)) +} + +#[cfg(feature = "bundled")] +pub(crate) fn runtime_archive() -> Option<&'static [u8]> { + pglite_oxide_assets::runtime_archive() +} + +#[cfg(not(feature = "bundled"))] +pub(crate) fn runtime_archive() -> Option<&'static [u8]> { + None +} + +#[cfg(feature = "bundled")] +pub(crate) fn pgdata_template_archive() -> Option<&'static [u8]> { + pglite_oxide_assets::pgdata_template_archive() +} + +#[cfg(not(feature = "bundled"))] +pub(crate) fn pgdata_template_archive() -> Option<&'static [u8]> { + None +} + +#[cfg(feature = "bundled")] +pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { + pglite_oxide_assets::pgdata_template_manifest() +} + +#[cfg(not(feature = "bundled"))] +pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { + None +} + +#[cfg(feature = "bundled")] +#[allow(dead_code)] +pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { + pglite_oxide_assets::pg_dump_wasm() +} + +#[cfg(not(feature = "bundled"))] +#[allow(dead_code)] +pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { + None +} + +#[cfg(feature = "bundled")] +#[allow(dead_code)] +pub(crate) fn initdb_wasm() -> Option<&'static [u8]> { + pglite_oxide_assets::initdb_wasm() +} + +#[cfg(not(feature = "bundled"))] +#[allow(dead_code)] +pub(crate) fn initdb_wasm() -> Option<&'static [u8]> { + None +} + +#[cfg(feature = "extensions")] +pub(crate) fn extension_archive(sql_name: &str) -> Option<&'static [u8]> { + pglite_oxide_assets::extension_archive(sql_name) +} + +#[cfg(feature = "bundled")] +pub(crate) fn expected_runtime_archive_sha256() -> Result { + Ok(asset_manifest() + .context("parse embedded asset manifest")? + .runtime + .sha256 + .clone()) +} + +#[cfg(feature = "extensions")] +pub(crate) fn expected_extension_archive_sha256(sql_name: &str) -> Result { + asset_manifest() + .context("parse embedded asset manifest")? + .extensions + .iter() + .find(|extension| extension.sql_name == sql_name) + .map(|extension| extension.sha256.clone()) + .ok_or_else(|| anyhow!("extension asset '{sql_name}' is missing from asset manifest")) +} + +#[cfg(feature = "bundled")] +pub(crate) fn expected_module_sha256(name: &str) -> Result { + let manifest = asset_manifest().context("parse embedded asset manifest")?; + if name == "runtime:pglite" { + return Ok(manifest.runtime.module_sha256.clone()); + } + if let Some(name) = name.strip_prefix("runtime-support:") { + return manifest + .runtime_support + .iter() + .find(|module| module.name == name) + .map(|module| module.module_sha256.clone()) + .ok_or_else(|| { + anyhow!("runtime support module '{name}' is missing from asset manifest") + }); + } + if name == "tool:pg_dump" { + return manifest + .pg_dump + .as_ref() + .map(|module| module.module_sha256.clone()) + .ok_or_else(|| anyhow!("pg_dump is missing from asset manifest")); + } + if name == "tool:initdb" { + return manifest + .initdb + .as_ref() + .map(|module| module.module_sha256.clone()) + .ok_or_else(|| anyhow!("initdb is missing from asset manifest")); + } + if let Some(sql_name) = name.strip_prefix("extension:") { + let module_sha256 = manifest + .extensions + .iter() + .find(|extension| extension.sql_name == sql_name) + .map(|extension| extension.module_sha256.clone()) + .ok_or_else(|| anyhow!("extension '{sql_name}' is missing from asset manifest"))?; + if module_sha256.is_empty() { + anyhow::bail!("extension '{sql_name}' has no native module in asset manifest"); + } + return Ok(module_sha256); + } + Err(anyhow!("unknown asset module '{name}'")) +} diff --git a/src/pglite/backend.rs b/src/pglite/backend.rs new file mode 100644 index 00000000..47919139 --- /dev/null +++ b/src/pglite/backend.rs @@ -0,0 +1,331 @@ +#[cfg(feature = "extensions")] +use anyhow::{Context, bail}; +use anyhow::{Result, ensure}; +use std::sync::{Mutex, MutexGuard, OnceLock}; + +use crate::pglite::base::InstallOutcome; +use crate::pglite::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use crate::pglite::extensions::{Extension, extension_session_setup_sql, extension_setup_sql}; +use crate::pglite::interface::DataTransferContainer; +use crate::pglite::postgres_mod::{ + PostgresMod, ProtocolPumpOutcome, ProtocolStream, StartupProtocolResponse, +}; +use crate::pglite::timing; +use crate::pglite::transport::Transport; +use crate::pglite::wire::raw_protocol_message_len; +#[cfg(feature = "extensions")] +use crate::pglite::wire::{response_contains_error, simple_query_message}; + +static WASIX_BACKEND_OPEN_LOCK: OnceLock> = OnceLock::new(); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BackendOpenKind { + Direct, + Proxy, +} + +pub(crate) struct BackendSession { + pg: PostgresMod, + transport: Transport, + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + #[cfg(feature = "extensions")] + preinstalled_extensions: Vec, + #[cfg(feature = "extensions")] + preloaded_extensions: Vec, +} + +impl BackendSession { + pub(crate) fn open( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + #[cfg(feature = "extensions")] + { + Self::open_with_extension_preload(outcome, postgres_config, startup_config, kind, &[]) + } + #[cfg(not(feature = "extensions"))] + { + Self::open_without_extension_preload(outcome, postgres_config, startup_config, kind) + } + } + + #[cfg(feature = "extensions")] + pub(crate) fn open_with_extension_preload( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + extensions: &[Extension], + ) -> Result { + Self::open_inner(outcome, postgres_config, startup_config, kind, extensions) + } + + #[cfg(not(feature = "extensions"))] + fn open_without_extension_preload( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + Self::open_inner(outcome, postgres_config, startup_config, kind) + } + + #[cfg(feature = "extensions")] + fn open_inner( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + extensions: &[Extension], + ) -> Result { + let _open_guard = wasix_backend_open_guard(); + let preinstalled_extensions = outcome.preinstalled_extensions.clone(); + let pg = Self::new_postgres( + outcome.clone(), + postgres_config.clone(), + startup_config.clone(), + kind, + )?; + for extension in extensions { + pg.preload_extension_module(*extension)?; + } + let (pg, transport) = Self::finish_open(pg, kind)?; + Ok(Self { + pg, + transport, + outcome, + postgres_config, + startup_config, + kind, + preinstalled_extensions, + preloaded_extensions: extensions.to_vec(), + }) + } + + #[cfg(not(feature = "extensions"))] + fn open_inner( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + let _open_guard = wasix_backend_open_guard(); + let pg = Self::new_postgres( + outcome.clone(), + postgres_config.clone(), + startup_config.clone(), + kind, + )?; + let (pg, transport) = Self::finish_open(pg, kind)?; + Ok(Self { + pg, + transport, + outcome, + postgres_config, + startup_config, + kind, + }) + } + + fn new_postgres( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + let pg = { + let _phase = timing::phase(match kind { + BackendOpenKind::Direct => "pglite.postgres_new", + BackendOpenKind::Proxy => "proxy.backend_postgres_new", + }); + PostgresMod::new_prepared_with_config( + outcome.paths, + outcome.runtime_layout, + postgres_config, + startup_config, + )? + }; + Ok(pg) + } + + fn finish_open(mut pg: PostgresMod, kind: BackendOpenKind) -> Result<(PostgresMod, Transport)> { + { + let _phase = timing::phase(match kind { + BackendOpenKind::Direct => "pglite.ensure_cluster", + BackendOpenKind::Proxy => "proxy.backend_ensure_cluster", + }); + pg.ensure_cluster()?; + } + let transport = { + let _phase = timing::phase(match kind { + BackendOpenKind::Direct => "pglite.transport_prepare", + BackendOpenKind::Proxy => "proxy.transport_prepare", + }); + Transport::prepare(&mut pg)? + }; + Ok((pg, transport)) + } + + pub(crate) fn paths(&self) -> &crate::pglite::base::PglitePaths { + self.pg.paths() + } + + pub(crate) fn pgdata_template_root(&self) -> Option<&std::path::Path> { + self.pg.pgdata_template_root() + } + + pub(crate) fn startup_config(&self) -> &StartupConfig { + &self.startup_config + } + + #[cfg(debug_assertions)] + pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { + self.pg.guest_bridge_allocation_counts() + } + + pub(crate) fn send_buffered( + &mut self, + message: &[u8], + requested: Option, + ) -> Result> { + self.transport.send(&mut self.pg, message, requested) + } + + pub(crate) fn send_framed_raw_stream( + &mut self, + message: &[u8], + requested: Option, + mut on_data: F, + ) -> Result<()> + where + F: FnMut(&[u8]) -> Result<()>, + { + let mut cursor = 0usize; + while cursor < message.len() { + let frame_len = raw_protocol_message_len(&message[cursor..])?; + let end = cursor + frame_len; + let data = self.send_buffered(&message[cursor..end], requested)?; + if !data.is_empty() { + on_data(&data)?; + } + cursor = end; + } + Ok(()) + } + + pub(crate) fn startup_with_packet( + &mut self, + message: &[u8], + ) -> Result { + self.pg.start_protocol_with_startup_packet(message) + } + + #[cfg(feature = "extensions")] + pub(crate) fn existing_startup_response(&self) -> Option> { + self.pg.existing_startup_response() + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_extension_module(&mut self, extension: Extension) -> Result<()> { + self.pg.preload_extension_module(extension) + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_installed_extension(&mut self, extension: Extension) -> Result<()> { + self.preload_extension_module(extension) + } + + #[cfg(feature = "extensions")] + pub(crate) fn enable_extensions(&mut self, extensions: &[Extension]) -> Result<()> { + for extension in extensions { + let setup_sql = if self.has_preinstalled_extension(*extension) { + self.preload_installed_extension(*extension)?; + extension_session_setup_sql(*extension) + } else { + extension_setup_sql(*extension) + }; + for sql in setup_sql { + let response = self + .send_buffered(&simple_query_message(&sql), None) + .with_context(|| { + format!("enable bundled extension '{}'", extension.sql_name()) + })?; + if response_contains_error(&response) { + bail!( + "enable bundled extension '{}' returned a Postgres error", + extension.sql_name() + ); + } + } + } + Ok(()) + } + + #[cfg(feature = "extensions")] + pub(crate) fn has_preinstalled_extension(&self, extension: Extension) -> bool { + self.preinstalled_extensions + .iter() + .any(|sql_name| sql_name == extension.sql_name()) + } + + pub(crate) fn supports_protocol_pump(&self) -> bool { + self.pg.supports_streaming_protocol() + } + + pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> + where + S: ProtocolStream + 'static, + { + self.pg.attach_protocol_stream(stream) + } + + pub(crate) fn send_with_protocol_pump( + &mut self, + message: &[u8], + continuation_prefix: impl FnOnce() -> Vec, + ) -> Result { + ensure!( + self.supports_protocol_pump(), + "WASIX runtime is missing backend-owned protocol pump exports" + ); + self.pg.send_protocol_pump(message, continuation_prefix) + } + + pub(crate) fn shutdown(&mut self) -> Result<()> { + self.pg.shutdown_backend() + } + + pub(crate) fn restart(&mut self) -> Result<()> { + let _open_guard = wasix_backend_open_guard(); + let pg = Self::new_postgres( + self.outcome.clone(), + self.postgres_config.clone(), + self.startup_config.clone(), + self.kind, + )?; + #[cfg(feature = "extensions")] + for extension in &self.preloaded_extensions { + pg.preload_extension_module(*extension)?; + } + let (pg, transport) = Self::finish_open(pg, self.kind)?; + self.pg = pg; + self.transport = transport; + Ok(()) + } +} + +fn wasix_backend_open_guard() -> MutexGuard<'static, ()> { + // Wasmer/WASIX backend startup uses process-wide runtime and module-cache + // state. Serialize creation and `_start`; already-open backends still run + // independently after startup. + WASIX_BACKEND_OPEN_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("WASIX backend open lock poisoned") +} diff --git a/src/pglite/base.rs b/src/pglite/base.rs index 222e7133..5d7c17c3 100644 --- a/src/pglite/base.rs +++ b/src/pglite/base.rs @@ -1,44 +1,72 @@ +use std::collections::BTreeSet; use std::ffi::OsStr; -use std::fs; -use std::io::{Cursor, Read, Write}; +use std::fs::{self, File, OpenOptions}; +use std::io::{Cursor, Read}; +#[cfg(windows)] +use std::os::windows::fs::OpenOptionsExt; use std::path::{Component, Path, PathBuf}; -use std::sync::{Arc, OnceLock}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::process::Command; +use std::sync::{Arc, Mutex, OnceLock}; use anyhow::{Context, Result, anyhow, bail, ensure}; use directories::ProjectDirs; use flate2::read::GzDecoder; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use tar::{Archive, Builder}; +use tar::Archive; use tracing::info; -use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder}; +use zstd::stream::read::Decoder as ZstdDecoder; use super::postgres_mod::PostgresMod; +use super::timing; +use crate::pglite::assets; +#[cfg(feature = "extensions")] +use crate::pglite::client::Pglite; +#[cfg(feature = "extensions")] +use crate::pglite::config::{PostgresConfig, StartupConfig}; +use crate::pglite::data_dir::unpack_pgdata_archive; +#[cfg(feature = "extensions")] +use crate::pglite::extensions::Extension; use tempfile::TempDir; -const RUNTIME_ARCHIVE_NAME: &str = "pglite-wasi.tar.zst"; -const EMBEDDED_RUNTIME_ARCHIVE: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/assets/pglite-wasi.tar.zst" -)); +const RUNTIME_ARCHIVE_NAME: &str = "pglite.wasix.tar.zst"; const PGDATA_TEMPLATE_ARCHIVE_NAME: &str = "pgdata-template.tar.zst"; -const EMBEDDED_PGDATA_TEMPLATE_ARCHIVE: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/assets/prepopulated/pgdata-template.tar.zst" -)); -const EMBEDDED_PGDATA_TEMPLATE_MANIFEST: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/assets/prepopulated/pgdata-template.json" -)); - -static TEMPLATE_CLUSTER: OnceLock, String>> = +const MOUNTFS_RUNTIME_MARKER: &str = ".pglite-oxide-mountfs-runtime"; +const RUNTIME_LAYOUT_MANIFEST_NAME: &str = ".pglite-oxide-runtime-layout.json"; +const PGDATA_OVERLAY_MANIFEST_NAME: &str = ".pglite-oxide-pgdata-overlay.json"; +// Bump these when cache materialization semantics change; old mutable PGDATA +// template caches may have been modified by earlier clone strategies. +const PGDATA_TEMPLATE_CACHE_FORMAT: &str = "v2"; +#[cfg(feature = "extensions")] +const EXTENSION_PGDATA_TEMPLATE_CACHE_FORMAT: &str = "v4"; +const DEFAULT_PASSWORD_FILE: &[u8] = b"password\n"; + +static RUNTIME_CACHE: OnceLock, String>> = OnceLock::new(); +static PGDATA_TEMPLATE_CACHE: OnceLock, String>> = OnceLock::new(); +static PGDATA_TEMPLATE_MANIFEST: OnceLock> = + OnceLock::new(); +#[cfg(feature = "extensions")] +static EXTENSION_TEMPLATE_CACHE_LOCK: OnceLock> = OnceLock::new(); +static ROOT_LOCKED_PATHS: OnceLock>> = OnceLock::new(); const TEMPLATE_RUNTIME_STATE_FILES: &[&str] = &["postmaster.pid", "postmaster.opts"]; #[derive(Debug)] -struct TemplateCluster { - root: PathBuf, - _temp_dir: TempDir, +struct CachedRuntime { + runtime_root: PathBuf, +} + +#[derive(Debug)] +struct CachedPgDataTemplate { + pgdata: PathBuf, +} + +#[cfg(feature = "extensions")] +#[derive(Debug)] +struct CachedExtensionPgDataTemplate { + pgdata: PathBuf, + manifest: ExtensionPgDataTemplateManifest, } #[derive(Debug, Clone)] @@ -47,14 +75,162 @@ pub struct PglitePaths { pub pgdata: PathBuf, } -/// Files generated by [`build_pgdata_template`]. +#[derive(Debug)] +pub(crate) struct RootLock { + path: PathBuf, + _file: File, +} + +#[derive(Debug)] +struct CacheLock { + _file: File, +} + +#[derive(Debug)] +pub(crate) struct PreparedRoot { + pub(crate) root: PathBuf, + pub(crate) temp_dir: Option, + pub(crate) root_lock: Option, + pub(crate) outcome: InstallOutcome, +} + +#[derive(Debug, Clone)] +pub(crate) struct RuntimeLayout { + pub(crate) kind: RuntimeLayoutKind, + #[cfg(feature = "extensions")] + pub(crate) local_root: PathBuf, + pub(crate) module_root: PathBuf, + pub(crate) pgdata_template_root: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) enum RuntimeLayoutKind { + FullLocal, + SharedRuntimeOverlay, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RuntimeLayoutManifest { + kind: RuntimeLayoutKind, + source_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PgDataOverlayManifest { + template_archive_sha256: String, + postgres_version: String, + #[serde(default)] + extension_sql_names: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RuntimeLayoutPolicy { + Auto, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + FullLocal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ClusterPolicy { + ExistingOrTemplate, + ExistingOrFreshInitdb, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct RootPrepareOptions { + pub(crate) runtime: RuntimeLayoutPolicy, + pub(crate) cluster: ClusterPolicy, +} + +#[derive(Debug, Clone)] +pub(crate) struct RootPlan { + pub(crate) target: RootTarget, + pub(crate) source: RootSource, + #[cfg(feature = "extensions")] + pub(crate) extensions: Vec, + #[cfg(feature = "extensions")] + pub(crate) postgres_config: PostgresConfig, +} + +#[derive(Debug, Clone)] +pub(crate) enum RootTarget { + Path(PathBuf), + AppId { + qualifier: String, + organization: String, + application: String, + }, + Temporary, +} + +#[derive(Debug, Clone)] +pub(crate) enum RootSource { + Template, + FreshInitdb, + DataDirArchive(Vec), +} + +impl RootPlan { + pub(crate) fn new(target: RootTarget, source: RootSource) -> Self { + Self { + target, + source, + #[cfg(feature = "extensions")] + extensions: Vec::new(), + #[cfg(feature = "extensions")] + postgres_config: PostgresConfig::default(), + } + } + + #[cfg(feature = "extensions")] + pub(crate) fn with_extensions( + mut self, + extensions: Vec, + postgres_config: PostgresConfig, + ) -> Self { + self.extensions = extensions; + self.postgres_config = postgres_config; + self + } +} + +impl RootPrepareOptions { + pub(crate) fn template() -> Self { + Self { + runtime: RuntimeLayoutPolicy::Auto, + cluster: ClusterPolicy::ExistingOrTemplate, + } + } + + pub(crate) fn fresh() -> Self { + Self { + runtime: RuntimeLayoutPolicy::Auto, + cluster: ClusterPolicy::ExistingOrFreshInitdb, + } + } +} + +impl RuntimeLayout { + pub(crate) fn module_path(&self) -> PathBuf { + self.module_root.join("bin/pglite") + } + + pub(crate) fn uses_shared_overlay(&self) -> bool { + self.kind == RuntimeLayoutKind::SharedRuntimeOverlay + } +} + +/// Files exported by [`build_pgdata_template`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PgDataTemplate { pub archive_path: PathBuf, pub manifest_path: PathBuf, } -/// Manifest that binds a PGDATA template to the PGlite WASI runtime it was +/// Manifest that binds a PGDATA template to the PGlite WASIX runtime it was /// created with. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -66,6 +242,20 @@ pub struct PgDataTemplateManifest { pub architecture_independent: bool, } +#[cfg(feature = "extensions")] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +struct ExtensionPgDataTemplateManifest { + version: u32, + postgres_version: String, + base_template_archive_sha256: String, + base_template_wasm_sha256: String, + extension_sql_names: Vec, + extension_archive_sha256s: Vec, + postgres_config: Vec<(String, String)>, + cache_key: String, +} + impl PglitePaths { pub fn new(app_qual: (&str, &str, &str)) -> Result { let pd = ProjectDirs::from(app_qual.0, app_qual.1, app_qual.2) @@ -92,6 +282,14 @@ impl PglitePaths { &self.pgroot } + pub(crate) fn install_root(&self) -> &Path { + self.pgroot.parent().unwrap_or(&self.pgroot) + } + + pub(crate) fn runtime_root(&self) -> PathBuf { + self.pgroot.join("pglite") + } + pub fn with_temp_dir() -> Result<(TempDir, Self)> { let tmp = TempDir::new().context("create temporary directory")?; let paths = Self::with_root(tmp.path()); @@ -102,8 +300,102 @@ impl PglitePaths { self.pgdata.join("PG_VERSION") } + fn marker_control_file(&self) -> PathBuf { + self.pgdata.join("global").join("pg_control") + } + pub fn is_cluster_initialized(&self) -> bool { - self.marker_cluster().exists() + cluster_is_complete(self) + } +} + +impl RootLock { + pub(crate) fn acquire(root: &Path) -> Result { + fs::create_dir_all(root) + .with_context(|| format!("create PGlite root {}", root.display()))?; + let canonical_root = dunce::canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); + { + let mut locked = ROOT_LOCKED_PATHS + .get_or_init(|| Mutex::new(BTreeSet::new())) + .lock() + .expect("root lock path set poisoned"); + ensure!( + locked.insert(canonical_root.clone()), + "PGlite root is already in use: {}", + root.display() + ); + } + let path = root.join(".pglite-oxide.lock"); + let file = match open_root_lock_file(&path) { + Ok(file) => file, + Err(err) => { + release_root_lock_path(&canonical_root); + return Err(err).with_context(|| { + format!( + "PGlite root is already in use or unavailable: {}", + root.display() + ) + }); + } + }; + if let Err(err) = file.try_lock() { + release_root_lock_path(&canonical_root); + return Err(err) + .with_context(|| format!("PGlite root is already in use: {}", root.display())); + } + Ok(Self { + path: canonical_root, + _file: file, + }) + } + + pub(crate) fn acquire_for_paths(paths: &PglitePaths) -> Result { + Self::acquire(paths.install_root()) + } +} + +impl Drop for RootLock { + fn drop(&mut self) { + let _ = self._file.unlock(); + release_root_lock_path(&self.path); + } +} + +fn open_root_lock_file(path: &Path) -> std::io::Result { + let mut options = OpenOptions::new(); + options.read(true).write(true).create(true).truncate(false); + #[cfg(windows)] + { + options.share_mode(0); + } + options.open(path) +} + +fn release_root_lock_path(path: &Path) { + if let Some(locked) = ROOT_LOCKED_PATHS.get() { + locked + .lock() + .expect("root lock path set poisoned") + .remove(path); + } +} + +impl CacheLock { + fn acquire(path: &Path) -> Result { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create cache lock directory {}", parent.display()))?; + } + let file = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path) + .with_context(|| format!("open cache lock {}", path.display()))?; + file.lock() + .with_context(|| format!("lock cache {}", path.display()))?; + Ok(Self { _file: file }) } } @@ -113,21 +405,47 @@ fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> { return None; } let pglite_bin_dir = pglite_dir.join("bin"); - let module = pglite_bin_dir.join("pglite.wasi"); + let module = pglite_bin_dir.join("pglite"); if !module.exists() { return None; } let share = pglite_dir.join("share").join("postgresql"); - if !share.exists() || !share.join("postgres.bki").exists() { + let required_share_files = [ + "postgres.bki", + "timezonesets/Default", + "timezone/UTC", + "timezone/America/New_York", + ]; + if !share.exists() + || required_share_files + .iter() + .any(|relative| !share.join(relative).is_file()) + { return None; } Some((module, pglite_bin_dir)) } -fn ensure_runtime(paths: &PglitePaths) -> Result { - if locate_runtime_module(paths).is_some() { - return Ok(false); +fn ensure_full_runtime(paths: &PglitePaths) -> Result { + let _phase = timing::phase("runtime.ensure"); + let existing_runtime = { + let _phase = timing::phase("runtime.locate_existing"); + locate_runtime_module(paths) + }; + if existing_runtime.is_some() { + let repaired_runtime = if runtime_support_files_need_repair(paths)? { + install_runtime_from_tar(paths)? + } else { + false + }; + write_runtime_layout_manifest( + &paths.runtime_root(), + RuntimeLayoutKind::FullLocal, + &runtime_cache_key()?, + )?; + ensure_runtime_password_file(&paths.runtime_root())?; + return Ok(repaired_runtime); } if let Some(parent) = paths.pgroot.parent() { @@ -144,10 +462,34 @@ fn ensure_runtime(paths: &PglitePaths) -> Result { paths.pgroot.display() ) })?; + write_runtime_layout_manifest( + &paths.runtime_root(), + RuntimeLayoutKind::FullLocal, + &runtime_cache_key()?, + )?; + ensure_runtime_password_file(&paths.runtime_root())?; Ok(true) } +fn runtime_support_files_need_repair(paths: &PglitePaths) -> Result { + for relative in [ + "password", + "share/postgresql/postgres.bki", + "share/postgresql/system_views.sql", + "share/postgresql/timezonesets/Default", + ] { + let path = paths.runtime_root().join(relative); + match fs::metadata(&path) { + Ok(metadata) if metadata.is_file() && metadata.len() > 0 => {} + Ok(_) => return Ok(true), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(true), + Err(err) => return Err(err).with_context(|| format!("stat {}", path.display())), + } + } + Ok(false) +} + fn runtime_tar_path() -> Option { if let Ok(path) = std::env::var("PGLITE_OXIDE_RUNTIME_ARCHIVE") .or_else(|_| std::env::var("PGLITE_OXIDE_RUNTIME_TAR")) @@ -162,56 +504,77 @@ fn runtime_tar_path() -> Option { } fn install_runtime_from_tar(paths: &PglitePaths) -> Result { + let _phase = timing::phase("runtime.archive_install"); if let Some(tar_path) = runtime_tar_path() { info!("installing runtime from tar archive {}", tar_path.display()); let file = fs::File::open(&tar_path) .with_context(|| format!("open runtime archive {}", tar_path.display()))?; unpack_runtime_archive_reader(file, &tar_path, &paths.pgroot)?; - } else { + } else if let Some(runtime_archive) = assets::runtime_archive() { info!("installing embedded runtime archive"); + maybe_validate_embedded_runtime_archive(runtime_archive)?; unpack_runtime_archive_reader( - Cursor::new(EMBEDDED_RUNTIME_ARCHIVE), + Cursor::new(runtime_archive), Path::new(RUNTIME_ARCHIVE_NAME), &paths.pgroot, )?; + } else { + bail!( + "no embedded PGlite runtime assets are available; enable the `bundled` feature or set PGLITE_OXIDE_RUNTIME_ARCHIVE" + ); } Ok(true) } +#[cfg(feature = "bundled")] +fn maybe_validate_embedded_runtime_archive(bytes: &[u8]) -> Result<()> { + if strict_asset_verification()? { + validate_embedded_runtime_archive_strict(bytes)?; + } + Ok(()) +} + +#[cfg(feature = "bundled")] +fn validate_embedded_runtime_archive_strict(bytes: &[u8]) -> Result<()> { + let expected = assets::expected_runtime_archive_sha256()?; + let actual = sha256_hex(bytes); + ensure!( + actual.eq_ignore_ascii_case(&expected), + "embedded runtime archive hash mismatch: manifest={expected} actual={actual}" + ); + Ok(()) +} + +#[cfg(not(feature = "bundled"))] +fn maybe_validate_embedded_runtime_archive(_bytes: &[u8]) -> Result<()> { + Ok(()) +} + fn unpack_runtime_archive_reader( reader: R, archive_path: &Path, destination: &Path, ) -> Result<()> { + let _phase = timing::phase("runtime.archive_unpack"); let decoder = ZstdDecoder::new(reader) .with_context(|| format!("decode zstd runtime archive {}", archive_path.display()))?; let mut archive = Archive::new(decoder); - for entry in archive - .entries() - .with_context(|| format!("read entries from {}", archive_path.display()))? - { - let mut entry = - entry.with_context(|| format!("read entry from {}", archive_path.display()))?; - let path = entry - .path() - .with_context(|| format!("read entry path from {}", archive_path.display()))? - .into_owned(); - let relative = path.strip_prefix("tmp").unwrap_or(&path); - let dest = archive_destination(destination, relative)?; - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create directory {}", parent.display()))?; - } - entry - .unpack(&dest) - .with_context(|| format!("unpack {} to {}", path.display(), dest.display()))?; - } + unpack_archive_entries_with_path_map(&mut archive, destination, runtime_archive_relative_path) + .with_context(|| format!("unpack runtime archive {}", archive_path.display()))?; Ok(()) } +fn runtime_archive_relative_path(path: &Path) -> &Path { + let mut without_dot = path; + if let Ok(stripped) = without_dot.strip_prefix(".") { + without_dot = stripped; + } + without_dot.strip_prefix("tmp").unwrap_or(without_dot) +} + fn archive_destination(root: &Path, archive_path: &Path) -> Result { let mut dest = root.to_path_buf(); for component in archive_path.components() { @@ -224,12 +587,24 @@ fn archive_destination(root: &Path, archive_path: &Path) -> Result { Ok(dest) } -fn install_extension_reader(paths: &PglitePaths, reader: R) -> Result<()> { - let mut ar = Archive::new(GzDecoder::new(reader)); +fn install_extension_reader(paths: &PglitePaths, mut reader: R) -> Result<()> { + let _phase = timing::phase("extension.archive_install"); + let mut bytes = Vec::new(); + reader + .read_to_end(&mut bytes) + .context("read extension archive")?; + let archive_reader: Box = if bytes.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) { + Box::new(ZstdDecoder::new(Cursor::new(bytes)).context("decode zstd extension archive")?) + } else if bytes.starts_with(&[0x1f, 0x8b]) { + Box::new(GzDecoder::new(Cursor::new(bytes))) + } else { + Box::new(Cursor::new(bytes)) + }; + let mut ar = Archive::new(archive_reader); let target = paths.pgroot.join("pglite"); std::fs::create_dir_all(&target) .with_context(|| format!("create extension target {}", target.display()))?; - ar.unpack(&target) + unpack_archive_entries(&mut ar, &target) .with_context(|| format!("unpack extension into {}", target.display()))?; Ok(()) } @@ -244,47 +619,51 @@ pub fn install_extension_bytes(paths: &PglitePaths, bytes: &[u8]) -> Result<()> install_extension_reader(paths, std::io::Cursor::new(bytes)) } +#[cfg(feature = "extensions")] +pub(crate) fn install_bundled_extension_bytes( + paths: &PglitePaths, + sql_name: &str, + bytes: &[u8], +) -> Result<()> { + if strict_asset_verification()? { + validate_bundled_extension_archive_strict(sql_name, bytes)?; + } + install_extension_bytes(paths, bytes) +} + +#[cfg(feature = "extensions")] +fn validate_bundled_extension_archive_strict(sql_name: &str, bytes: &[u8]) -> Result<()> { + let expected = assets::expected_extension_archive_sha256(sql_name)?; + let actual = sha256_hex(bytes); + ensure!( + actual.eq_ignore_ascii_case(&expected), + "embedded extension archive '{sql_name}' hash mismatch: manifest={expected} actual={actual}" + ); + Ok(()) +} + pub fn build_pgdata_template(output_dir: impl AsRef) -> Result { let output_dir = output_dir.as_ref(); fs::create_dir_all(output_dir) .with_context(|| format!("create template output dir {}", output_dir.display()))?; - let work_dir = output_dir.join(".build"); - if work_dir.exists() { - fs::remove_dir_all(&work_dir) - .with_context(|| format!("remove stale template build dir {}", work_dir.display()))?; - } - - let paths = PglitePaths::with_root(&work_dir); - let _outcome = install_into_internal_with_template(paths.clone(), false)?; - ensure_cluster_with_template(&paths, false)?; - - let (module_path, _) = locate_runtime_module(&paths).ok_or_else(|| { - anyhow!( - "runtime missing: could not locate module under {}", - paths.pgroot.display() - ) - })?; - let postgres_version = fs::read_to_string(paths.pgdata.join("PG_VERSION")) - .context("read generated template PG_VERSION")? - .trim() - .to_string(); - let archive_path = output_dir.join(PGDATA_TEMPLATE_ARCHIVE_NAME); - write_pgdata_template_archive(&paths.pgdata, &archive_path)?; + let manifest_path = output_dir.join("pgdata-template.json"); - let manifest = PgDataTemplateManifest { - postgres_version, - wasm_sha256: sha256_file(&module_path)?, - archive_sha256: sha256_file(&archive_path)?, - architecture_independent: true, + let Some(archive) = assets::pgdata_template_archive() else { + bail!("bundled PGDATA template archive is unavailable"); }; - let manifest_path = output_dir.join("pgdata-template.json"); - fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?) + let Some(manifest) = assets::pgdata_template_manifest() else { + bail!("bundled PGDATA template manifest is unavailable"); + }; + validated_embedded_pgdata_template_manifest()? + .context("bundled PGDATA template manifest is unavailable")?; + + fs::write(&archive_path, archive) + .with_context(|| format!("write template archive {}", archive_path.display()))?; + fs::write(&manifest_path, manifest) .with_context(|| format!("write template manifest {}", manifest_path.display()))?; - fs::remove_dir_all(&work_dir) - .with_context(|| format!("remove template build dir {}", work_dir.display()))?; Ok(PgDataTemplate { archive_path, manifest_path, @@ -292,65 +671,17 @@ pub fn build_pgdata_template(output_dir: impl AsRef) -> Result Result { - if paths.marker_cluster().exists() { + let _phase = timing::phase("pgdata.embedded_template_install"); + if cluster_is_complete(paths) { return Ok(false); } - let manifest: PgDataTemplateManifest = - serde_json::from_slice(EMBEDDED_PGDATA_TEMPLATE_MANIFEST) - .context("parse embedded PGDATA template manifest")?; - ensure!( - manifest.architecture_independent, - "embedded PGDATA template manifest must set architectureIndependent=true" - ); - - let actual_wasm = sha256_file(module_path)?; - ensure!( - actual_wasm.eq_ignore_ascii_case(&manifest.wasm_sha256), - "embedded PGDATA template wasm hash mismatch: manifest={} actual={actual_wasm}", - manifest.wasm_sha256 - ); - - let actual_archive = sha256_hex(EMBEDDED_PGDATA_TEMPLATE_ARCHIVE); - ensure!( - actual_archive.eq_ignore_ascii_case(&manifest.archive_sha256), - "embedded PGDATA template archive hash mismatch: manifest={} actual={actual_archive}", - manifest.archive_sha256 - ); - - let staging = paths.pgdata.with_file_name(format!( - ".pgdata-template-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or_default() - )); - if staging.exists() { - fs::remove_dir_all(&staging) - .with_context(|| format!("remove stale template staging {}", staging.display()))?; - } - fs::create_dir_all(&staging) - .with_context(|| format!("create template staging {}", staging.display()))?; - - if let Err(err) = unpack_pgdata_template_archive(EMBEDDED_PGDATA_TEMPLATE_ARCHIVE, &staging) { - let _ = fs::remove_dir_all(&staging); - return Err(err); - } - remove_template_runtime_state(&staging)?; + let Some(manifest) = validated_embedded_pgdata_template_manifest()? else { + return Ok(false); + }; - let pg_version = fs::read_to_string(staging.join("PG_VERSION")) - .with_context(|| format!("read {}", staging.join("PG_VERSION").display()))?; - ensure!( - pg_version.trim() == manifest.postgres_version.trim(), - "embedded PGDATA template postgres version mismatch: manifest={} actual={}", - manifest.postgres_version, - pg_version.trim() - ); - ensure!( - staging.join("global").join("pg_control").exists(), - "embedded PGDATA template did not contain global/pg_control at archive root" - ); + ensure_module_matches_template(module_path, &manifest)?; + let template = pgdata_template_cache()?; if let Some(parent) = paths.pgdata.parent() { fs::create_dir_all(parent) @@ -360,165 +691,836 @@ fn try_install_embedded_pgdata_template(paths: &PglitePaths, module_path: &Path) fs::remove_dir_all(&paths.pgdata) .with_context(|| format!("remove existing pgdata {}", paths.pgdata.display()))?; } - fs::rename(&staging, &paths.pgdata).with_context(|| { - format!( - "promote PGDATA template {} -> {}", - staging.display(), - paths.pgdata.display() - ) - })?; + { + let _phase = timing::phase("pgdata.cached_template_clone"); + clone_pgdata_template_dir(&template.pgdata, &paths.pgdata)?; + } + remove_template_runtime_state(&paths.pgdata)?; Ok(true) } -fn unpack_pgdata_template_archive(bytes: &[u8], destination: &Path) -> Result<()> { - let decoder = ZstdDecoder::new(Cursor::new(bytes)).context("decode PGDATA template archive")?; - let mut archive = Archive::new(decoder); - for entry in archive - .entries() - .context("read entries from PGDATA template archive")? - { - let mut entry = entry.context("read PGDATA template archive entry")?; - let path = entry - .path() - .context("read PGDATA template archive entry path")? - .into_owned(); - let dest = archive_destination(destination, &path)?; - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create directory {}", parent.display()))?; - } - entry - .unpack(&dest) - .with_context(|| format!("unpack PGDATA template {}", path.display()))?; +fn try_prepare_pgdata_template_overlay( + paths: &PglitePaths, + module_path: &Path, + runtime_layout: &mut RuntimeLayout, +) -> Result { + let _phase = timing::phase("pgdata.overlay_prepare"); + let Some(manifest) = validated_embedded_pgdata_template_manifest()? else { + return Ok(false); + }; + + ensure_module_matches_template(module_path, &manifest)?; + let template = pgdata_template_cache()?; + if let Some(existing) = read_pgdata_overlay_manifest(paths)? { + ensure!( + existing.template_archive_sha256 == manifest.archive_sha256, + "PGDATA overlay at {} was created for template {}, but this runtime provides {}; delete the root/cache and recreate it", + paths.pgdata.display(), + existing.template_archive_sha256, + manifest.archive_sha256 + ); + } else if paths.pgdata.exists() && !cluster_is_complete(paths) { + fs::remove_dir_all(&paths.pgdata).with_context(|| { + format!( + "remove interrupted PGDATA before overlay setup at {}", + paths.pgdata.display() + ) + })?; } - Ok(()) -} -fn write_pgdata_template_archive(pgdata: &Path, archive_path: &Path) -> Result<()> { - let file = fs::File::create(archive_path) - .with_context(|| format!("create template archive {}", archive_path.display()))?; - let mut encoder = ZstdEncoder::new(file, 19) - .with_context(|| format!("create zstd encoder for {}", archive_path.display()))?; - { - let mut builder = Builder::new(&mut encoder); - append_pgdata_template_dir(&mut builder, pgdata, Path::new(""))?; - builder - .finish() - .with_context(|| format!("finish tar archive {}", archive_path.display()))?; - } - encoder - .finish() - .with_context(|| format!("finish zstd archive {}", archive_path.display()))?; - Ok(()) + fs::create_dir_all(&paths.pgdata) + .with_context(|| format!("create PGDATA overlay upper {}", paths.pgdata.display()))?; + fs::write( + paths.pgdata.join("PG_VERSION"), + format!("{}\n", manifest.postgres_version.trim()), + ) + .with_context(|| format!("write {}", paths.pgdata.join("PG_VERSION").display()))?; + write_pgdata_overlay_manifest(paths, &manifest)?; + remove_template_runtime_state(&paths.pgdata)?; + runtime_layout.pgdata_template_root = Some(template.pgdata.clone()); + Ok(true) } -fn append_pgdata_template_dir( - builder: &mut Builder, - source: &Path, - archive_prefix: &Path, +#[cfg(feature = "extensions")] +fn install_extension_template_into_outcome( + outcome: &mut InstallOutcome, + extensions: &[Extension], + postgres_config: &PostgresConfig, ) -> Result<()> { - for entry in - fs::read_dir(source).with_context(|| format!("read directory {}", source.display()))? - { - let entry = entry.with_context(|| format!("read entry under {}", source.display()))?; - let file_name = entry.file_name(); - if should_skip_template_entry(&file_name) { - continue; - } - let source_path = entry.path(); - let archive_path = archive_prefix.join(&file_name); - let file_type = entry - .file_type() - .with_context(|| format!("stat {}", source_path.display()))?; - if file_type.is_dir() { - builder - .append_dir(&archive_path, &source_path) - .with_context(|| format!("append directory {}", archive_path.display()))?; - append_pgdata_template_dir(builder, &source_path, &archive_path)?; - } else if file_type.is_file() { - builder - .append_path_with_name(&source_path, &archive_path) - .with_context(|| format!("append file {}", archive_path.display()))?; - } + let normalized = normalize_extension_set(extensions); + if normalized.is_empty() { + return Ok(()); + } + + let template = extension_pgdata_template_cache( + &normalized, + &outcome.runtime_layout.module_path(), + postgres_config, + )?; + if outcome.runtime_layout.uses_shared_overlay() && pgdata_overlay_enabled() { + install_pgdata_template_overlay_from_extension_template( + &outcome.paths, + &mut outcome.runtime_layout, + &template, + )?; + } else { + install_pgdata_template_clone_from_extension_template(&outcome.paths, &template)?; + outcome.runtime_layout.pgdata_template_root = None; + } + + for extension in &normalized { + let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { + anyhow!( + "extension asset '{}' is not bundled in this pglite-oxide build", + extension.sql_name() + ) + })?; + install_bundled_extension_bytes(&outcome.paths, extension.sql_name(), bytes)?; } + outcome.preinstalled_extensions = template.manifest.extension_sql_names.clone(); Ok(()) } -fn remove_template_runtime_state(pgdata: &Path) -> Result<()> { - for name in TEMPLATE_RUNTIME_STATE_FILES { - let path = pgdata.join(name); - if path.exists() { - fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?; - } +#[cfg(feature = "extensions")] +fn install_pgdata_template_overlay_from_extension_template( + paths: &PglitePaths, + runtime_layout: &mut RuntimeLayout, + template: &CachedExtensionPgDataTemplate, +) -> Result<()> { + let _phase = timing::phase("pgdata.extension_template_overlay"); + if paths.pgdata.exists() { + fs::remove_dir_all(&paths.pgdata).with_context(|| { + format!( + "remove PGDATA before extension overlay {}", + paths.pgdata.display() + ) + })?; } + fs::create_dir_all(&paths.pgdata) + .with_context(|| format!("create PGDATA overlay upper {}", paths.pgdata.display()))?; + fs::write( + paths.pgdata.join("PG_VERSION"), + format!("{}\n", template.manifest.postgres_version.trim()), + ) + .with_context(|| format!("write {}", paths.pgdata.join("PG_VERSION").display()))?; + write_pgdata_overlay_manifest_values( + paths, + &template.manifest.cache_key, + &template.manifest.postgres_version, + &template.manifest.extension_sql_names, + )?; + remove_template_runtime_state(&paths.pgdata)?; + runtime_layout.pgdata_template_root = Some(template.pgdata.clone()); Ok(()) } -fn sha256_file(path: &Path) -> Result { - let bytes = fs::read(path).with_context(|| format!("read {}", path.display()))?; - Ok(sha256_hex(&bytes)) +#[cfg(feature = "extensions")] +fn install_pgdata_template_clone_from_extension_template( + paths: &PglitePaths, + template: &CachedExtensionPgDataTemplate, +) -> Result<()> { + let _phase = timing::phase("pgdata.extension_template_clone"); + if paths.pgdata.exists() { + fs::remove_dir_all(&paths.pgdata).with_context(|| { + format!( + "remove PGDATA before extension template clone {}", + paths.pgdata.display() + ) + })?; + } + if let Some(parent) = paths.pgdata.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create pgdata parent {}", parent.display()))?; + } + clone_pgdata_template_dir(&template.pgdata, &paths.pgdata)?; + remove_template_runtime_state(&paths.pgdata)?; + Ok(()) } -fn sha256_hex(bytes: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(bytes); - format!("{:x}", hasher.finalize()) +fn pgdata_overlay_manifest_path(paths: &PglitePaths) -> PathBuf { + paths.pgdata.join(PGDATA_OVERLAY_MANIFEST_NAME) +} + +fn pgdata_overlay_is_installed(paths: &PglitePaths) -> bool { + pgdata_overlay_manifest_path(paths).is_file() +} + +fn read_pgdata_overlay_manifest(paths: &PglitePaths) -> Result> { + let path = pgdata_overlay_manifest_path(paths); + match fs::read(&path) { + Ok(bytes) => { + let manifest = serde_json::from_slice(&bytes) + .with_context(|| format!("parse PGDATA overlay manifest {}", path.display()))?; + Ok(Some(manifest)) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err).with_context(|| format!("read {}", path.display())), + } +} + +fn write_pgdata_overlay_manifest( + paths: &PglitePaths, + manifest: &PgDataTemplateManifest, +) -> Result<()> { + write_pgdata_overlay_manifest_values( + paths, + &manifest.archive_sha256, + &manifest.postgres_version, + &[], + ) +} + +fn write_pgdata_overlay_manifest_values( + paths: &PglitePaths, + template_archive_sha256: &str, + postgres_version: &str, + extension_sql_names: &[String], +) -> Result<()> { + let overlay = PgDataOverlayManifest { + template_archive_sha256: template_archive_sha256.to_owned(), + postgres_version: postgres_version.to_owned(), + extension_sql_names: extension_sql_names.to_vec(), + }; + fs::write( + pgdata_overlay_manifest_path(paths), + serde_json::to_vec_pretty(&overlay)?, + ) + .with_context(|| { + format!( + "write PGDATA overlay manifest {}", + pgdata_overlay_manifest_path(paths).display() + ) + })?; + Ok(()) +} + +fn ensure_module_matches_template( + module_path: &Path, + manifest: &PgDataTemplateManifest, +) -> Result<()> { + if !strict_asset_verification()? { + #[cfg(feature = "bundled")] + if runtime_tar_path().is_none() { + let expected = assets::expected_module_sha256("runtime:pglite")?; + ensure!( + expected.eq_ignore_ascii_case(&manifest.wasm_sha256), + "embedded PGDATA template wasm hash mismatch: manifest={} assets={expected}", + manifest.wasm_sha256 + ); + } + return Ok(()); + } + + let actual_wasm = sha256_file(module_path)?; + ensure!( + actual_wasm.eq_ignore_ascii_case(&manifest.wasm_sha256), + "embedded PGDATA template wasm hash mismatch: manifest={} actual={actual_wasm}", + manifest.wasm_sha256 + ); + Ok(()) +} + +fn validated_embedded_pgdata_template_manifest() -> Result> { + let Some(template_manifest) = assets::pgdata_template_manifest() else { + return Ok(None); + }; + let Some(template_archive) = assets::pgdata_template_archive() else { + return Ok(None); + }; + + let manifest = PGDATA_TEMPLATE_MANIFEST + .get_or_init(|| { + let manifest: PgDataTemplateManifest = serde_json::from_slice(template_manifest) + .context("parse embedded PGDATA template manifest") + .map_err(|err| format!("{err:#}"))?; + if !manifest.architecture_independent { + return Err( + "embedded PGDATA template manifest must set architectureIndependent=true" + .to_string(), + ); + } + + Ok(manifest) + }) + .clone() + .map_err(|message| anyhow!(message))?; + if strict_asset_verification()? { + let actual_archive = sha256_hex(template_archive); + ensure!( + actual_archive.eq_ignore_ascii_case(&manifest.archive_sha256), + "embedded PGDATA template archive hash mismatch: manifest={} actual={actual_archive}", + manifest.archive_sha256 + ); + } + Ok(Some(manifest)) +} + +fn pgdata_template_cache() -> Result> { + PGDATA_TEMPLATE_CACHE + .get_or_init(|| { + build_pgdata_template_cache() + .map(Arc::new) + .map_err(|err| format!("{err:#}")) + }) + .clone() + .map_err(|message| anyhow!(message)) +} + +fn build_pgdata_template_cache() -> Result { + let _phase = timing::phase("pgdata.template_cache_install"); + let Some(manifest) = validated_embedded_pgdata_template_manifest()? else { + bail!("embedded PGDATA template manifest is unavailable"); + }; + let Some(template_archive) = assets::pgdata_template_archive() else { + bail!("embedded PGDATA template archive is unavailable"); + }; + + let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") + .context("could not resolve pglite-oxide cache directory")?; + let cache_root = dirs + .cache_dir() + .join("pgdata-template") + .join(PGDATA_TEMPLATE_CACHE_FORMAT); + let _cache_lock = CacheLock::acquire( + &cache_root + .join(".locks") + .join(format!("{}.lock", manifest.archive_sha256)), + )?; + let root = cache_root.join(&manifest.archive_sha256); + let pgdata = root.join("base"); + if pgdata.join("PG_VERSION").is_file() && pgdata.join("global/pg_control").is_file() { + return Ok(CachedPgDataTemplate { pgdata }); + } + + if root.exists() { + fs::remove_dir_all(&root) + .with_context(|| format!("remove stale PGDATA template cache {}", root.display()))?; + } + fs::create_dir_all(&root) + .with_context(|| format!("create PGDATA template cache {}", root.display()))?; + let staging = root.join(format!(".base-{}-{}", std::process::id(), tmp_suffix())); + if let Err(err) = unpack_pgdata_template_archive(template_archive, &staging) { + let _ = fs::remove_dir_all(&staging); + return Err(err); + } + validate_pgdata_template_dir(&staging, &manifest)?; + remove_template_runtime_state(&staging)?; + fs::rename(&staging, &pgdata).with_context(|| { + format!( + "promote PGDATA template cache {} -> {}", + staging.display(), + pgdata.display() + ) + })?; + Ok(CachedPgDataTemplate { pgdata }) } -fn ensure_pgdata(paths: &PglitePaths) -> Result<()> { - if !paths.pgdata.exists() { - fs::create_dir_all(&paths.pgdata).with_context(|| { +#[cfg(feature = "extensions")] +fn extension_pgdata_template_cache( + extensions: &[Extension], + module_path: &Path, + postgres_config: &PostgresConfig, +) -> Result> { + let normalized = normalize_extension_set(extensions); + ensure!( + !normalized.is_empty(), + "extension PGDATA template requires at least one extension" + ); + + let guard = EXTENSION_TEMPLATE_CACHE_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .map_err(|_| anyhow!("extension PGDATA template cache lock was poisoned"))?; + let template = build_extension_pgdata_template_cache(&normalized, module_path, postgres_config) + .map(Arc::new); + drop(guard); + template +} + +#[cfg(feature = "extensions")] +fn build_extension_pgdata_template_cache( + extensions: &[Extension], + module_path: &Path, + postgres_config: &PostgresConfig, +) -> Result { + let _phase = timing::phase("pgdata.extension_template_cache"); + let Some(base_manifest) = validated_embedded_pgdata_template_manifest()? else { + bail!("embedded PGDATA template manifest is unavailable"); + }; + ensure_module_matches_template(module_path, &base_manifest)?; + + let manifest = extension_pgdata_template_manifest(&base_manifest, extensions, postgres_config)?; + let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") + .context("could not resolve pglite-oxide cache directory")?; + let cache_root = dirs + .cache_dir() + .join("pgdata-extension-template") + .join(EXTENSION_PGDATA_TEMPLATE_CACHE_FORMAT); + let _cache_lock = CacheLock::acquire( + &cache_root + .join(".locks") + .join(format!("{}.lock", manifest.cache_key)), + )?; + let root = cache_root.join(&manifest.cache_key); + let pgdata = root.join("base"); + let manifest_path = root.join("extension-template.json"); + if extension_pgdata_template_is_valid(&pgdata, &manifest_path, &manifest)? { + return Ok(CachedExtensionPgDataTemplate { pgdata, manifest }); + } + + if root.exists() { + fs::remove_dir_all(&root).with_context(|| { format!( - "failed to create initial pgdata directory at {}", - paths.pgdata.display() + "remove stale extension PGDATA template cache {}", + root.display() ) })?; } + fs::create_dir_all(&root) + .with_context(|| format!("create extension PGDATA template cache {}", root.display()))?; + + let staging_root = root.join(format!(".build-{}-{}", std::process::id(), tmp_suffix())); + if let Err(err) = + build_extension_pgdata_template_staging(&staging_root, extensions, postgres_config) + { + let _ = fs::remove_dir_all(&staging_root); + return Err(err); + } + let staging_pgdata = PglitePaths::with_root(&staging_root).pgdata; + validate_pgdata_template_dir(&staging_pgdata, &base_manifest)?; + remove_template_runtime_state(&staging_pgdata)?; + fs::rename(&staging_pgdata, &pgdata).with_context(|| { + format!( + "promote extension PGDATA template cache {} -> {}", + staging_pgdata.display(), + pgdata.display() + ) + })?; + fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?).with_context(|| { + format!( + "write extension template manifest {}", + manifest_path.display() + ) + })?; + fs::remove_dir_all(&staging_root).with_context(|| { + format!( + "remove extension template build dir {}", + staging_root.display() + ) + })?; + Ok(CachedExtensionPgDataTemplate { pgdata, manifest }) +} + +#[cfg(feature = "extensions")] +fn build_extension_pgdata_template_staging( + staging_root: &Path, + extensions: &[Extension], + postgres_config: &PostgresConfig, +) -> Result<()> { + let _phase = timing::phase("pgdata.extension_template_build"); + if staging_root.exists() { + fs::remove_dir_all(staging_root) + .with_context(|| format!("remove stale build dir {}", staging_root.display()))?; + } + fs::create_dir_all(staging_root) + .with_context(|| format!("create build dir {}", staging_root.display()))?; + + let paths = PglitePaths::with_root(staging_root); + let (runtime_layout, unpacked_runtime) = + prepare_runtime_layout(&paths, RuntimeLayoutPolicy::FullLocal)?; + let base_template = pgdata_template_cache()?; + clone_pgdata_template_dir(&base_template.pgdata, &paths.pgdata)?; + remove_template_runtime_state(&paths.pgdata)?; + + let outcome = InstallOutcome { + paths, + unpacked_runtime, + runtime_layout, + preinstalled_extensions: Vec::new(), + }; + let mut db = Pglite::new_prepared_with_config( + outcome, + postgres_config.clone(), + StartupConfig::default(), + )?; + for extension in extensions { + db.enable_extension(*extension)?; + } + db.exec("CHECKPOINT", None) + .context("checkpoint extension PGDATA template")?; + db.close_for_template_cache() + .context("cleanly close extension PGDATA template")?; Ok(()) } -pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> { - ensure_cluster_with_template(paths, true) +#[cfg(feature = "extensions")] +fn extension_pgdata_template_is_valid( + pgdata: &Path, + manifest_path: &Path, + expected: &ExtensionPgDataTemplateManifest, +) -> Result { + if !pgdata.join("PG_VERSION").is_file() || !pgdata.join("global/pg_control").is_file() { + return Ok(false); + } + let bytes = match fs::read(manifest_path) { + Ok(bytes) => bytes, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("read {}", manifest_path.display())), + }; + let actual: ExtensionPgDataTemplateManifest = serde_json::from_slice(&bytes) + .with_context(|| format!("parse {}", manifest_path.display()))?; + Ok(&actual == expected) } -fn ensure_cluster_with_template(paths: &PglitePaths, use_template: bool) -> Result<()> { - if paths.marker_cluster().exists() { - return Ok(()); +#[cfg(feature = "extensions")] +fn extension_pgdata_template_manifest( + base_manifest: &PgDataTemplateManifest, + extensions: &[Extension], + postgres_config: &PostgresConfig, +) -> Result { + let extension_sql_names: Vec = extensions + .iter() + .map(|extension| extension.sql_name().to_owned()) + .collect(); + let extension_archive_sha256s: Vec = extensions + .iter() + .map(|extension| assets::expected_extension_archive_sha256(extension.sql_name())) + .collect::>()?; + let postgres_config_entries = postgres_config.stable_entries(); + + let mut hasher = Sha256::new(); + hasher.update(b"pglite-oxide-extension-pgdata-template-v4-startup-config\n"); + hasher.update(base_manifest.postgres_version.as_bytes()); + hasher.update(b"\n"); + hasher.update(base_manifest.archive_sha256.as_bytes()); + hasher.update(b"\n"); + hasher.update(base_manifest.wasm_sha256.as_bytes()); + hasher.update(b"\n"); + for (sql_name, archive_sha256) in extension_sql_names + .iter() + .zip(extension_archive_sha256s.iter()) + { + hasher.update(sql_name.as_bytes()); + hasher.update(b":"); + hasher.update(archive_sha256.as_bytes()); + hasher.update(b"\n"); + } + for (name, value) in &postgres_config_entries { + hasher.update(name.as_bytes()); + hasher.update(b"="); + hasher.update(value.as_bytes()); + hasher.update(b"\n"); } + let cache_key = format!("{:x}", hasher.finalize()); + + Ok(ExtensionPgDataTemplateManifest { + version: 3, + postgres_version: base_manifest.postgres_version.clone(), + base_template_archive_sha256: base_manifest.archive_sha256.clone(), + base_template_wasm_sha256: base_manifest.wasm_sha256.clone(), + extension_sql_names, + extension_archive_sha256s, + postgres_config: postgres_config_entries, + cache_key, + }) +} - ensure_runtime(paths)?; - if use_template { - let (module_path, _) = locate_runtime_module(paths).ok_or_else(|| { - anyhow!( - "runtime missing: could not locate module under {} after install", - paths.pgroot.display() +#[cfg(feature = "extensions")] +fn normalize_extension_set(extensions: &[Extension]) -> Vec { + let mut seen = BTreeSet::new(); + let mut normalized = Vec::new(); + for extension in extensions { + if seen.insert(extension.sql_name()) { + normalized.push(*extension); + } + } + normalized +} + +fn validate_pgdata_template_dir(pgdata: &Path, manifest: &PgDataTemplateManifest) -> Result<()> { + let pg_version = fs::read_to_string(pgdata.join("PG_VERSION")) + .with_context(|| format!("read {}", pgdata.join("PG_VERSION").display()))?; + ensure!( + pg_version.trim() == manifest.postgres_version.trim(), + "embedded PGDATA template postgres version mismatch: manifest={} actual={}", + manifest.postgres_version, + pg_version.trim() + ); + ensure!( + pgdata.join("global").join("pg_control").exists(), + "embedded PGDATA template did not contain global/pg_control at archive root" + ); + Ok(()) +} + +fn unpack_pgdata_template_archive(bytes: &[u8], destination: &Path) -> Result<()> { + let _phase = timing::phase("pgdata.template_unpack"); + let decoder = ZstdDecoder::new(Cursor::new(bytes)).context("decode PGDATA template archive")?; + let mut archive = Archive::new(decoder); + unpack_archive_entries(&mut archive, destination) +} + +fn unpack_archive_entries(archive: &mut Archive, destination: &Path) -> Result<()> { + unpack_archive_entries_with_path_map(archive, destination, |path| path) +} + +fn unpack_archive_entries_with_path_map( + archive: &mut Archive, + destination: &Path, + map_path: impl for<'path> Fn(&'path Path) -> &'path Path, +) -> Result<()> { + for entry in archive.entries().context("read archive entries")? { + let mut entry = entry.context("read archive entry")?; + let path = entry + .path() + .context("read archive entry path")? + .into_owned(); + let relative = map_path(&path); + let entry_type = entry.header().entry_type(); + let dest = archive_destination(destination, relative)?; + + if entry_type.is_dir() { + fs::create_dir_all(&dest) + .with_context(|| format!("create directory {}", dest.display()))?; + continue; + } + if !entry_type.is_file() { + bail!( + "unsafe archive entry {} has unsupported type {:?}", + path.display(), + entry_type + ); + } + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + + entry + .unpack(&dest) + .with_context(|| format!("unpack archive entry {}", path.display()))?; + } + Ok(()) +} + +fn remove_template_runtime_state(pgdata: &Path) -> Result<()> { + for name in TEMPLATE_RUNTIME_STATE_FILES { + let path = pgdata.join(name); + if path.exists() { + fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?; + } + } + Ok(()) +} + +fn cluster_is_complete(paths: &PglitePaths) -> bool { + (paths.marker_cluster().is_file() && paths.marker_control_file().is_file()) + || pgdata_overlay_is_installed(paths) +} + +fn remove_interrupted_pgdata(paths: &PglitePaths) -> Result<()> { + if paths.pgdata.exists() && !cluster_is_complete(paths) { + fs::remove_dir_all(&paths.pgdata).with_context(|| { + format!( + "remove interrupted PGDATA without complete cluster markers at {}", + paths.pgdata.display() ) })?; - if try_install_embedded_pgdata_template(paths, &module_path)? { - return Ok(()); - } } - ensure_pgdata(paths)?; + Ok(()) +} + +fn tmp_suffix() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or_default() +} + +fn strict_asset_verification() -> Result { + let Some(value) = std::env::var_os("PGLITE_OXIDE_AOT_VERIFY") else { + return Ok(false); + }; + let value = value.to_string_lossy().to_ascii_lowercase(); + match value.as_str() { + "" | "fast" | "metadata" | "receipt" | "0" | "false" | "off" => Ok(false), + "full" | "sha" | "sha256" | "strict" | "1" | "true" | "on" => Ok(true), + other => bail!("unsupported PGLITE_OXIDE_AOT_VERIFY={other}; use `fast` or `full`"), + } +} + +fn sha256_file(path: &Path) -> Result { + let bytes = fs::read(path).with_context(|| format!("read {}", path.display()))?; + Ok(sha256_hex(&bytes)) +} - let mut pg = PostgresMod::new(paths.clone())?; +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("{:x}", hasher.finalize()) +} + +pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> { + ensure_cluster_with_template(paths, true) +} + +fn ensure_cluster_with_template(paths: &PglitePaths, use_template: bool) -> Result<()> { + let outcome = prepare_database_root(paths.clone(), prepare_options_for_template(use_template))?; + let mut pg = PostgresMod::new_prepared(outcome.paths.clone(), outcome.runtime_layout.clone())?; pg.ensure_cluster() } pub fn preload_runtime_module(paths: &PglitePaths) -> Result<()> { - ensure_runtime(paths)?; - let (module_path, _) = locate_runtime_module(paths).ok_or_else(|| { - anyhow!( - "runtime missing: could not locate module under {}", - paths.pgroot.display() - ) - })?; + let _ = paths; + let cached_runtime = runtime_cache()?; + let module_path = cached_runtime.runtime_root.join("bin/pglite"); PostgresMod::preload_module(&module_path) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct InstallOutcome { pub paths: PglitePaths, pub unpacked_runtime: bool, + pub(crate) runtime_layout: RuntimeLayout, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + pub(crate) preinstalled_extensions: Vec, +} + +impl InstallOutcome { + #[cfg(feature = "extensions")] + pub(crate) fn has_preinstalled_extension(&self, extension: Extension) -> bool { + self.preinstalled_extensions + .iter() + .any(|sql_name| sql_name == extension.sql_name()) + } +} + +fn prepare_root_from_paths( + paths: PglitePaths, + root: PathBuf, + temp_dir: Option, + root_lock: Option, + use_template: bool, +) -> Result { + let outcome = prepare_database_root(paths, prepare_options_for_template(use_template))?; + Ok(PreparedRoot { + root, + temp_dir, + root_lock, + outcome, + }) +} + +pub(crate) fn prepare_root(plan: RootPlan) -> Result { + let (paths, root, temp_dir, root_lock, temporary) = match plan.target { + RootTarget::Path(root) => { + let paths = PglitePaths::with_root(&root); + let root_lock = RootLock::acquire(&root)?; + (paths, root, None, Some(root_lock), false) + } + RootTarget::AppId { + qualifier, + organization, + application, + } => { + let paths = PglitePaths::new(( + qualifier.as_str(), + organization.as_str(), + application.as_str(), + ))?; + let root = paths.install_root().to_path_buf(); + let root_lock = RootLock::acquire_for_paths(&paths)?; + (paths, root, None, Some(root_lock), false) + } + RootTarget::Temporary => { + let temp_dir = TempDir::new().context("create temporary pglite directory")?; + let root = temp_dir.path().to_path_buf(); + let paths = PglitePaths::with_root(&root); + (paths, root, Some(temp_dir), None, true) + } + }; + + match plan.source { + RootSource::DataDirArchive(archive) => { + prepare_root_from_data_dir_archive(paths, root, temp_dir, root_lock, &archive) + } + source @ (RootSource::Template | RootSource::FreshInitdb) => { + let use_template = matches!(source, RootSource::Template); + let prepared = prepare_root_from_paths(paths, root, temp_dir, root_lock, use_template)?; + #[cfg(feature = "extensions")] + { + let mut prepared = prepared; + if temporary && use_template { + install_extension_template_into_outcome( + &mut prepared.outcome, + &plan.extensions, + &plan.postgres_config, + )?; + } + Ok(prepared) + } + #[cfg(not(feature = "extensions"))] + { + let _ = temporary; + Ok(prepared) + } + } + } +} + +fn prepare_root_from_data_dir_archive( + paths: PglitePaths, + root: PathBuf, + temp_dir: Option, + root_lock: Option, + archive: &[u8], +) -> Result { + let (runtime_layout, unpacked_runtime) = + prepare_runtime_layout(&paths, RuntimeLayoutPolicy::Auto)?; + if paths.pgdata.exists() { + fs::remove_dir_all(&paths.pgdata) + .with_context(|| format!("remove existing PGDATA {}", paths.pgdata.display()))?; + } + fs::create_dir_all(&paths.pgdata) + .with_context(|| format!("create PGDATA {}", paths.pgdata.display()))?; + unpack_pgdata_archive(archive, &paths.pgdata) + .with_context(|| format!("load PGDATA archive into {}", paths.pgdata.display()))?; + remove_template_runtime_state(&paths.pgdata)?; + ensure!( + paths.marker_cluster().is_file() && paths.marker_control_file().is_file(), + "loaded PGDATA archive did not contain PG_VERSION and global/pg_control" + ); + Ok(PreparedRoot { + root, + temp_dir, + root_lock, + outcome: InstallOutcome { + paths, + unpacked_runtime, + runtime_layout, + preinstalled_extensions: Vec::new(), + }, + }) +} + +#[cfg(feature = "extensions")] +pub(crate) fn install_missing_extension_archives( + outcome: &InstallOutcome, + extensions: &[Extension], +) -> Result<()> { + for extension in extensions { + if outcome.has_preinstalled_extension(*extension) { + continue; + } + let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { + anyhow!( + "extension asset '{}' is not bundled in this pglite-oxide build", + extension.sql_name() + ) + })?; + install_bundled_extension_bytes(&outcome.paths, extension.sql_name(), bytes)?; + } + Ok(()) } #[derive(Debug, Clone, Copy)] @@ -561,55 +1563,95 @@ impl MountInfo { pub fn install_default(app_id: (&str, &str, &str)) -> Result { let paths = PglitePaths::new(app_id)?; - install_into_internal(paths) + prepare_database_root(paths, RootPrepareOptions::template()) } pub fn install_into(root: &Path) -> Result { let paths = PglitePaths::with_root(root); - install_into_internal(paths) + prepare_database_root(paths, RootPrepareOptions::template()) } -pub(crate) fn install_temporary_from_template() -> Result<(TempDir, InstallOutcome)> { - let template = template_cluster()?; - let temp_dir = TempDir::new().context("create temporary pglite directory")?; - copy_dir_filtered(&template.root, temp_dir.path())?; - - let outcome = InstallOutcome { - paths: PglitePaths::with_root(temp_dir.path()), - unpacked_runtime: false, - }; - Ok((temp_dir, outcome)) +pub(crate) fn prepare_database_root( + paths: PglitePaths, + options: RootPrepareOptions, +) -> Result { + let (mut runtime_layout, unpacked_runtime) = prepare_runtime_layout(&paths, options.runtime)?; + prepare_pgdata(&paths, options.cluster, &mut runtime_layout)?; + Ok(InstallOutcome { + paths, + unpacked_runtime, + runtime_layout, + preinstalled_extensions: Vec::new(), + }) } -fn install_into_internal(paths: PglitePaths) -> Result { - install_into_internal_with_template(paths, true) +fn prepare_pgdata( + paths: &PglitePaths, + cluster_policy: ClusterPolicy, + runtime_layout: &mut RuntimeLayout, +) -> Result<()> { + let _phase = timing::phase("pgdata.initialize"); + if pgdata_overlay_is_installed(paths) { + ensure!( + runtime_layout.uses_shared_overlay(), + "PGDATA at {} uses the template overlay; delete the root/cache and recreate it with the shared runtime layout", + paths.pgdata.display() + ); + if try_prepare_pgdata_template_overlay( + paths, + &runtime_layout.module_path(), + runtime_layout, + )? { + return Ok(()); + } + } + if cluster_is_complete(paths) { + remove_template_runtime_state(&paths.pgdata)?; + return Ok(()); + } + if cluster_policy == ClusterPolicy::ExistingOrTemplate + && pgdata_overlay_enabled() + && runtime_layout.uses_shared_overlay() + && try_prepare_pgdata_template_overlay( + paths, + &runtime_layout.module_path(), + runtime_layout, + )? + { + return Ok(()); + } + if cluster_policy == ClusterPolicy::ExistingOrTemplate + && try_install_embedded_pgdata_template(paths, &runtime_layout.module_path())? + { + return Ok(()); + } + remove_interrupted_pgdata(paths)?; + { + let _phase = timing::phase("pgdata.fresh_initdb"); + PostgresMod::run_split_initdb(paths, runtime_layout)?; + } + ensure!( + cluster_is_complete(paths), + "split WASIX initdb finished but did not create a complete PGDATA cluster at {}", + paths.pgdata.display() + ); + remove_template_runtime_state(&paths.pgdata) } -fn install_into_internal_with_template( - paths: PglitePaths, - use_template: bool, -) -> Result { - let unpacked_runtime = ensure_runtime(&paths)?; - if use_template && !paths.marker_cluster().exists() { - let (module_path, _) = locate_runtime_module(&paths).ok_or_else(|| { - anyhow!( - "runtime missing: could not locate module under {} after install", - paths.pgroot.display() - ) - })?; - try_install_embedded_pgdata_template(&paths, &module_path)?; +fn prepare_options_for_template(use_template: bool) -> RootPrepareOptions { + if use_template { + RootPrepareOptions::template() + } else { + RootPrepareOptions::fresh() } - ensure_pgdata(&paths)?; - Ok(InstallOutcome { - paths, - unpacked_runtime, - }) } pub fn install_and_init(app_id: (&str, &str, &str)) -> Result { let outcome = install_default(app_id)?; - if !outcome.paths.marker_cluster().exists() { - ensure_cluster(&outcome.paths)?; + if !cluster_is_complete(&outcome.paths) { + let mut pg = + PostgresMod::new_prepared(outcome.paths.clone(), outcome.runtime_layout.clone())?; + pg.ensure_cluster()?; } Ok(MountInfo { mount: outcome.paths.pgroot.clone(), @@ -620,8 +1662,10 @@ pub fn install_and_init(app_id: (&str, &str, &str)) -> Result { pub fn install_and_init_in>(root: P) -> Result { let outcome = install_into(root.as_ref())?; - if !outcome.paths.marker_cluster().exists() { - ensure_cluster(&outcome.paths)?; + if !cluster_is_complete(&outcome.paths) { + let mut pg = + PostgresMod::new_prepared(outcome.paths.clone(), outcome.runtime_layout.clone())?; + pg.ensure_cluster()?; } Ok(MountInfo { mount: outcome.paths.pgroot.clone(), @@ -631,23 +1675,23 @@ pub fn install_and_init_in>(root: P) -> Result { } pub fn install_with_options(paths: PglitePaths, options: InstallOptions) -> Result { - let unpacked_runtime = ensure_runtime(&paths)?; - if options.ensure_cluster && !paths.marker_cluster().exists() { - ensure_cluster(&paths)?; - } else { - ensure_pgdata(&paths)?; + let outcome = prepare_database_root(paths, RootPrepareOptions::template())?; + if options.ensure_cluster && !cluster_is_complete(&outcome.paths) { + let mut pg = + PostgresMod::new_prepared(outcome.paths.clone(), outcome.runtime_layout.clone())?; + pg.ensure_cluster()?; } Ok(MountInfo { - mount: paths.pgroot.clone(), - paths, - reused_existing: !unpacked_runtime, + mount: outcome.paths.pgroot.clone(), + paths: outcome.paths, + reused_existing: !outcome.unpacked_runtime, }) } -fn template_cluster() -> Result> { - TEMPLATE_CLUSTER +fn runtime_cache() -> Result> { + RUNTIME_CACHE .get_or_init(|| { - build_template_cluster() + build_runtime_cache() .map(Arc::new) .map_err(|err| format!("{err:#}")) }) @@ -655,43 +1699,341 @@ fn template_cluster() -> Result> { .map_err(|message| anyhow!(message)) } -fn build_template_cluster() -> Result { - let temp_dir = TempDir::new().context("create pglite template cluster directory")?; - let outcome = install_into(temp_dir.path())?; - ensure_cluster(&outcome.paths)?; +pub(crate) fn shared_runtime_overlay_enabled() -> bool { + true +} - Ok(TemplateCluster { - root: temp_dir.path().to_path_buf(), - _temp_dir: temp_dir, - }) +pub(crate) fn pgdata_overlay_enabled() -> bool { + true +} + +fn prepare_runtime_layout( + paths: &PglitePaths, + policy: RuntimeLayoutPolicy, +) -> Result<(RuntimeLayout, bool)> { + match resolve_runtime_layout_kind(paths, policy)? { + RuntimeLayoutKind::FullLocal => { + let unpacked_runtime = ensure_full_runtime(paths)?; + let (module_path, _) = locate_runtime_module(paths).ok_or_else(|| { + anyhow!( + "runtime missing: could not locate module under {} after install", + paths.pgroot.display() + ) + })?; + let module_root = module_path + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .unwrap_or_else(|| paths.runtime_root()); + Ok(( + RuntimeLayout { + kind: RuntimeLayoutKind::FullLocal, + #[cfg(feature = "extensions")] + local_root: module_root.clone(), + module_root, + pgdata_template_root: None, + }, + unpacked_runtime, + )) + } + RuntimeLayoutKind::SharedRuntimeOverlay => { + let cached_runtime = runtime_cache()?; + prepare_shared_runtime_upper_root(&cached_runtime.runtime_root, paths)?; + Ok(( + RuntimeLayout { + kind: RuntimeLayoutKind::SharedRuntimeOverlay, + #[cfg(feature = "extensions")] + local_root: paths.runtime_root(), + module_root: cached_runtime.runtime_root.clone(), + pgdata_template_root: None, + }, + false, + )) + } + } +} + +fn resolve_runtime_layout_kind( + paths: &PglitePaths, + policy: RuntimeLayoutPolicy, +) -> Result { + match policy { + RuntimeLayoutPolicy::FullLocal => return Ok(RuntimeLayoutKind::FullLocal), + RuntimeLayoutPolicy::Auto => {} + } + + if let Some(manifest) = read_runtime_layout_manifest(&paths.runtime_root())? + && manifest.kind == RuntimeLayoutKind::SharedRuntimeOverlay + { + return Ok(RuntimeLayoutKind::SharedRuntimeOverlay); + } + if paths.runtime_root().join(MOUNTFS_RUNTIME_MARKER).is_file() { + return Ok(RuntimeLayoutKind::SharedRuntimeOverlay); + } + if shared_runtime_overlay_enabled() { + return Ok(RuntimeLayoutKind::SharedRuntimeOverlay); + } + Ok(RuntimeLayoutKind::FullLocal) +} + +fn write_runtime_layout_manifest( + runtime_root: &Path, + kind: RuntimeLayoutKind, + source_key: &str, +) -> Result<()> { + fs::create_dir_all(runtime_root) + .with_context(|| format!("create runtime root {}", runtime_root.display()))?; + let manifest = RuntimeLayoutManifest { + kind, + source_key: source_key.to_owned(), + }; + fs::write( + runtime_root.join(RUNTIME_LAYOUT_MANIFEST_NAME), + serde_json::to_vec_pretty(&manifest)?, + ) + .with_context(|| { + format!( + "write runtime layout manifest {}", + runtime_root.join(RUNTIME_LAYOUT_MANIFEST_NAME).display() + ) + })?; + Ok(()) +} + +fn read_runtime_layout_manifest(runtime_root: &Path) -> Result> { + let path = runtime_root.join(RUNTIME_LAYOUT_MANIFEST_NAME); + match fs::read(&path) { + Ok(bytes) => { + let manifest = serde_json::from_slice(&bytes) + .with_context(|| format!("parse runtime layout manifest {}", path.display()))?; + Ok(Some(manifest)) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err).with_context(|| format!("read {}", path.display())), + } +} + +fn build_runtime_cache() -> Result { + let _phase = timing::phase("runtime.cache_install"); + let key = { + let _phase = timing::phase("runtime.cache_key"); + runtime_cache_key()? + }; + let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") + .context("could not resolve pglite-oxide cache directory")?; + let cache_root = dirs.cache_dir().join("runtime"); + let _cache_lock = CacheLock::acquire(&cache_root.join(".locks").join(format!("{key}.lock")))?; + let root = cache_root.join(&key); + let paths = PglitePaths::with_root(root); + { + let _phase = timing::phase("runtime.cache_ensure_full"); + ensure_full_runtime(&paths)?; + } + let (module_path, _) = { + let _phase = timing::phase("runtime.cache_locate_module"); + locate_runtime_module(&paths).ok_or_else(|| { + anyhow!( + "runtime missing: could not locate module under {} after cache install", + paths.pgroot.display() + ) + })? + }; + if strict_asset_verification()? + && let Some(manifest) = validated_embedded_pgdata_template_manifest()? + { + ensure_module_matches_template(&module_path, &manifest)?; + } + let runtime_root = module_path + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .unwrap_or_else(|| paths.runtime_root()); + { + let _phase = timing::phase("runtime.cache_reset_mutable"); + reset_runtime_cache_mutable_state(&runtime_root)?; + } + Ok(CachedRuntime { runtime_root }) +} + +fn reset_runtime_cache_mutable_state(runtime_root: &Path) -> Result<()> { + for relative in ["base", "tmp", "dev/shm"] { + let path = runtime_root.join(relative); + match fs::remove_dir_all(&path) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => { + return Err(err).with_context(|| { + format!("remove mutable runtime-cache state {}", path.display()) + }); + } + } + } + fs::create_dir_all(runtime_root.join("tmp")) + .with_context(|| format!("create runtime cache tmp under {}", runtime_root.display()))?; + fs::create_dir_all(runtime_root.join("dev/shm")).with_context(|| { + format!( + "create runtime cache shared-memory dir under {}", + runtime_root.display() + ) + })?; + ensure_runtime_password_file(runtime_root)?; + Ok(()) +} + +fn ensure_runtime_password_file(runtime_root: &Path) -> Result<()> { + let path = runtime_root.join("password"); + let needs_repair = match fs::read(&path) { + Ok(bytes) => bytes.is_empty(), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => true, + Err(err) => return Err(err).with_context(|| format!("read {}", path.display())), + }; + if needs_repair { + fs::write(&path, DEFAULT_PASSWORD_FILE) + .with_context(|| format!("write {}", path.display()))?; + } + Ok(()) +} + +fn runtime_cache_key() -> Result { + if assets::runtime_archive().is_some() { + return embedded_runtime_archive_sha256(); + } + if let Some(path) = runtime_tar_path() { + if strict_asset_verification()? { + return sha256_file(&path); + } + return file_metadata_cache_key(&path); + } + bail!( + "no embedded PGlite runtime assets are available; enable the `bundled` feature or set PGLITE_OXIDE_RUNTIME_ARCHIVE" + ) +} + +#[cfg(feature = "bundled")] +fn embedded_runtime_archive_sha256() -> Result { + assets::expected_runtime_archive_sha256() +} + +#[cfg(not(feature = "bundled"))] +fn embedded_runtime_archive_sha256() -> Result { + bail!("embedded runtime archive is unavailable without the `bundled` feature") +} + +fn file_metadata_cache_key(path: &Path) -> Result { + let metadata = fs::metadata(path).with_context(|| format!("stat {}", path.display()))?; + let modified_nanos = metadata + .modified() + .ok() + .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|duration| duration.as_nanos()) + .unwrap_or_default(); + Ok(format!("external-{}-{modified_nanos}", metadata.len())) +} + +fn prepare_shared_runtime_upper_root(src_runtime: &Path, paths: &PglitePaths) -> Result<()> { + let _phase = timing::phase("runtime.mountfs_upper_root"); + let dest_runtime = paths.runtime_root(); + + { + let _phase = timing::phase("runtime.mountfs_upper_dirs"); + for path in [ + dest_runtime.to_path_buf(), + dest_runtime.join("home"), + dest_runtime.join("dev"), + ] { + fs::create_dir_all(&path).with_context(|| format!("create {}", path.display()))?; + } + } + + { + let _phase = timing::phase("runtime.mountfs_upper_reset"); + reset_dir(&dest_runtime.join("tmp"))?; + reset_dir(&dest_runtime.join("dev/shm"))?; + } + + { + let _phase = timing::phase("runtime.mountfs_upper_identity"); + copy_runtime_file_if_exists(src_runtime.join("password"), dest_runtime.join("password"))?; + } + + fs::write(dest_runtime.join(MOUNTFS_RUNTIME_MARKER), b"mountfs\n").with_context(|| { + format!( + "write {}", + dest_runtime.join(MOUNTFS_RUNTIME_MARKER).display() + ) + })?; + write_runtime_layout_manifest( + &dest_runtime, + RuntimeLayoutKind::SharedRuntimeOverlay, + &runtime_cache_key()?, + )?; + Ok(()) +} + +fn reset_dir(path: &Path) -> Result<()> { + if path.exists() { + fs::remove_dir_all(path).with_context(|| format!("remove {}", path.display()))?; + } + fs::create_dir_all(path).with_context(|| format!("create {}", path.display()))?; + Ok(()) +} + +fn copy_runtime_file_if_exists(src: PathBuf, dest: PathBuf) -> Result<()> { + if !src.exists() { + return Ok(()); + } + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; + } + if dest.exists() { + fs::remove_file(&dest).with_context(|| format!("remove {}", dest.display()))?; + } + fs::copy(&src, &dest) + .with_context(|| format!("copy {} -> {}", src.display(), dest.display()))?; + Ok(()) +} + +#[cfg(test)] +fn copy_template_pgdata(template_root: &Path, dest_root: &Path) -> Result<()> { + let source_pgdata = template_root.join("tmp/pglite/base"); + clone_pgdata_template_dir(&source_pgdata, &dest_root.join("tmp/pglite/base")) +} + +fn clone_pgdata_template_dir(source_pgdata: &Path, dest_pgdata: &Path) -> Result<()> { + if try_clone_dir(source_pgdata, dest_pgdata)? { + return Ok(()); + } + copy_pgdata_template_dir_inner(source_pgdata, dest_pgdata) } -fn copy_dir_filtered(src: &Path, dest: &Path) -> Result<()> { - fs::create_dir_all(dest).with_context(|| format!("create directory {}", dest.display()))?; +fn copy_pgdata_template_dir_inner(source_pgdata: &Path, dest_pgdata: &Path) -> Result<()> { + fs::create_dir_all(dest_pgdata) + .with_context(|| format!("create directory {}", dest_pgdata.display()))?; - for entry in fs::read_dir(src).with_context(|| format!("read directory {}", src.display()))? { - let entry = entry.with_context(|| format!("read entry under {}", src.display()))?; + for entry in fs::read_dir(source_pgdata) + .with_context(|| format!("read directory {}", source_pgdata.display()))? + { + let entry = + entry.with_context(|| format!("read entry under {}", source_pgdata.display()))?; let file_name = entry.file_name(); if should_skip_template_entry(&file_name) { continue; } let src_path = entry.path(); - let dest_path = dest.join(&file_name); + let dest_path = dest_pgdata.join(&file_name); let file_type = entry .file_type() .with_context(|| format!("stat {}", src_path.display()))?; if file_type.is_dir() { - copy_dir_filtered(&src_path, &dest_path)?; + copy_pgdata_template_dir_inner(&src_path, &dest_path)?; } else if file_type.is_file() { if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent) .with_context(|| format!("create directory {}", parent.display()))?; } - fs::copy(&src_path, &dest_path).with_context(|| { - format!("copy {} to {}", src_path.display(), dest_path.display()) - })?; + clone_mutable_template_file(&src_path, &dest_path)?; } else if file_type.is_symlink() { copy_symlink(&src_path, &dest_path)?; } @@ -700,6 +2042,98 @@ fn copy_dir_filtered(src: &Path, dest: &Path) -> Result<()> { Ok(()) } +fn clone_mutable_template_file(src: &Path, dest: &Path) -> Result<()> { + if std::env::var_os("PGLITE_OXIDE_TEMPLATE_REFLINK").is_some() && try_reflink_file(src, dest)? { + return Ok(()); + } + copy_template_file(src, dest) +} + +fn try_clone_dir(src: &Path, dest: &Path) -> Result { + if dest.exists() { + fs::remove_dir_all(dest).with_context(|| format!("remove {}", dest.display()))?; + } + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; + } + + let status = clone_dir_command(src, dest); + match status { + Ok(status) if status.success() && dest.exists() => Ok(true), + Ok(_) | Err(_) => { + if dest.exists() { + fs::remove_dir_all(dest).with_context(|| { + format!("remove failed cloned directory {}", dest.display()) + })?; + } + Ok(false) + } + } +} + +#[cfg(target_os = "linux")] +fn clone_dir_command(src: &Path, dest: &Path) -> std::io::Result { + Command::new("cp") + .arg("-a") + .arg("--reflink=auto") + .arg("--") + .arg(src) + .arg(dest) + .status() +} + +#[cfg(target_os = "macos")] +fn clone_dir_command(src: &Path, dest: &Path) -> std::io::Result { + Command::new("cp").arg("-cR").arg(src).arg(dest).status() +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn clone_dir_command(_src: &Path, _dest: &Path) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "directory clone is unsupported on this platform", + )) +} + +fn copy_template_file(src: &Path, dest: &Path) -> Result<()> { + fs::copy(src, dest).with_context(|| format!("copy {} to {}", src.display(), dest.display()))?; + Ok(()) +} + +#[cfg(target_os = "linux")] +fn try_reflink_file(src: &Path, dest: &Path) -> Result { + let status = Command::new("cp") + .arg("--reflink=always") + .arg("--") + .arg(src) + .arg(dest) + .status(); + match status { + Ok(status) if status.success() && dest.exists() => Ok(true), + Ok(_) | Err(_) => { + let _ = fs::remove_file(dest); + Ok(false) + } + } +} + +#[cfg(target_os = "macos")] +fn try_reflink_file(src: &Path, dest: &Path) -> Result { + let status = Command::new("cp").arg("-c").arg(src).arg(dest).status(); + match status { + Ok(status) if status.success() && dest.exists() => Ok(true), + Ok(_) | Err(_) => { + let _ = fs::remove_file(dest); + Ok(false) + } + } +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn try_reflink_file(_src: &Path, _dest: &Path) -> Result { + Ok(false) +} + fn should_skip_template_entry(file_name: &OsStr) -> bool { let name = file_name.to_string_lossy(); name.starts_with(".s.PGSQL.") || TEMPLATE_RUNTIME_STATE_FILES.contains(&name.as_ref()) @@ -727,7 +2161,7 @@ fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { }; if target_path.is_dir() { - copy_dir_filtered(&target_path, dest) + copy_pgdata_template_dir_inner(&target_path, dest) } else { if let Some(parent) = dest.parent() { fs::create_dir_all(parent) @@ -751,25 +2185,162 @@ mod tests { fs::write(pgdata.join("PG_VERSION"), b"17\n")?; fs::write(pgdata.join("postmaster.pid"), b"stale pid")?; fs::write(pgdata.join("postmaster.opts"), b"stale opts")?; - fs::write(source.path().join(".s.PGSQL.5432"), b"socket")?; - fs::write(source.path().join(".s.PGSQL.5432.lock"), b"lock")?; + fs::write(pgdata.join(".s.PGSQL.5432"), b"socket")?; + fs::write(pgdata.join(".s.PGSQL.5432.lock"), b"lock")?; + + let dest = TempDir::new()?; + let dest_pgdata = dest.path().join("tmp/pglite/base"); + copy_pgdata_template_dir_inner(&pgdata, &dest_pgdata)?; + + assert!( + dest_pgdata.join("PG_VERSION").exists(), + "destination entries: {}", + list_test_entries(dest.path())? + ); + assert!(!dest_pgdata.join("postmaster.pid").exists()); + assert!(!dest_pgdata.join("postmaster.opts").exists()); + assert!(!dest_pgdata.join(".s.PGSQL.5432").exists()); + assert!(!dest_pgdata.join(".s.PGSQL.5432.lock").exists()); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn template_clone_does_not_hardlink_mutable_pgdata_files() -> Result<()> { + use std::os::unix::fs::MetadataExt; + + let source = TempDir::new()?; + let pgdata = source.path().join("tmp/pglite/base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + + let dest = TempDir::new()?; + let dest_pgdata = dest.path().join("tmp/pglite/base"); + copy_pgdata_template_dir_inner(&pgdata, &dest_pgdata)?; + + let source_pg_version = pgdata.join("PG_VERSION"); + let dest_pg_version = dest_pgdata.join("PG_VERSION"); + assert!( + source_pg_version.exists(), + "source PG_VERSION should exist at {}", + source_pg_version.display() + ); + assert!( + dest_pg_version.exists(), + "cloned PG_VERSION should exist at {}; destination entries: {}", + dest_pg_version.display(), + list_test_entries(dest.path())? + ); + let source_meta = fs::metadata(&source_pg_version)?; + let dest_meta = fs::metadata(&dest_pg_version)?; + assert_ne!( + (source_meta.dev(), source_meta.ino()), + (dest_meta.dev(), dest_meta.ino()), + "mutable PGDATA template files must be copied or reflinked, not hardlinked" + ); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn fallback_template_pgdata_copy_does_not_hardlink_mutable_files() -> Result<()> { + use std::os::unix::fs::MetadataExt; + + let source = TempDir::new()?; + let pgdata = source.path().join("tmp/pglite/base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + + let dest = TempDir::new()?; + copy_template_pgdata(source.path(), dest.path())?; + + let source_pg_version = pgdata.join("PG_VERSION"); + let dest_pg_version = dest.path().join("tmp/pglite/base/PG_VERSION"); + assert!(dest_pg_version.exists()); + let source_meta = fs::metadata(&source_pg_version)?; + let dest_meta = fs::metadata(&dest_pg_version)?; + assert_ne!( + (source_meta.dev(), source_meta.ino()), + (dest_meta.dev(), dest_meta.ino()), + "fallback PGDATA template copy must not hardlink mutable files" + ); + Ok(()) + } + + #[test] + fn fallback_template_pgdata_copy_does_not_share_mutable_files() -> Result<()> { + let source = TempDir::new()?; + let pgdata = source.path().join("base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; let dest = TempDir::new()?; - copy_dir_filtered(source.path(), dest.path())?; + let cloned = dest.path().join("base"); + copy_pgdata_template_dir_inner(&pgdata, &cloned)?; + fs::write(cloned.join("PG_VERSION"), b"changed\n")?; + + assert_eq!( + fs::read(pgdata.join("PG_VERSION"))?, + b"17\n", + "fallback PGDATA template copy must not share mutable file storage with the source" + ); + Ok(()) + } + + fn list_test_entries(root: &Path) -> Result { + let mut entries = Vec::new(); + collect_test_entries(root, root, &mut entries)?; + entries.sort(); + Ok(entries.join(", ")) + } - assert!(dest.path().join("tmp/pglite/base/PG_VERSION").exists()); - assert!(!dest.path().join("tmp/pglite/base/postmaster.pid").exists()); - assert!(!dest.path().join("tmp/pglite/base/postmaster.opts").exists()); - assert!(!dest.path().join(".s.PGSQL.5432").exists()); - assert!(!dest.path().join(".s.PGSQL.5432.lock").exists()); + fn collect_test_entries(root: &Path, current: &Path, entries: &mut Vec) -> Result<()> { + for entry in fs::read_dir(current)? { + let entry = entry?; + let path = entry.path(); + let relative = path.strip_prefix(root).unwrap_or(&path); + entries.push(relative.display().to_string()); + if entry.file_type()?.is_dir() { + collect_test_entries(root, &path, entries)?; + } + } Ok(()) } + #[cfg(feature = "extensions")] #[test] fn embedded_pgdata_template_installs_valid_cluster() -> Result<()> { + if !embedded_pgdata_template_is_available() { + return Ok(()); + } + + let temp_dir = TempDir::new()?; + let paths = PglitePaths::with_root(temp_dir.path()); + ensure_full_runtime(&paths)?; + + let (module_path, _) = + locate_runtime_module(&paths).context("runtime module should be installed")?; + assert!(try_install_embedded_pgdata_template(&paths, &module_path)?); + + assert!(paths.pgdata.join("PG_VERSION").exists()); + assert!(paths.pgdata.join("global/pg_control").exists()); + assert!(!paths.pgdata.join("postmaster.pid").exists()); + Ok(()) + } + + #[cfg(feature = "extensions")] + #[test] + fn embedded_pgdata_template_replaces_interrupted_pgdata() -> Result<()> { + if !embedded_pgdata_template_is_available() { + return Ok(()); + } + let temp_dir = TempDir::new()?; let paths = PglitePaths::with_root(temp_dir.path()); - ensure_runtime(&paths)?; + ensure_full_runtime(&paths)?; + fs::create_dir_all(paths.pgdata.join("global"))?; + fs::write(paths.pgdata.join("postmaster.pid"), b"stale pid")?; + fs::write(paths.pgdata.join("base.tmp"), b"interrupted initdb")?; let (module_path, _) = locate_runtime_module(&paths).context("runtime module should be installed")?; @@ -778,6 +2349,77 @@ mod tests { assert!(paths.pgdata.join("PG_VERSION").exists()); assert!(paths.pgdata.join("global/pg_control").exists()); assert!(!paths.pgdata.join("postmaster.pid").exists()); + assert!(!paths.pgdata.join("base.tmp").exists()); + Ok(()) + } + + #[cfg(feature = "extensions")] + fn embedded_pgdata_template_is_available() -> bool { + assets::pgdata_template_archive().is_some() && assets::pgdata_template_manifest().is_some() + } + + #[cfg(feature = "extensions")] + #[test] + fn fresh_initdb_removes_interrupted_pgdata() -> Result<()> { + if assets::runtime_archive().is_none() { + return Ok(()); + } + let temp_dir = TempDir::new()?; + let paths = PglitePaths::with_root(temp_dir.path()); + fs::create_dir_all(&paths.pgdata)?; + fs::write(paths.pgdata.join("postmaster.pid"), b"stale pid")?; + fs::write(paths.pgdata.join("partial"), b"interrupted initdb")?; + + match prepare_database_root(paths.clone(), RootPrepareOptions::fresh()) { + Ok(_) => assert!(cluster_is_complete(&paths)), + Err(err) => assert!( + format!("{err:#}").contains("split WASIX initdb module is not installed"), + "unexpected fresh initdb error: {err:#}" + ), + } + assert!(!paths.pgdata.join("postmaster.pid").exists()); + assert!(!paths.pgdata.join("partial").exists()); + Ok(()) + } + + #[cfg(feature = "extensions")] + #[test] + fn fresh_initdb_removes_incomplete_pgdata_even_with_pg_version() -> Result<()> { + if assets::runtime_archive().is_none() { + return Ok(()); + } + let temp_dir = TempDir::new()?; + let paths = PglitePaths::with_root(temp_dir.path()); + fs::create_dir_all(&paths.pgdata)?; + fs::write(paths.pgdata.join("PG_VERSION"), b"17\n")?; + fs::write( + paths.pgdata.join("partial-bootstrap.sql"), + b"interrupted initdb", + )?; + + match prepare_database_root(paths.clone(), RootPrepareOptions::fresh()) { + Ok(_) => assert!(cluster_is_complete(&paths)), + Err(err) => assert!( + format!("{err:#}").contains("split WASIX initdb module is not installed"), + "unexpected fresh initdb error: {err:#}" + ), + } + assert!(!paths.pgdata.join("partial-bootstrap.sql").exists()); + Ok(()) + } + + #[test] + fn root_lock_is_exclusive_until_dropped() -> Result<()> { + let temp_dir = TempDir::new()?; + let first = RootLock::acquire(temp_dir.path())?; + assert!(temp_dir.path().join(".pglite-oxide.lock").exists()); + + let err = + RootLock::acquire(temp_dir.path()).expect_err("second root lock should be rejected"); + assert!(format!("{err:#}").contains("PGlite root is already in use")); + + drop(first); + let _second = RootLock::acquire(temp_dir.path())?; Ok(()) } @@ -787,4 +2429,90 @@ mod tests { .expect_err("parent components must be rejected"); assert!(err.to_string().contains("unsafe archive path")); } + + fn tar_bytes_with_entry(path: &[u8], entry_type: u8, body: &[u8], link_name: &[u8]) -> Vec { + let mut header = [0u8; 512]; + header[..path.len()].copy_from_slice(path); + header[100..108].copy_from_slice(b"0000644\0"); + header[108..116].copy_from_slice(b"0000000\0"); + header[116..124].copy_from_slice(b"0000000\0"); + header[124..136].copy_from_slice(format!("{:011o}\0", body.len()).as_bytes()); + header[136..148].copy_from_slice(b"00000000000\0"); + header[148..156].fill(b' '); + header[156] = entry_type; + if !link_name.is_empty() { + header[157..157 + link_name.len()].copy_from_slice(link_name); + } + header[257..263].copy_from_slice(b"ustar\0"); + header[263..265].copy_from_slice(b"00"); + + let checksum: u32 = header.iter().map(|byte| *byte as u32).sum(); + header[148..156].copy_from_slice(format!("{checksum:06o}\0 ").as_bytes()); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&header); + bytes.extend_from_slice(body); + let padding = (512 - (body.len() % 512)) % 512; + bytes.resize(bytes.len() + padding, 0); + bytes.resize(bytes.len() + 1024, 0); + bytes + } + + #[test] + fn extension_archive_rejects_parent_components() -> Result<()> { + let bytes = tar_bytes_with_entry(b"../escape", b'0', b"nope", b""); + let temp_dir = TempDir::new()?; + let paths = PglitePaths::with_root(temp_dir.path()); + let err = install_extension_bytes(&paths, &bytes).expect_err("unsafe archive must fail"); + assert!(err.to_string().contains("unpack extension")); + Ok(()) + } + + #[test] + fn extension_archive_rejects_symlink_entries() -> Result<()> { + let bytes = tar_bytes_with_entry( + b"lib/postgresql/vector.so", + b'2', + b"", + b"/tmp/attacker-owned-vector.so", + ); + let temp_dir = TempDir::new()?; + let paths = PglitePaths::with_root(temp_dir.path()); + let err = install_extension_bytes(&paths, &bytes).expect_err("symlink archive must fail"); + assert!( + err.chain() + .any(|cause| cause.to_string().contains("unsupported type")), + "{err:#}" + ); + Ok(()) + } + + #[cfg(feature = "extensions")] + #[test] + fn embedded_runtime_archive_hash_is_validated() -> Result<()> { + let mut bytes = assets::runtime_archive() + .expect("embedded runtime archive") + .to_vec(); + bytes[0] ^= 0xff; + let err = validate_embedded_runtime_archive_strict(&bytes) + .expect_err("corrupted runtime archive hash must fail"); + assert!(err.to_string().contains("runtime archive hash mismatch")); + Ok(()) + } + + #[cfg(feature = "extensions")] + #[test] + fn bundled_extension_archive_hash_is_validated() -> Result<()> { + let mut bytes = assets::extension_archive("vector") + .expect("embedded vector archive") + .to_vec(); + bytes[0] ^= 0xff; + let err = validate_bundled_extension_archive_strict("vector", &bytes) + .expect_err("corrupted extension archive hash must fail"); + assert!( + err.to_string() + .contains("extension archive 'vector' hash mismatch") + ); + Ok(()) + } } diff --git a/src/pglite/builder.rs b/src/pglite/builder.rs index 5f830a86..f9ed32a9 100644 --- a/src/pglite/builder.rs +++ b/src/pglite/builder.rs @@ -1,16 +1,24 @@ use std::path::PathBuf; use anyhow::{Result, bail}; -use tempfile::TempDir; -use crate::pglite::base::{install_default, install_into, install_temporary_from_template}; +use crate::pglite::base::{PreparedRoot, RootPlan, RootSource, RootTarget, prepare_root}; use crate::pglite::client::Pglite; +use crate::pglite::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use crate::pglite::extensions::{Extension, resolve_extension_set}; +use crate::pglite::interface::DebugLevel; /// Builder for opening persistent or temporary [`Pglite`] databases. #[derive(Debug, Clone)] pub struct PgliteBuilder { target: Option, template_cache: bool, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + load_data_dir_archive: Option>, + #[cfg(feature = "extensions")] + extensions: Vec, } #[derive(Debug, Clone)] @@ -29,6 +37,11 @@ impl Default for PgliteBuilder { Self { target: None, template_cache: true, + postgres_config: PostgresConfig::default(), + startup_config: StartupConfig::default(), + load_data_dir_archive: None, + #[cfg(feature = "extensions")] + extensions: Vec::new(), } } } @@ -75,53 +88,182 @@ impl PgliteBuilder { self } - /// Control whether temporary databases are cloned from the process-local - /// template cluster cache. + /// Control whether new databases are cloned from the process-local or + /// embedded PGDATA template cache. pub fn template_cache(mut self, enabled: bool) -> Self { self.template_cache = enabled; self } /// Open an ephemeral database with a fresh `initdb`. + /// + /// This is a compatibility alias for + /// `temporary().template_cache(false)`. Fresh initdb uses the bundled split + /// WASIX `initdb` module; cached temporary databases remain the production + /// fast path. pub fn fresh_temporary(self) -> Self { self.temporary().template_cache(false) } + /// Set a PostgreSQL startup GUC for this embedded backend. + pub fn postgres_config(mut self, name: impl Into, value: impl Into) -> Self { + self.postgres_config.insert(name, value); + self + } + + /// Set multiple PostgreSQL startup GUCs for this embedded backend. + pub fn postgres_configs(mut self, settings: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { + for (name, value) in settings { + self.postgres_config.insert(name, value); + } + self + } + + /// Connect as a PostgreSQL role. The role must already exist in the + /// cluster. + pub fn username(mut self, username: impl Into) -> Self { + self.startup_config.username = username.into(); + self + } + + /// Connect to a PostgreSQL database. The database must already exist in the + /// cluster. + pub fn database(mut self, database: impl Into) -> Self { + self.startup_config.database = database.into(); + self + } + + /// Enable PostgreSQL debug logging level `0..=5` for the embedded backend. + pub fn debug_level(mut self, level: DebugLevel) -> Self { + self.startup_config.debug_level = Some(level); + self + } + + /// Use lower durability settings for ephemeral or cacheable local + /// workloads. + pub fn relaxed_durability(mut self, enabled: bool) -> Self { + self.startup_config.relaxed_durability = enabled; + self + } + + /// Append an advanced PostgreSQL startup argument. Prefer + /// [`postgres_config`](Self::postgres_config) for GUCs. + pub fn startup_arg(mut self, arg: impl Into) -> Self { + self.startup_config.extra_args.push(arg.into()); + self + } + + /// Append advanced PostgreSQL startup arguments. + pub fn startup_args(mut self, args: impl IntoIterator>) -> Self { + self.startup_config + .extra_args + .extend(args.into_iter().map(Into::into)); + self + } + + /// Load a previously dumped PGDATA tar archive before opening the database. + pub fn load_data_dir_archive(mut self, archive: impl Into>) -> Self { + self.load_data_dir_archive = Some(archive.into()); + self + } + + /// Enable a bundled Postgres extension before returning the database. + #[cfg(feature = "extensions")] + pub fn extension(mut self, extension: Extension) -> Self { + self.extensions.push(extension); + self + } + + /// Enable bundled Postgres extensions before returning the database. + #[cfg(feature = "extensions")] + pub fn extensions(mut self, extensions: impl IntoIterator) -> Self { + self.extensions.extend(extensions); + self + } + /// Install, initialize, and start the selected database. pub fn open(self) -> Result { - match self.target { - Some(PgliteTarget::Path(root)) => { - let outcome = install_into(&root)?; - Pglite::new(outcome.paths) - } + self.postgres_config.validate()?; + self.startup_config.validate()?; + let target = match self.target.clone() { + Some(PgliteTarget::Path(root)) => RootTarget::Path(root), Some(PgliteTarget::AppId { qualifier, organization, application, - }) => { - let outcome = install_default((&qualifier, &organization, &application))?; - Pglite::new(outcome.paths) - } - Some(PgliteTarget::Temporary) => self.open_temporary(), + }) => RootTarget::AppId { + qualifier, + organization, + application, + }, + Some(PgliteTarget::Temporary) => RootTarget::Temporary, None => { bail!( "PgliteBuilder target is not set; call path, app_id, or temporary before open" ) } - } - } - - fn open_temporary(self) -> Result { - let (temp_dir, outcome) = if self.template_cache { - install_temporary_from_template()? + }; + let source = if let Some(archive) = self.load_data_dir_archive.clone() { + RootSource::DataDirArchive(archive) + } else if self.template_cache { + RootSource::Template } else { - let temp_dir = TempDir::new()?; - let outcome = install_into(temp_dir.path())?; - (temp_dir, outcome) + RootSource::FreshInitdb }; + #[cfg(feature = "extensions")] + let extensions = resolve_extension_set(&self.extensions)?; + let plan = RootPlan::new(target, source); + #[cfg(feature = "extensions")] + let plan = plan.with_extensions(extensions.clone(), self.postgres_config.clone()); + let prepared = prepare_root(plan)?; + #[cfg(feature = "extensions")] + { + self.open_prepared_root(prepared, extensions) + } + #[cfg(not(feature = "extensions"))] + { + self.open_prepared_root(prepared) + } + } - let mut instance = Pglite::new(outcome.paths)?; - instance.attach_temp_dir(temp_dir); + fn open_prepared_root( + self, + prepared: PreparedRoot, + #[cfg(feature = "extensions")] extensions: Vec, + ) -> Result { + let PreparedRoot { + temp_dir, + root_lock, + outcome, + .. + } = prepared; + #[cfg(feature = "extensions")] + let preinstalled_extensions = outcome.preinstalled_extensions.clone(); + let mut instance = + Pglite::new_prepared_with_config(outcome, self.postgres_config, self.startup_config)?; + if let Some(lock) = root_lock { + instance.attach_root_lock(lock); + } + if let Some(temp_dir) = temp_dir { + instance.attach_temp_dir(temp_dir); + } + #[cfg(feature = "extensions")] + let mut instance = instance; + #[cfg(feature = "extensions")] + for extension in extensions { + if preinstalled_extensions + .iter() + .any(|sql_name| sql_name == extension.sql_name()) + { + instance.enable_preinstalled_extension(extension)?; + } else { + instance.enable_extension(extension)?; + } + } Ok(instance) } } diff --git a/src/pglite/client.rs b/src/pglite/client.rs index b29a6547..4c7b7cb1 100644 --- a/src/pglite/client.rs +++ b/src/pglite/client.rs @@ -1,27 +1,51 @@ use anyhow::{Context, Result, anyhow, bail}; use serde_json::Value; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::io; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tempfile::TempDir; - -use crate::pglite::base::PglitePaths; +#[cfg(feature = "extensions")] +use tokio::io::{AsyncWrite, AsyncWriteExt}; +#[cfg(feature = "extensions")] +use tokio::runtime::Runtime; +#[cfg(feature = "extensions")] +use wasmer_wasix::virtual_net::VirtualTcpSocket; +#[cfg(feature = "extensions")] +use wasmer_wasix::virtual_net::tcp_pair::TcpSocketHalfRx; + +use crate::pglite::aot; +#[cfg(feature = "extensions")] +use crate::pglite::assets; +use crate::pglite::backend::{BackendOpenKind, BackendSession}; +#[cfg(feature = "extensions")] +use crate::pglite::base::install_bundled_extension_bytes; +use crate::pglite::base::{InstallOutcome, PglitePaths, RootLock}; use crate::pglite::builder::PgliteBuilder; +use crate::pglite::config::{PostgresConfig, StartupConfig}; +use crate::pglite::data_dir::{DataDirArchiveFormat, dump_pgdata_archive}; use crate::pglite::errors::PgliteError; +#[cfg(feature = "extensions")] +use crate::pglite::extensions::{ + Extension, by_sql_name, extension_session_setup_sql, extension_setup_sql, resolve_extension_set, +}; use crate::pglite::interface::{ DataTransferContainer, DescribeQueryParam, DescribeQueryResult, DescribeResultField, - ExecProtocolOptions, ExecProtocolResult, ParserMap, QueryOptions, Results, Serializer, - SerializerMap, TypeParser, + ExecProtocolOptions, ExecProtocolResult, ParserMap, QueryOptions, Results, SerializerMap, }; use crate::pglite::parse::{parse_describe_statement_results, parse_results}; +#[cfg(feature = "extensions")] +use crate::pglite::pg_dump::{PgDumpOptions, PgDumpVirtualSocket, dump_direct_sql}; +#[cfg(feature = "extensions")] use crate::pglite::postgres_mod::PostgresMod; -use crate::pglite::transport::Transport; +use crate::pglite::timing; use crate::pglite::types::{ - DEFAULT_PARSERS, DEFAULT_SERIALIZERS, TEXT, parse_array_text, serialize_array_value, + ArrayTypeInfo, DEFAULT_PARSERS, DEFAULT_SERIALIZERS, TEXT, register_array_type, }; +#[cfg(feature = "extensions")] +use crate::pglite::wire::{FrontendFrameKind, FrontendFrameReader, classify_frontend_message}; use crate::protocol::messages::{BackendMessage, DatabaseError}; use crate::protocol::parser::Parser as ProtocolParser; use crate::protocol::serializer::{BindConfig, BindValue, PortalTarget, Serialize}; @@ -69,13 +93,13 @@ struct GlobalListener { /// Primary entry point for interacting with the embedded Postgres runtime. pub struct Pglite { - pg: PostgresMod, + backend: BackendSession, _temp_dir: Option, - transport: Transport, + _root_lock: Option, parser: ProtocolParser, serializers: SerializerMap, parsers: ParserMap, - array_types_initialized: bool, + array_type_lookup_misses: HashSet, in_transaction: bool, ready: bool, closing: bool, @@ -108,37 +132,162 @@ impl Pglite { Self::builder().temporary().open() } + /// Warm the runtime module and bundled AOT artifact cache without opening a database. + pub fn preload() -> Result<()> { + let (temp_dir, paths) = { + let _phase = timing::phase("preload.tempdir"); + PglitePaths::with_temp_dir()? + }; + { + let _phase = timing::phase("preload.runtime_module"); + crate::pglite::base::preload_runtime_module(&paths)?; + } + { + let _phase = timing::phase("preload.aot_runtime"); + aot::preload_runtime_artifact()?; + } + drop(temp_dir); + Ok(()) + } + + /// Warm bundled extension artifacts without permanently opening a database. + #[cfg(feature = "extensions")] + pub fn preload_extensions(extensions: impl IntoIterator) -> Result<()> { + Self::preload()?; + let extensions = extensions.into_iter().collect::>(); + for extension in resolve_extension_set(&extensions)? { + let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { + anyhow!( + "extension asset '{}' is not bundled in this pglite-oxide build", + extension.sql_name() + ) + })?; + let (temp_dir, paths) = { + let _phase = timing::phase("preload.extension_tempdir"); + PglitePaths::with_temp_dir()? + }; + { + let _phase = timing::phase("preload.extension_runtime_module"); + crate::pglite::base::preload_runtime_module(&paths)?; + } + { + let _phase = timing::phase("preload.extension_archive_install"); + install_bundled_extension_bytes(&paths, extension.sql_name(), bytes)?; + } + { + let _phase = timing::phase("preload.extension_side_module"); + PostgresMod::preload_extension_module_from_paths(&paths, extension)?; + } + { + let _phase = timing::phase("preload.extension_aot"); + aot::preload_extension_artifact(extension)?; + } + drop(temp_dir); + } + Ok(()) + } + /// Create a new Pglite instance backed by the provided runtime paths. #[doc(hidden)] pub fn new(paths: PglitePaths) -> Result { - let mut pg = PostgresMod::new(paths)?; - pg.ensure_cluster()?; - let transport = Transport::prepare(&mut pg)?; - - let mut instance = Self { - pg, - _temp_dir: None, - transport, - parser: ProtocolParser::new(), - serializers: DEFAULT_SERIALIZERS.clone(), - parsers: DEFAULT_PARSERS.clone(), - array_types_initialized: false, - in_transaction: false, - ready: true, - closing: false, - closed: false, - blob_input_provided: false, - notify_listeners: HashMap::new(), - global_notify_listeners: Vec::new(), - next_listener_id: 1, - next_global_listener_id: 1, + let outcome = crate::pglite::base::prepare_database_root( + paths, + crate::pglite::base::RootPrepareOptions::template(), + )?; + Self::new_prepared(outcome) + } + + pub(crate) fn new_prepared(outcome: InstallOutcome) -> Result { + Self::new_prepared_with_config(outcome, PostgresConfig::default(), StartupConfig::default()) + } + + pub(crate) fn new_prepared_with_config( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + ) -> Result { + let _phase = timing::phase("pglite.open"); + let session_startup_config = startup_config.clone(); + let backend = BackendSession::open( + outcome, + postgres_config, + startup_config, + BackendOpenKind::Direct, + )?; + + let mut instance = { + let _phase = timing::phase("pglite.client_struct_init"); + Self { + backend, + _temp_dir: None, + _root_lock: None, + parser: ProtocolParser::new(), + serializers: DEFAULT_SERIALIZERS.clone(), + parsers: DEFAULT_PARSERS.clone(), + array_type_lookup_misses: HashSet::new(), + in_transaction: false, + ready: true, + closing: false, + closed: false, + blob_input_provided: false, + notify_listeners: HashMap::new(), + global_notify_listeners: Vec::new(), + next_listener_id: 1, + next_global_listener_id: 1, + } }; - instance.exec_internal("SET search_path TO public;", None)?; - instance.init_array_types(true)?; + if session_startup_config.username != "postgres" { + let sql = format!( + "SET ROLE {}", + crate::pglite::templating::quote_identifier(&session_startup_config.username) + ); + instance + .exec(&sql, None) + .with_context(|| format!("set startup role {}", session_startup_config.username))?; + } + Ok(instance) } + /// Install and enable a bundled Postgres extension. + #[cfg(feature = "extensions")] + pub fn enable_extension(&mut self, extension: Extension) -> Result<()> { + let _phase = timing::phase("extension.enable"); + let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { + anyhow!( + "extension asset '{}' is not bundled in this pglite-oxide build", + extension.sql_name() + ) + })?; + install_bundled_extension_bytes(self.paths(), extension.sql_name(), bytes)?; + self.backend.preload_extension_module(extension)?; + for sql in extension_setup_sql(extension) { + self.exec(&sql, None)?; + } + Ok(()) + } + + #[cfg(feature = "extensions")] + pub(crate) fn enable_preinstalled_extension(&mut self, extension: Extension) -> Result<()> { + let _phase = timing::phase("extension.enable_preinstalled"); + self.backend.preload_installed_extension(extension)?; + for sql in extension_session_setup_sql(extension) { + self.exec(&sql, None)?; + } + Ok(()) + } + + /// Refresh direct API array parser and serializer registrations. + /// + /// This mirrors upstream PGlite's `refreshArrayTypes()` escape hatch. Most + /// applications should not need it because built-in arrays are registered + /// statically and runtime custom arrays are discovered lazily when possible. + pub fn refresh_array_types(&mut self) -> Result<()> { + self.check_ready()?; + self.refresh_array_types_internal() + } + /// Execute a SQL query using the extended protocol. pub fn query( &mut self, @@ -147,7 +296,6 @@ impl Pglite { options: Option<&QueryOptions>, ) -> Result { self.check_ready()?; - self.init_array_types(false)?; self.query_internal(sql, params, options) } @@ -178,48 +326,45 @@ impl Pglite { &query_opts.param_types }; - let parse_msg = Serialize::parse(None, sql, param_types); - let ExecProtocolResult { messages } = - self.exec_protocol(&parse_msg, exec_opts.clone())?; - collected_messages.extend(messages); - - let describe_msg = Serialize::describe(&PortalTarget::new('S', None)); - let ExecProtocolResult { messages } = - self.exec_protocol(&describe_msg, exec_opts.clone())?; - let data_type_ids = parse_describe_statement_results(&messages); + let mut messages = { + let _phase = timing::phase("client.query.parse_describe"); + self.parse_and_describe(sql, param_types, exec_opts.clone())? + }; + let mut data_type_ids = parse_describe_statement_results(&messages); + if self.ensure_array_types_for_bind_values(params, &data_type_ids, query_opts)? { + messages = { + let _phase = timing::phase("client.query.parse_describe_after_array_register"); + self.parse_and_describe(sql, param_types, exec_opts.clone())? + }; + data_type_ids = parse_describe_statement_results(&messages); + } collected_messages.extend(messages); - - let bind_values = self.prepare_bind_values(params, &data_type_ids, query_opts)?; + let bind_values = { + let _phase = timing::phase("client.query.prepare_bind_values"); + self.prepare_bind_values(params, &data_type_ids, query_opts)? + }; let bind_config = BindConfig { values: bind_values, ..Default::default() }; - let bind_msg = Serialize::bind(&bind_config); - let ExecProtocolResult { messages } = - self.exec_protocol(&bind_msg, exec_opts.clone())?; - collected_messages.extend(messages); - - let describe_portal = Serialize::describe(&PortalTarget::new('P', None)); - let ExecProtocolResult { messages } = - self.exec_protocol(&describe_portal, exec_opts.clone())?; - collected_messages.extend(messages); - - let exec_msg = Serialize::execute(None); - let ExecProtocolResult { messages } = - self.exec_protocol(&exec_msg, exec_opts.clone())?; + let execute_batch = { + let _phase = timing::phase("client.query.serialize_execute"); + let mut execute_batch = Vec::new(); + execute_batch.extend(Serialize::bind(&bind_config)); + execute_batch.extend(Serialize::describe(&PortalTarget::new('P', None))); + execute_batch.extend(Serialize::execute(None)); + execute_batch.extend(Serialize::sync()); + execute_batch + }; + let ExecProtocolResult { messages, .. } = { + let _phase = timing::phase("client.query.execute_roundtrip"); + self.exec_protocol(&execute_batch, exec_opts.clone())? + }; collected_messages.extend(messages); Ok(()) })(); - match self.exec_protocol(&Serialize::sync(), exec_opts.clone()) { - Ok(ExecProtocolResult { messages }) => collected_messages.extend(messages), - Err(err) if result.is_ok() => { - return Err(err.context(format!("failed to synchronize extended query: {sql}"))); - } - Err(_) => {} - } - if let Err(err) = result { match err.downcast::() { Ok(db_err) => { @@ -232,7 +377,10 @@ impl Pglite { } } - self.finish_query(collected_messages, options) + { + let _phase = timing::phase("client.query.finish"); + self.finish_query(collected_messages, options) + } } /// Return `true` if the instance is ready for new work. @@ -243,13 +391,236 @@ impl Pglite { /// Return the host-side runtime and data-directory paths backing this instance. #[doc(hidden)] pub fn paths(&self) -> &PglitePaths { - self.pg.paths() + self.backend.paths() + } + + /// Return debug-build bridge allocation/free counters for ownership tests. + #[doc(hidden)] + #[cfg(debug_assertions)] + pub fn guest_bridge_allocation_counts(&self) -> (u64, u64) { + self.backend.guest_bridge_allocation_counts() + } + + /// Dump the physical PGDATA directory to a gzipped tar archive. + /// + /// The archive is intended to be loaded back into pglite-oxide/PGlite with + /// the same PostgreSQL/PGlite version. Use [`dump_sql`](Self::dump_sql) for + /// logical backups across versions. + pub fn dump_data_dir(&mut self) -> Result> { + self.dump_data_dir_with_format(DataDirArchiveFormat::TarGz) + } + + /// Dump the physical PGDATA directory with the selected archive format. + pub fn dump_data_dir_with_format(&mut self, format: DataDirArchiveFormat) -> Result> { + self.check_ready()?; + self.archive_quiesced_pgdata("dump PGDATA archive", format) + } + + /// Clone this database into a new temporary [`Pglite`] instance. + pub fn try_clone(&mut self) -> Result { + #[cfg(feature = "extensions")] + let extensions = self.bundled_extensions_in_database()?; + let archive = self.dump_data_dir_with_format(DataDirArchiveFormat::Tar)?; + let builder = Self::builder().temporary().load_data_dir_archive(archive); + #[cfg(feature = "extensions")] + let builder = builder.extensions(extensions); + builder.open() + } + + /// Run the bundled WASIX `pg_dump` against this database and return SQL text. + #[cfg(feature = "extensions")] + pub fn dump_sql(&mut self, options: PgDumpOptions) -> Result { + self.check_ready()?; + options.validate()?; + self.checkpoint_backend_for_physical_snapshot("direct pg_dump")?; + self.dump_sql_via_direct_protocol(&options) + } + + /// Run the bundled WASIX `pg_dump` and return UTF-8 SQL bytes. + #[cfg(feature = "extensions")] + pub fn dump_bytes(&mut self, options: PgDumpOptions) -> Result> { + Ok(self.dump_sql(options)?.into_bytes()) + } + + fn checkpoint_backend_for_physical_snapshot(&mut self, operation: &'static str) -> Result<()> { + if self.in_transaction { + bail!("{operation} cannot run while a direct transaction is active"); + } + self.exec("CHECKPOINT", None) + .with_context(|| format!("checkpoint before {operation}"))?; + Ok(()) + } + + fn archive_quiesced_pgdata( + &mut self, + operation: &'static str, + format: DataDirArchiveFormat, + ) -> Result> { + self.checkpoint_backend_for_physical_snapshot(operation)?; + self.backend + .shutdown() + .with_context(|| format!("quiesce backend before {operation}"))?; + + let archive = dump_pgdata_archive( + &self.backend.paths().pgdata, + self.backend.pgdata_template_root(), + format, + ) + .with_context(|| format!("materialize physical PGDATA archive for {operation}")); + let restart = self + .backend + .restart() + .and_then(|_| self.restore_session_state_after_backend_restart()) + .with_context(|| format!("restart backend after {operation}")); + + match (archive, restart) { + (Ok(archive), Ok(())) => Ok(archive), + (Err(err), Ok(())) => Err(err), + (Ok(_), Err(err)) => { + self.ready = false; + self.closed = true; + Err(err) + } + (Err(err), Err(restart_err)) => { + self.ready = false; + self.closed = true; + Err(err.context(format!( + "backend restart after failed {operation} also failed: {restart_err:#}" + ))) + } + } + } + + fn restore_session_state_after_backend_restart(&mut self) -> Result<()> { + let username = self.backend.startup_config().username.clone(); + if username != "postgres" { + let sql = format!( + "SET ROLE {}", + crate::pglite::templating::quote_identifier(&username) + ); + self.exec(&sql, None).with_context(|| { + format!("restore startup role {username} after backend restart") + })?; + } + + let channels = self + .notify_listeners + .iter() + .filter(|(_, listeners)| !listeners.is_empty()) + .map(|(channel, _)| channel.clone()) + .collect::>(); + for channel in channels { + let quoted_channel = crate::pglite::templating::quote_identifier(&channel); + self.exec_internal(&format!("LISTEN {quoted_channel}"), None) + .with_context(|| format!("restore LISTEN {channel} after backend restart"))?; + } + Ok(()) + } + + #[cfg(feature = "extensions")] + fn dump_sql_via_direct_protocol(&mut self, options: &PgDumpOptions) -> Result { + ensure_direct_pg_dump_options_match_session(self.backend.startup_config(), options)?; + let result = dump_direct_sql(options, |socket| self.serve_direct_pg_dump_protocol(socket)); + let cleanup_result = self.cleanup_after_direct_pg_dump_session(); + + match (result, cleanup_result) { + (Ok(sql), Ok(())) => Ok(sql), + (Err(err), Ok(())) => Err(err), + (Ok(_), Err(err)) => Err(err), + (Err(err), Err(cleanup_err)) => Err(err.context(format!( + "direct pg_dump cleanup also failed: {cleanup_err:#}" + ))), + } + } + + #[cfg(feature = "extensions")] + fn cleanup_after_direct_pg_dump_session(&mut self) -> Result<()> { + self.exec("DEALLOCATE ALL; SET search_path TO DEFAULT;", None) + .context("reset direct pg_dump session state")?; + Ok(()) + } + + #[cfg(feature = "extensions")] + fn serve_direct_pg_dump_protocol(&mut self, mut socket: PgDumpVirtualSocket) -> Result<()> { + let _ = socket.set_nodelay(true); + let (mut socket_tx, mut socket_rx) = socket.split(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("create direct pg_dump virtual socket runtime")?; + let mut reader = FrontendFrameReader::default(); + let mut buffer = [0u8; 64 * 1024]; + loop { + let read = read_direct_pg_dump_socket(&runtime, &mut socket_rx, &mut buffer) + .context("read direct pg_dump protocol socket")?; + if read == 0 { + return Ok(()); + } + for message in reader.push(&buffer[..read])? { + match classify_frontend_message(&message)? { + FrontendFrameKind::SslOrGssRequest => { + write_direct_pg_dump_socket(&runtime, &mut socket_tx, b"N") + .context("write direct pg_dump SSL refusal")?; + } + FrontendFrameKind::CancelRequest | FrontendFrameKind::Terminate => { + return Ok(()); + } + FrontendFrameKind::Startup => { + if let Some(response) = self.backend.existing_startup_response() { + write_direct_pg_dump_socket(&runtime, &mut socket_tx, &response) + .context("write direct pg_dump existing startup response")?; + } else { + let response = self.backend.startup_with_packet(&message)?; + write_direct_pg_dump_socket(&runtime, &mut socket_tx, &response.output) + .context("write direct pg_dump startup response")?; + if !response.accepted { + return Ok(()); + } + } + } + FrontendFrameKind::Protocol => { + self.exec_protocol_raw_stream( + &message, + ExecProtocolOptions::no_sync(), + |chunk| { + write_direct_pg_dump_socket(&runtime, &mut socket_tx, chunk) + .context("write direct pg_dump backend protocol chunk")?; + Ok(()) + }, + )?; + } + } + } + flush_direct_pg_dump_socket(&runtime, &mut socket_tx) + .context("flush direct pg_dump socket")?; + } + } + + #[cfg(feature = "extensions")] + fn bundled_extensions_in_database(&mut self) -> Result> { + let results = self.query( + "SELECT extname FROM pg_catalog.pg_extension ORDER BY extname", + &[], + None, + )?; + let extensions = results + .rows + .iter() + .filter_map(|row| row.get("extname")) + .filter_map(|value| value.as_str()) + .filter_map(by_sql_name) + .collect(); + Ok(extensions) } pub(crate) fn attach_temp_dir(&mut self, temp_dir: TempDir) { self._temp_dir = Some(temp_dir); } + pub(crate) fn attach_root_lock(&mut self, root_lock: RootLock) { + self._root_lock = Some(root_lock); + } + /// Return `true` if the instance has already been closed. pub fn is_closed(&self) -> bool { self.closed @@ -257,6 +628,10 @@ impl Pglite { /// Shut down the embedded Postgres runtime. pub fn close(&mut self) -> Result<()> { + self.close_backend() + } + + fn close_backend(&mut self) -> Result<()> { if self.closed { return Ok(()); } @@ -265,17 +640,10 @@ impl Pglite { } self.closing = true; - let result = { - let options = ExecProtocolOptions { - throw_on_error: false, - sync_to_fs: false, - ..ExecProtocolOptions::default() - }; - - let end_message = Serialize::end(); - let _ = self.exec_protocol(&end_message, options); + let result = (|| { + self.backend.shutdown()?; self.sync_to_fs() - }; + })(); self.closing = false; if result.is_ok() { @@ -283,14 +651,19 @@ impl Pglite { self.ready = false; self.notify_listeners.clear(); self.global_notify_listeners.clear(); + self._root_lock = None; } result } + #[cfg(feature = "extensions")] + pub(crate) fn close_for_template_cache(&mut self) -> Result<()> { + self.close_backend() + } + /// Execute a simple SQL statement that may contain multiple commands. pub fn exec(&mut self, sql: &str, options: Option<&QueryOptions>) -> Result> { self.check_ready()?; - self.init_array_types(false)?; self.exec_internal(sql, options) } @@ -307,24 +680,10 @@ impl Pglite { let mut collected_messages: Vec = Vec::new(); - let result: Result<()> = (|| { - let message = Serialize::query(sql); - let ExecProtocolResult { messages } = - self.exec_protocol(&message, exec_opts.clone())?; - collected_messages.extend(messages); - Ok(()) - })(); - - match self.exec_protocol(&Serialize::sync(), exec_opts.clone()) { - Ok(ExecProtocolResult { messages }) => collected_messages.extend(messages), - Err(err) if result.is_ok() => { - return Err(err.context(format!("failed to synchronize simple query: {sql}"))); - } - Err(_) => {} - } - - if let Err(err) = result { - match err.downcast::() { + let message = Serialize::query(sql); + let ExecProtocolResult { messages, .. } = match self.exec_protocol(&message, exec_opts) { + Ok(result) => result, + Err(err) => match err.downcast::() { Ok(db_err) => { let enriched = PgliteError::new(db_err, sql, Vec::new(), options_snapshot); return Err(enriched.into()); @@ -332,8 +691,9 @@ impl Pglite { Err(err) => { return Err(err.context(format!("failed to execute simple query: {sql}"))); } - } - } + }, + }; + collected_messages.extend(messages); self.finish_exec(collected_messages, options) } @@ -344,16 +704,16 @@ impl Pglite { F: Fn(&str) + Send + Sync + 'static, { self.check_ready()?; - self.init_array_types(false)?; - let normalized = to_postgres_name(channel); + let quoted_channel = crate::pglite::templating::quote_identifier(channel); + let normalized = channel.to_string(); let should_listen = match self.notify_listeners.get(&normalized) { Some(existing) => existing.is_empty(), None => true, }; if should_listen { - self.exec_internal(&format!("LISTEN {}", channel), None)?; + self.exec_internal(&format!("LISTEN {quoted_channel}"), None)?; } let callback: ChannelCallback = Arc::new(callback); @@ -375,7 +735,8 @@ impl Pglite { listeners.retain(|listener| listener.id != handle.id); if listeners.is_empty() { self.notify_listeners.remove(&handle.normalized_channel); - self.exec_internal(&format!("UNLISTEN {}", handle.channel), None)?; + let quoted_channel = crate::pglite::templating::quote_identifier(&handle.channel); + self.exec_internal(&format!("UNLISTEN {quoted_channel}"), None)?; } } Ok(()) @@ -383,9 +744,10 @@ impl Pglite { /// Remove all listeners for the specified channel. pub fn unlisten_channel(&mut self, channel: &str) -> Result<()> { - let normalized = to_postgres_name(channel); + let quoted_channel = crate::pglite::templating::quote_identifier(channel); + let normalized = channel.to_string(); if self.notify_listeners.remove(&normalized).is_some() { - self.exec_internal(&format!("UNLISTEN {}", channel), None)?; + self.exec_internal(&format!("UNLISTEN {quoted_channel}"), None)?; } Ok(()) } @@ -416,7 +778,6 @@ impl Pglite { options: Option<&QueryOptions>, ) -> Result { self.check_ready()?; - self.init_array_types(false)?; let default_options = QueryOptions::default(); let query_opts = options.unwrap_or(&default_options); @@ -435,26 +796,23 @@ impl Pglite { &query_opts.param_types }; - let parse_msg = Serialize::parse(None, sql, param_types); - // Ignore returned messages; we just need to ensure the statement parses. - let _ = self.exec_protocol(&parse_msg, exec_opts.clone())?; - - let describe_msg = Serialize::describe(&PortalTarget::new('S', None)); - let ExecProtocolResult { messages } = - self.exec_protocol(&describe_msg, exec_opts.clone())?; + let mut describe_batch = Vec::new(); + describe_batch.extend(Serialize::parse(None, sql, param_types)); + describe_batch.extend(Serialize::describe(&PortalTarget::new('S', None))); + describe_batch.extend(Serialize::sync()); + let ExecProtocolResult { messages, .. } = + self.exec_protocol(&describe_batch, exec_opts.clone())?; + if !messages + .iter() + .any(|message| matches!(message, BackendMessage::ParseComplete { .. })) + { + bail!("extended query parse did not complete"); + } describe_messages.extend(messages); Ok(()) })(); - match self.exec_protocol(&Serialize::sync(), exec_opts.clone()) { - Ok(ExecProtocolResult { messages }) => describe_messages.extend(messages), - Err(err) if result.is_ok() => { - return Err(err.context(format!("failed to synchronize describe query: {sql}"))); - } - Err(_) => {} - } - if let Err(err) = result { match err.downcast::() { Ok(db_err) => { @@ -468,6 +826,17 @@ impl Pglite { } let param_type_ids = parse_describe_statement_results(&describe_messages); + self.ensure_array_types_for_oids(param_type_ids.iter().copied(), Some(query_opts))?; + let result_type_ids = describe_messages + .iter() + .filter_map(|msg| match msg { + BackendMessage::RowDescription(desc) => Some(desc), + _ => None, + }) + .flat_map(|desc| desc.fields.iter().map(|field| field.data_type_id)) + .collect::>(); + self.ensure_array_types_for_oids(result_type_ids.iter().copied(), Some(query_opts))?; + let query_params = param_type_ids .into_iter() .map(|oid| DescribeQueryParam { @@ -505,7 +874,6 @@ impl Pglite { F: FnMut(&mut Transaction<'_>) -> Result, { self.check_ready()?; - self.init_array_types(false)?; // Begin transaction self.run_exec_command("BEGIN")?; @@ -533,16 +901,13 @@ impl Pglite { txn_result } - /// Flush runtime writes to the underlying filesystem. Currently a no-op on the host. + /// Flush runtime writes to the underlying filesystem. + /// + /// The WASIX backend uses host-mounted files and PostgreSQL's own fsync/WAL + /// behavior for durability. Adding an unconditional host directory + /// `sync_all` after every direct query is both expensive and weaker than the + /// database's file-level fsyncs, so the Rust-level hook remains a no-op. pub fn sync_to_fs(&mut self) -> Result<()> { - let mount_root = self.pg.paths().mount_root(); - if let Ok(file) = std::fs::OpenOptions::new().read(true).open(mount_root) { - let _ = file.sync_all(); - } - let data_root = mount_root.join("pglite"); - if let Ok(file) = std::fs::OpenOptions::new().read(true).open(&data_root) { - let _ = file.sync_all(); - } Ok(()) } @@ -587,6 +952,26 @@ impl Pglite { Ok(values) } + fn parse_and_describe( + &mut self, + sql: &str, + param_types: &[i32], + exec_opts: ExecProtocolOptions, + ) -> Result> { + let mut prepare_batch = Vec::new(); + prepare_batch.extend(Serialize::parse(None, sql, param_types)); + prepare_batch.extend(Serialize::describe(&PortalTarget::new('S', None))); + prepare_batch.extend(Serialize::sync()); + let ExecProtocolResult { messages, .. } = self.exec_protocol(&prepare_batch, exec_opts)?; + if !messages + .iter() + .any(|message| matches!(message, BackendMessage::ParseComplete { .. })) + { + bail!("extended query parse did not complete"); + } + Ok(messages) + } + fn default_serialize_value(&self, value: &Value) -> String { Self::default_serialize_value_static(value) } @@ -611,12 +996,26 @@ impl Pglite { messages: Vec, options: Option<&QueryOptions>, ) -> Result { - let blob = self.get_written_blob()?; - self.cleanup_blob()?; + let blob = { + let _phase = timing::phase("client.finish.blob_read"); + self.get_written_blob()? + }; + { + let _phase = timing::phase("client.finish.blob_cleanup"); + self.cleanup_blob()?; + } if !self.in_transaction { + let _phase = timing::phase("client.finish.sync_to_fs"); self.sync_to_fs()?; } - let parsed = parse_results(&messages, &self.parsers, options, blob); + { + let _phase = timing::phase("client.finish.ensure_array_types"); + self.ensure_array_types_for_result_messages(&messages, options)?; + } + let parsed = { + let _phase = timing::phase("client.finish.parse_results"); + parse_results(&messages, &self.parsers, options, blob) + }; parsed .into_iter() .next() @@ -628,15 +1027,32 @@ impl Pglite { messages: Vec, options: Option<&QueryOptions>, ) -> Result> { - let blob = self.get_written_blob()?; - self.cleanup_blob()?; + let blob = { + let _phase = timing::phase("client.finish.blob_read"); + self.get_written_blob()? + }; + { + let _phase = timing::phase("client.finish.blob_cleanup"); + self.cleanup_blob()?; + } if !self.in_transaction { + let _phase = timing::phase("client.finish.sync_to_fs"); self.sync_to_fs()?; } - Ok(parse_results(&messages, &self.parsers, options, blob)) + { + let _phase = timing::phase("client.finish.ensure_array_types"); + self.ensure_array_types_for_result_messages(&messages, options)?; + } + let parsed = { + let _phase = timing::phase("client.finish.parse_results"); + parse_results(&messages, &self.parsers, options, blob) + }; + Ok(parsed) } - fn exec_protocol( + /// Execute raw PostgreSQL frontend protocol bytes and parse backend + /// protocol messages. + pub fn exec_protocol( &mut self, message: &[u8], options: ExecProtocolOptions, @@ -648,24 +1064,31 @@ impl Pglite { data_transfer_container, } = options; - let data = self.exec_protocol_raw(message, sync_to_fs, data_transfer_container)?; + let data = { + let _phase = timing::phase("client.protocol_roundtrip"); + self.exec_protocol_raw_inner(message, sync_to_fs, data_transfer_container)? + }; let mut messages = Vec::new(); let on_notice_cb = on_notice.clone(); - if let Err(err) = self.parser.parse(&data, |msg| { - if let BackendMessage::Error(db_err) = &msg - && throw_on_error - { - return Err(anyhow!(db_err.clone())); - } - if let Some(callback) = on_notice_cb.as_ref() - && let BackendMessage::Notice(notice) = &msg - { - callback(notice); - } - messages.push(msg); - Ok(()) - }) { + let parse_result = { + let _phase = timing::phase("client.protocol_parse"); + self.parser.parse(&data, |msg| { + if let BackendMessage::Error(db_err) = &msg + && throw_on_error + { + return Err(anyhow!(db_err.clone())); + } + if let Some(callback) = on_notice_cb.as_ref() + && let BackendMessage::Notice(notice) = &msg + { + callback(notice); + } + messages.push(msg); + Ok(()) + }) + }; + if let Err(err) = parse_result { match err.downcast::() { Ok(db_err) => { self.parser = ProtocolParser::new(); @@ -677,8 +1100,7 @@ impl Pglite { for message in &messages { if let BackendMessage::Notification(note) = message { - let key = to_postgres_name(¬e.channel); - if let Some(listeners) = self.notify_listeners.get(&key) { + if let Some(listeners) = self.notify_listeners.get(¬e.channel) { for listener in listeners { (listener.callback)(¬e.payload); } @@ -689,83 +1111,182 @@ impl Pglite { } } - Ok(ExecProtocolResult { messages }) + Ok(ExecProtocolResult { data, messages }) + } + + /// Execute raw PostgreSQL frontend protocol bytes and return raw backend + /// protocol bytes. + pub fn exec_protocol_raw( + &mut self, + message: &[u8], + options: ExecProtocolOptions, + ) -> Result> { + self.exec_protocol_raw_inner(message, options.sync_to_fs, options.data_transfer_container) + } + + /// Execute raw protocol bytes and pass the returned backend bytes to + /// `on_data`. + pub fn exec_protocol_raw_stream( + &mut self, + message: &[u8], + options: ExecProtocolOptions, + mut on_data: F, + ) -> Result<()> + where + F: FnMut(&[u8]) -> Result<()>, + { + self.backend.send_framed_raw_stream( + message, + options.data_transfer_container, + &mut on_data, + )?; + if options.sync_to_fs { + let _phase = timing::phase("client.protocol_stream_sync_to_fs"); + self.sync_to_fs()?; + } + Ok(()) } - fn exec_protocol_raw( + fn exec_protocol_raw_inner( &mut self, message: &[u8], sync_to_fs: bool, data_transfer_container: Option, ) -> Result> { - let data = self - .transport - .send(&mut self.pg, message, data_transfer_container)?; + let data = { + let _phase = timing::phase("client.protocol_transport_send"); + self.backend + .send_buffered(message, data_transfer_container)? + }; if sync_to_fs { + let _phase = timing::phase("client.protocol_sync_to_fs"); self.sync_to_fs()?; } Ok(data) } - fn init_array_types(&mut self, force: bool) -> Result<()> { - if self.array_types_initialized && !force { - return Ok(()); + fn ensure_array_types_for_bind_values( + &mut self, + params: &[Value], + data_type_ids: &[i32], + options: &QueryOptions, + ) -> Result { + let mut registered = false; + for (idx, value) in params.iter().enumerate() { + if !value.is_array() { + continue; + } + let oid = data_type_ids.get(idx).copied().unwrap_or(TEXT); + if options.serializers.contains_key(&oid) || self.serializers.contains_key(&oid) { + continue; + } + registered |= self.try_register_array_type_by_array_oid(oid)?; } + Ok(registered) + } - let prev = self.array_types_initialized; - self.array_types_initialized = true; - - let result: Result<()> = { - let sql = " - SELECT b.oid, b.typarray - FROM pg_catalog.pg_type a - LEFT JOIN pg_catalog.pg_type b ON b.oid = a.typelem - WHERE a.typcategory = 'A' - GROUP BY b.oid, b.typarray - ORDER BY b.oid - "; - let results = self.exec(sql, None)?; - let result_set = results - .into_iter() - .next() - .ok_or_else(|| anyhow!("array type discovery returned no results"))?; + fn ensure_array_types_for_result_messages( + &mut self, + messages: &[BackendMessage], + options: Option<&QueryOptions>, + ) -> Result<()> { + let oids = messages + .iter() + .filter_map(|msg| match msg { + BackendMessage::RowDescription(desc) => Some(desc), + _ => None, + }) + .flat_map(|desc| desc.fields.iter().map(|field| field.data_type_id)) + .collect::>(); + self.ensure_array_types_for_oids(oids, options) + } - for row in result_set.rows { - let map = match row { - Value::Object(map) => map, - _ => continue, - }; - let element_oid = value_to_i32(map.get("oid")).unwrap_or(0); - let array_oid = value_to_i32(map.get("typarray")).unwrap_or(0); + fn ensure_array_types_for_oids( + &mut self, + oids: impl IntoIterator, + options: Option<&QueryOptions>, + ) -> Result<()> { + for oid in oids { + if oid <= 0 || self.parsers.contains_key(&oid) { + continue; + } + if options.is_some_and(|options| options.parsers.contains_key(&oid)) { + continue; + } + self.try_register_array_type_by_array_oid(oid)?; + } + Ok(()) + } - if element_oid == 0 || array_oid == 0 { - continue; - } + fn refresh_array_types_internal(&mut self) -> Result<()> { + let sql = " + SELECT e.oid, a.oid AS typarray, e.typdelim::text AS typdelim + FROM pg_catalog.pg_type a + JOIN pg_catalog.pg_type e ON e.oid = a.typelem + WHERE a.typcategory = 'A' + AND a.typelem <> 0 + ORDER BY e.oid + "; + let results = { + let _phase = timing::phase("pglite.array_type_catalog_query"); + self.exec_internal(sql, None)? + }; + let result_set = results + .into_iter() + .next() + .ok_or_else(|| anyhow!("array type discovery returned no results"))?; - let element_parser = self.parsers.get(&element_oid).cloned(); - let element_serializer = self.serializers.get(&element_oid).cloned(); + { + let _phase = timing::phase("pglite.array_type_register"); + for row in result_set.rows { + if let Some(info) = array_type_info_from_row(&row) { + self.register_array_type(info); + } + } + } + Ok(()) + } - let parser_clone = element_parser.clone(); - let array_parser: TypeParser = Arc::new(move |text: &str, _| { - parse_array_text(text, parser_clone.clone(), element_oid, array_oid) - }); - self.parsers.insert(array_oid, array_parser); + fn try_register_array_type_by_array_oid(&mut self, array_oid: i32) -> Result { + if array_oid <= 0 + || self.parsers.contains_key(&array_oid) + || self.array_type_lookup_misses.contains(&array_oid) + { + return Ok(false); + } - let serializer_clone = element_serializer.clone(); - let array_serializer: Serializer = Arc::new(move |value: &Value| { - serialize_array_value(value, serializer_clone.clone(), array_oid) - }); - self.serializers.insert(array_oid, array_serializer); - } - Ok(()) + let sql = format!( + "SELECT e.oid, a.oid AS typarray, e.typdelim::text AS typdelim \ + FROM pg_catalog.pg_type a \ + JOIN pg_catalog.pg_type e ON e.oid = a.typelem \ + WHERE a.oid = {array_oid}::oid \ + AND a.typcategory = 'A' \ + AND a.typelem <> 0" + ); + let results = { + let _phase = timing::phase("pglite.array_type_targeted_lookup"); + self.exec_internal(&sql, None)? + }; + let Some(result_set) = results.into_iter().next() else { + self.array_type_lookup_misses.insert(array_oid); + return Ok(false); + }; + let Some(row) = result_set.rows.into_iter().next() else { + self.array_type_lookup_misses.insert(array_oid); + return Ok(false); + }; + let Some(info) = array_type_info_from_row(&row) else { + self.array_type_lookup_misses.insert(array_oid); + return Ok(false); }; - if let Err(err) = result { - self.array_types_initialized = prev; - Err(err) - } else { - Ok(()) - } + self.register_array_type(info); + Ok(true) + } + + fn register_array_type(&mut self, info: ArrayTypeInfo) { + register_array_type(&mut self.parsers, &mut self.serializers, info); + self.array_type_lookup_misses.remove(&info.array_oid); } fn run_exec_command(&mut self, sql: &str) -> Result<()> { @@ -791,7 +1312,7 @@ impl Pglite { } fn dev_blob_path(&self) -> PathBuf { - self.pg.paths().pgroot.join("dev/blob") + self.backend.paths().runtime_root().join("dev/blob") } fn cleanup_blob(&mut self) -> Result<()> { @@ -850,12 +1371,73 @@ impl Drop for Pglite { } } -fn to_postgres_name(input: &str) -> String { - if input.starts_with('"') && input.ends_with('"') && input.len() >= 2 { - input[1..input.len() - 1].to_string() - } else { - input.to_lowercase() +#[cfg(feature = "extensions")] +fn ensure_direct_pg_dump_options_match_session( + startup_config: &StartupConfig, + options: &PgDumpOptions, +) -> Result<()> { + if options.database_ref() != startup_config.database { + bail!( + "direct pg_dump runs against the already-open embedded backend database '{}'; requested database '{}' would require a separate server connection", + startup_config.database, + options.database_ref() + ); } + if options.username_ref() != startup_config.username { + bail!( + "direct pg_dump runs through the already-open embedded backend user '{}'; requested user '{}' would require a separate server connection", + startup_config.username, + options.username_ref() + ); + } + Ok(()) +} + +#[cfg(feature = "extensions")] +fn read_direct_pg_dump_socket( + runtime: &Runtime, + reader: &mut TcpSocketHalfRx, + buffer: &mut [u8], +) -> Result { + runtime + .block_on(async { + std::future::poll_fn(|cx| { + let read = match reader.poll_fill_buf(cx) { + std::task::Poll::Ready(Ok(available)) => { + let read = available.len().min(buffer.len()); + buffer[..read].copy_from_slice(&available[..read]); + read + } + std::task::Poll::Ready(Err(err)) => return std::task::Poll::Ready(Err(err)), + std::task::Poll::Pending => return std::task::Poll::Pending, + }; + reader.consume(read); + std::task::Poll::Ready(Ok(read)) + }) + .await + }) + .context("read direct pg_dump virtual socket") +} + +#[cfg(feature = "extensions")] +fn write_direct_pg_dump_socket( + runtime: &Runtime, + writer: &mut (impl AsyncWrite + Unpin), + bytes: &[u8], +) -> Result<()> { + runtime + .block_on(writer.write_all(bytes)) + .context("write direct pg_dump virtual socket") +} + +#[cfg(feature = "extensions")] +fn flush_direct_pg_dump_socket( + runtime: &Runtime, + writer: &mut (impl AsyncWrite + Unpin), +) -> Result<()> { + runtime + .block_on(writer.flush()) + .context("flush direct pg_dump virtual socket") } fn value_to_i32(value: Option<&Value>) -> Option { @@ -866,6 +1448,26 @@ fn value_to_i32(value: Option<&Value>) -> Option { } } +fn value_to_char(value: Option<&Value>) -> Option { + match value? { + Value::String(string) => string.chars().next(), + _ => None, + } +} + +fn array_type_info_from_row(row: &Value) -> Option { + let Value::Object(map) = row else { + return None; + }; + let element_oid = value_to_i32(map.get("oid"))?; + let array_oid = value_to_i32(map.get("typarray"))?; + if element_oid == 0 || array_oid == 0 { + return None; + } + let delimiter = value_to_char(map.get("typdelim")).unwrap_or(','); + Some(ArrayTypeInfo::new(element_oid, array_oid, delimiter)) +} + /// Transaction handle used within [`Pglite::transaction`]. pub struct Transaction<'a> { client: &'a mut Pglite, @@ -916,6 +1518,11 @@ impl<'a> Transaction<'a> { self.client.exec_internal(sql, options) } + pub fn refresh_array_types(&mut self) -> Result<()> { + self.ensure_open()?; + self.client.refresh_array_types_internal() + } + pub fn commit(&mut self) -> Result<()> { self.commit_internal() } diff --git a/src/pglite/config.rs b/src/pglite/config.rs new file mode 100644 index 00000000..081b9960 --- /dev/null +++ b/src/pglite/config.rs @@ -0,0 +1,157 @@ +use std::collections::BTreeMap; + +use anyhow::{Result, bail, ensure}; + +use crate::pglite::interface::DebugLevel; + +/// PostgreSQL startup configuration applied through normal `postgres -c` GUC +/// handling before the embedded backend starts. +/// +/// Settings added here override `pglite-oxide`'s default startup profile because +/// they are appended after the defaults in the generated PostgreSQL argv. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PostgresConfig { + settings: BTreeMap, +} + +impl PostgresConfig { + /// Create an empty startup configuration. + pub fn new() -> Self { + Self::default() + } + + /// Set or replace one PostgreSQL GUC. + pub fn set(mut self, name: impl Into, value: impl Into) -> Self { + self.settings.insert(name.into(), value.into()); + self + } + + pub(crate) fn insert(&mut self, name: impl Into, value: impl Into) { + self.settings.insert(name.into(), value.into()); + } + + pub(crate) fn validate(&self) -> Result<()> { + for (name, value) in &self.settings { + validate_guc_name(name)?; + ensure!( + !value.contains('\0'), + "Postgres config value for '{name}' must not contain NUL bytes" + ); + } + Ok(()) + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.settings + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + } + + #[cfg(feature = "extensions")] + pub(crate) fn stable_entries(&self) -> Vec<(String, String)> { + self.settings + .iter() + .map(|(name, value)| (name.clone(), value.clone())) + .collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct StartupConfig { + pub(crate) username: String, + pub(crate) database: String, + pub(crate) debug_level: Option, + pub(crate) relaxed_durability: bool, + pub(crate) extra_args: Vec, +} + +impl Default for StartupConfig { + fn default() -> Self { + Self { + username: "postgres".to_owned(), + database: "template1".to_owned(), + debug_level: None, + relaxed_durability: false, + extra_args: Vec::new(), + } + } +} + +impl StartupConfig { + pub(crate) fn validate(&self) -> Result<()> { + validate_startup_value("username", &self.username)?; + validate_startup_value("database", &self.database)?; + if let Some(level) = self.debug_level { + ensure!( + level <= 5, + "Postgres debug level must be between 0 and 5, got {level}" + ); + } + for arg in &self.extra_args { + ensure!( + !arg.contains('\0'), + "Postgres startup argument must not contain NUL bytes" + ); + } + Ok(()) + } +} + +fn validate_guc_name(name: &str) -> Result<()> { + ensure!(!name.is_empty(), "Postgres config name must not be empty"); + ensure!( + !name.contains('\0') && !name.contains('='), + "Postgres config name '{name}' must not contain NUL bytes or '='" + ); + + for part in name.split('.') { + if part.is_empty() { + bail!("Postgres config name '{name}' contains an empty identifier part"); + } + let mut chars = part.chars(); + let first = chars.next().expect("part is non-empty"); + if !(first == '_' || first.is_ascii_alphabetic()) { + bail!("Postgres config name '{name}' must start each identifier with a letter or '_'"); + } + if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) { + bail!("Postgres config name '{name}' may only contain letters, digits, '_', and '.'"); + } + } + + Ok(()) +} + +fn validate_startup_value(name: &str, value: &str) -> Result<()> { + ensure!( + !value.is_empty(), + "Postgres startup {name} must not be empty" + ); + ensure!( + !value.contains('\0'), + "Postgres startup {name} must not contain NUL bytes" + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::PostgresConfig; + + #[test] + fn validates_builtin_and_extension_guc_names() { + PostgresConfig::new() + .set("synchronous_commit", "off") + .set("pg_stat_statements.track", "all") + .validate() + .unwrap(); + } + + #[test] + fn rejects_invalid_guc_names_before_startup() { + let err = PostgresConfig::new() + .set("bad=name", "off") + .validate() + .expect_err("invalid GUC name should be rejected"); + assert!(err.to_string().contains("must not contain")); + } +} diff --git a/src/pglite/data_dir.rs b/src/pglite/data_dir.rs new file mode 100644 index 00000000..5376c0e4 --- /dev/null +++ b/src/pglite/data_dir.rs @@ -0,0 +1,359 @@ +use std::collections::BTreeMap; +use std::fs::{self, File}; +use std::io::{Cursor, Read, Write}; +use std::path::{Component, Path, PathBuf}; + +use anyhow::{Context, Result, bail}; +use flate2::Compression; +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use tar::{Archive, Builder, EntryType, Header}; + +const PGDATA_OVERLAY_MANIFEST_NAME: &str = ".pglite-oxide-pgdata-overlay.json"; +const RUNTIME_STATE_FILES: &[&str] = &["postmaster.pid", "postmaster.opts"]; +const OVERLAY_WHITEOUT_PREFIX: &str = ".wh."; + +/// Compression format for physical PGDATA archives. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataDirArchiveFormat { + Tar, + TarGz, +} + +#[derive(Debug, Clone)] +enum EntrySource { + Directory, + File(PathBuf), +} + +pub(crate) fn dump_pgdata_archive( + pgdata_upper: &Path, + pgdata_lower: Option<&Path>, + format: DataDirArchiveFormat, +) -> Result> { + let materialized = materialize_pgdata_view(pgdata_upper, pgdata_lower)?; + dump_materialized_pgdata_archive(materialized.path(), format) +} + +fn dump_materialized_pgdata_archive( + pgdata: &Path, + format: DataDirArchiveFormat, +) -> Result> { + let mut entries = BTreeMap::::new(); + collect_pgdata_entries(pgdata, pgdata, &mut entries)?; + + let mut tar_bytes = Vec::new(); + { + let mut builder = Builder::new(&mut tar_bytes); + for (relative, source) in entries { + let archive_path = archive_path(&relative); + match source { + EntrySource::Directory => { + let mut header = Header::new_gnu(); + header.set_entry_type(EntryType::Directory); + header.set_mode(0o755); + header.set_mtime(0); + header.set_size(0); + header.set_cksum(); + builder + .append_data(&mut header, archive_path, Cursor::new(Vec::::new())) + .context("append PGDATA directory to archive")?; + } + EntrySource::File(path) => { + let mut file = + File::open(&path).with_context(|| format!("open {}", path.display()))?; + let size = file + .metadata() + .with_context(|| format!("stat {}", path.display()))? + .len(); + let mut header = Header::new_gnu(); + header.set_entry_type(EntryType::Regular); + header.set_mode(0o644); + header.set_mtime(0); + header.set_size(size); + header.set_cksum(); + builder + .append_data(&mut header, archive_path, &mut file) + .with_context(|| format!("append {}", path.display()))?; + } + } + } + builder.finish().context("finish PGDATA tar archive")?; + } + + match format { + DataDirArchiveFormat::Tar => Ok(tar_bytes), + DataDirArchiveFormat::TarGz => { + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder + .write_all(&tar_bytes) + .context("gzip PGDATA archive")?; + encoder.finish().context("finish gzipped PGDATA archive") + } + } +} + +fn materialize_pgdata_view( + pgdata_upper: &Path, + pgdata_lower: Option<&Path>, +) -> Result { + let temp = tempfile::TempDir::new().context("create materialized PGDATA archive view")?; + if let Some(lower) = pgdata_lower { + copy_pgdata_tree(lower, lower, temp.path(), false)?; + } + copy_pgdata_tree(pgdata_upper, pgdata_upper, temp.path(), true)?; + Ok(temp) +} + +pub(crate) fn unpack_pgdata_archive(bytes: &[u8], destination: &Path) -> Result<()> { + let reader: Box = if bytes.starts_with(&[0x1f, 0x8b]) { + Box::new(GzDecoder::new(Cursor::new(bytes))) + } else { + Box::new(Cursor::new(bytes)) + }; + let mut archive = Archive::new(reader); + for entry in archive.entries().context("read PGDATA archive entries")? { + let mut entry = entry.context("read PGDATA archive entry")?; + let path = entry + .path() + .context("read PGDATA archive entry path")? + .into_owned(); + let relative = normalize_archive_path(&path)?; + if relative.as_os_str().is_empty() { + continue; + } + if should_skip_relative(&relative) { + continue; + } + let dest = destination.join(&relative); + let entry_type = entry.header().entry_type(); + if entry_type.is_dir() { + fs::create_dir_all(&dest) + .with_context(|| format!("create PGDATA directory {}", dest.display()))?; + continue; + } + if !entry_type.is_file() { + bail!( + "PGDATA archive entry {} has unsupported type {:?}", + path.display(), + entry_type + ); + } + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create PGDATA directory {}", parent.display()))?; + } + entry + .unpack(&dest) + .with_context(|| format!("unpack PGDATA archive entry {}", path.display()))?; + } + Ok(()) +} + +fn collect_pgdata_entries( + root: &Path, + current: &Path, + entries: &mut BTreeMap, +) -> Result<()> { + if !current.exists() { + return Ok(()); + } + let mut children = fs::read_dir(current) + .with_context(|| format!("read PGDATA directory {}", current.display()))? + .collect::>>() + .with_context(|| format!("read PGDATA directory entries {}", current.display()))?; + children.sort_by_key(|entry| entry.path()); + + for child in children { + let path = child.path(); + let relative = path + .strip_prefix(root) + .with_context(|| format!("strip PGDATA root {}", root.display()))? + .to_path_buf(); + if should_skip_relative(&relative) { + continue; + } + let file_type = child + .file_type() + .with_context(|| format!("stat {}", path.display()))?; + if file_type.is_dir() { + entries.insert(relative.clone(), EntrySource::Directory); + collect_pgdata_entries(root, &path, entries)?; + } else if file_type.is_file() { + entries.insert(relative, EntrySource::File(path)); + } + } + Ok(()) +} + +fn copy_pgdata_tree( + root: &Path, + current: &Path, + destination_root: &Path, + apply_whiteouts: bool, +) -> Result<()> { + if !current.exists() { + return Ok(()); + } + let mut children = fs::read_dir(current) + .with_context(|| format!("read PGDATA directory {}", current.display()))? + .collect::>>() + .with_context(|| format!("read PGDATA directory entries {}", current.display()))?; + children.sort_by_key(|entry| entry.path()); + + for child in children { + let src = child.path(); + let relative = src + .strip_prefix(root) + .with_context(|| format!("strip PGDATA root {}", root.display()))? + .to_path_buf(); + if apply_whiteouts && let Some(target) = whiteout_target_relative(&relative) { + let dest = destination_root.join(target); + remove_materialized_entry(&dest)?; + continue; + } + if should_skip_relative(&relative) { + continue; + } + + let dest = destination_root.join(&relative); + let file_type = child + .file_type() + .with_context(|| format!("stat {}", src.display()))?; + if file_type.is_dir() { + fs::create_dir_all(&dest).with_context(|| { + format!("create materialized PGDATA directory {}", dest.display()) + })?; + copy_pgdata_tree(root, &src, destination_root, apply_whiteouts)?; + } else if file_type.is_file() { + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent).with_context(|| { + format!("create materialized PGDATA directory {}", parent.display()) + })?; + } + fs::copy(&src, &dest).with_context(|| { + format!( + "copy PGDATA archive file {} -> {}", + src.display(), + dest.display() + ) + })?; + } + } + Ok(()) +} + +fn remove_materialized_entry(path: &Path) -> Result<()> { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_dir() => fs::remove_dir_all(path) + .with_context(|| format!("remove materialized whiteout directory {}", path.display())), + Ok(_) => fs::remove_file(path) + .with_context(|| format!("remove materialized whiteout file {}", path.display())), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(err) + .with_context(|| format!("stat materialized whiteout target {}", path.display())), + } +} + +fn should_skip_relative(relative: &Path) -> bool { + relative == Path::new(PGDATA_OVERLAY_MANIFEST_NAME) + || whiteout_target_relative(relative).is_some() + || RUNTIME_STATE_FILES + .iter() + .any(|name| relative == Path::new(name)) +} + +fn whiteout_target_relative(relative: &Path) -> Option { + let file_name = relative.file_name()?.to_string_lossy(); + let target_file_name = file_name.strip_prefix(OVERLAY_WHITEOUT_PREFIX)?; + let mut target = relative.to_path_buf(); + target.set_file_name(target_file_name); + Some(target) +} + +fn archive_path(relative: &Path) -> String { + relative.to_string_lossy().replace('\\', "/") +} + +fn normalize_archive_path(path: &Path) -> Result { + let mut dest = PathBuf::new(); + for component in path.components() { + match component { + Component::RootDir | Component::CurDir => {} + Component::Normal(part) => dest.push(part), + Component::ParentDir | Component::Prefix(_) => { + bail!("unsafe PGDATA archive path {}", path.display()) + } + } + } + Ok(dest) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + #[test] + fn pgdata_archive_applies_overlay_whiteouts() -> Result<()> { + let temp = tempfile::TempDir::new()?; + let lower = temp.path().join("lower"); + let upper = temp.path().join("upper"); + fs::create_dir_all(lower.join("base/1/tree"))?; + fs::create_dir_all(upper.join("base/1"))?; + fs::write(lower.join("base/1/deleted"), b"lower-deleted")?; + fs::write(lower.join("base/1/kept"), b"lower-kept")?; + fs::write(lower.join("base/1/tree/child"), b"lower-child")?; + fs::write(upper.join("base/1/.wh.deleted"), b"")?; + fs::write(upper.join("base/1/.wh.tree"), b"")?; + + let archive = dump_pgdata_archive(&upper, Some(&lower), DataDirArchiveFormat::Tar)?; + let entries = archive_entries(&archive)?; + + assert!(entries.contains("base/1/kept")); + assert!(!entries.contains("base/1/deleted")); + assert!(!entries.contains("base/1/tree")); + assert!(!entries.contains("base/1/tree/child")); + assert!(!entries.iter().any(|entry| entry.contains(".wh."))); + Ok(()) + } + + #[test] + fn pgdata_archive_keeps_upper_file_recreated_after_whiteout() -> Result<()> { + let temp = tempfile::TempDir::new()?; + let lower = temp.path().join("lower"); + let upper = temp.path().join("upper"); + fs::create_dir_all(lower.join("base/1"))?; + fs::create_dir_all(upper.join("base/1"))?; + fs::write(lower.join("base/1/recreated"), b"lower")?; + fs::write(upper.join("base/1/.wh.recreated"), b"")?; + fs::write(upper.join("base/1/recreated"), b"upper")?; + + let archive = dump_pgdata_archive(&upper, Some(&lower), DataDirArchiveFormat::Tar)?; + let mut unpacked = Archive::new(Cursor::new(archive)); + let mut found = false; + for entry in unpacked.entries()? { + let mut entry = entry?; + let path = entry.path()?.into_owned(); + if normalize_archive_path(&path)? == Path::new("base/1/recreated") { + let mut contents = Vec::new(); + entry.read_to_end(&mut contents)?; + assert_eq!(contents, b"upper"); + found = true; + } + } + assert!(found, "expected recreated upper file in archive"); + Ok(()) + } + + fn archive_entries(bytes: &[u8]) -> Result> { + let mut archive = Archive::new(Cursor::new(bytes)); + let mut paths = BTreeSet::new(); + for entry in archive.entries()? { + let entry = entry?; + let path = entry.path()?.into_owned(); + paths.insert(archive_path(&normalize_archive_path(&path)?)); + } + Ok(paths) + } +} diff --git a/src/pglite/extensions.rs b/src/pglite/extensions.rs new file mode 100644 index 00000000..f3d2ddd1 --- /dev/null +++ b/src/pglite/extensions.rs @@ -0,0 +1,625 @@ +use std::collections::BTreeSet; + +use anyhow::{Result, bail}; + +#[path = "generated_extensions.rs"] +mod generated; + +pub use generated::*; + +/// A bundled Postgres extension that can be installed into a PGlite database. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Extension { + name: &'static str, + sql_name: &'static str, + archive_name: &'static str, + native_module_file: Option<&'static str>, + aot_name: Option<&'static str>, + dependencies: &'static [&'static str], + setup: ExtensionSetup, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct ExtensionSetup { + create_extension: bool, + create_schema: Option<&'static str>, + load_sql: &'static [&'static str], + post_create_sql: &'static [&'static str], +} + +impl ExtensionSetup { + pub(crate) const fn new( + create_extension: bool, + create_schema: Option<&'static str>, + load_sql: &'static [&'static str], + post_create_sql: &'static [&'static str], + ) -> Self { + Self { + create_extension, + create_schema, + load_sql, + post_create_sql, + } + } +} + +impl Extension { + #[allow(dead_code)] + pub(crate) const fn new( + name: &'static str, + sql_name: &'static str, + archive_name: &'static str, + native_module_file: Option<&'static str>, + aot_name: Option<&'static str>, + dependencies: &'static [&'static str], + setup: ExtensionSetup, + ) -> Self { + Self { + name, + sql_name, + archive_name, + native_module_file, + aot_name, + dependencies, + setup, + } + } + + /// Human-facing extension name. + pub const fn name(self) -> &'static str { + self.name + } + + /// SQL extension name used in `CREATE EXTENSION`. + pub const fn sql_name(self) -> &'static str { + self.sql_name + } + + /// Archive path inside the asset manifest. + pub const fn archive_name(self) -> &'static str { + self.archive_name + } + + /// AOT artifact key for the extension side module. + pub const fn aot_name(self) -> Option<&'static str> { + self.aot_name + } + + /// Native side-module file installed into `/lib/postgresql`, when the + /// extension has one. + pub const fn native_module_file(self) -> Option<&'static str> { + self.native_module_file + } + + /// SQL extension names that must be installed before this extension. + pub const fn dependencies(self) -> &'static [&'static str] { + self.dependencies + } + + pub(crate) const fn setup(self) -> ExtensionSetup { + self.setup + } +} + +pub fn by_sql_name(sql_name: &str) -> Option { + ALL.iter() + .copied() + .find(|extension| extension.sql_name == sql_name) +} + +pub(crate) fn candidate_by_sql_name(sql_name: &str) -> Option { + generated::CANDIDATES + .iter() + .copied() + .find(|extension| extension.sql_name == sql_name) +} + +pub(crate) fn resolve_extension_set(extensions: &[Extension]) -> Result> { + let mut visiting = BTreeSet::new(); + let mut visited = BTreeSet::new(); + let mut resolved = Vec::new(); + let mut requested = extensions.to_vec(); + requested.sort_by_key(|extension| extension.sql_name()); + for extension in requested { + visit_extension(extension, &mut visiting, &mut visited, &mut resolved)?; + } + Ok(resolved) +} + +fn visit_extension( + extension: Extension, + visiting: &mut BTreeSet<&'static str>, + visited: &mut BTreeSet<&'static str>, + resolved: &mut Vec, +) -> Result<()> { + if visited.contains(extension.sql_name()) { + return Ok(()); + } + if !visiting.insert(extension.sql_name()) { + bail!( + "cyclic bundled extension dependency involving '{}'", + extension.sql_name() + ); + } + for dependency in extension.dependencies() { + let dependency_extension = candidate_by_sql_name(dependency).ok_or_else(|| { + anyhow::anyhow!( + "bundled extension '{}' depends on missing packaged extension '{}'", + extension.sql_name(), + dependency + ) + })?; + visit_extension(dependency_extension, visiting, visited, resolved)?; + } + visiting.remove(extension.sql_name()); + visited.insert(extension.sql_name()); + resolved.push(extension); + Ok(()) +} + +pub(crate) fn extension_setup_sql(extension: Extension) -> Vec { + let setup = extension.setup(); + let mut statements = Vec::new(); + if setup.create_extension { + if let Some(schema) = setup.create_schema.filter(|schema| *schema != "pg_catalog") { + statements.push(format!( + "CREATE SCHEMA IF NOT EXISTS {};", + crate::pglite::templating::quote_identifier(schema) + )); + } + let mut sql = format!( + "CREATE EXTENSION IF NOT EXISTS {}", + crate::pglite::templating::quote_identifier(extension.sql_name()) + ); + if let Some(schema) = setup.create_schema { + sql.push_str(" WITH SCHEMA "); + sql.push_str(&crate::pglite::templating::quote_identifier(schema)); + } + sql.push(';'); + statements.push(sql); + } + statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned())); + statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned())); + statements +} + +pub(crate) fn extension_session_setup_sql(extension: Extension) -> Vec { + let setup = extension.setup(); + let mut statements = Vec::new(); + statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned())); + statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned())); + statements +} + +#[cfg(all(test, feature = "extensions"))] +mod candidate_tests { + use super::*; + use crate::{Pglite, PgliteServer}; + use anyhow::{Context, Result, ensure}; + use sqlx::{Connection, PgConnection}; + use std::collections::BTreeSet; + use std::path::{Path, PathBuf}; + + #[test] + fn public_extensions_pass_direct_and_restart_smoke() -> Result<()> { + run_direct_and_restart_smoke_set(generated::ALL) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn public_extensions_pass_server_smoke() -> Result<()> { + run_server_smoke_set(generated::ALL).await + } + + #[test] + fn public_extensions_materialize_only_requested_libraries() -> Result<()> { + run_lifecycle_materialization_set(generated::ALL) + } + + #[test] + #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + fn packaged_candidate_extensions_pass_direct_and_restart_smoke() -> Result<()> { + run_direct_and_restart_smoke_set(generated::CANDIDATES) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + async fn packaged_candidate_extensions_pass_server_smoke() -> Result<()> { + run_server_smoke_set(generated::CANDIDATES).await + } + + #[test] + #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + fn packaged_candidate_extensions_materialize_only_requested_libraries() -> Result<()> { + run_lifecycle_materialization_set(generated::CANDIDATES) + } + + fn run_direct_and_restart_smoke_set(extensions: &[Extension]) -> Result<()> { + let mut failures = Vec::new(); + for extension in extensions { + if let Err(error) = run_one_direct_and_restart_smoke(*extension) { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension direct/restart smoke failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + fn run_one_direct_and_restart_smoke(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + { + let mut db = Pglite::builder() + .temporary() + .extension(extension) + .open() + .with_context(|| format!("open temporary database with extension {name}"))?; + run_direct_smoke(&mut db, extension)?; + db.close() + .with_context(|| format!("close temporary database with extension {name}"))?; + } + + let root = tempfile::TempDir::new() + .with_context(|| format!("create restart root for extension {name}"))?; + { + let mut db = Pglite::builder() + .path(root.path()) + .extension(extension) + .open() + .with_context(|| { + format!("open persistent database with extension {name} before restart") + })?; + run_direct_smoke(&mut db, extension)?; + assert_extension_catalog_state(&mut db, extension)?; + db.close() + .with_context(|| format!("close persistent database with extension {name}"))?; + } + { + let mut db = Pglite::builder() + .path(root.path()) + .extension(extension) + .open() + .with_context(|| { + format!("reopen persistent database with extension {name} after restart") + })?; + assert_extension_catalog_state(&mut db, extension)?; + db.close() + .with_context(|| format!("close restarted database with extension {name}"))?; + } + Ok(()) + } + + async fn run_server_smoke_set(extensions: &[Extension]) -> Result<()> { + let mut failures = Vec::new(); + for extension in extensions { + if let Err(error) = run_one_server_smoke(*extension).await { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension server smoke failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + async fn run_one_server_smoke(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + let server = PgliteServer::builder() + .temporary() + .extension(extension) + .start() + .with_context(|| format!("start server with extension {name}"))?; + let mut conn = PgConnection::connect(&server.database_url()) + .await + .with_context(|| format!("connect server with extension {name}"))?; + run_server_smoke(&mut conn, extension).await?; + drop(conn); + server + .shutdown() + .with_context(|| format!("shutdown server with extension {name}"))?; + Ok(()) + } + + fn run_lifecycle_materialization_set(extensions: &[Extension]) -> Result<()> { + let mut failures = Vec::new(); + for extension in extensions { + if let Err(error) = run_one_lifecycle_materialization(*extension) { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension lifecycle/materialization failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + fn run_one_lifecycle_materialization(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + let root = tempfile::TempDir::new() + .with_context(|| format!("create lifecycle root for extension {name}"))?; + { + let mut db = Pglite::builder() + .path(root.path()) + .extension(extension) + .open() + .with_context(|| format!("open lifecycle database with extension {name}"))?; + db.close() + .with_context(|| format!("close lifecycle database with extension {name}"))?; + } + assert_only_resolved_extension_libraries_are_materialized(root.path(), extension) + } + + fn run_direct_smoke(db: &mut Pglite, extension: Extension) -> Result<()> { + for statement in smoke_sql(extension.sql_name()) { + db.exec(statement, None).with_context(|| { + format!( + "direct smoke failed for extension {} while running:\n{}", + extension.sql_name(), + statement + ) + })?; + } + Ok(()) + } + + async fn run_server_smoke(conn: &mut PgConnection, extension: Extension) -> Result<()> { + for statement in smoke_sql(extension.sql_name()) { + sqlx::query(statement) + .fetch_all(&mut *conn) + .await + .with_context(|| { + format!( + "server smoke failed for extension {} while running:\n{}", + extension.sql_name(), + statement + ) + })?; + } + Ok(()) + } + + fn assert_extension_catalog_state(db: &mut Pglite, extension: Extension) -> Result<()> { + if extension.setup().create_extension { + let result = db.query( + "SELECT count(*)::int4 AS count FROM pg_extension WHERE extname = $1", + &[serde_json::json!(extension.sql_name())], + None, + )?; + ensure!( + result.rows[0]["count"] == serde_json::json!(1), + "extension {} should survive restart in pg_extension", + extension.sql_name() + ); + } else { + let result = db.query("SELECT 1::int4 AS ok", &[], None)?; + ensure!( + result.rows[0]["ok"] == serde_json::json!(1), + "extension {} should reopen cleanly", + extension.sql_name() + ); + } + Ok(()) + } + + fn assert_only_resolved_extension_libraries_are_materialized( + root: &Path, + extension: Extension, + ) -> Result<()> { + let expected = resolve_extension_set(&[extension])? + .into_iter() + .filter_map(|extension| extension.native_module_file().map(PathBuf::from)) + .collect::>(); + let actual = relative_files(&root.join("tmp/pglite/lib/postgresql")) + .into_iter() + .collect::>(); + ensure!( + actual == expected, + "upper runtime library layer for {} should contain only resolved requested libraries; expected {:?}, got {:?}", + extension.sql_name(), + expected, + actual + ); + Ok(()) + } + + fn relative_files(root: &Path) -> Vec { + fn walk(base: &Path, current: &Path, files: &mut Vec) { + let Ok(entries) = std::fs::read_dir(current) else { + return; + }; + for entry in entries { + let entry = entry.expect("read runtime test directory entry"); + let path = entry.path(); + if path.is_dir() { + walk(base, &path, files); + } else if path.is_file() { + files.push( + path.strip_prefix(base) + .expect("relative extension library path") + .to_path_buf(), + ); + } + } + } + + let mut files = Vec::new(); + walk(root, root, &mut files); + files.sort(); + files + } + + fn smoke_sql(sql_name: &str) -> &'static [&'static str] { + // These are compact Rust ports of the PGlite extension smoke tests in + // assets/checkouts/pglite/packages/pglite/tests. + match sql_name { + "age" => &[ + "SELECT ag_catalog.create_graph('oxide_graph')", + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM ag_catalog.ag_graph WHERE name = 'oxide_graph') THEN RAISE EXCEPTION 'age graph was not created'; END IF; END $$", + "SELECT * FROM ag_catalog.cypher('oxide_graph', $$ RETURN 1 $$) AS (one agtype)", + ], + "amcheck" => &[ + "CREATE TEMP TABLE oxide_amcheck (id int PRIMARY KEY, value text)", + "INSERT INTO oxide_amcheck SELECT i, 'v' || i::text FROM generate_series(1, 8) AS i", + "SELECT bt_index_check('oxide_amcheck_pkey'::regclass)", + ], + "auto_explain" => &["EXPLAIN SELECT count(*) FROM pg_class"], + "bloom" => &[ + "CREATE TEMP TABLE oxide_bloom (id int, value int)", + "CREATE INDEX oxide_bloom_idx ON oxide_bloom USING bloom (id, value)", + "INSERT INTO oxide_bloom SELECT i, i % 3 FROM generate_series(1, 20) AS i", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_bloom WHERE id = 7 AND value = 1; IF n <> 1 THEN RAISE EXCEPTION 'bloom lookup failed: %', n; END IF; END $$", + ], + "btree_gin" => &[ + "CREATE TEMP TABLE oxide_btree_gin (id int)", + "CREATE INDEX oxide_btree_gin_idx ON oxide_btree_gin USING gin (id)", + "INSERT INTO oxide_btree_gin SELECT generate_series(1, 10)", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gin WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gin lookup failed: %', n; END IF; END $$", + ], + "btree_gist" => &[ + "CREATE TEMP TABLE oxide_btree_gist (id int)", + "CREATE INDEX oxide_btree_gist_idx ON oxide_btree_gist USING gist (id)", + "INSERT INTO oxide_btree_gist SELECT generate_series(1, 10)", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gist WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gist lookup failed: %', n; END IF; END $$", + ], + "citext" => &[ + "CREATE TEMP TABLE oxide_citext (value citext)", + "INSERT INTO oxide_citext VALUES ('Postgres')", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_citext WHERE value = 'postgres'; IF n <> 1 THEN RAISE EXCEPTION 'citext comparison failed: %', n; END IF; END $$", + ], + "cube" => &[ + "DO $$ DECLARE d float8; BEGIN SELECT cube(array[1,2,3]) <-> cube(array[1,2,4]) INTO d; IF d <> 1 THEN RAISE EXCEPTION 'cube distance failed: %', d; END IF; END $$", + ], + "dict_int" => &[ + "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('intdict', '40865854'), ',') INTO lex; IF lex <> '408658' THEN RAISE EXCEPTION 'dict_int lexize failed: %', lex; END IF; END $$", + ], + "dict_xsyn" => &[ + "ALTER TEXT SEARCH DICTIONARY xsyn (RULES = 'xsyn_sample', KEEPORIG = true, MATCHORIG = true, KEEPSYNONYMS = true, MATCHSYNONYMS = false)", + "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('xsyn', 'supernova'), ',') INTO lex; IF lex IS NULL OR lex !~ 'sn' THEN RAISE EXCEPTION 'dict_xsyn lexize failed: %', lex; END IF; END $$", + ], + "earthdistance" => &[ + "DO $$ DECLARE d float8; BEGIN SELECT earth_distance(ll_to_earth(0, 0), ll_to_earth(0, 1)) INTO d; IF d <= 0 THEN RAISE EXCEPTION 'earthdistance failed: %', d; END IF; END $$", + ], + "file_fdw" => &[ + "CREATE SERVER oxide_file_server FOREIGN DATA WRAPPER file_fdw", + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_foreign_data_wrapper WHERE fdwname = 'file_fdw') THEN RAISE EXCEPTION 'file_fdw wrapper missing'; END IF; END $$", + ], + "fuzzystrmatch" => &[ + "DO $$ BEGIN IF levenshtein('kitten', 'sitting') <> 3 THEN RAISE EXCEPTION 'levenshtein failed'; END IF; IF soundex('kitten') <> 'K350' THEN RAISE EXCEPTION 'soundex failed'; END IF; END $$", + ], + "hstore" => &[ + "CREATE TEMP TABLE oxide_hstore (attrs hstore)", + "INSERT INTO oxide_hstore VALUES ('a=>1,b=>2'::hstore)", + "DO $$ DECLARE v text; BEGIN SELECT attrs -> 'b' INTO v FROM oxide_hstore; IF v <> '2' THEN RAISE EXCEPTION 'hstore lookup failed: %', v; END IF; END $$", + ], + "intarray" => &[ + "CREATE TEMP TABLE oxide_intarray (tags int[])", + "INSERT INTO oxide_intarray VALUES (ARRAY[1, 2, 5]), (ARRAY[3, 4])", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_intarray WHERE tags && ARRAY[2, 9]; IF n <> 1 THEN RAISE EXCEPTION 'intarray overlap failed: %', n; END IF; SELECT count(*) INTO n FROM oxide_intarray WHERE tags @@ '1 & (2|3)'::query_int; IF n <> 1 THEN RAISE EXCEPTION 'intarray query_int failed: %', n; END IF; END $$", + ], + "isn" => &[ + "DO $$ BEGIN IF isbn('978-0-393-04002-9')::text <> '0-393-04002-X' THEN RAISE EXCEPTION 'isbn failed'; END IF; IF isbn13('0901690546')::text <> '978-0-901690-54-8' THEN RAISE EXCEPTION 'isbn13 failed'; END IF; IF issn('1436-4522')::text <> '1436-4522' THEN RAISE EXCEPTION 'issn failed'; END IF; END $$", + ], + "lo" => &[ + "CREATE TEMP TABLE oxide_lo (id int, data oid)", + "CREATE TRIGGER oxide_lo_manage BEFORE UPDATE OR DELETE ON oxide_lo FOR EACH ROW EXECUTE FUNCTION lo_manage(data)", + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'oxide_lo_manage') THEN RAISE EXCEPTION 'lo trigger missing'; END IF; END $$", + ], + "ltree" => &[ + "CREATE TEMP TABLE oxide_ltree (path ltree)", + "INSERT INTO oxide_ltree VALUES ('Top.Science.Astronomy'), ('Top.Collections.Pictures')", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ltree WHERE path <@ 'Top.Science'; IF n <> 1 THEN RAISE EXCEPTION 'ltree ancestor query failed: %', n; END IF; END $$", + ], + "pageinspect" => &[ + "CREATE TEMP TABLE oxide_pageinspect (id int)", + "INSERT INTO oxide_pageinspect SELECT generate_series(1, 5)", + "SELECT * FROM page_header(get_raw_page('oxide_pageinspect', 0))", + ], + "pg_buffercache" => &[ + "SELECT * FROM pg_buffercache_summary()", + "SELECT * FROM pg_buffercache_usage_counts()", + ], + "pg_freespacemap" => &[ + "CREATE TEMP TABLE oxide_fsm (id int, value text)", + "INSERT INTO oxide_fsm SELECT i, repeat('x', 200) FROM generate_series(1, 20) AS i", + "DELETE FROM oxide_fsm WHERE id % 2 = 0", + "SELECT * FROM pg_freespace('oxide_fsm') LIMIT 1", + ], + "pg_hashids" => &[ + "DO $$ BEGIN IF id_encode(1001) <> 'jNl' THEN RAISE EXCEPTION 'pg_hashids encode failed'; END IF; IF id_decode_once('jNl') <> 1001 THEN RAISE EXCEPTION 'pg_hashids decode failed'; END IF; END $$", + ], + "pg_ivm" => &[ + "CREATE TABLE oxide_ivm_orders (id int, amount int)", + "INSERT INTO oxide_ivm_orders VALUES (1, 10), (2, 20)", + "SELECT pgivm.create_immv('oxide_ivm_summary', $$ SELECT id, amount FROM oxide_ivm_orders $$)", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ivm_summary; IF n <> 2 THEN RAISE EXCEPTION 'pg_ivm initial count failed: %', n; END IF; END $$", + ], + "pg_surgery" => &[ + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'heap_force_kill') THEN RAISE EXCEPTION 'pg_surgery function missing'; END IF; END $$", + ], + "pg_textsearch" => &[ + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_am WHERE amname = 'bm25') THEN RAISE EXCEPTION 'bm25 access method missing'; END IF; END $$", + "SELECT to_bm25query('postgres wasm')", + ], + "pg_trgm" => &[ + "DO $$ DECLARE score float8; BEGIN SELECT similarity('postgres', 'postgrex') INTO score; IF score <= 0 THEN RAISE EXCEPTION 'pg_trgm similarity failed: %', score; END IF; END $$", + ], + "pg_uuidv7" => &[ + "DO $$ DECLARE id uuid; ts timestamptz; BEGIN SELECT uuid_generate_v7() INTO id; IF length(id::text) <> 36 THEN RAISE EXCEPTION 'uuidv7 length failed'; END IF; SELECT uuid_v7_to_timestamptz('018570bb-4a7d-7c7e-8df4-6d47afd8c8fc') INTO ts; IF ts IS NULL THEN RAISE EXCEPTION 'uuidv7 timestamp failed'; END IF; END $$", + ], + "pg_visibility" => &[ + "CREATE TEMP TABLE oxide_visibility (id int)", + "INSERT INTO oxide_visibility SELECT generate_series(1, 5)", + "SELECT * FROM pg_visibility('oxide_visibility') LIMIT 1", + "SELECT * FROM pg_visibility_map('oxide_visibility') LIMIT 1", + ], + "pg_walinspect" => &[ + "CREATE TEMP TABLE oxide_walinspect (value text)", + "CREATE TEMP TABLE oxide_walinspect_lsn AS SELECT pg_current_wal_lsn() AS before_lsn", + "INSERT INTO oxide_walinspect SELECT 'row ' || i::text FROM generate_series(1, 5) AS i", + "SELECT * FROM pg_get_wal_block_info((SELECT before_lsn FROM oxide_walinspect_lsn), pg_current_wal_lsn()) ORDER BY start_lsn, block_id LIMIT 20", + ], + "pgtap" => &[ + "BEGIN", + "SELECT plan(1)", + "SELECT pass('pgtap smoke')", + "SELECT * FROM finish()", + "ROLLBACK", + ], + "seg" => &[ + "DO $$ BEGIN IF '7(+-)1'::seg::text <> '6 .. 8' THEN RAISE EXCEPTION 'seg cast failed'; END IF; END $$", + ], + "tablefunc" => &[ + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM normal_rand(10, 5, 3); IF n <> 10 THEN RAISE EXCEPTION 'normal_rand failed: %', n; END IF; END $$", + "SELECT * FROM crosstab('SELECT 1, 1, 10 UNION ALL SELECT 1, 2, 20') AS ct(rowid int, c1 int, c2 int)", + ], + "tcn" => &[ + "CREATE TEMP TABLE oxide_tcn (id int PRIMARY KEY, value text)", + "CREATE TRIGGER oxide_tcn_trigger AFTER INSERT OR UPDATE OR DELETE ON oxide_tcn FOR EACH ROW EXECUTE FUNCTION triggered_change_notification()", + "INSERT INTO oxide_tcn VALUES (1, 'one')", + ], + "tsm_system_rows" => &[ + "CREATE TEMP TABLE oxide_tsm_rows AS SELECT i FROM generate_series(1, 20) AS i", + "SELECT * FROM oxide_tsm_rows TABLESAMPLE SYSTEM_ROWS(5)", + ], + "tsm_system_time" => &[ + "CREATE TEMP TABLE oxide_tsm_time AS SELECT i FROM generate_series(1, 20) AS i", + "SELECT * FROM oxide_tsm_time TABLESAMPLE SYSTEM_TIME(50)", + ], + "unaccent" => &[ + "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('unaccent', 'Hôtel'), ',') INTO lex; IF lex <> 'Hotel' THEN RAISE EXCEPTION 'unaccent failed: %', lex; END IF; END $$", + ], + "vector" => &[ + "CREATE TEMP TABLE oxide_vector (embedding vector(3))", + "INSERT INTO oxide_vector VALUES ('[1,2,3]')", + "DO $$ DECLARE d float8; BEGIN SELECT embedding <-> '[1,2,4]'::vector INTO d FROM oxide_vector; IF d <> 1 THEN RAISE EXCEPTION 'vector distance failed: %', d; END IF; END $$", + ], + other => panic!("missing smoke SQL for packaged extension candidate {other}"), + } + } +} diff --git a/src/pglite/generated_extensions.rs b/src/pglite/generated_extensions.rs new file mode 100644 index 00000000..a1332489 --- /dev/null +++ b/src/pglite/generated_extensions.rs @@ -0,0 +1,832 @@ +// @generated by `cargo run -p xtask -- extensions generate` + +use super::{Extension, ExtensionSetup}; + +const EMPTY_SQL_NAMES: &[&str] = &[]; +const EMPTY_SQL: &[&str] = &[]; + +const CANDIDATE_AGE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_AGE_LOAD_SQL: &[&str] = &["LOAD 'age';"]; +const CANDIDATE_AGE_POST_CREATE_SQL: &[&str] = + &["SET search_path = ag_catalog, \"$user\", public;"]; + +pub(crate) const CANDIDATE_AGE: Extension = Extension::new( + "Apache AGE", + "age", + "extensions/age.tar.zst", + Some("age.so"), + Some("extension:age"), + CANDIDATE_AGE_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("ag_catalog"), + CANDIDATE_AGE_LOAD_SQL, + CANDIDATE_AGE_POST_CREATE_SQL, + ), +); + +const CANDIDATE_AMCHECK_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_AMCHECK_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_AMCHECK_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_AMCHECK: Extension = Extension::new( + "amcheck", + "amcheck", + "extensions/amcheck.tar.zst", + Some("amcheck.so"), + Some("extension:amcheck"), + CANDIDATE_AMCHECK_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_AMCHECK_LOAD_SQL, + CANDIDATE_AMCHECK_POST_CREATE_SQL, + ), +); + +const CANDIDATE_AUTO_EXPLAIN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_AUTO_EXPLAIN_LOAD_SQL: &[&str] = &[ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';", +]; +const CANDIDATE_AUTO_EXPLAIN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_AUTO_EXPLAIN: Extension = Extension::new( + "auto_explain", + "auto_explain", + "extensions/auto_explain.tar.zst", + Some("auto_explain.so"), + Some("extension:auto_explain"), + CANDIDATE_AUTO_EXPLAIN_DEPENDENCIES, + ExtensionSetup::new( + false, + None, + CANDIDATE_AUTO_EXPLAIN_LOAD_SQL, + CANDIDATE_AUTO_EXPLAIN_POST_CREATE_SQL, + ), +); + +const CANDIDATE_BLOOM_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_BLOOM_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_BLOOM_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_BLOOM: Extension = Extension::new( + "bloom", + "bloom", + "extensions/bloom.tar.zst", + Some("bloom.so"), + Some("extension:bloom"), + CANDIDATE_BLOOM_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_BLOOM_LOAD_SQL, + CANDIDATE_BLOOM_POST_CREATE_SQL, + ), +); + +const CANDIDATE_BTREE_GIN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_BTREE_GIN_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_BTREE_GIN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_BTREE_GIN: Extension = Extension::new( + "btree_gin", + "btree_gin", + "extensions/btree_gin.tar.zst", + Some("btree_gin.so"), + Some("extension:btree_gin"), + CANDIDATE_BTREE_GIN_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_BTREE_GIN_LOAD_SQL, + CANDIDATE_BTREE_GIN_POST_CREATE_SQL, + ), +); + +const CANDIDATE_BTREE_GIST_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_BTREE_GIST_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_BTREE_GIST_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_BTREE_GIST: Extension = Extension::new( + "btree_gist", + "btree_gist", + "extensions/btree_gist.tar.zst", + Some("btree_gist.so"), + Some("extension:btree_gist"), + CANDIDATE_BTREE_GIST_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_BTREE_GIST_LOAD_SQL, + CANDIDATE_BTREE_GIST_POST_CREATE_SQL, + ), +); + +const CANDIDATE_CITEXT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_CITEXT_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_CITEXT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_CITEXT: Extension = Extension::new( + "citext", + "citext", + "extensions/citext.tar.zst", + Some("citext.so"), + Some("extension:citext"), + CANDIDATE_CITEXT_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_CITEXT_LOAD_SQL, + CANDIDATE_CITEXT_POST_CREATE_SQL, + ), +); + +const CANDIDATE_CUBE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_CUBE_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_CUBE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_CUBE: Extension = Extension::new( + "cube", + "cube", + "extensions/cube.tar.zst", + Some("cube.so"), + Some("extension:cube"), + CANDIDATE_CUBE_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_CUBE_LOAD_SQL, + CANDIDATE_CUBE_POST_CREATE_SQL, + ), +); + +const CANDIDATE_DICT_INT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_DICT_INT_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_DICT_INT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_DICT_INT: Extension = Extension::new( + "dict_int", + "dict_int", + "extensions/dict_int.tar.zst", + Some("dict_int.so"), + Some("extension:dict_int"), + CANDIDATE_DICT_INT_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_DICT_INT_LOAD_SQL, + CANDIDATE_DICT_INT_POST_CREATE_SQL, + ), +); + +const CANDIDATE_DICT_XSYN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_DICT_XSYN_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_DICT_XSYN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_DICT_XSYN: Extension = Extension::new( + "dict_xsyn", + "dict_xsyn", + "extensions/dict_xsyn.tar.zst", + Some("dict_xsyn.so"), + Some("extension:dict_xsyn"), + CANDIDATE_DICT_XSYN_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_DICT_XSYN_LOAD_SQL, + CANDIDATE_DICT_XSYN_POST_CREATE_SQL, + ), +); + +const CANDIDATE_EARTHDISTANCE_DEPENDENCIES: &[&str] = &["cube"]; +const CANDIDATE_EARTHDISTANCE_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_EARTHDISTANCE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_EARTHDISTANCE: Extension = Extension::new( + "earthdistance", + "earthdistance", + "extensions/earthdistance.tar.zst", + Some("earthdistance.so"), + Some("extension:earthdistance"), + CANDIDATE_EARTHDISTANCE_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_EARTHDISTANCE_LOAD_SQL, + CANDIDATE_EARTHDISTANCE_POST_CREATE_SQL, + ), +); + +const CANDIDATE_FILE_FDW_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_FILE_FDW_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_FILE_FDW_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_FILE_FDW: Extension = Extension::new( + "file_fdw", + "file_fdw", + "extensions/file_fdw.tar.zst", + Some("file_fdw.so"), + Some("extension:file_fdw"), + CANDIDATE_FILE_FDW_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_FILE_FDW_LOAD_SQL, + CANDIDATE_FILE_FDW_POST_CREATE_SQL, + ), +); + +const CANDIDATE_FUZZYSTRMATCH_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_FUZZYSTRMATCH_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_FUZZYSTRMATCH_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_FUZZYSTRMATCH: Extension = Extension::new( + "fuzzystrmatch", + "fuzzystrmatch", + "extensions/fuzzystrmatch.tar.zst", + Some("fuzzystrmatch.so"), + Some("extension:fuzzystrmatch"), + CANDIDATE_FUZZYSTRMATCH_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_FUZZYSTRMATCH_LOAD_SQL, + CANDIDATE_FUZZYSTRMATCH_POST_CREATE_SQL, + ), +); + +const CANDIDATE_HSTORE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_HSTORE_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_HSTORE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_HSTORE: Extension = Extension::new( + "hstore", + "hstore", + "extensions/hstore.tar.zst", + Some("hstore.so"), + Some("extension:hstore"), + CANDIDATE_HSTORE_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_HSTORE_LOAD_SQL, + CANDIDATE_HSTORE_POST_CREATE_SQL, + ), +); + +const CANDIDATE_INTARRAY_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_INTARRAY_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_INTARRAY_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_INTARRAY: Extension = Extension::new( + "intarray", + "intarray", + "extensions/intarray.tar.zst", + Some("_int.so"), + Some("extension:intarray"), + CANDIDATE_INTARRAY_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_INTARRAY_LOAD_SQL, + CANDIDATE_INTARRAY_POST_CREATE_SQL, + ), +); + +const CANDIDATE_ISN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_ISN_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_ISN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_ISN: Extension = Extension::new( + "isn", + "isn", + "extensions/isn.tar.zst", + Some("isn.so"), + Some("extension:isn"), + CANDIDATE_ISN_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_ISN_LOAD_SQL, + CANDIDATE_ISN_POST_CREATE_SQL, + ), +); + +const CANDIDATE_LO_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_LO_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_LO_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_LO: Extension = Extension::new( + "lo", + "lo", + "extensions/lo.tar.zst", + Some("lo.so"), + Some("extension:lo"), + CANDIDATE_LO_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_LO_LOAD_SQL, + CANDIDATE_LO_POST_CREATE_SQL, + ), +); + +const CANDIDATE_LTREE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_LTREE_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_LTREE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_LTREE: Extension = Extension::new( + "ltree", + "ltree", + "extensions/ltree.tar.zst", + Some("ltree.so"), + Some("extension:ltree"), + CANDIDATE_LTREE_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_LTREE_LOAD_SQL, + CANDIDATE_LTREE_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PAGEINSPECT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PAGEINSPECT_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PAGEINSPECT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PAGEINSPECT: Extension = Extension::new( + "pageinspect", + "pageinspect", + "extensions/pageinspect.tar.zst", + Some("pageinspect.so"), + Some("extension:pageinspect"), + CANDIDATE_PAGEINSPECT_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PAGEINSPECT_LOAD_SQL, + CANDIDATE_PAGEINSPECT_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_BUFFERCACHE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_BUFFERCACHE_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_BUFFERCACHE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_BUFFERCACHE: Extension = Extension::new( + "pg_buffercache", + "pg_buffercache", + "extensions/pg_buffercache.tar.zst", + Some("pg_buffercache.so"), + Some("extension:pg_buffercache"), + CANDIDATE_PG_BUFFERCACHE_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_BUFFERCACHE_LOAD_SQL, + CANDIDATE_PG_BUFFERCACHE_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_FREESPACEMAP_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_FREESPACEMAP_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_FREESPACEMAP_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_FREESPACEMAP: Extension = Extension::new( + "pg_freespacemap", + "pg_freespacemap", + "extensions/pg_freespacemap.tar.zst", + Some("pg_freespacemap.so"), + Some("extension:pg_freespacemap"), + CANDIDATE_PG_FREESPACEMAP_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_FREESPACEMAP_LOAD_SQL, + CANDIDATE_PG_FREESPACEMAP_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_HASHIDS_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_HASHIDS_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_HASHIDS_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_HASHIDS: Extension = Extension::new( + "pg_hashids", + "pg_hashids", + "extensions/pg_hashids.tar.zst", + Some("pg_hashids.so"), + Some("extension:pg_hashids"), + CANDIDATE_PG_HASHIDS_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_HASHIDS_LOAD_SQL, + CANDIDATE_PG_HASHIDS_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_IVM_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_IVM_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_IVM_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_IVM: Extension = Extension::new( + "pg_ivm", + "pg_ivm", + "extensions/pg_ivm.tar.zst", + Some("pg_ivm.so"), + Some("extension:pg_ivm"), + CANDIDATE_PG_IVM_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_IVM_LOAD_SQL, + CANDIDATE_PG_IVM_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_SURGERY_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_SURGERY_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_SURGERY_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_SURGERY: Extension = Extension::new( + "pg_surgery", + "pg_surgery", + "extensions/pg_surgery.tar.zst", + Some("pg_surgery.so"), + Some("extension:pg_surgery"), + CANDIDATE_PG_SURGERY_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_SURGERY_LOAD_SQL, + CANDIDATE_PG_SURGERY_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_TEXTSEARCH_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_TEXTSEARCH_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_TEXTSEARCH_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_TEXTSEARCH: Extension = Extension::new( + "pg_textsearch", + "pg_textsearch", + "extensions/pg_textsearch.tar.zst", + Some("pg_textsearch.so"), + Some("extension:pg_textsearch"), + CANDIDATE_PG_TEXTSEARCH_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_TEXTSEARCH_LOAD_SQL, + CANDIDATE_PG_TEXTSEARCH_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_TRGM_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_TRGM_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_TRGM_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_TRGM: Extension = Extension::new( + "pg_trgm", + "pg_trgm", + "extensions/pg_trgm.tar.zst", + Some("pg_trgm.so"), + Some("extension:pg_trgm"), + CANDIDATE_PG_TRGM_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_TRGM_LOAD_SQL, + CANDIDATE_PG_TRGM_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_UUIDV7_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_UUIDV7_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_UUIDV7_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_UUIDV7: Extension = Extension::new( + "pg_uuidv7", + "pg_uuidv7", + "extensions/pg_uuidv7.tar.zst", + Some("pg_uuidv7.so"), + Some("extension:pg_uuidv7"), + CANDIDATE_PG_UUIDV7_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_UUIDV7_LOAD_SQL, + CANDIDATE_PG_UUIDV7_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_VISIBILITY_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_VISIBILITY_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_VISIBILITY_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_VISIBILITY: Extension = Extension::new( + "pg_visibility", + "pg_visibility", + "extensions/pg_visibility.tar.zst", + Some("pg_visibility.so"), + Some("extension:pg_visibility"), + CANDIDATE_PG_VISIBILITY_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_VISIBILITY_LOAD_SQL, + CANDIDATE_PG_VISIBILITY_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PG_WALINSPECT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PG_WALINSPECT_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_WALINSPECT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PG_WALINSPECT: Extension = Extension::new( + "pg_walinspect", + "pg_walinspect", + "extensions/pg_walinspect.tar.zst", + Some("pg_walinspect.so"), + Some("extension:pg_walinspect"), + CANDIDATE_PG_WALINSPECT_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PG_WALINSPECT_LOAD_SQL, + CANDIDATE_PG_WALINSPECT_POST_CREATE_SQL, + ), +); + +const CANDIDATE_PGTAP_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PGTAP_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PGTAP_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_PGTAP: Extension = Extension::new( + "pgtap", + "pgtap", + "extensions/pgtap.tar.zst", + None, + None, + CANDIDATE_PGTAP_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_PGTAP_LOAD_SQL, + CANDIDATE_PGTAP_POST_CREATE_SQL, + ), +); + +const CANDIDATE_SEG_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_SEG_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_SEG_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_SEG: Extension = Extension::new( + "seg", + "seg", + "extensions/seg.tar.zst", + Some("seg.so"), + Some("extension:seg"), + CANDIDATE_SEG_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_SEG_LOAD_SQL, + CANDIDATE_SEG_POST_CREATE_SQL, + ), +); + +const CANDIDATE_TABLEFUNC_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_TABLEFUNC_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TABLEFUNC_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_TABLEFUNC: Extension = Extension::new( + "tablefunc", + "tablefunc", + "extensions/tablefunc.tar.zst", + Some("tablefunc.so"), + Some("extension:tablefunc"), + CANDIDATE_TABLEFUNC_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_TABLEFUNC_LOAD_SQL, + CANDIDATE_TABLEFUNC_POST_CREATE_SQL, + ), +); + +const CANDIDATE_TCN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_TCN_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TCN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_TCN: Extension = Extension::new( + "tcn", + "tcn", + "extensions/tcn.tar.zst", + Some("tcn.so"), + Some("extension:tcn"), + CANDIDATE_TCN_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_TCN_LOAD_SQL, + CANDIDATE_TCN_POST_CREATE_SQL, + ), +); + +const CANDIDATE_TSM_SYSTEM_ROWS_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_TSM_SYSTEM_ROWS_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TSM_SYSTEM_ROWS_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_TSM_SYSTEM_ROWS: Extension = Extension::new( + "tsm_system_rows", + "tsm_system_rows", + "extensions/tsm_system_rows.tar.zst", + Some("tsm_system_rows.so"), + Some("extension:tsm_system_rows"), + CANDIDATE_TSM_SYSTEM_ROWS_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_TSM_SYSTEM_ROWS_LOAD_SQL, + CANDIDATE_TSM_SYSTEM_ROWS_POST_CREATE_SQL, + ), +); + +const CANDIDATE_TSM_SYSTEM_TIME_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_TSM_SYSTEM_TIME_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TSM_SYSTEM_TIME_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_TSM_SYSTEM_TIME: Extension = Extension::new( + "tsm_system_time", + "tsm_system_time", + "extensions/tsm_system_time.tar.zst", + Some("tsm_system_time.so"), + Some("extension:tsm_system_time"), + CANDIDATE_TSM_SYSTEM_TIME_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_TSM_SYSTEM_TIME_LOAD_SQL, + CANDIDATE_TSM_SYSTEM_TIME_POST_CREATE_SQL, + ), +); + +const CANDIDATE_UNACCENT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_UNACCENT_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_UNACCENT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_UNACCENT: Extension = Extension::new( + "unaccent", + "unaccent", + "extensions/unaccent.tar.zst", + Some("unaccent.so"), + Some("extension:unaccent"), + CANDIDATE_UNACCENT_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_UNACCENT_LOAD_SQL, + CANDIDATE_UNACCENT_POST_CREATE_SQL, + ), +); + +const CANDIDATE_VECTOR_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_VECTOR_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_VECTOR_POST_CREATE_SQL: &[&str] = EMPTY_SQL; + +pub(crate) const CANDIDATE_VECTOR: Extension = Extension::new( + "pgvector", + "vector", + "extensions/vector.tar.zst", + Some("vector.so"), + Some("extension:vector"), + CANDIDATE_VECTOR_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_VECTOR_LOAD_SQL, + CANDIDATE_VECTOR_POST_CREATE_SQL, + ), +); + +pub const AGE: Extension = CANDIDATE_AGE; +pub const AMCHECK: Extension = CANDIDATE_AMCHECK; +pub const AUTO_EXPLAIN: Extension = CANDIDATE_AUTO_EXPLAIN; +pub const BLOOM: Extension = CANDIDATE_BLOOM; +pub const BTREE_GIN: Extension = CANDIDATE_BTREE_GIN; +pub const BTREE_GIST: Extension = CANDIDATE_BTREE_GIST; +pub const CITEXT: Extension = CANDIDATE_CITEXT; +pub const CUBE: Extension = CANDIDATE_CUBE; +pub const DICT_INT: Extension = CANDIDATE_DICT_INT; +pub const DICT_XSYN: Extension = CANDIDATE_DICT_XSYN; +pub const EARTHDISTANCE: Extension = CANDIDATE_EARTHDISTANCE; +pub const FILE_FDW: Extension = CANDIDATE_FILE_FDW; +pub const FUZZYSTRMATCH: Extension = CANDIDATE_FUZZYSTRMATCH; +pub const HSTORE: Extension = CANDIDATE_HSTORE; +pub const INTARRAY: Extension = CANDIDATE_INTARRAY; +pub const ISN: Extension = CANDIDATE_ISN; +pub const LO: Extension = CANDIDATE_LO; +pub const LTREE: Extension = CANDIDATE_LTREE; +pub const PAGEINSPECT: Extension = CANDIDATE_PAGEINSPECT; +pub const PG_BUFFERCACHE: Extension = CANDIDATE_PG_BUFFERCACHE; +pub const PG_FREESPACEMAP: Extension = CANDIDATE_PG_FREESPACEMAP; +pub const PG_HASHIDS: Extension = CANDIDATE_PG_HASHIDS; +pub const PG_IVM: Extension = CANDIDATE_PG_IVM; +pub const PG_SURGERY: Extension = CANDIDATE_PG_SURGERY; +pub const PG_TEXTSEARCH: Extension = CANDIDATE_PG_TEXTSEARCH; +pub const PG_TRGM: Extension = CANDIDATE_PG_TRGM; +pub const PG_UUIDV7: Extension = CANDIDATE_PG_UUIDV7; +pub const PG_VISIBILITY: Extension = CANDIDATE_PG_VISIBILITY; +pub const PG_WALINSPECT: Extension = CANDIDATE_PG_WALINSPECT; +pub const PGTAP: Extension = CANDIDATE_PGTAP; +pub const SEG: Extension = CANDIDATE_SEG; +pub const TABLEFUNC: Extension = CANDIDATE_TABLEFUNC; +pub const TCN: Extension = CANDIDATE_TCN; +pub const TSM_SYSTEM_ROWS: Extension = CANDIDATE_TSM_SYSTEM_ROWS; +pub const TSM_SYSTEM_TIME: Extension = CANDIDATE_TSM_SYSTEM_TIME; +pub const UNACCENT: Extension = CANDIDATE_UNACCENT; +pub const VECTOR: Extension = CANDIDATE_VECTOR; + +pub const ALL: &[Extension] = &[ + AGE, + AMCHECK, + AUTO_EXPLAIN, + BLOOM, + BTREE_GIN, + BTREE_GIST, + CITEXT, + CUBE, + DICT_INT, + DICT_XSYN, + EARTHDISTANCE, + FILE_FDW, + FUZZYSTRMATCH, + HSTORE, + INTARRAY, + ISN, + LO, + LTREE, + PAGEINSPECT, + PG_BUFFERCACHE, + PG_FREESPACEMAP, + PG_HASHIDS, + PG_IVM, + PG_SURGERY, + PG_TEXTSEARCH, + PG_TRGM, + PG_UUIDV7, + PG_VISIBILITY, + PG_WALINSPECT, + PGTAP, + SEG, + TABLEFUNC, + TCN, + TSM_SYSTEM_ROWS, + TSM_SYSTEM_TIME, + UNACCENT, + VECTOR, +]; +pub(crate) const CANDIDATES: &[Extension] = &[ + CANDIDATE_AGE, + CANDIDATE_AMCHECK, + CANDIDATE_AUTO_EXPLAIN, + CANDIDATE_BLOOM, + CANDIDATE_BTREE_GIN, + CANDIDATE_BTREE_GIST, + CANDIDATE_CITEXT, + CANDIDATE_CUBE, + CANDIDATE_DICT_INT, + CANDIDATE_DICT_XSYN, + CANDIDATE_EARTHDISTANCE, + CANDIDATE_FILE_FDW, + CANDIDATE_FUZZYSTRMATCH, + CANDIDATE_HSTORE, + CANDIDATE_INTARRAY, + CANDIDATE_ISN, + CANDIDATE_LO, + CANDIDATE_LTREE, + CANDIDATE_PAGEINSPECT, + CANDIDATE_PG_BUFFERCACHE, + CANDIDATE_PG_FREESPACEMAP, + CANDIDATE_PG_HASHIDS, + CANDIDATE_PG_IVM, + CANDIDATE_PG_SURGERY, + CANDIDATE_PG_TEXTSEARCH, + CANDIDATE_PG_TRGM, + CANDIDATE_PG_UUIDV7, + CANDIDATE_PG_VISIBILITY, + CANDIDATE_PG_WALINSPECT, + CANDIDATE_PGTAP, + CANDIDATE_SEG, + CANDIDATE_TABLEFUNC, + CANDIDATE_TCN, + CANDIDATE_TSM_SYSTEM_ROWS, + CANDIDATE_TSM_SYSTEM_TIME, + CANDIDATE_UNACCENT, + CANDIDATE_VECTOR, +]; diff --git a/src/pglite/interface.rs b/src/pglite/interface.rs index 6842ef56..213a79da 100644 --- a/src/pglite/interface.rs +++ b/src/pglite/interface.rs @@ -87,6 +87,7 @@ impl Default for ExecProtocolOptions { #[derive(Debug, Clone)] pub struct ExecProtocolResult { + pub data: Vec, pub messages: Vec, } diff --git a/src/pglite/mod.rs b/src/pglite/mod.rs index 80ee3e91..756516ee 100644 --- a/src/pglite/mod.rs +++ b/src/pglite/mod.rs @@ -1,15 +1,27 @@ +pub(crate) mod aot; +pub(crate) mod assets; +pub(crate) mod backend; pub(crate) mod base; pub(crate) mod builder; pub(crate) mod client; +pub(crate) mod config; +pub(crate) mod data_dir; pub(crate) mod errors; +#[cfg(feature = "extensions")] +pub mod extensions; pub(crate) mod interface; pub(crate) mod parse; +#[cfg(feature = "extensions")] +pub mod pg_dump; pub(crate) mod postgres_mod; pub(crate) mod proxy; pub(crate) mod server; +pub(crate) mod sync_host_fs; pub(crate) mod templating; +pub(crate) mod timing; pub(crate) mod transport; pub(crate) mod types; +pub(crate) mod wire; pub use base::{ InstallOptions, InstallOutcome, MountInfo, PgDataTemplate, PgDataTemplateManifest, PglitePaths, @@ -19,12 +31,22 @@ pub use base::{ }; pub use builder::PgliteBuilder; pub use client::{GlobalListenerHandle, ListenerHandle, Pglite, Transaction}; +pub use config::PostgresConfig; +pub use data_dir::DataDirArchiveFormat; pub use errors::PgliteError; pub use interface::{ DataTransferContainer, DebugLevel, DescribeQueryParam, DescribeQueryResult, - DescribeResultField, FieldInfo, NoticeCallback, ParserMap, QueryOptions, Results, RowMode, - Serializer, SerializerMap, TypeParser, + DescribeResultField, ExecProtocolOptions, ExecProtocolResult, FieldInfo, NoticeCallback, + ParserMap, QueryOptions, Results, RowMode, Serializer, SerializerMap, TypeParser, +}; +#[cfg(feature = "extensions")] +pub use pg_dump::PgDumpOptions; +#[doc(hidden)] +pub use postgres_mod::{FsTraceSnapshot, fs_trace_snapshot, reset_fs_trace}; +pub use proxy::{ + PgliteProxy, ProtocolStatsSnapshot, disable_protocol_stats, protocol_stats_snapshot, + reset_protocol_stats, }; -pub use proxy::PgliteProxy; pub use server::{PgliteServer, PgliteServerBuilder}; pub use templating::{QueryTemplate, TemplatedQuery, format_query, quote_identifier}; +pub use timing::{PhaseTiming, capture_phase_timings, measure_phase, record_phase_timing}; diff --git a/src/pglite/pg_dump.rs b/src/pglite/pg_dump.rs new file mode 100644 index 00000000..f9a9e1d8 --- /dev/null +++ b/src/pglite/pg_dump.rs @@ -0,0 +1,962 @@ +use std::fmt; +use std::io::{Read, Seek, Write}; +use std::mem::MaybeUninit; +use std::net::Shutdown; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::pin::Pin; +use std::sync::mpsc::{self, Receiver, SyncSender}; +use std::sync::{Arc, Mutex}; +use std::task::{Context as TaskContext, Poll}; +use std::thread; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, anyhow, bail}; +use tempfile::TempDir; +use wasmer::Store; +use wasmer_types::ModuleHash; +use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; +use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager; +use wasmer_wasix::virtual_fs::{self, AsyncRead, AsyncSeek, AsyncWrite}; +use wasmer_wasix::virtual_net::tcp_pair::TcpSocketHalf; +use wasmer_wasix::virtual_net::{ + self, InterestHandler, NetworkError, SocketStatus, VirtualConnectedSocket, VirtualIoSource, + VirtualNetworking, VirtualSocket, VirtualTcpSocket, +}; +use wasmer_wasix::{LocalNetworking, PluggableRuntime, VirtualFile}; + +use crate::pglite::sync_host_fs::SyncHostFileSystem; +use crate::pglite::timing; +use crate::pglite::{aot, assets}; + +/// Options for the bundled WASIX `pg_dump` runner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PgDumpOptions { + args: Vec, + database: String, + username: String, +} + +impl Default for PgDumpOptions { + fn default() -> Self { + Self { + args: Vec::new(), + database: "template1".to_owned(), + username: "postgres".to_owned(), + } + } +} + +impl PgDumpOptions { + pub fn new() -> Self { + Self::default() + } + + /// Add one raw `pg_dump` argument. + pub fn arg(mut self, arg: impl Into) -> Self { + self.args.push(arg.into()); + self + } + + /// Add raw `pg_dump` arguments. + pub fn args(mut self, args: impl IntoIterator>) -> Self { + self.args.extend(args.into_iter().map(Into::into)); + self + } + + /// Select the database to dump. + pub fn database(mut self, database: impl Into) -> Self { + self.database = database.into(); + self + } + + /// Select the user passed to `pg_dump`. + pub fn username(mut self, username: impl Into) -> Self { + self.username = username.into(); + self + } + + pub(crate) fn validate(&self) -> Result<()> { + for (name, value) in [("database", &self.database), ("username", &self.username)] { + anyhow::ensure!( + !value.is_empty() && !value.contains('\0'), + "pg_dump {name} must not be empty or contain NUL bytes" + ); + } + for arg in &self.args { + anyhow::ensure!( + !arg.contains('\0'), + "pg_dump argument must not contain NUL bytes" + ); + validate_passthrough_arg(arg)?; + } + Ok(()) + } + + pub(crate) fn database_ref(&self) -> &str { + &self.database + } + + pub(crate) fn username_ref(&self) -> &str { + &self.username + } +} + +fn validate_passthrough_arg(arg: &str) -> Result<()> { + if let Some(flag) = disallowed_pg_dump_flag(arg) { + anyhow::bail!( + "pg_dump argument '{arg}' conflicts with pglite-oxide's managed {flag}; use PgDumpOptions typed setters where available" + ); + } + Ok(()) +} + +fn disallowed_pg_dump_flag(arg: &str) -> Option<&'static str> { + const LONG_FLAGS: &[(&str, &str)] = &[ + ("--file", "output file"), + ("--format", "output format"), + ("--host", "host"), + ("--port", "port"), + ("--username", "username"), + ("--dbname", "database"), + ("--jobs", "job count"), + ]; + for (flag, label) in LONG_FLAGS { + if arg == *flag + || arg + .strip_prefix(*flag) + .is_some_and(|tail| tail.starts_with('=')) + { + return Some(label); + } + } + + const SHORT_FLAGS: &[(&str, &str)] = &[ + ("-f", "output file"), + ("-F", "output format"), + ("-h", "host"), + ("-p", "port"), + ("-U", "username"), + ("-d", "database"), + ("-j", "job count"), + ]; + for (flag, label) in SHORT_FLAGS { + if arg == *flag || (arg.starts_with(*flag) && arg.len() > flag.len()) { + return Some(label); + } + } + None +} + +pub(crate) fn dump_server_sql(addr: SocketAddr, options: &PgDumpOptions) -> Result { + dump_sql_with_networking(addr, options, LocalNetworking::new()) +} + +pub(crate) type PgDumpVirtualSocket = TcpSocketHalf; + +pub(crate) fn dump_direct_sql(options: &PgDumpOptions, serve: F) -> Result +where + F: FnOnce(PgDumpVirtualSocket) -> Result<()>, +{ + options.validate()?; + let (socket_tx, socket_rx) = mpsc::sync_channel(1); + let networking = DirectPgDumpNetworking::new(socket_tx); + let runner_options = options.clone(); + let runner = thread::spawn(move || { + dump_sql_with_networking(DIRECT_PG_DUMP_ADDR, &runner_options, networking) + }); + + let accepted = receive_direct_pg_dump_socket(&socket_rx, &runner) + .context("accept direct pg_dump virtual protocol connection"); + let serve_result = match accepted { + Ok(socket) => serve(socket), + Err(err) => Err(err), + }; + let dump_result = runner + .join() + .map_err(|_| anyhow!("direct pg_dump runner thread panicked"))?; + + match (serve_result, dump_result) { + (Ok(()), Ok(sql)) => Ok(sql), + (Err(err), Ok(_)) => Err(err), + (Ok(()), Err(err)) => Err(err), + (Err(err), Err(dump_err)) => { + Err(err.context(format!("direct pg_dump runner also failed: {dump_err:#}"))) + } + } +} + +fn dump_sql_with_networking( + addr: SocketAddr, + options: &PgDumpOptions, + networking: N, +) -> Result +where + N: VirtualNetworking + Sync, +{ + options.validate()?; + let _phase = timing::phase("pg_dump"); + let wasm = { + let _phase = timing::phase("pg_dump.load_embedded_module"); + assets::pg_dump_wasm() + .ok_or_else(|| anyhow!("WASIX pg_dump asset is not bundled in this build"))? + }; + let engine = aot::headless_engine(); + let module = { + let _phase = timing::phase("pg_dump.load_aot"); + aot::load_pg_dump_module(&engine)? + }; + let _store = Store::new(engine.clone()); + + let fs_root = TempDir::new().context("create pg_dump WASIX filesystem root")?; + let runtime = { + let _phase = timing::phase("pg_dump.tokio_runtime"); + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create Tokio runtime for WASIX pg_dump")? + }; + let (host_fs, wasix_runtime) = { + let _phase = timing::phase("pg_dump.wasix_runtime"); + let _runtime_guard = runtime.enter(); + let host_fs = SyncHostFileSystem::new(fs_root.path()).with_context(|| { + format!( + "create host filesystem rooted at {}", + fs_root.path().display() + ) + })?; + let host_fs = Arc::new(host_fs) as Arc; + let mut wasix_runtime = PluggableRuntime::new(Arc::new(TokioTaskManager::new( + tokio::runtime::Handle::current(), + ))); + wasix_runtime.set_engine(engine.clone()); + wasix_runtime.set_networking_implementation(networking); + (host_fs, wasix_runtime) + }; + + let output_path = "/host/out.sql"; + let port = addr.port().to_string(); + let host = match addr { + SocketAddr::V4(addr) => addr.ip().to_string(), + SocketAddr::V6(addr) => addr.ip().to_string(), + }; + let mut args = options.args.clone(); + args.extend([ + "-U".to_owned(), + options.username.clone(), + "-h".to_owned(), + host, + "-p".to_owned(), + port, + "--inserts".to_owned(), + "-j".to_owned(), + "1".to_owned(), + "-f".to_owned(), + output_path.to_owned(), + ]); + args.push(options.database.clone()); + + let stdout = Arc::new(Mutex::new(Vec::new())); + let stderr = Arc::new(Mutex::new(Vec::new())); + let mut runner = WasiRunner::new(); + runner + .with_mount("/host".to_owned(), host_fs) + .with_current_dir("/") + .with_args(args) + .with_envs([ + ("PGUSER", options.username.as_str()), + ("PGPASSWORD", "password"), + ("PGSSLMODE", "disable"), + ]) + .with_stdout(Box::new(CaptureFile::new(Arc::clone(&stdout)))) + .with_stderr(Box::new(CaptureFile::new(Arc::clone(&stderr)))); + { + let _phase = timing::phase("pg_dump.run_wasm"); + runner + .run_wasm( + RuntimeOrEngine::Runtime(Arc::new(wasix_runtime)), + "pg_dump", + module, + ModuleHash::sha256(wasm), + ) + .map_err(|err| { + let stderr = + String::from_utf8_lossy(&stderr.lock().expect("stderr capture poisoned")) + .trim() + .to_owned(); + if stderr.is_empty() { + anyhow!(err) + } else { + anyhow!("{err}; pg_dump stderr: {stderr}") + } + }) + .context("run WASIX pg_dump")?; + } + + { + let _phase = timing::phase("pg_dump.read_output"); + match std::fs::read_to_string(fs_root.path().join("out.sql")) { + Ok(sql) => Ok(sql), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + let stdout = stdout.lock().expect("stdout capture poisoned"); + if stdout.is_empty() { + Err(err).with_context(|| { + format!( + "read pg_dump output {}", + fs_root.path().join("out.sql").display() + ) + }) + } else { + String::from_utf8(stdout.clone()).context("decode pg_dump stdout as UTF-8") + } + } + Err(err) => Err(err).with_context(|| { + format!( + "read pg_dump output {}", + fs_root.path().join("out.sql").display() + ) + }), + } + } +} + +const DIRECT_PG_DUMP_PORT: u16 = 65_432; +const DIRECT_PG_DUMP_SOCKET_BUFFER: usize = 8 * 1024 * 1024; +const DIRECT_PG_DUMP_LOCAL_PORT: u16 = 65_431; +const DIRECT_PG_DUMP_ADDR: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DIRECT_PG_DUMP_PORT); +const DIRECT_PG_DUMP_LOCAL_ADDR: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DIRECT_PG_DUMP_LOCAL_PORT); + +struct DirectPgDumpNetworking { + socket_tx: Mutex>>, +} + +impl DirectPgDumpNetworking { + fn new(socket_tx: SyncSender) -> Self { + Self { + socket_tx: Mutex::new(Some(socket_tx)), + } + } +} + +impl fmt::Debug for DirectPgDumpNetworking { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DirectPgDumpNetworking") + .finish_non_exhaustive() + } +} + +#[async_trait::async_trait] +impl VirtualNetworking for DirectPgDumpNetworking { + async fn connect_tcp( + &self, + addr: SocketAddr, + peer: SocketAddr, + ) -> virtual_net::Result> { + if peer != DIRECT_PG_DUMP_ADDR { + return Err(NetworkError::ConnectionRefused); + } + + let sender = self + .socket_tx + .lock() + .map_err(|_| NetworkError::IOError)? + .take() + .ok_or(NetworkError::ConnectionRefused)?; + let local = if addr.port() == 0 { + DIRECT_PG_DUMP_LOCAL_ADDR + } else { + addr + }; + let (guest, host) = TcpSocketHalf::channel(DIRECT_PG_DUMP_SOCKET_BUFFER, local, peer); + sender + .send(host) + .map_err(|_| NetworkError::ConnectionAborted)?; + Ok(Box::new(DirectPgDumpTcpSocket { + inner: guest, + first_write_ready_probe: true, + })) + } + + async fn resolve( + &self, + host: &str, + _port: Option, + _dns_server: Option, + ) -> virtual_net::Result> { + match host { + "localhost" | "127.0.0.1" => Ok(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]), + _ => Err(NetworkError::AddressNotAvailable), + } + } +} + +#[derive(Debug)] +struct DirectPgDumpTcpSocket { + inner: TcpSocketHalf, + // WASIX probes writability once while completing a blocking connect. + // `TcpSocketHalf` suppresses an immediate second write-ready poll until a + // write happens, but libpq polls again before its first StartupMessage. + // Keep the adapter level-triggered for that connect-to-first-write handoff. + first_write_ready_probe: bool, +} + +impl VirtualIoSource for DirectPgDumpTcpSocket { + fn remove_handler(&mut self) { + self.inner.remove_handler(); + } + + fn poll_read_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll> { + self.inner.poll_read_ready(cx) + } + + fn poll_write_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll> { + if self.first_write_ready_probe { + self.first_write_ready_probe = false; + return Poll::Ready(Ok(self.inner.send_buf_size().unwrap_or(1).max(1))); + } + self.inner.poll_write_ready(cx) + } +} + +impl VirtualSocket for DirectPgDumpTcpSocket { + fn set_ttl(&mut self, ttl: u32) -> virtual_net::Result<()> { + self.inner.set_ttl(ttl) + } + + fn ttl(&self) -> virtual_net::Result { + self.inner.ttl() + } + + fn addr_local(&self) -> virtual_net::Result { + self.inner.addr_local() + } + + fn status(&self) -> virtual_net::Result { + self.inner.status() + } + + fn set_handler( + &mut self, + handler: Box, + ) -> virtual_net::Result<()> { + self.inner.set_handler(handler) + } +} + +impl VirtualConnectedSocket for DirectPgDumpTcpSocket { + fn set_linger(&mut self, linger: Option) -> virtual_net::Result<()> { + self.inner.set_linger(linger) + } + + fn linger(&self) -> virtual_net::Result> { + self.inner.linger() + } + + fn try_send(&mut self, data: &[u8]) -> virtual_net::Result { + self.inner.try_send(data) + } + + fn try_flush(&mut self) -> virtual_net::Result<()> { + self.inner.try_flush() + } + + fn close(&mut self) -> virtual_net::Result<()> { + self.inner.close() + } + + fn try_recv(&mut self, buf: &mut [MaybeUninit], peek: bool) -> virtual_net::Result { + self.inner.try_recv(buf, peek) + } +} + +impl VirtualTcpSocket for DirectPgDumpTcpSocket { + fn set_recv_buf_size(&mut self, size: usize) -> virtual_net::Result<()> { + self.inner.set_recv_buf_size(size) + } + + fn recv_buf_size(&self) -> virtual_net::Result { + self.inner.recv_buf_size() + } + + fn set_send_buf_size(&mut self, size: usize) -> virtual_net::Result<()> { + self.inner.set_send_buf_size(size) + } + + fn send_buf_size(&self) -> virtual_net::Result { + self.inner.send_buf_size() + } + + fn set_nodelay(&mut self, reuse: bool) -> virtual_net::Result<()> { + self.inner.set_nodelay(reuse) + } + + fn nodelay(&self) -> virtual_net::Result { + self.inner.nodelay() + } + + fn set_keepalive(&mut self, keepalive: bool) -> virtual_net::Result<()> { + self.inner.set_keepalive(keepalive) + } + + fn keepalive(&self) -> virtual_net::Result { + self.inner.keepalive() + } + + fn set_dontroute(&mut self, keepalive: bool) -> virtual_net::Result<()> { + self.inner.set_dontroute(keepalive) + } + + fn dontroute(&self) -> virtual_net::Result { + self.inner.dontroute() + } + + fn addr_peer(&self) -> virtual_net::Result { + self.inner.addr_peer() + } + + fn shutdown(&mut self, how: Shutdown) -> virtual_net::Result<()> { + self.inner.shutdown(how) + } + + fn is_closed(&self) -> bool { + self.inner.is_closed() + } +} + +fn receive_direct_pg_dump_socket( + socket_rx: &Receiver, + runner: &thread::JoinHandle>, +) -> Result { + let started = Instant::now(); + loop { + match socket_rx.recv_timeout(Duration::from_millis(5)) { + Ok(socket) => return Ok(socket), + Err(mpsc::RecvTimeoutError::Timeout) => { + if runner.is_finished() { + bail!("pg_dump exited before opening the direct virtual protocol connection"); + } + if started.elapsed() > Duration::from_secs(30) { + bail!( + "timed out waiting for pg_dump to open the direct virtual protocol connection" + ); + } + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + bail!("pg_dump direct virtual networking channel closed before connect") + } + } + } +} + +#[derive(Debug)] +struct CaptureFile { + buffer: Arc>>, +} + +impl CaptureFile { + fn new(buffer: Arc>>) -> Self { + Self { buffer } + } +} + +impl VirtualFile for CaptureFile { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + self.buffer.lock().expect("capture lock poisoned").len() as u64 + } + + fn set_len(&mut self, _new_size: u64) -> Result<(), wasmer_wasix::FsError> { + Err(wasmer_wasix::FsError::PermissionDenied) + } + + fn unlink(&mut self) -> Result<(), wasmer_wasix::FsError> { + Ok(()) + } + + fn poll_read_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(0)) + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(8192)) + } +} + +impl AsyncRead for CaptureFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + _buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for CaptureFile { + fn poll_write( + mut self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(self.write(buf)) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncSeek for CaptureFile { + fn start_seek(self: Pin<&mut Self>, _position: std::io::SeekFrom) -> std::io::Result<()> { + Ok(()) + } + + fn poll_complete( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(0)) + } +} + +impl Read for CaptureFile { + fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { + Ok(0) + } +} + +impl Write for CaptureFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buffer + .lock() + .expect("capture lock poisoned") + .extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Seek for CaptureFile { + fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result { + Ok(0) + } +} + +#[cfg(all(test, feature = "extensions"))] +mod tests { + use super::*; + use crate::pglite::Pglite; + use crate::pglite::extensions; + use crate::pglite::server::PgliteServer; + use serde_json::json; + use sqlx::{Connection, Executor, Row}; + + #[test] + fn pg_dump_options_reject_managed_args() { + for arg in [ + "-f", + "-f/tmp/out.sql", + "--file", + "--file=/tmp/out.sql", + "-F", + "-Fc", + "--format", + "--format=custom", + "-h", + "-hlocalhost", + "--host=localhost", + "-p", + "-p5432", + "--port=5432", + "-U", + "-Upostgres", + "--username=postgres", + "-d", + "-dpostgres", + "--dbname=postgres", + "-j", + "-j2", + "--jobs=2", + ] { + let err = PgDumpOptions::new() + .arg(arg) + .validate() + .expect_err("managed pg_dump arg should be rejected"); + assert!( + err.to_string().contains("conflicts with pglite-oxide"), + "unexpected error for {arg}: {err:#}" + ); + } + } + + #[test] + fn pg_dump_options_allow_dump_shaping_args() -> Result<()> { + PgDumpOptions::new() + .args([ + "--schema-only", + "--quote-all-identifiers", + "-n", + "public", + "-t", + "dump_items", + ]) + .validate() + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn pg_dump_round_trip_plain_sql() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("connect to PGlite server")?; + conn.execute( + "CREATE TABLE dump_items(id INTEGER PRIMARY KEY, value TEXT); + CREATE INDEX dump_items_value_idx ON dump_items(value); + CREATE SEQUENCE dump_items_seq START WITH 10; + CREATE VIEW dump_item_values AS SELECT value FROM dump_items; + INSERT INTO dump_items(id, value) VALUES (1, 'alpha'), (2, 'beta'); + SELECT nextval('dump_items_seq');", + ) + .await + .context("seed pg_dump source data")?; + drop(conn); + + let (server, dump) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::default())?; + Ok((server, dump)) + }) + .await + .context("join pg_dump task")??; + + assert!(dump.contains("PostgreSQL database dump")); + assert!( + dump.contains("CREATE TABLE public.dump_items"), + "dump did not contain dump_items table DDL:\n{dump}" + ); + assert!(dump.contains("CREATE INDEX dump_items_value_idx")); + assert!(dump.contains("CREATE SEQUENCE public.dump_items_seq")); + assert!(dump.contains("CREATE VIEW public.dump_item_values")); + assert!(dump.contains("INSERT INTO")); + + let (server, schema_only) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + Ok((server, dump)) + }) + .await + .context("join schema-only pg_dump task")??; + assert!(schema_only.contains("CREATE TABLE public.dump_items")); + assert!( + !schema_only.contains("INSERT INTO public.dump_items"), + "schema-only dump unexpectedly contained data:\n{schema_only}" + ); + + let (server, quoted) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::new().arg("--quote-all-identifiers"))?; + Ok((server, dump)) + }) + .await + .context("join quoted pg_dump task")??; + assert!(quoted.contains("CREATE TABLE \"public\".\"dump_items\"")); + assert!(quoted.contains("INSERT INTO \"public\".\"dump_items\"")); + + let mut usable = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("reconnect after pg_dump")?; + let row = sqlx::query("SELECT count(*)::int4 AS count FROM public.dump_items") + .fetch_one(&mut usable) + .await + .context("server should remain usable after pg_dump")?; + assert_eq!(row.try_get::("count")?, 2); + usable.close().await?; + + server.shutdown()?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut restored = Pglite::builder().temporary().open()?; + restored.exec(&dump, None).context("restore pg_dump SQL")?; + let result = restored.query( + "SELECT value FROM public.dump_items WHERE id = $1", + &[json!(2)], + None, + )?; + let value = result + .rows + .first() + .and_then(|row| row.get("value")) + .cloned(); + assert_eq!(value, Some(json!("beta"))); + let view = restored.query( + "SELECT count(*)::int AS count FROM public.dump_item_values", + &[], + None, + )?; + assert_eq!(view.rows[0]["count"], json!(2)); + let sequence = restored.query( + "SELECT nextval('public.dump_items_seq')::int AS next_value", + &[], + None, + )?; + assert_eq!(sequence.rows[0]["next_value"], json!(11)); + restored.close()?; + Ok(()) + }) + .await + .context("join restore task")??; + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn pg_dump_round_trip_vector_extension() -> Result<()> { + let server = PgliteServer::builder() + .temporary() + .extension(extensions::VECTOR) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("connect to extension-enabled PGlite server")?; + conn.execute( + "CREATE TABLE vector_dump_items(id INTEGER PRIMARY KEY, embedding vector(3)); + INSERT INTO vector_dump_items(id, embedding) VALUES (1, '[1,2,3]');", + ) + .await + .context("seed vector pg_dump source data")?; + drop(conn); + + let (server, dump) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::default())?; + Ok((server, dump)) + }) + .await + .context("join vector pg_dump task")??; + server.shutdown()?; + + assert!( + dump.contains("CREATE EXTENSION IF NOT EXISTS vector"), + "dump did not contain vector extension DDL:\n{dump}" + ); + assert!(dump.contains("CREATE TABLE public.vector_dump_items")); + assert!(dump.contains("'[1,2,3]'")); + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut restored = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + restored + .exec(&dump, None) + .context("restore vector dump SQL")?; + let result = restored.query( + "SELECT embedding <-> '[1,2,4]'::vector AS distance \ + FROM public.vector_dump_items WHERE id = $1", + &[json!(1)], + None, + )?; + let distance = result + .rows + .first() + .and_then(|row| row.get("distance")) + .and_then(|value| value.as_f64()); + assert_eq!(distance, Some(1.0)); + restored.close()?; + Ok(()) + }) + .await + .context("join vector restore task")??; + Ok(()) + } + + #[test] + fn direct_pg_dump_public_api_round_trip() -> Result<()> { + let mut db = Pglite::temporary()?; + db.exec("CREATE TABLE direct_dump_items(value TEXT)", None)?; + db.exec("INSERT INTO direct_dump_items VALUES ('alpha')", None)?; + + let mismatched_database = db + .dump_sql(PgDumpOptions::new().database("other_database")) + .expect_err("direct pg_dump should reject database switching"); + assert!( + mismatched_database + .to_string() + .contains("already-open embedded backend database"), + "unexpected direct pg_dump database mismatch error: {mismatched_database:#}" + ); + + let dump = db.dump_sql(PgDumpOptions::new())?; + assert!(dump.contains("CREATE TABLE public.direct_dump_items")); + assert!(dump.contains("INSERT INTO")); + let source_still_usable = db.query( + "SELECT count(*)::int AS count FROM direct_dump_items", + &[], + None, + )?; + assert_eq!(source_still_usable.rows[0]["count"], json!(1)); + + let mut restored = Pglite::temporary()?; + restored.exec(&dump, None)?; + let result = restored.query("SELECT value FROM public.direct_dump_items", &[], None)?; + assert_eq!(result.rows[0]["value"], json!("alpha")); + + restored.close()?; + db.close()?; + Ok(()) + } + + #[test] + fn direct_pg_dump_round_trip_vector_extension() -> Result<()> { + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.exec( + "CREATE TABLE direct_vector_dump_items(id INTEGER PRIMARY KEY, embedding vector(3)); + INSERT INTO direct_vector_dump_items(id, embedding) VALUES (1, '[1,2,3]');", + None, + )?; + + let dump = db.dump_sql(PgDumpOptions::new())?; + assert!(dump.contains("CREATE EXTENSION IF NOT EXISTS vector")); + assert!(dump.contains("CREATE TABLE public.direct_vector_dump_items")); + + let mut restored = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + restored.exec(&dump, None)?; + let result = restored.query( + "SELECT embedding <-> '[1,2,4]'::vector AS distance \ + FROM public.direct_vector_dump_items WHERE id = $1", + &[json!(1)], + None, + )?; + assert_eq!(result.rows[0]["distance"], json!(1.0)); + + restored.close()?; + db.close()?; + Ok(()) + } +} diff --git a/src/pglite/postgres_mod.rs b/src/pglite/postgres_mod.rs index 0a567657..1770411e 100644 --- a/src/pglite/postgres_mod.rs +++ b/src/pglite/postgres_mod.rs @@ -1,523 +1,3358 @@ -use anyhow::{Context, Result, anyhow, bail, ensure}; -use directories::ProjectDirs; -use getrandom::fill as fill_random; -use sha2::{Digest, Sha256}; +#[cfg(debug_assertions)] +use std::cell::Cell; +use std::collections::{HashSet, VecDeque}; use std::fmt; use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, Mutex}; +use std::future::Future; +use std::io::{self, Read, Write}; +use std::path::{Component, Path, PathBuf}; +use std::pin::Pin; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::task::{Context as TaskContext, Poll}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, ensure}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use tokio::io::ReadBuf; +use tokio::runtime::Runtime as TokioRuntime; use tracing::warn; -use wasmtime::OptLevel; -use wasmtime::{ - Config, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, WasmParams, WasmResults, -}; -use wasmtime_wasi::p1::{WasiP1Ctx, add_to_linker_sync}; -use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder}; - -use super::base::PglitePaths; +use wasmer::{Engine, Instance, Module, Store, TypedFunction, WasmTypeList}; +use wasmer_config::package::{PackageHash, PackageId}; +use wasmer_types::ModuleHash; +use wasmer_wasix::bin_factory::{BinaryPackage, BinaryPackageCommand, spawn_exec}; +use wasmer_wasix::fs::WasiFsRoot; +use wasmer_wasix::runners::wasi::{PackageOrHash, RuntimeOrEngine, WasiRunner}; +use wasmer_wasix::runtime::module_cache::ModuleCache; +use wasmer_wasix::runtime::module_cache::SharedCache; +use wasmer_wasix::runtime::task_manager::VirtualTaskManagerExt; +use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager; +use wasmer_wasix::runtime::{PluggableRuntime, Runtime}; +use wasmer_wasix::virtual_fs::null_file::NullFile; +use wasmer_wasix::{WasiError, WasiFunctionEnv, virtual_fs}; +use webc::metadata::Command as WebcCommand; +use webc::metadata::annotations::{WASI_RUNNER_URI, Wasi}; + +use super::aot; +use super::base::{PglitePaths, RuntimeLayout}; +use super::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use super::extensions::Extension; +use super::sync_host_fs::SyncHostFileSystem; +use super::timing; + +const PGLITE_EXE_PATH: &str = "/bin/pglite"; +const PGDATA_DIR: &str = "/base"; +const WASM_PREFIX: &str = "/"; +const RUNTIME_SIDE_MODULES: &[(&str, &str)] = &[ + ("plpgsql.so", "runtime-support:plpgsql"), + ("dict_snowball.so", "runtime-support:dict_snowball"), +]; +const PGLITE_EXIT_ALIVE: i32 = 99; +const POSTGRES_MAIN_LONGJMP: i32 = 100; + +#[derive(Debug, Default)] +struct TailCaptureState { + bytes: VecDeque, +} -const WASM_PREFIX: &str = "/tmp/pglite"; -const PGDATA_DIR: &str = "/tmp/pglite/base"; -const WASMTIME_CACHE_VERSION: &str = "wasmtime-44"; -const WASMTIME_CONFIG_ID: &str = "opt-none-wasi-p1-v1"; +#[derive(Debug, Clone)] +struct TailCaptureFile { + inner: Arc>, + limit: usize, +} -pub struct PostgresMod { - _engine: Engine, - store: Store, - _instance: Instance, - memory: Memory, - exports: Exports, - paths: PglitePaths, - transport: TransportMode, - wire_enabled: bool, +#[derive(Debug, Clone)] +struct TailCaptureHandle { + inner: Arc>, } -enum TransportMode { - Cma { - buffer_addr: usize, - buffer_len: usize, - }, - File, +impl TailCaptureFile { + fn new(limit: usize) -> (Self, TailCaptureHandle) { + let inner = Arc::new(Mutex::new(TailCaptureState::default())); + ( + Self { + inner: inner.clone(), + limit, + }, + TailCaptureHandle { inner }, + ) + } + + fn push_tail(&self, bytes: &[u8]) { + let Ok(mut state) = self.inner.lock() else { + return; + }; + for byte in bytes { + state.bytes.push_back(*byte); + while state.bytes.len() > self.limit { + state.bytes.pop_front(); + } + } + } } -struct State { - wasi: WasiP1Ctx, +impl TailCaptureHandle { + fn text(&self) -> String { + let Ok(state) = self.inner.lock() else { + return "".to_owned(); + }; + let bytes = state.bytes.iter().copied().collect::>(); + String::from_utf8_lossy(&bytes).into_owned() + } } -static ENGINE: LazyLock = LazyLock::new(build_engine); -static MODULE_CACHE: LazyLock>> = - LazyLock::new(|| Mutex::new(std::collections::HashMap::new())); +impl virtual_fs::AsyncSeek for TailCaptureFile { + fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { + Ok(()) + } -fn with_wasmtime_context( - result: std::result::Result, - context: impl fmt::Display, -) -> Result { - result.map_err(|err| anyhow!("{context}: {err}")) + fn poll_complete(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(0)) + } } -fn build_engine() -> Engine { - let mut config = Config::new(); +impl virtual_fs::AsyncRead for TailCaptureFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} - config.cranelift_opt_level(OptLevel::None); +impl virtual_fs::AsyncWrite for TailCaptureFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + self.push_tail(buf); + Poll::Ready(Ok(buf.len())) + } - #[cfg(feature = "runtime-cache")] - match wasmtime::Cache::new(wasmtime::CacheConfig::new()) { - Ok(cache) => { - config.cache(Some(cache)); - } - Err(err) => { - warn!("failed to enable Wasmtime compile cache: {err}"); + fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + let mut total = 0; + for buf in bufs { + self.push_tail(buf); + total += buf.len(); } + Poll::Ready(Ok(total)) + } + + fn is_write_vectored(&self) -> bool { + true + } +} + +#[async_trait::async_trait] +impl virtual_fs::VirtualFile for TailCaptureFile { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + self.inner + .lock() + .map(|state| state.bytes.len() as u64) + .unwrap_or(0) + } + + fn set_len(&mut self, _new_size: u64) -> virtual_fs::Result<()> { + Ok(()) + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + Ok(()) + } + + fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(0)) } - Engine::new(&config).expect("failed to create Wasmtime engine") + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(self.limit)) + } +} +const BACKEND_C_TIMINGS: &[(i32, &str)] = &[ + (1, "postgres.backend.c.main_pre"), + (2, "postgres.backend.c.restart_single_user_main"), + (3, "postgres.backend.c.async_single_user_main"), + (4, "postgres.backend.c.standalone_process"), + (5, "postgres.backend.c.guc_init"), + (6, "postgres.backend.c.switch_parse"), + (7, "postgres.backend.c.config_files"), + (8, "postgres.backend.c.data_dir_lock"), + (9, "postgres.backend.c.control_file"), + (10, "postgres.backend.c.preload_libraries"), + (11, "postgres.backend.c.shared_memory"), + (12, "postgres.backend.c.base_init"), + (13, "postgres.backend.c.init_postgres"), + (14, "postgres.backend.c.post_init"), + (15, "postgres.backend.c.message_contexts"), + (16, "postgres.backend.c.postmaster_environment"), + (17, "postgres.backend.c.init_proc_phase2"), + (18, "postgres.backend.c.startup_xlog"), + (19, "postgres.backend.c.relcache_catcache_init"), + (20, "postgres.backend.c.transaction_snapshot"), + (21, "postgres.backend.c.session_user"), + (22, "postgres.backend.c.database_lookup"), + (23, "postgres.backend.c.database_lock_recheck"), + (24, "postgres.backend.c.database_path"), + (25, "postgres.backend.c.relcache_phase3"), + (26, "postgres.backend.c.check_my_database"), + (27, "postgres.backend.c.startup_options"), + (28, "postgres.backend.c.process_settings"), + (29, "postgres.backend.c.session_initialization"), + (30, "postgres.backend.c.session_preload_libraries"), + (31, "postgres.backend.c.init_max_backends"), + (32, "postgres.backend.c.create_shared_memory"), + (33, "postgres.backend.c.init_process"), + (34, "postgres.backend.c.relation_cache_phase3"), + (35, "postgres.backend.c.initialize_acl"), + (36, "postgres.backend.c.exec_simple_query"), + (37, "postgres.backend.c.exec_start_xact"), + (38, "postgres.backend.c.exec_drop_unnamed"), + (39, "postgres.backend.c.exec_parse"), + (40, "postgres.backend.c.exec_snapshot"), + (41, "postgres.backend.c.exec_analyze_rewrite"), + (42, "postgres.backend.c.exec_plan"), + (43, "postgres.backend.c.exec_portal_start"), + (44, "postgres.backend.c.exec_dest_receiver"), + (45, "postgres.backend.c.exec_portal_run"), + (46, "postgres.backend.c.exec_finish_xact"), + (47, "postgres.backend.c.exec_command_counter"), + (48, "postgres.backend.c.exec_end_command"), +]; + +static FS_TRACE: FsTraceState = FsTraceState::new(); + +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FsTraceSnapshot { + enabled: bool, + open_count: u64, + read_count: u64, + read_bytes: u64, + write_count: u64, + write_bytes: u64, + seek_count: u64, + metadata_count: u64, + read_dir_count: u64, + create_dir_count: u64, + remove_file_count: u64, + remove_dir_count: u64, + rename_count: u64, + set_len_count: u64, + unlink_count: u64, + total_elapsed_micros: u64, + read_elapsed_micros: u64, + write_elapsed_micros: u64, + seek_elapsed_micros: u64, } -fn module_cache_key(bytes: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(bytes); - let wasm_sha256 = format!("{:x}", hasher.finalize()); - let target = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH); - format!("{WASMTIME_CACHE_VERSION}-{target}-{WASMTIME_CONFIG_ID}-{wasm_sha256}") +struct FsTraceState { + open_count: AtomicU64, + read_count: AtomicU64, + read_bytes: AtomicU64, + write_count: AtomicU64, + write_bytes: AtomicU64, + seek_count: AtomicU64, + metadata_count: AtomicU64, + read_dir_count: AtomicU64, + create_dir_count: AtomicU64, + remove_file_count: AtomicU64, + remove_dir_count: AtomicU64, + rename_count: AtomicU64, + set_len_count: AtomicU64, + unlink_count: AtomicU64, + total_elapsed_micros: AtomicU64, + read_elapsed_micros: AtomicU64, + write_elapsed_micros: AtomicU64, + seek_elapsed_micros: AtomicU64, } -fn load_module(module_path: &Path) -> Result<(Engine, Module)> { - let bytes = fs::read(module_path) - .with_context(|| format!("failed to read {}", module_path.display()))?; - let key = module_cache_key(&bytes); - let engine = ENGINE.clone(); +impl FsTraceState { + const fn new() -> Self { + Self { + open_count: AtomicU64::new(0), + read_count: AtomicU64::new(0), + read_bytes: AtomicU64::new(0), + write_count: AtomicU64::new(0), + write_bytes: AtomicU64::new(0), + seek_count: AtomicU64::new(0), + metadata_count: AtomicU64::new(0), + read_dir_count: AtomicU64::new(0), + create_dir_count: AtomicU64::new(0), + remove_file_count: AtomicU64::new(0), + remove_dir_count: AtomicU64::new(0), + rename_count: AtomicU64::new(0), + set_len_count: AtomicU64::new(0), + unlink_count: AtomicU64::new(0), + total_elapsed_micros: AtomicU64::new(0), + read_elapsed_micros: AtomicU64::new(0), + write_elapsed_micros: AtomicU64::new(0), + seek_elapsed_micros: AtomicU64::new(0), + } + } - if let Some(module) = MODULE_CACHE - .lock() - .map_err(|err| anyhow!("module cache lock poisoned: {err}"))? - .get(&key) - .cloned() - { - return Ok((engine, module)); + fn reset(&self) { + for counter in [ + &self.open_count, + &self.read_count, + &self.read_bytes, + &self.write_count, + &self.write_bytes, + &self.seek_count, + &self.metadata_count, + &self.read_dir_count, + &self.create_dir_count, + &self.remove_file_count, + &self.remove_dir_count, + &self.rename_count, + &self.set_len_count, + &self.unlink_count, + &self.total_elapsed_micros, + &self.read_elapsed_micros, + &self.write_elapsed_micros, + &self.seek_elapsed_micros, + ] { + counter.store(0, Ordering::Relaxed); + } + } + + fn record_total(&self, elapsed: Duration) { + self.total_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); } - let module = match load_serialized_module(&engine, &key) { - Ok(Some(module)) => module, - Ok(None) => compile_and_cache_module(&engine, module_path, &bytes, &key)?, - Err(err) => { - warn!("failed to read compiled module cache: {err:#}"); - compile_module(&engine, module_path, &bytes)? + fn snapshot(&self) -> FsTraceSnapshot { + FsTraceSnapshot { + enabled: fs_trace_enabled(), + open_count: self.open_count.load(Ordering::Relaxed), + read_count: self.read_count.load(Ordering::Relaxed), + read_bytes: self.read_bytes.load(Ordering::Relaxed), + write_count: self.write_count.load(Ordering::Relaxed), + write_bytes: self.write_bytes.load(Ordering::Relaxed), + seek_count: self.seek_count.load(Ordering::Relaxed), + metadata_count: self.metadata_count.load(Ordering::Relaxed), + read_dir_count: self.read_dir_count.load(Ordering::Relaxed), + create_dir_count: self.create_dir_count.load(Ordering::Relaxed), + remove_file_count: self.remove_file_count.load(Ordering::Relaxed), + remove_dir_count: self.remove_dir_count.load(Ordering::Relaxed), + rename_count: self.rename_count.load(Ordering::Relaxed), + set_len_count: self.set_len_count.load(Ordering::Relaxed), + unlink_count: self.unlink_count.load(Ordering::Relaxed), + total_elapsed_micros: self.total_elapsed_micros.load(Ordering::Relaxed), + read_elapsed_micros: self.read_elapsed_micros.load(Ordering::Relaxed), + write_elapsed_micros: self.write_elapsed_micros.load(Ordering::Relaxed), + seek_elapsed_micros: self.seek_elapsed_micros.load(Ordering::Relaxed), } - }; - MODULE_CACHE - .lock() - .map_err(|err| anyhow!("module cache lock poisoned: {err}"))? - .insert(key, module.clone()); + } +} - Ok((engine, module)) +pub fn reset_fs_trace() { + FS_TRACE.reset(); } -fn load_serialized_module(engine: &Engine, key: &str) -> Result> { - let Some(cache_path) = serialized_module_cache_path(key) else { - return Ok(None); - }; - if !cache_path.exists() { - return Ok(None); +pub fn fs_trace_snapshot() -> FsTraceSnapshot { + FS_TRACE.snapshot() +} +static WASIX_PROCESS_RUNTIME: OnceLock, String>> = + OnceLock::new(); +static SEEDED_SIDE_MODULES: OnceLock>> = OnceLock::new(); + +struct WasixProcessRuntime { + tokio_runtime: Arc, + wasix_module_cache: Arc, + wasix_runtime: Arc, +} + +pub struct PostgresMod { + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + engine: Engine, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + tokio_runtime: Arc, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + wasix_module_cache: Arc, + _wasix_runtime: Arc, + store: Store, + _instance: Instance, + env: WasiFunctionEnv, + guest_allocator: GuestAllocator, + io: WasixPgliteIo, + lifecycle: PgliteLifecycleExports, + protocol: WasixProtocolExports, + protocol_stdio: Option, + protocol_stdio_file: ProtocolStdioFile, + wasi_stderr: TailCaptureHandle, + protocol_stdio_attachment: Option, + paths: PglitePaths, + pgdata_template_root: Option, + startup_config: StartupConfig, + startup_response: Option>, + cluster_ready: bool, + backend_started: bool, + started: bool, +} + +pub(crate) struct StartupProtocolResponse { + pub(crate) output: Vec, + pub(crate) accepted: bool, +} + +#[derive(Debug)] +pub(crate) struct StartupErrorResponse { + output: Vec, + summary: String, +} + +impl StartupErrorResponse { + fn new(output: Vec) -> Self { + let summary = summarize_protocol(&output); + Self { output, summary } } - match deserialize_trusted_module_cache_file(engine, &cache_path) { - Ok(module) => Ok(Some(module)), - Err(err) => { - warn!( - "ignoring invalid compiled module cache {}: {err}", - cache_path.display() - ); - let _ = fs::remove_file(&cache_path); - Ok(None) - } + pub(crate) fn output(&self) -> &[u8] { + &self.output } } -#[allow(unsafe_code)] -fn deserialize_trusted_module_cache_file( - engine: &Engine, - cache_path: &Path, -) -> wasmtime::Result { - // SAFETY: Wasmtime compiled modules are only deserialized from this crate's - // private cache directory, and the file name is keyed by the runtime WASM - // SHA-256, Wasmtime major version, target, and config id. Corrupt or stale - // files are discarded and rebuilt by the caller. - unsafe { Module::deserialize_file(engine, cache_path) } +impl fmt::Display for StartupErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Postgres startup returned a protocol ErrorResponse: {}", + self.summary + ) + } } -fn compile_and_cache_module( - engine: &Engine, - module_path: &Path, - bytes: &[u8], - key: &str, -) -> Result { - let module = compile_module(engine, module_path, bytes)?; - let Some(cache_path) = serialized_module_cache_path(key) else { - return Ok(module); - }; +impl std::error::Error for StartupErrorResponse {} - if let Err(err) = write_serialized_module(&module, &cache_path) { - warn!( - "failed to write compiled module cache {}: {err:#}", - cache_path.display() - ); +pub(crate) fn startup_error_response_output(err: &anyhow::Error) -> Option<&[u8]> { + err.downcast_ref::() + .map(StartupErrorResponse::output) +} + +pub(crate) enum ProtocolPumpOutcome { + Buffered(Vec), + Streamed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ProtocolTransportMode { + Buffered = 0, + Stream = 1, + Hybrid = 2, +} + +impl ProtocolTransportMode { + fn from_i32(value: i32) -> Result { + match value { + 0 => Ok(Self::Buffered), + 1 => Ok(Self::Stream), + 2 => Ok(Self::Hybrid), + other => anyhow::bail!("invalid WASIX protocol transport mode {other}"), + } } - Ok(module) } -fn compile_module(engine: &Engine, module_path: &Path, bytes: &[u8]) -> Result { - with_wasmtime_context( - Module::from_binary(engine, bytes), - format!("failed to compile {}", module_path.display()), - ) +struct PgliteLifecycleExports { + wasi_start: TypedFunction<(), ()>, + set_force_host_error_recovery: Option>, + set_active: TypedFunction, + start_pglite: TypedFunction<(), ()>, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + run_atexit_funcs: Option>, + backend_timing_reset: Option>, + backend_timing_elapsed_us: Option>, } -fn serialized_module_cache_path(key: &str) -> Option { - ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide").map(|dirs| { - dirs.cache_dir() - .join("cwasm") - .join(format!("pglite-{key}.cwasm")) - }) +struct WasixProtocolExports { + get_port: TypedFunction<(), i32>, + process_startup: TypedFunction<(i32, i32, i32), i32>, + send_conn_data: TypedFunction<(), ()>, + pq_flush: TypedFunction<(), ()>, + pq_buffer_remaining_data: TypedFunction<(), i32>, + main_loop: TypedFunction<(), ()>, + send_ready: TypedFunction<(), ()>, + recover_error: TypedFunction<(), ()>, } -fn write_serialized_module(module: &Module, cache_path: &Path) -> Result<()> { - if let Some(parent) = cache_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create module cache dir {}", parent.display()))?; - } - let bytes = with_wasmtime_context(module.serialize(), "serialize compiled pglite module")?; - let tmp_path = cache_path.with_extension("cwasm.tmp"); - fs::write(&tmp_path, bytes) - .with_context(|| format!("write compiled module cache {}", tmp_path.display()))?; - fs::rename(&tmp_path, cache_path).with_context(|| { - format!( - "promote compiled module cache {} -> {}", - tmp_path.display(), - cache_path.display() - ) - })?; - Ok(()) +#[derive(Clone)] +struct WasixProtocolStdioExports { + set_protocol_transport: TypedFunction, + protocol_stream_active: TypedFunction<(), i32>, } -struct Exports { - pgl_initdb: TypedFunc<(), i32>, - pgl_backend: TypedFunc<(), ()>, - use_wire: TypedFunc, - interactive_write: TypedFunc, - interactive_one: TypedFunc<(), ()>, - interactive_read: TypedFunc<(), i32>, - get_channel: TypedFunc<(), i32>, - get_buffer_size: TypedFunc, - get_buffer_addr: TypedFunc, +struct WasixPgliteIo { + input_reset: TypedFunction<(), i32>, + input_write: TypedFunction<(i32, i32), i32>, + input_available: TypedFunction<(), i32>, + output_reset: TypedFunction<(), i32>, + output_len: TypedFunction<(), i32>, + output_read: TypedFunction<(i32, i32), i32>, } -impl PostgresMod { - pub(crate) fn preload_module(module_path: &Path) -> Result<()> { - let _ = load_module(module_path)?; - Ok(()) +struct GuestAllocator { + malloc: TypedFunction, + free: TypedFunction, + #[cfg(debug_assertions)] + allocations: Cell, + #[cfg(debug_assertions)] + frees: Cell, +} + +pub(crate) trait ProtocolStream: Read + Write + Send { + fn read_ready(&mut self) -> io::Result; +} + +#[derive(Clone)] +struct ProtocolStdioFile { + state: Arc, +} + +struct ProtocolStdioState { + inner: Mutex, +} + +#[derive(Default)] +struct ProtocolStdioInner { + stream: Option>, + prefix: Vec, + prefix_offset: usize, +} + +struct ProtocolStdioAttachment { + file: ProtocolStdioFile, +} + +impl ProtocolStdioFile { + fn new() -> Self { + Self { + state: Arc::new(ProtocolStdioState { + inner: Mutex::new(ProtocolStdioInner::default()), + }), + } } - pub fn new(paths: PglitePaths) -> Result { - let module_path = paths.pgroot.join("pglite/bin/pglite.wasi"); + fn attach(&self, stream: S) -> Result + where + S: ProtocolStream + 'static, + { + let mut guard = self + .state + .inner + .lock() + .map_err(|_| anyhow::anyhow!("protocol stdio lock poisoned"))?; + ensure!( + guard.stream.is_none(), + "WASIX protocol stdio stream is already attached" + ); + guard.stream = Some(Box::new(stream)); + guard.prefix.clear(); + guard.prefix_offset = 0; + Ok(ProtocolStdioAttachment { file: self.clone() }) + } - if !module_path.exists() { - return Err(anyhow!( - "pglite.wasi binary not found at {}", - module_path.display() - )); + fn detach(&self) { + if let Ok(mut guard) = self.state.inner.lock() { + guard.stream = None; + guard.prefix.clear(); + guard.prefix_offset = 0; } + } - let (engine, module) = load_module(&module_path)?; + fn set_prefix(&self, prefix: Vec) -> Result<()> { + let mut guard = self + .state + .inner + .lock() + .map_err(|_| anyhow::anyhow!("protocol stdio lock poisoned"))?; + guard.prefix = prefix; + guard.prefix_offset = 0; + Ok(()) + } - let mut linker: Linker = Linker::new(&engine); - with_wasmtime_context( - add_to_linker_sync(&mut linker, |state| &mut state.wasi), - "failed to add WASI to linker", - )?; + fn clear_prefix(&self) -> Result<()> { + self.set_prefix(Vec::new()) + } - let wasi = build_wasi_ctx(&paths)?; - let mut store = Store::new(&engine, State { wasi }); + fn with_inner( + &self, + f: impl FnOnce(&mut ProtocolStdioInner) -> io::Result, + ) -> io::Result { + let mut guard = self + .state + .inner + .lock() + .map_err(|_| io::Error::other("protocol stdio lock poisoned"))?; + f(&mut guard) + } +} - let instance = with_wasmtime_context( - linker.instantiate(&mut store, &module), - "failed to instantiate pglite module", - )?; +impl Drop for ProtocolStdioAttachment { + fn drop(&mut self) { + self.file.detach(); + } +} - let memory = instance - .get_memory(&mut store, "memory") - .context("pglite module is missing exported memory")?; +impl fmt::Debug for ProtocolStdioFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProtocolStdioFile").finish_non_exhaustive() + } +} - if let Ok(start) = instance.get_typed_func::<(), ()>(&mut store, "_start") - && let Err(err) = start.call(&mut store, ()) - { - warn!("_start trapped during startup and was ignored: {err}"); +impl virtual_fs::VirtualFile for ProtocolStdioFile { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + 0 + } + + fn set_len(&mut self, _new_size: u64) -> virtual_fs::Result<()> { + Err(virtual_fs::FsError::PermissionDenied) + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + Ok(()) + } + + fn is_open(&self) -> bool { + self.state + .inner + .lock() + .map(|inner| inner.stream.is_some()) + .unwrap_or(false) + } + + fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + match self.with_inner(|inner| { + if inner.prefix_offset < inner.prefix.len() { + return Ok(true); + } + let stream = inner.stream.as_mut().ok_or_else(|| { + io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + ) + })?; + stream.read_ready() + }) { + Ok(true) => Poll::Ready(Ok(1)), + Ok(false) => Poll::Pending, + Err(err) => Poll::Ready(Err(err)), } + } - let exports = Exports::load(&mut store, &instance)?; + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + match self.with_inner(|inner| { + if inner.stream.is_some() { + Ok(8192) + } else { + Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + )) + } + }) { + Ok(ready) => Poll::Ready(Ok(ready)), + Err(err) => Poll::Ready(Err(err)), + } + } +} - let channel_id = with_wasmtime_context( - exports.get_channel.call(&mut store, ()), - "call _get_channel", - )?; - let transport = if channel_id >= 0 { - let addr = with_wasmtime_context( - exports.get_buffer_addr.call(&mut store, channel_id), - "call _get_buffer_addr", +impl virtual_fs::AsyncRead for ProtocolStdioFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + if buf.remaining() == 0 { + return Poll::Ready(Ok(())); + } + let read = self.with_inner(|inner| { + let unfilled = buf.initialize_unfilled(); + if inner.prefix_offset < inner.prefix.len() { + let remaining = &inner.prefix[inner.prefix_offset..]; + let read = remaining.len().min(unfilled.len()); + unfilled[..read].copy_from_slice(&remaining[..read]); + inner.prefix_offset += read; + if inner.prefix_offset == inner.prefix.len() { + inner.prefix.clear(); + inner.prefix_offset = 0; + } + return Ok(read); + } + let stream = inner.stream.as_mut().ok_or_else(|| { + io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + ) + })?; + stream.read(unfilled) + }); + match read { + Ok(read) => { + buf.advance(read); + Poll::Ready(Ok(())) + } + Err(err) => Poll::Ready(Err(err)), + } + } +} + +impl virtual_fs::AsyncWrite for ProtocolStdioFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + let written = self + .state + .inner + .lock() + .map_err(|_| io::Error::other("protocol stdio lock poisoned")) + .and_then(|mut inner| match inner.stream.as_mut() { + Some(stream) => stream.write(buf), + None => Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + )), + }); + Poll::Ready(written) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + let flushed = self + .state + .inner + .lock() + .map_err(|_| io::Error::other("protocol stdio lock poisoned")) + .and_then(|mut inner| match inner.stream.as_mut() { + Some(stream) => stream.flush(), + None => Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + )), + }); + Poll::Ready(flushed) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl virtual_fs::AsyncSeek for ProtocolStdioFile { + fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { + Ok(()) + } + + fn poll_complete(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(0)) + } +} + +impl PostgresMod { + pub(crate) fn preload_module(module_path: &std::path::Path) -> Result<()> { + let runtime_root = module_path + .parent() + .and_then(Path::parent) + .context("runtime module path must be under bin/pglite")?; + let (engine, _) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + preload_runtime_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + runtime_root, + ) + } + + pub(crate) fn new_prepared(paths: PglitePaths, runtime_layout: RuntimeLayout) -> Result { + Self::new_prepared_with_config( + paths, + runtime_layout, + PostgresConfig::default(), + StartupConfig::default(), + ) + } + + pub(crate) fn new_prepared_with_config( + paths: PglitePaths, + runtime_layout: RuntimeLayout, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + ) -> Result { + postgres_config.validate()?; + startup_config.validate()?; + ensure_runtime_dirs(&paths)?; + #[cfg(feature = "extensions")] + let runtime_root = runtime_layout.local_root.clone(); + let module_runtime_root = runtime_layout.module_root.clone(); + ensure!( + module_runtime_root.join("bin/pglite").exists(), + "WASIX PGlite executable not found at {}", + module_runtime_root.join("bin/pglite").display() + ); + + let (engine, module) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + { + let _phase = timing::phase("wasix.preload_runtime_side_modules"); + preload_runtime_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &module_runtime_root, )?; - let len = with_wasmtime_context( - exports.get_buffer_size.call(&mut store, channel_id), - "call _get_buffer_size", + } + #[cfg(feature = "extensions")] + { + let _phase = timing::phase("wasix.preload_installed_extension_side_modules"); + preload_installed_extension_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &runtime_root, )?; - ensure!(addr >= 0, "interactive buffer address is negative: {addr}"); - ensure!(len >= 0, "interactive buffer length is negative: {len}"); - TransportMode::Cma { - buffer_addr: addr as usize, - buffer_len: len as usize, - } - } else { - TransportMode::File + } + let mut store = Store::new(engine.clone()); + + let _phase = timing::phase("wasix.instance_create"); + let (instance, env, protocol_stdio_file, wasi_stderr) = + instantiate_wasix_module(WasixInstantiateInput { + runtime: &process_runtime.tokio_runtime, + wasix_runtime: &process_runtime.wasix_runtime, + store: &mut store, + paths: &paths, + runtime_layout: &runtime_layout, + postgres_config: &postgres_config, + startup_config: &startup_config, + module: module.clone(), + })?; + seed_exported_c_string_value(&mut store, &instance, &env, "my_exec_path", PGLITE_EXE_PATH)?; + + let (guest_allocator, io, lifecycle, protocol, protocol_stdio) = { + let _phase = timing::phase("wasix.export_load"); + let guest_allocator = GuestAllocator::load(&mut store, &instance)?; + let io = WasixPgliteIo::new(&mut store, &instance)?; + ensure_integrated_pglite_contract(&instance)?; + let lifecycle = PgliteLifecycleExports::load(&mut store, &instance)?; + let protocol = WasixProtocolExports::load(&mut store, &instance)?; + let protocol_stdio = WasixProtocolStdioExports::load(&mut store, &instance)?; + (guest_allocator, io, lifecycle, protocol, protocol_stdio) }; - Ok(Self { - _engine: engine, + let pg = Self { + engine, + tokio_runtime: process_runtime.tokio_runtime.clone(), + wasix_module_cache: process_runtime.wasix_module_cache.clone(), + _wasix_runtime: process_runtime.wasix_runtime.clone(), store, _instance: instance, - memory, - exports, + env, + guest_allocator, + io, + lifecycle, + protocol, + protocol_stdio, + protocol_stdio_file, + wasi_stderr, + protocol_stdio_attachment: None, paths, - transport, - wire_enabled: false, - }) + pgdata_template_root: runtime_layout.pgdata_template_root.clone(), + startup_config, + startup_response: None, + cluster_ready: false, + backend_started: false, + started: false, + }; + Ok(pg) } pub fn paths(&self) -> &PglitePaths { &self.paths } + pub(crate) fn pgdata_template_root(&self) -> Option<&Path> { + self.pgdata_template_root.as_deref() + } + + #[cfg(debug_assertions)] + pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { + self.guest_allocator.allocation_counts() + } + pub fn ensure_cluster(&mut self) -> Result<()> { - let had_cluster = self.paths.is_cluster_initialized(); - // PGlite uses this export for runtime setup as well as first-time - // cluster creation, so existing clusters still need the call. - let rc = self - .exports - .pgl_initdb - .call(&mut self.store, ()) - .map_err(|err| anyhow!("failed to execute _pgl_initdb: {err}"))?; - - if rc != 0 { - if self.paths.is_cluster_initialized() { - if !had_cluster { - warn!("_pgl_initdb returned status {rc}, but PG_VERSION exists; continuing"); - } - return Ok(()); - } - return Err(anyhow!("_pgl_initdb returned non-zero status: {}", rc)); - } + self.initialize_cluster()?; + self.start_backend() + } - if !self.paths.is_cluster_initialized() { - return Err(anyhow!( - "_pgl_initdb returned success but PG_VERSION is missing" - )); + pub fn initialize_cluster(&mut self) -> Result<()> { + if self.cluster_ready { + return Ok(()); } + ensure!( + self.paths.is_cluster_initialized(), + "PGDATA is not initialized; install the WASIX runtime assets and PGDATA template before opening" + ); + self.cluster_ready = true; Ok(()) } - pub fn buffer_addr(&self) -> Option { - match self.transport { - TransportMode::Cma { buffer_addr, .. } => Some(buffer_addr), - TransportMode::File => None, + fn start_backend(&mut self) -> Result<()> { + if self.backend_started { + return Ok(()); + } + let _phase = timing::phase("postgres.backend_start"); + self.reset_backend_c_timings()?; + self.configure_host_error_recovery()?; + { + let _phase = timing::phase("postgres.backend_start.set_active"); + self.lifecycle + .set_active + .call(&mut self.store, 1) + .context("pgl_setPGliteActive(1)")?; + } + { + let _phase = timing::phase("postgres.backend_start.single_user_main"); + match self.lifecycle.wasi_start.call(&mut self.store) { + Ok(()) => {} + Err(err) if runtime_error_exit_code(&err) == Some(PGLITE_EXIT_ALIVE) => {} + Err(err) => return self.startup_failure(err, "_start PGlite single-user backend"), + } } + if let Err(err) = self.lifecycle.start_pglite.call(&mut self.store) { + return self.startup_failure(err, "pgl_startPGlite"); + } + self.record_backend_c_timings()?; + self.backend_started = true; + Ok(()) } - pub fn buffer_len(&self) -> Option { - match self.transport { - TransportMode::Cma { buffer_len, .. } => Some(buffer_len), - TransportMode::File => None, - } + fn configure_host_error_recovery(&mut self) -> Result<()> { + let force = host_requires_process_exit_error_recovery(); + let Some(set_force) = &self.lifecycle.set_force_host_error_recovery else { + if force { + anyhow::bail!( + "WASIX runtime does not export pgl_set_force_host_error_recovery required by this host" + ); + } + return Ok(()); + }; + + set_force + .call(&mut self.store, i32::from(force)) + .context("pgl_set_force_host_error_recovery")?; + Ok(()) } - pub fn write_memory(&mut self, offset: usize, data: &[u8]) -> Result<()> { - self.memory - .write(&mut self.store, offset, data) - .with_context(|| format!("write {} bytes at 0x{offset:x}", data.len())) + fn startup_failure(&mut self, err: wasmer::RuntimeError, context: &str) -> Result<()> { + if let Some(output) = self.take_startup_output_after_failure() { + if protocol_response_contains_error(&output) { + return Err(StartupErrorResponse::new(output).into()); + } + return Err(err).context(format!( + "{context}{}", + self.startup_failure_detail(Some(&output)) + )); + } + Err(err).context(format!("{context}{}", self.startup_failure_detail(None))) } - pub fn read_memory(&mut self, offset: usize, buf: &mut [u8]) -> Result<()> { - self.memory - .read(&mut self.store, offset, buf) - .with_context(|| format!("read {} bytes at 0x{offset:x}", buf.len())) + fn take_startup_output_after_failure(&mut self) -> Option> { + let _ = self.protocol.pq_flush.call(&mut self.store); + match self + .io + .take_output(&mut self.store, &self.env, &self.guest_allocator) + { + Ok(output) if !output.is_empty() => Some(output), + Ok(_) => None, + Err(err) => { + warn!("failed to read startup output after backend failure: {err}"); + None + } + } } - pub fn interactive_write(&mut self, len: i32) -> Result<()> { - self.exports - .interactive_write - .call(&mut self.store, len) - .map_err(|err| anyhow!("call _interactive_write: {err}"))?; - Ok(()) + fn startup_failure_detail(&self, output: Option<&[u8]>) -> String { + let mut detail = String::new(); + let stderr = self.wasi_stderr.text(); + if !stderr.trim().is_empty() { + detail.push_str("\nWASIX stderr tail:\n"); + detail.push_str(stderr.trim_end()); + } + if let Some(output) = output { + detail.push_str("\nWASIX startup output tail:\n"); + detail.push_str(&format_output_tail(output)); + } + detail } - pub fn interactive_one(&mut self) -> Result<()> { - self.exports - .interactive_one - .call(&mut self.store, ()) - .map_err(|err| anyhow!("call _interactive_one: {err}"))?; + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + pub(crate) fn shutdown_backend(&mut self) -> Result<()> { + let _phase = timing::phase("postgres.backend_shutdown"); + self.lifecycle + .set_active + .call(&mut self.store, 0) + .context("pgl_setPGliteActive(0)")?; + if let Some(run_atexit_funcs) = &self.lifecycle.run_atexit_funcs { + run_atexit_funcs + .call(&mut self.store) + .context("pgl_run_atexit_funcs")?; + } + self.backend_started = false; + self.started = false; + self.startup_response = None; + self.cluster_ready = false; Ok(()) } - pub fn interactive_read(&mut self) -> Result { - self.exports - .interactive_read - .call(&mut self.store, ()) - .map_err(|err| anyhow!("call _interactive_read: {err}")) - } + fn record_backend_c_timings(&mut self) -> Result<()> { + let Some(elapsed) = &self.lifecycle.backend_timing_elapsed_us else { + return Ok(()); + }; - pub fn use_wire(&mut self, enabled: bool) -> Result<()> { - self.exports - .use_wire - .call(&mut self.store, if enabled { 1 } else { 0 }) - .map_err(|err| anyhow!("call _use_wire: {err}"))?; - self.wire_enabled = enabled; + for &(id, name) in BACKEND_C_TIMINGS { + let elapsed_micros = elapsed + .call(&mut self.store, id) + .with_context(|| format!("pgl_backend_timing_elapsed_us({id})"))?; + if elapsed_micros > 0 { + timing::record_phase_timing(name, Duration::from_micros(elapsed_micros as u64)); + } + } Ok(()) } - pub fn backend(&mut self) -> Result<()> { - self.exports - .pgl_backend - .call(&mut self.store, ()) - .map_err(|err| anyhow!("call _pgl_backend: {err}"))?; + fn reset_backend_c_timings(&mut self) -> Result<()> { + let Some(reset) = &self.lifecycle.backend_timing_reset else { + return Ok(()); + }; + + reset + .call(&mut self.store) + .context("pgl_backend_timing_reset")?; Ok(()) } -} -impl Exports { - fn load(store: &mut Store, instance: &Instance) -> Result { - fn get_typed( - store: &mut Store, - instance: &Instance, - names: &[&str], - ) -> Result> - where - P: WasmParams, - R: WasmResults, - { - for name in names { - if let Ok(func) = instance.get_typed_func::(&mut *store, name) { - return Ok(func); - } - } - bail!("missing expected export {:?}", names) - } + #[cfg(feature = "extensions")] + pub fn preload_extension_module(&self, extension: Extension) -> Result<()> { + let Some(module_file) = extension.native_module_file() else { + return Ok(()); + }; + let Some(aot_name) = extension.aot_name() else { + return Ok(()); + }; + let runtime_root = self.paths.runtime_root(); + let library = runtime_root + .join("lib") + .join("postgresql") + .join(module_file); + ensure!( + library.exists(), + "extension library for '{}' is not installed at {}", + extension.sql_name(), + library.display() + ); - let pgl_initdb = get_typed(store, instance, &["_pgl_initdb", "pgl_initdb"])?; - let pgl_backend = get_typed(store, instance, &["_pgl_backend", "pgl_backend"])?; - let use_wire = get_typed(store, instance, &["_use_wire", "use_wire"])?; - let interactive_write = get_typed( - store, - instance, - &["_interactive_write", "interactive_write"], + seed_side_module_cache( + &self.tokio_runtime, + &self.engine, + &self.wasix_module_cache, + &library, + aot_name, + &format!("extension '{}'", extension.sql_name()), )?; - let interactive_one = get_typed(store, instance, &["_interactive_one", "interactive_one"])?; - let interactive_read = - get_typed(store, instance, &["_interactive_read", "interactive_read"])?; - let get_channel = get_typed(store, instance, &["_get_channel", "get_channel"])?; - let get_buffer_size = get_typed(store, instance, &["_get_buffer_size", "get_buffer_size"])?; - let get_buffer_addr = get_typed(store, instance, &["_get_buffer_addr", "get_buffer_addr"])?; - - Ok(Self { - pgl_initdb, - pgl_backend, - use_wire, - interactive_write, - interactive_one, - interactive_read, - get_channel, - get_buffer_size, - get_buffer_addr, - }) + Ok(()) } -} -fn build_wasi_ctx(paths: &PglitePaths) -> Result { - ensure_runtime_dirs(paths)?; + #[cfg(feature = "extensions")] + pub(crate) fn preload_extension_module_from_paths( + paths: &PglitePaths, + extension: Extension, + ) -> Result<()> { + let Some(module_file) = extension.native_module_file() else { + return Ok(()); + }; + let Some(aot_name) = extension.aot_name() else { + return Ok(()); + }; + let runtime_root = paths.runtime_root(); + let library = runtime_root + .join("lib") + .join("postgresql") + .join(module_file); + ensure!( + library.exists(), + "extension library for '{}' is not installed at {}", + extension.sql_name(), + library.display() + ); - let mut builder = WasiCtxBuilder::new(); + let (engine, _) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + seed_side_module_cache( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &library, + aot_name, + &format!("extension '{}'", extension.sql_name()), + ) + } - builder - .env("PREFIX", WASM_PREFIX) - .env("PGDATA", PGDATA_DIR) - .env("PGUSER", "postgres") - .env("PGDATABASE", "template1") - .env("MODE", "REACT") - .env("REPL", "N") - .env("PGSYSCONFDIR", WASM_PREFIX) - .env("PGCLIENTENCODING", "UTF8") - .env("LC_CTYPE", "C.UTF-8") - .env("TZ", "UTC") - .env("PGTZ", "UTC") - .env("PG_COLOR", "never"); - - builder.arg(format!("PGDATA={}", PGDATA_DIR)); - builder.arg(format!("PREFIX={}", WASM_PREFIX)); - builder.arg("PGUSER=postgres"); - builder.arg("PGDATABASE=template1"); - builder.arg("MODE=REACT"); - builder.arg("REPL=N"); - - let host_tmp = paths.pgroot.clone(); - builder - .preopened_dir(&host_tmp, "/tmp", DirPerms::all(), FilePerms::all()) - .map_err(|err| anyhow!("failed to preopen {} as /tmp: {err}", host_tmp.display()))?; + pub(crate) fn run_split_initdb( + paths: &PglitePaths, + runtime_layout: &RuntimeLayout, + ) -> Result<()> { + run_split_initdb(paths, runtime_layout) + } - let home_path = paths.pgroot.join("home"); - if !home_path.exists() { - fs::create_dir_all(&home_path) - .with_context(|| format!("failed to create {}", home_path.display()))?; + pub fn send_protocol(&mut self, payload: &[u8]) -> Result> { + { + let _phase = timing::phase("postgres.protocol.ensure_started"); + self.start_protocol()?; + } + if payload.is_empty() { + return Ok(Vec::new()); + } + self.send_protocol_inner(payload) } - builder - .preopened_dir(&home_path, "/home", DirPerms::all(), FilePerms::all()) - .map_err(|err| anyhow!("failed to preopen {} as /home: {err}", home_path.display()))?; - builder - .preopened_dir( - &paths.pgdata, - "/tmp/pglite/base", - DirPerms::all(), - FilePerms::all(), + pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> + where + S: ProtocolStream + 'static, + { + ensure!( + self.protocol_stdio.is_some(), + "WASIX runtime does not export protocol stream transport" + ); + if self.protocol_stdio_attachment.is_none() { + let attachment = self.protocol_stdio_file.attach(stream)?; + self.protocol_stdio_attachment = Some(attachment); + } + Ok(()) + } + + pub(crate) fn set_protocol_stream_prefix(&mut self, prefix: Vec) -> Result<()> { + self.protocol_stdio_file.set_prefix(prefix) + } + + pub(crate) fn clear_protocol_stream_prefix(&mut self) -> Result<()> { + self.protocol_stdio_file.clear_prefix() + } + + pub(crate) fn send_protocol_pump( + &mut self, + payload: &[u8], + continuation_prefix: impl FnOnce() -> Vec, + ) -> Result { + { + let _phase = timing::phase("postgres.protocol.ensure_started"); + self.start_protocol()?; + } + if payload.is_empty() { + return Ok(ProtocolPumpOutcome::Buffered(Vec::new())); + } + ensure!( + self.protocol_stdio_attachment.is_some(), + "WASIX protocol pump requires an attached stream" + ); + let previous_mode = self.set_protocol_transport(ProtocolTransportMode::Hybrid)?; + ensure!( + previous_mode == ProtocolTransportMode::Buffered, + "WASIX protocol transport was not buffered before protocol pump" + ); + let result = self.send_protocol_inner(payload); + let active = self.protocol_stream_active().unwrap_or(false); + if active { + self.set_protocol_stream_prefix(continuation_prefix())?; + let stream_result = result.and_then(|_| self.serve_protocol_stream_inner()); + let restore_result = self.restore_protocol_transport(previous_mode); + let clear_result = self.clear_protocol_stream_prefix(); + stream_result.and(restore_result).and(clear_result)?; + Ok(ProtocolPumpOutcome::Streamed) + } else { + let output = result; + let restore_result = self.restore_protocol_transport(previous_mode); + restore_result?; + let output = output?; + Ok(ProtocolPumpOutcome::Buffered(output)) + } + } + + fn send_protocol_inner(&mut self, payload: &[u8]) -> Result> { + self.reset_backend_c_timings()?; + + { + let _phase = timing::phase("postgres.protocol.input_reset"); + self.io.reset(&mut self.store)?; + } + { + let _phase = timing::phase("postgres.protocol.input_write"); + self.io + .push_input(&mut self.store, &self.env, &self.guest_allocator, payload)?; + } + + { + let _phase = timing::phase("postgres.protocol.dispatch_buffer"); + let max_attempts = (payload.len() / 5).saturating_add(2).max(1); + let mut attempts = 0usize; + let mut recovered_protocol_error = false; + while self.protocol_input_remaining()? > 0 { + attempts += 1; + ensure!( + attempts <= max_attempts, + "Postgres protocol dispatch did not drain buffered input after {attempts} attempts" + ); + if let Err(err) = self.protocol.main_loop.call(&mut self.store) { + if runtime_error_exit_code(&err) == Some(POSTGRES_MAIN_LONGJMP) { + warn!( + "PostgresMainLoopOnce used host longjmp fallback; recovering protocol error" + ); + self.recover_protocol_error(payload.len())?; + recovered_protocol_error = true; + } else { + warn!("PostgresMainLoopOnce trapped; attempting protocol recovery: {err}"); + self.recover_protocol_error(payload.len())?; + recovered_protocol_error = true; + } + } + } + + { + let _phase = timing::phase("postgres.protocol.send_ready"); + self.protocol + .send_ready + .call(&mut self.store) + .context("PostgresSendReadyForQueryIfNecessary")?; + } + { + let _phase = timing::phase("postgres.protocol.pq_flush"); + self.protocol + .pq_flush + .call(&mut self.store) + .context("pgl_pq_flush after protocol buffer")?; + } + let output = { + let _phase = timing::phase("postgres.protocol.output_read"); + self.io + .take_output(&mut self.store, &self.env, &self.guest_allocator) + .context("take backend output after protocol buffer")? + }; + if !recovered_protocol_error && protocol_response_contains_error(&output) { + self.recover_non_trapping_protocol_error()?; + } + self.record_backend_c_timings()?; + Ok(output) + } + } + + pub(crate) fn supports_streaming_protocol(&self) -> bool { + self.protocol_stdio.is_some() + } + + fn serve_protocol_stream_inner(&mut self) -> Result<()> { + self.reset_backend_c_timings()?; + loop { + if let Err(err) = self.protocol.main_loop.call(&mut self.store) { + if runtime_error_exit_code(&err) == Some(PGLITE_EXIT_ALIVE) { + break; + } + if runtime_error_exit_code(&err) == Some(POSTGRES_MAIN_LONGJMP) + || is_wasm_uncaught_exception(&err) + { + warn!( + "PostgresMainLoopOnce used host trap fallback while serving streaming protocol" + ); + self.protocol.recover_error.call(&mut self.store).context( + "recover Postgres main-loop error while serving streaming protocol", + )?; + } else { + return Err(err).context("PostgresMainLoopOnce streaming protocol"); + } + } + self.protocol + .send_ready + .call(&mut self.store) + .context("PostgresSendReadyForQueryIfNecessary streaming protocol")?; + self.protocol + .pq_flush + .call(&mut self.store) + .context("pgl_pq_flush streaming protocol")?; + } + self.record_backend_c_timings()?; + Ok(()) + } + + fn set_protocol_transport( + &mut self, + mode: ProtocolTransportMode, + ) -> Result { + let stdio = self + .protocol_stdio + .as_ref() + .context("WASIX runtime does not export protocol stdio switching")?; + let previous = stdio + .set_protocol_transport + .call(&mut self.store, mode as i32) + .context("pgl_set_protocol_transport")?; + ProtocolTransportMode::from_i32(previous) + } + + fn restore_protocol_transport(&mut self, previous_mode: ProtocolTransportMode) -> Result<()> { + let current = self.set_protocol_transport(previous_mode)?; + ensure!( + current != previous_mode, + "pgl_set_protocol_transport restore observed unchanged current mode" + ); + Ok(()) + } + + fn protocol_stream_active(&mut self) -> Result { + let stdio = self + .protocol_stdio + .as_ref() + .context("WASIX runtime does not export protocol stream state")?; + Ok(stdio + .protocol_stream_active + .call(&mut self.store) + .context("pgl_protocol_stream_active")? + != 0) + } + + fn start_protocol(&mut self) -> Result<()> { + if self.started { + return Ok(()); + } + let startup = startup_packet(&self.startup_config.username, &self.startup_config.database); + let response = self.start_protocol_with_startup_packet(&startup)?; + ensure!( + response.accepted, + "PGlite WASIX startup packet was rejected: {}", + summarize_protocol(&response.output) + ); + ensure!( + !protocol_response_contains_error(&response.output), + "PGlite WASIX startup packet returned an error: {}", + summarize_protocol(&response.output) + ); + Ok(()) + } + + pub(crate) fn start_protocol_with_startup_packet( + &mut self, + startup: &[u8], + ) -> Result { + self.ensure_cluster()?; + ensure!( + !self.started, + "PGlite WASIX protocol startup has already completed for this backend" + ); + + let _phase = timing::phase("postgres.startup_packet"); + { + let _phase = timing::phase("postgres.startup_packet.input_reset"); + self.io.reset(&mut self.store)?; + } + { + let _phase = timing::phase("postgres.startup_packet.input_write"); + self.io + .push_input(&mut self.store, &self.env, &self.guest_allocator, startup)?; + } + + // The upstream lifecycle is already running by this point. These calls + // open the Rust-owned direct wire-protocol transport on top of that + // lifecycle; they must not grow into a second backend lifecycle. + let port = { + let _phase = timing::phase("postgres.startup_packet.get_port"); + self.protocol + .get_port + .call(&mut self.store) + .context("pgl_getMyProcPort")? + }; + ensure!(port > 0, "pgl_getMyProcPort returned null"); + + let status = { + let _phase = timing::phase("postgres.startup_packet.process_startup"); + self.protocol + .process_startup + .call(&mut self.store, port, 1, 1) + .context("ProcessStartupPacket")? + }; + if status != 0 { + let _ = self.protocol.pq_flush.call(&mut self.store); + let output = self + .io + .take_output(&mut self.store, &self.env, &self.guest_allocator)?; + return Ok(StartupProtocolResponse { + output, + accepted: false, + }); + } + let output = { + let _phase = timing::phase("postgres.startup_packet.ready"); + { + let _phase = timing::phase("postgres.startup_packet.send_conn_data"); + self.protocol + .send_conn_data + .call(&mut self.store) + .context("pgl_sendConnData")?; + } + { + let _phase = timing::phase("postgres.startup_packet.pq_flush"); + self.protocol + .pq_flush + .call(&mut self.store) + .context("pgl_pq_flush after startup")?; + } + { + let _phase = timing::phase("postgres.startup_packet.output_read"); + self.io + .take_output(&mut self.store, &self.env, &self.guest_allocator)? + } + }; + self.started = true; + self.startup_response = Some(output.clone()); + Ok(StartupProtocolResponse { + output, + accepted: true, + }) + } + + #[cfg(feature = "extensions")] + pub(crate) fn existing_startup_response(&self) -> Option> { + self.startup_response.clone() + } + + fn recover_protocol_error(&mut self, payload_len: usize) -> Result<()> { + self.protocol + .recover_error + .call(&mut self.store) + .context("PostgresMainLongJmp after protocol trap")?; + + // PostgreSQL extended-query errors skip messages until Sync. If Sync was + // already in this host buffer, re-enter the loop to drain it and produce + // ReadyForQuery from PostgreSQL rather than inventing one in Rust. + let max_drain_attempts = (payload_len / 5).saturating_add(2).max(1); + let mut drain_attempts = 0usize; + while self.protocol_input_remaining()? > 0 { + drain_attempts += 1; + ensure!( + drain_attempts <= max_drain_attempts, + "Postgres protocol recovery did not drain buffered input after {drain_attempts} attempts" + ); + if let Err(drain_err) = self.protocol.main_loop.call(&mut self.store) { + warn!("PostgresMainLoopOnce trapped while draining after recovery: {drain_err}"); + self.protocol + .recover_error + .call(&mut self.store) + .context("PostgresMainLongJmp while draining after protocol trap")?; + } + } + Ok(()) + } + + fn recover_non_trapping_protocol_error(&mut self) -> Result<()> { + self.protocol + .recover_error + .call(&mut self.store) + .context("PostgresMainLongJmp after backend ErrorResponse")?; + self.protocol + .send_ready + .call(&mut self.store) + .context("PostgresSendReadyForQueryIfNecessary after backend ErrorResponse")?; + self.protocol + .pq_flush + .call(&mut self.store) + .context("pgl_pq_flush after backend ErrorResponse recovery")?; + let _ = self + .io + .take_output(&mut self.store, &self.env, &self.guest_allocator)?; + Ok(()) + } + + fn protocol_input_remaining(&mut self) -> Result { + let host_remaining = self.io.available(&mut self.store)?; + if host_remaining > 0 { + return Ok(host_remaining); + } + self.protocol + .pq_buffer_remaining_data + .call(&mut self.store) + .context("pq_buffer_remaining_data") + } +} + +fn process_wasix_runtime(engine: &Engine) -> Result> { + WASIX_PROCESS_RUNTIME + .get_or_init(|| { + let _phase = timing::phase("wasix.runtime_construct"); + let tokio_runtime = { + let _phase = timing::phase("wasix.runtime_construct.tokio"); + Arc::new( + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create Tokio runtime for Wasmer/WASIX filesystem") + .map_err(|err| format!("{err:#}"))?, + ) + }; + let wasix_module_cache = { + let _phase = timing::phase("wasix.runtime_construct.module_cache"); + Arc::new(SharedCache::new()) + }; + let wasix_runtime = { + let _phase = timing::phase("wasix.runtime_construct.pluggable_runtime"); + build_wasix_runtime(&tokio_runtime, engine, wasix_module_cache.clone()) + }; + + Ok(Arc::new(WasixProcessRuntime { + tokio_runtime, + wasix_module_cache, + wasix_runtime, + })) + }) + .clone() + .map_err(|message| anyhow::anyhow!(message)) +} + +struct WasixInstantiateInput<'a> { + runtime: &'a TokioRuntime, + wasix_runtime: &'a Arc, + store: &'a mut Store, + paths: &'a PglitePaths, + runtime_layout: &'a RuntimeLayout, + postgres_config: &'a PostgresConfig, + startup_config: &'a StartupConfig, + module: Module, +} + +fn instantiate_wasix_module( + input: WasixInstantiateInput<'_>, +) -> Result<( + Instance, + WasiFunctionEnv, + ProtocolStdioFile, + TailCaptureHandle, +)> { + let _phase = timing::phase("wasix.instantiate"); + let _guard = input.runtime.enter(); + let root_fs = { + let _phase = timing::phase("wasix.instantiate.root_fs"); + if input.runtime_layout.uses_shared_overlay() { + mountfs_overlay_wasi_root(input.paths, input.runtime_layout)? + } else { + host_wasi_root(&input.paths.runtime_root())? + } + }; + + let mut runner = WasiRunner::new(); + runner.with_current_dir("/"); + let protocol_stdio_file = ProtocolStdioFile::new(); + let (stderr_file, stderr_capture) = TailCaptureFile::new(16 * 1024); + runner.with_stdin(Box::new(protocol_stdio_file.clone())); + runner.with_stdout(Box::new(protocol_stdio_file.clone())); + runner.with_stderr(Box::new(stderr_file)); + let wasi = Wasi::new(PGLITE_EXE_PATH); + let mut builder = { + let _phase = timing::phase("wasix.instantiate.prepare_env"); + runner + .prepare_webc_env( + PGLITE_EXE_PATH, + &wasi, + PackageOrHash::Hash(ModuleHash::random()), + RuntimeOrEngine::Runtime(input.wasix_runtime.clone()), + Some(root_fs), + ) + .context("prepare Wasmer/WASIX runner environment")? + }; + { + let _phase = timing::phase("wasix.instantiate.pgdata_preopen"); + add_pgdata_preopen(&mut builder)?; + } + add_pglite_env(&mut builder, input.startup_config); + add_pglite_args(&mut builder, input.postgres_config, input.startup_config)?; + + { + let _phase = timing::phase("wasix.instantiate.module"); + builder + .instantiate(input.module, input.store) + .context("instantiate PGlite WASIX module") + .map(|(instance, env)| (instance, env, protocol_stdio_file, stderr_capture)) + } +} + +fn add_pgdata_preopen(builder: &mut wasmer_wasix::WasiEnvBuilder) -> Result<()> { + builder + .add_preopen_build(|preopen| { + preopen + .directory(PGDATA_DIR) + .alias(PGDATA_DIR.trim_start_matches('/')) + .read(true) + .write(true) + .create(true) + }) + .context("preopen PGDATA directory for Wasmer/WASIX")?; + Ok(()) +} + +fn host_wasi_root(runtime_root: &Path) -> Result { + Ok(WasiFsRoot::from_filesystem(maybe_trace_filesystem( + host_filesystem(runtime_root)?, + ))) +} + +fn mountfs_overlay_wasi_root( + paths: &PglitePaths, + runtime_layout: &RuntimeLayout, +) -> Result { + let _phase = timing::phase("wasix.mountfs_overlay_construct"); + let runtime_root = paths.runtime_root(); + let primary = + virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem(&runtime_root)?)); + let secondary = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( + &runtime_layout.module_root, + )?)); + let overlay = Arc::new(virtual_fs::OverlayFileSystem::new(primary, [secondary])); + let root: Arc = + if let Some(pgdata) = pgdata_overlay_filesystem(paths, runtime_layout)? { + wasi_root_with_pgdata_mount(overlay, pgdata)? + } else { + overlay + }; + + Ok(WasiFsRoot::from_filesystem(root)) +} + +fn pgdata_overlay_filesystem( + paths: &PglitePaths, + runtime_layout: &RuntimeLayout, +) -> Result>> { + if let Some(pgdata_template_root) = &runtime_layout.pgdata_template_root { + let fs = + EagerCopyOverlayFileSystem::new(paths.pgdata.clone(), pgdata_template_root.clone())?; + return Ok(Some(maybe_trace_filesystem(Arc::new(fs)))); + } + Ok(None) +} + +fn wasi_root_with_pgdata_mount( + root: Arc, + pgdata: Arc, +) -> virtual_fs::Result> { + let mount = virtual_fs::MountFileSystem::new(); + mount.mount(Path::new("/"), root)?; + mount.mount(Path::new(PGDATA_DIR), pgdata)?; + Ok(Arc::new(mount)) +} + +struct EagerCopyOverlayFileSystem { + upper_root: PathBuf, + lower_root: PathBuf, + overlay: + virtual_fs::OverlayFileSystem, +} + +impl fmt::Debug for EagerCopyOverlayFileSystem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EagerCopyOverlayFileSystem") + .field("upper_root", &self.upper_root) + .field("lower_root", &self.lower_root) + .finish_non_exhaustive() + } +} + +impl EagerCopyOverlayFileSystem { + fn new(upper_root: PathBuf, lower_root: PathBuf) -> Result { + fs::create_dir_all(&upper_root) + .with_context(|| format!("create PGDATA overlay upper {}", upper_root.display()))?; + let upper_root = upper_root.canonicalize().with_context(|| { + format!("canonicalize PGDATA overlay upper {}", upper_root.display()) + })?; + let lower_root = lower_root.canonicalize().with_context(|| { + format!("canonicalize PGDATA overlay lower {}", lower_root.display()) + })?; + let upper = virtual_fs::ArcFileSystem::new(host_filesystem(&upper_root)?); + let lower = virtual_fs::ArcFileSystem::new(host_filesystem(&lower_root)?); + Ok(Self { + upper_root, + lower_root, + overlay: virtual_fs::OverlayFileSystem::new(upper, [lower]), + }) + } + + fn ensure_upper_copy( + &self, + path: &Path, + conf: &virtual_fs::OpenOptionsConfig, + ) -> virtual_fs::Result<()> { + let Some(relative) = normalize_overlay_path(path)? else { + return Ok(()); + }; + + let upper = self.upper_root.join(&relative); + if upper.exists() { + return Ok(()); + } + + let lower = self.lower_root.join(&relative); + let metadata = match fs::symlink_metadata(&lower) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + if conf.create || conf.create_new { + self.ensure_upper_parent(&relative)?; + } + return Ok(()); + } + Err(err) => return Err(err.into()), + }; + + if conf.create_new { + return Err(virtual_fs::FsError::AlreadyExists); + } + if metadata.is_dir() { + return Ok(()); + } + if !metadata.is_file() { + return Err(virtual_fs::FsError::Unsupported); + } + + if let Some(parent) = upper.parent() { + fs::create_dir_all(parent).map_err(virtual_fs::FsError::from)?; + } + if conf.truncate && !conf.read && !conf.append { + fs::File::create(&upper).map_err(virtual_fs::FsError::from)?; + } else { + fs::copy(&lower, &upper).map_err(virtual_fs::FsError::from)?; + } + Ok(()) + } + + fn ensure_upper_parent(&self, relative: &Path) -> virtual_fs::Result<()> { + let Some(parent) = relative.parent() else { + return Ok(()); + }; + if parent.as_os_str().is_empty() { + return Ok(()); + } + + let upper_parent = self.upper_root.join(parent); + if upper_parent.is_dir() { + return Ok(()); + } + + let lower_parent = self.lower_root.join(parent); + let metadata = match fs::symlink_metadata(&lower_parent) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(virtual_fs::FsError::EntryNotFound); + } + Err(err) => return Err(err.into()), + }; + if !metadata.is_dir() { + return Err(virtual_fs::FsError::BaseNotDirectory); + } + + fs::create_dir_all(upper_parent).map_err(virtual_fs::FsError::from) + } +} + +impl virtual_fs::FileSystem for EagerCopyOverlayFileSystem { + fn readlink(&self, path: &Path) -> virtual_fs::Result { + self.overlay.readlink(path) + } + + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + self.overlay.read_dir(path) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + if let Some(relative) = normalize_overlay_path(path)? { + self.ensure_upper_parent(&relative)?; + } + self.overlay.create_dir(path) + } + + fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> { + if let Some(relative) = normalize_overlay_path(target)? { + self.ensure_upper_parent(&relative)?; + } + self.overlay.create_symlink(source, target) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.overlay.remove_dir(path) + } + + fn rename<'a>( + &'a self, + from: &'a Path, + to: &'a Path, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + self.ensure_upper_copy(from, &mutating_open_config())?; + if let Some(relative) = normalize_overlay_path(to)? { + self.ensure_upper_parent(&relative)?; + } + self.overlay.rename(from, to).await + }) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + self.overlay.metadata(path) + } + + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { + self.overlay.symlink_metadata(path) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + self.overlay.remove_file(path) + } + + fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { + virtual_fs::OpenOptions::new(self) + } +} + +impl virtual_fs::FileOpener for EagerCopyOverlayFileSystem { + fn open( + &self, + path: &Path, + conf: &virtual_fs::OpenOptionsConfig, + ) -> virtual_fs::Result> { + if conf.would_mutate() { + self.ensure_upper_copy(path, conf)?; + } + virtual_fs::FileSystem::new_open_options(&self.overlay) + .options(conf.clone()) + .open(path) + } +} + +fn normalize_overlay_path(path: &Path) -> virtual_fs::Result> { + let mut relative = PathBuf::new(); + for component in path.components() { + match component { + Component::RootDir | Component::CurDir => {} + Component::Normal(part) => relative.push(part), + Component::ParentDir | Component::Prefix(_) => { + return Err(virtual_fs::FsError::PermissionDenied); + } + } + } + if relative.as_os_str().is_empty() { + Ok(None) + } else { + Ok(Some(relative)) + } +} + +fn mutating_open_config() -> virtual_fs::OpenOptionsConfig { + virtual_fs::OpenOptionsConfig { + read: true, + write: true, + create_new: false, + create: false, + append: false, + truncate: false, + } +} + +fn host_filesystem(host_path: &Path) -> Result> { + let host_fs = SyncHostFileSystem::new(host_path) + .with_context(|| format!("create host fs rooted at {}", host_path.display()))?; + Ok(Arc::new(host_fs) as Arc) +} + +fn fs_trace_enabled() -> bool { + env_flag_enabled("PGLITE_OXIDE_WASIX_FS_TRACE") +} + +fn env_flag_enabled(name: &str) -> bool { + let Some(value) = std::env::var_os(name) else { + return false; + }; + !matches!( + value.to_string_lossy().to_ascii_lowercase().as_str(), + "" | "0" | "false" | "off" | "no" + ) +} + +fn maybe_trace_filesystem( + inner: Arc, +) -> Arc { + if fs_trace_enabled() { + Arc::new(TracedFileSystem { inner }) as Arc + } else { + inner + } +} + +#[derive(Debug)] +struct TracedFileSystem { + inner: Arc, +} + +impl TracedFileSystem { + fn record(&self, counter: &AtomicU64, operation: impl FnOnce() -> T) -> T { + counter.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = operation(); + FS_TRACE.record_total(started.elapsed()); + result + } +} + +impl virtual_fs::FileSystem for TracedFileSystem { + fn readlink(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.metadata_count, || self.inner.readlink(path)) + } + + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.read_dir_count, || self.inner.read_dir(path)) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.create_dir_count, || self.inner.create_dir(path)) + } + + fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.create_dir_count, || { + self.inner.create_symlink(source, target) + }) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.remove_dir_count, || self.inner.remove_dir(path)) + } + + fn rename<'a>( + &'a self, + from: &'a Path, + to: &'a Path, + ) -> Pin> + Send + 'a>> { + FS_TRACE.rename_count.fetch_add(1, Ordering::Relaxed); + Box::pin(async move { + let started = Instant::now(); + let result = self.inner.rename(from, to).await; + FS_TRACE.record_total(started.elapsed()); + result + }) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.metadata_count, || self.inner.metadata(path)) + } + + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.metadata_count, || { + self.inner.symlink_metadata(path) + }) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.remove_file_count, || self.inner.remove_file(path)) + } + + fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { + virtual_fs::OpenOptions::new(self) + } +} + +impl virtual_fs::FileOpener for TracedFileSystem { + fn open( + &self, + path: &Path, + conf: &virtual_fs::OpenOptionsConfig, + ) -> virtual_fs::Result> { + FS_TRACE.open_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let file = virtual_fs::FileSystem::new_open_options(&self.inner) + .options(conf.clone()) + .open(path); + FS_TRACE.record_total(started.elapsed()); + file.map(|inner| Box::new(TracedVirtualFile { inner }) as _) + } +} + +#[derive(Debug)] +struct TracedVirtualFile { + inner: Box, +} + +impl virtual_fs::VirtualFile for TracedVirtualFile { + fn last_accessed(&self) -> u64 { + self.inner.last_accessed() + } + + fn last_modified(&self) -> u64 { + self.inner.last_modified() + } + + fn created_time(&self) -> u64 { + self.inner.created_time() + } + + fn set_times(&mut self, atime: Option, mtime: Option) -> virtual_fs::Result<()> { + self.inner.set_times(atime, mtime) + } + + fn size(&self) -> u64 { + self.inner.size() + } + + fn set_len(&mut self, new_size: u64) -> virtual_fs::Result<()> { + FS_TRACE.set_len_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = self.inner.set_len(new_size); + FS_TRACE.record_total(started.elapsed()); + result + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + FS_TRACE.unlink_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = self.inner.unlink(); + FS_TRACE.record_total(started.elapsed()); + result + } + + fn is_open(&self) -> bool { + self.inner.is_open() + } + + fn get_special_fd(&self) -> Option { + self.inner.get_special_fd() + } + + fn write_from_mmap(&mut self, offset: u64, len: u64) -> io::Result<()> { + self.inner.write_from_mmap(offset, len) + } + + fn poll_read_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_read_ready(cx) + } + + fn poll_write_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_write_ready(cx) + } +} + +impl virtual_fs::AsyncRead for TracedVirtualFile { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut TaskContext<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + let before = buf.filled().len(); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).poll_read(cx, buf); + if let Poll::Ready(Ok(())) = &result { + let bytes = buf.filled().len().saturating_sub(before) as u64; + FS_TRACE.read_count.fetch_add(1, Ordering::Relaxed); + FS_TRACE.read_bytes.fetch_add(bytes, Ordering::Relaxed); + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.read_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + result + } +} + +impl virtual_fs::AsyncWrite for TracedVirtualFile { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).poll_write(cx, buf); + if let Poll::Ready(Ok(bytes)) = &result { + FS_TRACE.write_count.fetch_add(1, Ordering::Relaxed); + FS_TRACE + .write_bytes + .fetch_add(*bytes as u64, Ordering::Relaxed); + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.write_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + result + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_shutdown(cx) + } +} + +impl virtual_fs::AsyncSeek for TracedVirtualFile { + fn start_seek(self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + let this = self.get_mut(); + FS_TRACE.seek_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).start_seek(position); + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.seek_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + result + } + + fn poll_complete(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).poll_complete(cx); + if let Poll::Ready(Ok(_)) = &result { + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.seek_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + result + } +} + +fn build_wasix_runtime( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: Arc, +) -> Arc { + let _guard = runtime.enter(); + let task_manager = Arc::new(TokioTaskManager::new(runtime.handle().clone())); + let mut wasix_runtime = PluggableRuntime::new(task_manager); + wasix_runtime.set_engine(engine.clone()); + wasix_runtime.set_module_cache(module_cache); + Arc::new(wasix_runtime) +} + +fn run_split_initdb(paths: &PglitePaths, runtime_layout: &RuntimeLayout) -> Result<()> { + let _phase = timing::phase("initdb.split_wasix"); + let initdb_module = runtime_layout.module_root.join("bin/initdb"); + let postgres_module = runtime_layout.module_root.join("bin/postgres"); + ensure!( + initdb_module.exists(), + "split WASIX initdb module is not installed at {}; regenerate assets with `xtask assets template`", + initdb_module.display() + ); + ensure!( + postgres_module.exists(), + "WASIX postgres module is not installed at {}", + postgres_module.display() + ); + + fs::create_dir_all(&paths.pgdata) + .with_context(|| format!("create fresh PGDATA {}", paths.pgdata.display()))?; + + let (engine, _) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + seed_wasix_module_cache( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &initdb_module, + "tool:initdb", + "split initdb command", + )?; + seed_wasix_module_cache( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &postgres_module, + "runtime:pglite", + "initdb child postgres command", + )?; + preload_runtime_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &runtime_layout.module_root, + )?; + // initdb execs child postgres commands; isolate that command process tree + // from concurrently running backends while keeping the module cache shared. + let initdb_runtime = build_wasix_runtime( + &process_runtime.tokio_runtime, + &engine, + process_runtime.wasix_module_cache.clone(), + ); + + let package = split_initdb_binary_package(&initdb_module, &postgres_module)?; + let root_fs = split_initdb_root_filesystem(paths, runtime_layout)?; + root_fs + .read_dir(Path::new(PGDATA_DIR)) + .with_context(|| format!("verify split initdb {PGDATA_DIR} mount"))?; + + let (stdout_file, stdout_capture) = TailCaptureFile::new(8 * 1024); + let (stderr_file, stderr_capture) = TailCaptureFile::new(8 * 1024); + + let mut runner = WasiRunner::new(); + runner + .with_current_dir("/") + .with_injected_package(package.clone()) + .with_args(split_initdb_args()) + .with_envs([ + ("PGDATA", PGDATA_DIR), + ("PGSYSCONFDIR", PGDATA_DIR), + ("HOME", "/home/postgres"), + ("USER", "postgres"), + ("LOGNAME", "postgres"), + ("PGCLIENTENCODING", "UTF8"), + ("PATH", "/bin"), + ("LC_CTYPE", "C.UTF-8"), + ("TZ", "UTC"), + ("PGTZ", "UTC"), + ("PG_COLOR", "never"), + ]) + .with_stdin(Box::::default()) + .with_stdout(Box::new(stdout_file)) + .with_stderr(Box::new(stderr_file)); + + { + let _phase = timing::phase("initdb.split_wasix.run_command"); + let result = + run_package_command_with_root(&runner, "initdb", &package, initdb_runtime, root_fs); + if let Err(err) = result { + let stdout = stdout_capture.text(); + let stderr = stderr_capture.text(); + let diagnostics = split_initdb_diagnostics(paths, runtime_layout); + return Err(err).with_context(|| { + format!( + "run split WASIX initdb\n{}\ninitdb stdout:\n{}\ninitdb stderr:\n{}", + diagnostics, + if stdout.trim().is_empty() { + "" + } else { + stdout.trim_end() + }, + if stderr.trim().is_empty() { + "" + } else { + stderr.trim_end() + } + ) + }); + } + } + Ok(()) +} + +fn split_initdb_root_filesystem( + paths: &PglitePaths, + runtime_layout: &RuntimeLayout, +) -> Result> { + let root: Arc = + if runtime_layout.uses_shared_overlay() { + let upper = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( + &paths.runtime_root(), + )?)); + let lower = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( + &runtime_layout.module_root, + )?)); + Arc::new(virtual_fs::OverlayFileSystem::new(upper, [lower])) + } else { + maybe_trace_filesystem(host_filesystem(&paths.runtime_root())?) + }; + + let pgdata = maybe_trace_filesystem(host_filesystem(&paths.pgdata)?); + // initdb execs a child postgres command during bootstrap. Keep PGDATA inside + // the root filesystem view so both commands inherit the same /base mount. + let root = wasi_root_with_pgdata_mount(root, pgdata)?; + // Wasmer's runner normally starts from a temporary root that provides WASIX + // device files. Keep the real runtime/PGDATA root primary so writes land on + // the host-backed filesystems, and read device nodes through the support + // layer when the runtime root does not provide them itself. + let devices = Arc::new(virtual_fs::RootFileSystemBuilder::default().build_tmp_ext(&[])); + Ok(Arc::new(virtual_fs::OverlayFileSystem::new( + virtual_fs::ArcFileSystem::new(root), + [virtual_fs::ArcFileSystem::new(devices)], + ))) +} + +fn run_package_command_with_root( + runner: &WasiRunner, + command_name: &str, + package: &BinaryPackage, + runtime: Arc, + root_fs: Arc, +) -> Result<()> { + let cmd = package.get_command(command_name).with_context(|| { + format!("split initdb package does not contain command {command_name:?}") + })?; + let wasi = cmd + .metadata() + .annotation("wasi")? + .unwrap_or_else(|| Wasi::new(command_name)); + let exec_name = wasi.exec_name.as_deref().unwrap_or(command_name); + let mut builder = runner + .prepare_webc_env( + exec_name, + &wasi, + PackageOrHash::Package(package), + RuntimeOrEngine::Runtime(runtime), + Some(WasiFsRoot::from_filesystem(root_fs)), ) - .map_err(|err| { - anyhow!( - "failed to preopen {} as /tmp/pglite/base: {err}", - paths.pgdata.display() - ) + .with_context(|| format!("prepare WASIX command environment for {command_name:?}"))?; + add_pgdata_preopen(&mut builder)?; + + let env = builder.build()?; + let runtime = env.runtime.clone(); + let tasks = runtime.task_manager().clone(); + let package = package.clone(); + let command_name = command_name.to_owned(); + let exit_code = tasks.spawn_and_block_on(async move { + let mut task_handle = spawn_exec(package, &command_name, env, &runtime) + .await + .with_context(|| format!("spawn WASIX command {command_name:?}"))?; + task_handle + .wait_finished() + .await + .map_err(|err| anyhow::anyhow!("{err}")) + .with_context(|| format!("wait for WASIX command {command_name:?}")) + })??; + + ensure!(exit_code.raw() == 0, "WASI exited with code: {exit_code}"); + Ok(()) +} + +fn split_initdb_diagnostics(paths: &PglitePaths, runtime_layout: &RuntimeLayout) -> String { + let pgdata_parent = paths.pgdata.parent().unwrap_or(&paths.pgdata); + format!( + "initdb diagnostics:\n layout_kind={:?}\n pgdata_host={}\n pgdata_parent={}\n runtime_root={}\n module_root={}\n pgdata_entries={}", + runtime_layout.kind, + path_state(&paths.pgdata), + path_state(pgdata_parent), + path_state(&paths.runtime_root()), + path_state(&runtime_layout.module_root), + dir_entry_sample(&paths.pgdata), + ) +} + +fn path_state(path: &Path) -> String { + match fs::metadata(path) { + Ok(metadata) => format!( + "{} ({})", + path.display(), + if metadata.is_dir() { + "dir" + } else if metadata.is_file() { + "file" + } else { + "other" + } + ), + Err(err) => format!("{} ({})", path.display(), err), + } +} + +fn dir_entry_sample(path: &Path) -> String { + let entries = match fs::read_dir(path) { + Ok(entries) => entries, + Err(err) => return format!(""), + }; + let mut names = entries + .filter_map(|entry| { + entry + .ok() + .map(|entry| entry.file_name().to_string_lossy().into_owned()) + }) + .take(8) + .collect::>(); + names.sort(); + if names.is_empty() { + "".to_owned() + } else { + names.join(", ") + } +} + +fn split_initdb_args() -> Vec<&'static str> { + vec![ + "--allow-group-access", + "--encoding", + "UTF8", + "--locale", + "C.UTF-8", + "--locale-provider", + "libc", + "--auth", + "trust", + "-D", + PGDATA_DIR, + ] +} + +fn split_initdb_binary_package( + initdb_module: &Path, + postgres_module: &Path, +) -> Result { + let initdb_wasm = + fs::read(initdb_module).with_context(|| format!("read {}", initdb_module.display()))?; + let postgres_wasm = + fs::read(postgres_module).with_context(|| format!("read {}", postgres_module.display()))?; + + let mut package_hash = Sha256::new(); + package_hash.update(b"pglite-oxide-split-initdb-package-v1\n"); + package_hash.update(&initdb_wasm); + package_hash.update(&postgres_wasm); + let package_hash: [u8; 32] = package_hash.finalize().into(); + let package_id = PackageId::Hash(PackageHash::from_sha256_bytes(package_hash)); + + Ok(BinaryPackage { + id: package_id.clone(), + package_ids: vec![package_id.clone()], + when_cached: None, + entrypoint_cmd: Some("initdb".to_owned()), + hash: Default::default(), + package_mounts: None, + commands: vec![ + split_initdb_command("initdb", initdb_wasm, &package_id), + split_initdb_command("postgres", postgres_wasm, &package_id), + ], + uses: Vec::new(), + file_system_memory_footprint: 0, + additional_host_mapped_directories: Vec::new(), + }) +} + +fn split_initdb_command(name: &str, wasm: Vec, package_id: &PackageId) -> BinaryPackageCommand { + let hash = ModuleHash::new(&wasm); + let atom: webc::compat::SharedBytes = wasm.into(); + BinaryPackageCommand::new( + name.to_owned(), + WebcCommand { + runner: WASI_RUNNER_URI.to_owned(), + annotations: Default::default(), + }, + atom, + hash, + None, + package_id.clone(), + package_id.clone(), + ) +} + +fn preload_runtime_side_modules( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + runtime_root: &Path, +) -> Result<()> { + let _phase = timing::phase("wasix.seed_runtime_side_modules"); + let lib_dir = runtime_root.join("lib/postgresql"); + for (file_name, artifact_name) in RUNTIME_SIDE_MODULES { + let library = lib_dir.join(file_name); + ensure!( + library.exists(), + "runtime support module '{}' is not installed at {}", + file_name, + library.display() + ); + + seed_side_module_cache( + runtime, + engine, + module_cache, + &library, + artifact_name, + &format!("runtime support module '{file_name}'"), + )?; + } + Ok(()) +} + +#[cfg(feature = "extensions")] +fn preload_installed_extension_side_modules( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + runtime_root: &Path, +) -> Result<()> { + let _phase = timing::phase("wasix.seed_extension_side_modules"); + let lib_dir = runtime_root.join("lib/postgresql"); + for extension in super::extensions::ALL { + let Some(module_file) = extension.native_module_file() else { + continue; + }; + let Some(aot_name) = extension.aot_name() else { + continue; + }; + let library = lib_dir.join(module_file); + if !library.exists() { + continue; + } + seed_side_module_cache( + runtime, + engine, + module_cache, + &library, + aot_name, + &format!("installed extension '{}'", extension.sql_name()), + )?; + } + Ok(()) +} + +fn seed_side_module_cache( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + library: &Path, + artifact_name: &'static str, + label: &str, +) -> Result<()> { + seed_wasix_module_cache(runtime, engine, module_cache, library, artifact_name, label) +} + +fn seed_wasix_module_cache( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + wasm_path: &Path, + artifact_name: &str, + label: &str, +) -> Result<()> { + let wasm = { + let _phase = timing::phase("wasix.seed_side_module.read_wasm"); + fs::read(wasm_path).with_context(|| format!("read WASIX module {}", wasm_path.display()))? + }; + let module_hash = { + let _phase = timing::phase("wasix.seed_side_module.module_hash"); + ModuleHash::new(&wasm) + }; + let seed_key = format!("{artifact_name}:{}:{module_hash}", aot::engine_identity()); + let mut seeded_side_modules = SEEDED_SIDE_MODULES + .get_or_init(|| Mutex::new(HashSet::new())) + .lock() + .expect("seeded side module cache poisoned"); + if seeded_side_modules.contains(&seed_key) { + return Ok(()); + } + + // Keep the process-wide seed check and SharedCache write atomic. Wasmer's + // shared cache is global to all concurrent PGlite instances in this process. + let module = { + let _phase = timing::phase("wasix.seed_side_module.load_aot"); + aot::load_artifact_module(engine, artifact_name)? + }; + { + let _phase = timing::phase("wasix.seed_side_module.save_cache"); + block_on_tokio_runtime(runtime, module_cache.save(module_hash, engine, &module)) + .with_context(|| format!("seed Wasmer module cache for {label} ({module_hash})"))?; + } + seeded_side_modules.insert(seed_key); + Ok(()) +} + +fn block_on_tokio_runtime(runtime: &TokioRuntime, future: F) -> T +where + F: Future + Send, + T: Send, +{ + if tokio::runtime::Handle::try_current().is_ok() { + return std::thread::scope(|scope| { + scope + .spawn(move || runtime.block_on(future)) + .join() + .unwrap_or_else(|payload| std::panic::resume_unwind(payload)) + }); + } + + runtime.block_on(future) +} + +impl PgliteLifecycleExports { + fn load(store: &mut Store, instance: &Instance) -> Result { + let wasi_start = typed_export(store, instance, "_start")?; + let set_force_host_error_recovery = + optional_typed_export(store, instance, "pgl_set_force_host_error_recovery")?; + let set_active = typed_export(store, instance, "pgl_setPGliteActive")?; + let start_pglite = typed_export(store, instance, "pgl_startPGlite")?; + let run_atexit_funcs = optional_typed_export(store, instance, "pgl_run_atexit_funcs")?; + let backend_timing_reset = + optional_typed_export(store, instance, "pgl_backend_timing_reset")?; + let backend_timing_elapsed_us = + optional_typed_export(store, instance, "pgl_backend_timing_elapsed_us")?; + + Ok(Self { + wasi_start, + set_force_host_error_recovery, + set_active, + start_pglite, + run_atexit_funcs, + backend_timing_reset, + backend_timing_elapsed_us, + }) + } +} + +impl WasixProtocolExports { + fn load(store: &mut Store, instance: &Instance) -> Result { + let get_port = typed_export(store, instance, "pgl_getMyProcPort")?; + let process_startup = typed_export(store, instance, "ProcessStartupPacket")?; + let send_conn_data = typed_export(store, instance, "pgl_sendConnData")?; + let pq_flush = typed_export(store, instance, "pgl_pq_flush")?; + let pq_buffer_remaining_data = typed_export(store, instance, "pq_buffer_remaining_data")?; + let main_loop = typed_export(store, instance, "PostgresMainLoopOnce")?; + let send_ready = typed_export(store, instance, "PostgresSendReadyForQueryIfNecessary")?; + let recover_error = typed_export(store, instance, "PostgresMainLongJmp")?; + + Ok(Self { + get_port, + process_startup, + send_conn_data, + pq_flush, + pq_buffer_remaining_data, + main_loop, + send_ready, + recover_error, + }) + } +} + +impl WasixProtocolStdioExports { + fn load(store: &mut Store, instance: &Instance) -> Result> { + let Some(set_protocol_transport) = + optional_typed_export::(store, instance, "pgl_set_protocol_transport")? + else { + return Ok(None); + }; + let protocol_stream_active = + typed_export::<(), i32>(store, instance, "pgl_protocol_stream_active")?; + Ok(Some(Self { + set_protocol_transport, + protocol_stream_active, + })) + } +} + +fn ensure_integrated_pglite_contract(instance: &Instance) -> Result<()> { + for name in [ + "pgl_startPGlite", + "pgl_setPGliteActive", + "PostgresMainLongJmp", + ] { + ensure!( + instance.exports.get_function(name).is_ok() + || instance.exports.get_function(&format!("_{name}")).is_ok(), + "WASIX runtime is missing integrated PGlite lifecycle export {name}" + ); + } + Ok(()) +} + +impl WasixPgliteIo { + fn new(store: &mut Store, instance: &Instance) -> Result { + let io = Self { + input_reset: typed_export(store, instance, "pgl_wasix_input_reset")?, + input_write: typed_export(store, instance, "pgl_wasix_input_write")?, + input_available: typed_export(store, instance, "pgl_wasix_input_available")?, + output_reset: typed_export(store, instance, "pgl_wasix_output_reset")?, + output_len: typed_export(store, instance, "pgl_wasix_output_len")?, + output_read: typed_export(store, instance, "pgl_wasix_output_read")?, + }; + io.reset(store)?; + Ok(io) + } + + fn reset(&self, store: &mut Store) -> Result<()> { + ensure!( + self.input_reset + .call(&mut *store) + .context("pgl_wasix_input_reset")? + == 0, + "pgl_wasix_input_reset failed" + ); + ensure!( + self.output_reset + .call(&mut *store) + .context("pgl_wasix_output_reset")? + == 0, + "pgl_wasix_output_reset failed" + ); + Ok(()) + } + + fn push_input( + &self, + store: &mut Store, + env: &WasiFunctionEnv, + allocator: &GuestAllocator, + bytes: &[u8], + ) -> Result<()> { + if bytes.is_empty() { + return Ok(()); + } + let written = allocator.with_bytes(store, env, bytes, |store, ptr| { + self.input_write + .call(&mut *store, ptr, bytes.len() as i32) + .context("pgl_wasix_input_write") })?; + ensure!( + written == bytes.len() as i32, + "pgl_wasix_input_write wrote {written}, expected {}", + bytes.len() + ); + Ok(()) + } - let dev_path = paths.pgroot.join("dev"); - builder - .preopened_dir(&dev_path, "/dev", DirPerms::all(), FilePerms::all()) - .map_err(|err| anyhow!("failed to preopen {} as /dev: {err}", dev_path.display()))?; + fn available(&self, store: &mut Store) -> Result { + let available = self + .input_available + .call(store) + .context("pgl_wasix_input_available")?; + ensure!( + available >= 0, + "pgl_wasix_input_available returned negative length {available}" + ); + Ok(available) + } + + fn take_output( + &self, + store: &mut Store, + env: &WasiFunctionEnv, + allocator: &GuestAllocator, + ) -> Result> { + let len = self + .output_len + .call(&mut *store) + .context("pgl_wasix_output_len")?; + ensure!( + len >= 0, + "pgl_wasix_output_len returned negative length {len}" + ); + if len == 0 { + return Ok(Vec::new()); + } + let bytes = allocator.with_allocation(store, len, |store, ptr| { + let read = self + .output_read + .call(&mut *store, ptr, len) + .context("pgl_wasix_output_read")?; + ensure!( + read >= 0 && read <= len, + "invalid pgl_wasix_output_read length {read}" + ); + + let mut bytes = vec![0u8; read as usize]; + let view = env + .data(&*store) + .try_memory_view(&*store) + .context("get WASIX memory view")?; + view.read(ptr as u64, &mut bytes) + .with_context(|| format!("read SQL output at 0x{ptr:x}"))?; + Ok(bytes) + })?; + ensure!( + self.output_reset + .call(&mut *store) + .context("pgl_wasix_output_reset after read")? + == 0, + "pgl_wasix_output_reset after read failed" + ); + Ok(bytes) + } +} + +impl GuestAllocator { + fn load(store: &mut Store, instance: &Instance) -> Result { + let malloc = typed_export::(store, instance, "malloc")?; + let free = typed_export::(store, instance, "pg_free") + .or_else(|_| typed_export::(store, instance, "free")) + .context("get pg_free/free export")?; + Ok(Self { + malloc, + free, + #[cfg(debug_assertions)] + allocations: Cell::new(0), + #[cfg(debug_assertions)] + frees: Cell::new(0), + }) + } + + #[cfg(debug_assertions)] + fn allocation_counts(&self) -> (u64, u64) { + (self.allocations.get(), self.frees.get()) + } + + fn with_bytes( + &self, + store: &mut Store, + env: &WasiFunctionEnv, + bytes: &[u8], + f: impl FnOnce(&mut Store, i32) -> Result, + ) -> Result { + let ptr = self.allocate(store, bytes.len() as i32)?; + self.run_and_free(store, ptr, |store, ptr| { + let view = env + .data(&*store) + .try_memory_view(&*store) + .context("get WASIX memory view")?; + view.write(ptr as u64, bytes) + .with_context(|| format!("write guest bytes at 0x{ptr:x}"))?; + f(store, ptr) + }) + } + + fn with_allocation( + &self, + store: &mut Store, + len: i32, + f: impl FnOnce(&mut Store, i32) -> Result, + ) -> Result { + let ptr = self.allocate(store, len)?; + self.run_and_free(store, ptr, f) + } + + fn allocate(&self, store: &mut Store, len: i32) -> Result { + let ptr = self + .malloc + .call(&mut *store, len) + .context("malloc guest allocation")?; + ensure!(ptr > 0, "malloc returned null for guest allocation"); + #[cfg(debug_assertions)] + self.allocations.set(self.allocations.get() + 1); + Ok(ptr) + } + + fn run_and_free( + &self, + store: &mut Store, + ptr: i32, + f: impl FnOnce(&mut Store, i32) -> Result, + ) -> Result { + let result = f(store, ptr); + let free_result = self + .free + .call(&mut *store, ptr) + .with_context(|| format!("free guest allocation at 0x{ptr:x}")); + #[cfg(debug_assertions)] + if free_result.is_ok() { + self.frees.set(self.frees.get() + 1); + } + match (result, free_result) { + (Ok(value), Ok(())) => Ok(value), + (Ok(_), Err(err)) => Err(err), + (Err(err), Ok(())) => Err(err), + (Err(err), Err(free_err)) => Err(err.context(format!( + "failed to free guest allocation at 0x{ptr:x} after previous error: {free_err:#}" + ))), + } + } +} + +fn typed_export( + store: &mut Store, + instance: &Instance, + name: &str, +) -> Result> +where + Args: WasmTypeList, + Rets: WasmTypeList, +{ + instance + .exports + .get_typed_function::(&*store, name) + .or_else(|_| { + instance + .exports + .get_typed_function::(&*store, &format!("_{name}")) + }) + .with_context(|| format!("get {name} export")) +} + +fn optional_typed_export( + store: &mut Store, + instance: &Instance, + name: &str, +) -> Result>> +where + Args: WasmTypeList, + Rets: WasmTypeList, +{ + let underscored_name = format!("_{name}"); + if instance.exports.get_function(name).is_err() + && instance.exports.get_function(&underscored_name).is_err() + { + return Ok(None); + } + typed_export(store, instance, name).map(Some) +} + +fn runtime_error_exit_code(err: &wasmer::RuntimeError) -> Option { + err.downcast_ref::().and_then(|err| match err { + WasiError::Exit(code) => Some(code.raw()), + _ => None, + }) +} + +fn is_wasm_uncaught_exception(err: &wasmer::RuntimeError) -> bool { + // Wasmer reports an uncaught WebAssembly exception when PostgreSQL ERROR + // unwinds across the exported loop boundary. The C recovery export then + // performs the normal Postgres error cleanup and emits ErrorResponse. + err.message().contains("uncaught exception") +} + +fn host_requires_process_exit_error_recovery() -> bool { + // Wasmer 7.2.0-alpha.2 does not implement nested WebAssembly exception + // throws on MSVC hosts. The WASIX bridge therefore routes PostgreSQL ERROR + // longjmps through the existing process-exit recovery boundary on that + // host capability, while preserving normal nested unwinding elsewhere. + cfg!(target_env = "msvc") +} - Ok(builder.build_p1()) +fn add_pglite_env(builder: &mut wasmer_wasix::WasiEnvBuilder, startup_config: &StartupConfig) { + for (key, value) in [ + ("PREFIX", WASM_PREFIX), + ("PGDATA", PGDATA_DIR), + ("PGUSER", startup_config.username.as_str()), + ("PGDATABASE", startup_config.database.as_str()), + ("MODE", "REACT"), + ("REPL", "N"), + ("PGSYSCONFDIR", PGDATA_DIR), + ("PGCLIENTENCODING", "UTF8"), + ("LC_CTYPE", "C.UTF-8"), + ("TZ", "UTC"), + ("PGTZ", "UTC"), + ("PG_COLOR", "never"), + ] { + builder.add_env(key, value); + } +} + +fn add_pglite_args( + builder: &mut wasmer_wasix::WasiEnvBuilder, + postgres_config: &PostgresConfig, + startup_config: &StartupConfig, +) -> Result<()> { + postgres_config.validate()?; + startup_config.validate()?; + for arg in ["--single", "-F", "-O", "-j"] { + builder.add_arg(arg); + } + if let Some(level) = startup_config.debug_level { + builder.add_arg("-d"); + builder.add_arg(level.to_string()); + } + for (name, value) in DEFAULT_STARTUP_GUCS { + builder.add_arg("-c"); + builder.add_arg(format!("{name}={value}")); + } + if startup_config.relaxed_durability { + builder.add_arg("-c"); + builder.add_arg("synchronous_commit=off"); + } + for (name, value) in postgres_config.iter() { + builder.add_arg("-c"); + builder.add_arg(format!("{name}={value}")); + } + for arg in &startup_config.extra_args { + builder.add_arg(arg); + } + for arg in ["-D", PGDATA_DIR, startup_config.database.as_str()] { + builder.add_arg(arg); + } + Ok(()) } +const DEFAULT_STARTUP_GUCS: &[(&str, &str)] = &[ + ("search_path", "public"), + ("exit_on_error", "false"), + ("log_checkpoints", "false"), + ("max_worker_processes", "0"), + ("max_parallel_workers", "0"), + ("max_parallel_workers_per_gather", "0"), + ("wal_buffers", "4MB"), + ("min_wal_size", "80MB"), + ("shared_buffers", "128MB"), +]; + fn ensure_runtime_dirs(paths: &PglitePaths) -> Result<()> { - let dev_path = paths.pgroot.join("dev"); - if !dev_path.exists() { - std::fs::create_dir_all(&dev_path) - .with_context(|| format!("failed to create {}", dev_path.display()))?; + for path in [ + paths.runtime_root(), + paths.pgdata.clone(), + paths.runtime_root().join("home"), + paths.runtime_root().join("dev"), + paths.runtime_root().join("dev/shm"), + paths.runtime_root().join("tmp"), + ] { + fs::create_dir_all(&path).with_context(|| format!("create {}", path.display()))?; } - let urandom = dev_path.join("urandom"); + + let urandom = paths.runtime_root().join("dev/urandom"); if !urandom.exists() { - let mut buf = [0u8; 128]; - fill_random(&mut buf).context("seed urandom")?; - std::fs::write(&urandom, buf) - .with_context(|| format!("failed to seed {}", urandom.display()))?; + fs::write(&urandom, [42u8; 128]).with_context(|| format!("seed {}", urandom.display()))?; + } + for name in ["null", "stdout", "stderr", "zero"] { + let path = paths.runtime_root().join("dev").join(name); + if !path.exists() { + fs::write(&path, []).with_context(|| format!("create {}", path.display()))?; + } + } + Ok(()) +} + +fn startup_packet(user: &str, database: &str) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&196608i32.to_be_bytes()); + for (key, value) in [ + ("user", user), + ("database", database), + ("client_encoding", "UTF8"), + ("DateStyle", "ISO, MDY"), + ("TimeZone", "UTC"), + ] { + body.extend_from_slice(key.as_bytes()); + body.push(0); + body.extend_from_slice(value.as_bytes()); + body.push(0); + } + body.push(0); + + let mut packet = Vec::with_capacity(body.len() + 4); + packet.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); + packet.extend_from_slice(&body); + packet +} + +fn protocol_response_contains_error(response: &[u8]) -> bool { + let mut cursor = 0usize; + while cursor + 5 <= response.len() { + let tag = response[cursor]; + let len = i32::from_be_bytes(response[cursor + 1..cursor + 5].try_into().unwrap()); + if len < 4 { + return false; + } + let total = 1usize.saturating_add(len as usize); + if cursor + total > response.len() { + return false; + } + if tag == b'E' { + return true; + } + cursor += total; } + false +} - if !paths.pgdata.exists() { - std::fs::create_dir_all(&paths.pgdata) - .with_context(|| format!("failed to create {}", paths.pgdata.display()))?; +fn format_output_tail(bytes: &[u8]) -> String { + const LIMIT: usize = 512; + let skipped = bytes.len().saturating_sub(LIMIT); + let tail = &bytes[skipped..]; + let mut hex = String::new(); + for (index, byte) in tail.iter().enumerate() { + if index > 0 { + hex.push(' '); + } + hex.push_str(&format!("{byte:02x}")); } + let text = String::from_utf8_lossy(tail); + format!( + "{} bytes total, showing last {} bytes\nhex: {hex}\nutf8-lossy:\n{text}", + bytes.len(), + tail.len() + ) +} +fn seed_exported_c_string_value( + store: &mut Store, + instance: &Instance, + env: &WasiFunctionEnv, + name: &str, + value: &str, +) -> Result<()> { + let Ok(global) = instance.exports.get_global(name) else { + return Ok(()); + }; + let wasmer::Value::I32(ptr) = global.get(&mut *store) else { + return Ok(()); + }; + if ptr <= 0 { + return Ok(()); + } + let mut bytes = value.as_bytes().to_vec(); + bytes.push(0); + let view = env + .data(&*store) + .try_memory_view(&*store) + .context("get WASIX memory view")?; + view.write(ptr as u64, &bytes) + .with_context(|| format!("seed {name} at 0x{ptr:x}"))?; Ok(()) } + +fn summarize_protocol(bytes: &[u8]) -> String { + if bytes.is_empty() { + return "0 bytes".to_owned(); + } + + let mut cursor = 0usize; + let mut messages = Vec::new(); + while cursor + 5 <= bytes.len() { + let tag = bytes[cursor] as char; + let len = i32::from_be_bytes([ + bytes[cursor + 1], + bytes[cursor + 2], + bytes[cursor + 3], + bytes[cursor + 4], + ]); + if len < 4 { + messages.push(format!("{tag}(bad-len:{len})")); + break; + } + let end = cursor + 1 + len as usize; + if end > bytes.len() { + messages.push(format!("{tag}(truncated:{len})")); + break; + } + messages.push(format!("{tag}({} bytes)", len - 4)); + cursor = end; + } + if cursor < bytes.len() { + messages.push(format!("tail:{} bytes", bytes.len() - cursor)); + } + format!("{} bytes [{}]", bytes.len(), messages.join(", ")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn protocol_stdio_fails_closed_when_detached() -> Result<()> { + use std::task::{Context, Poll, Waker}; + use wasmer_wasix::VirtualFile; + use wasmer_wasix::virtual_fs::AsyncWrite; + + let mut file = ProtocolStdioFile::new(); + let mut cx = Context::from_waker(Waker::noop()); + + match Pin::new(&mut file).poll_write_ready(&mut cx) { + Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), + other => panic!("unexpected detached write-ready result: {other:?}"), + } + match Pin::new(&mut file).poll_write(&mut cx, b"lost bytes") { + Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), + other => panic!("unexpected detached write result: {other:?}"), + } + match Pin::new(&mut file).poll_flush(&mut cx) { + Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), + other => panic!("unexpected detached flush result: {other:?}"), + } + + Ok(()) + } + + #[test] + fn block_on_tokio_runtime_works_inside_tokio_runtime() -> Result<()> { + let worker = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let host = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let value = host.block_on(async { block_on_tokio_runtime(&worker, async { 42 }) }); + + assert_eq!(value, 42); + Ok(()) + } + + #[test] + fn mountfs_pgdata_overlay_exposes_lower_template_files() -> Result<()> { + use tokio::io::AsyncWriteExt; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + let temp = tempfile::TempDir::new()?; + let runtime_root = temp.path().join("runtime"); + let pgdata_upper = runtime_root.join("base"); + let pgdata_lower = temp.path().join("template"); + fs::create_dir_all(&pgdata_upper)?; + fs::create_dir_all(&pgdata_lower)?; + fs::write(pgdata_lower.join("postgresql.conf"), b"from-template\n")?; + + let root = virtual_fs::MountFileSystem::new(); + root.mount(Path::new("/"), host_filesystem(&runtime_root)?)?; + root.mount( + Path::new(PGDATA_DIR), + Arc::new(EagerCopyOverlayFileSystem::new( + pgdata_upper.clone(), + pgdata_lower.clone(), + )?), + )?; + + virtual_fs::FileSystem::metadata(&root, Path::new("/base/postgresql.conf"))?; + virtual_fs::FileSystem::new_open_options(&root) + .read(true) + .open("/base/postgresql.conf")?; + let mut writable = virtual_fs::FileSystem::new_open_options(&root) + .write(true) + .open("/base/postgresql.conf")?; + runtime.block_on(async { + writable.write_all(b"upper-only\n").await?; + writable.flush().await + })?; + assert!(pgdata_upper.join("postgresql.conf").is_file()); + assert_eq!( + fs::read_to_string(pgdata_lower.join("postgresql.conf"))?, + "from-template\n" + ); + Ok(()) + } + + #[test] + fn mountfs_pgdata_overlay_creates_files_in_lower_only_directories() -> Result<()> { + use tokio::io::AsyncWriteExt; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + let temp = tempfile::TempDir::new()?; + let runtime_root = temp.path().join("runtime"); + let pgdata_upper = runtime_root.join("base"); + let pgdata_lower = temp.path().join("template"); + fs::create_dir_all(&pgdata_upper)?; + fs::create_dir_all(pgdata_lower.join("global"))?; + + let root = virtual_fs::MountFileSystem::new(); + root.mount(Path::new("/"), host_filesystem(&runtime_root)?)?; + root.mount( + Path::new(PGDATA_DIR), + Arc::new(EagerCopyOverlayFileSystem::new( + pgdata_upper.clone(), + pgdata_lower, + )?), + )?; + + let mut writable = virtual_fs::FileSystem::new_open_options(&root) + .write(true) + .create(true) + .open("/base/global/postmaster.pid")?; + runtime.block_on(async { + writable.write_all(b"lock\n").await?; + writable.flush().await + })?; + + assert_eq!( + fs::read_to_string(pgdata_upper.join("global/postmaster.pid"))?, + "lock\n" + ); + Ok(()) + } + + #[test] + fn mountfs_root_filesystem_routes_pgdata_as_mutable_subtree() -> Result<()> { + use tokio::io::AsyncWriteExt; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + let temp = tempfile::TempDir::new()?; + let runtime_root = temp.path().join("runtime"); + let pgdata_upper = runtime_root.join("base"); + let pgdata_lower = temp.path().join("template"); + fs::create_dir_all(&pgdata_upper)?; + fs::create_dir_all(pgdata_lower.join("global"))?; + fs::write(pgdata_lower.join("PG_VERSION"), b"17\n")?; + fs::write(pgdata_lower.join("global/pg_control"), b"control\n")?; + + let root = wasi_root_with_pgdata_mount( + host_filesystem(&runtime_root)?, + Arc::new(EagerCopyOverlayFileSystem::new( + pgdata_upper.clone(), + pgdata_lower, + )?), + )?; + + virtual_fs::FileSystem::metadata(root.as_ref(), Path::new("/base/PG_VERSION"))?; + let mut entries = + virtual_fs::FileSystem::read_dir(root.as_ref(), Path::new("/base/global"))?; + let entry = entries.next().transpose()?.context("expected pg_control")?; + assert_eq!(entry.path, Path::new("/base/global/pg_control")); + + let mut lock_file = virtual_fs::FileSystem::new_open_options(root.as_ref()) + .read(true) + .write(true) + .create_new(true) + .open("/base/postmaster.pid")?; + runtime.block_on(async { + lock_file.write_all(b"lock\n").await?; + lock_file.flush().await + })?; + + assert_eq!( + fs::read_to_string(pgdata_upper.join("postmaster.pid"))?, + "lock\n" + ); + Ok(()) + } +} diff --git a/src/pglite/proxy.rs b/src/pglite/proxy.rs index bd969d9a..a4cae7b6 100644 --- a/src/pglite/proxy.rs +++ b/src/pglite/proxy.rs @@ -1,35 +1,200 @@ use anyhow::{Context, Result, anyhow, bail}; -use std::io::{ErrorKind, Read, Write}; -use std::net::{TcpListener, ToSocketAddrs}; +use serde::Serialize; +use std::io::{self, Read, Write}; +use std::net::{TcpListener, TcpStream, ToSocketAddrs}; #[cfg(unix)] -use std::os::unix::net::UnixListener; +use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, mpsc::SyncSender, }; -use std::thread; -use std::time::Duration; -use crate::pglite::base::install_into; -use crate::pglite::postgres_mod::PostgresMod; -use crate::pglite::transport::Transport; +use crate::pglite::backend::{BackendOpenKind, BackendSession}; +#[cfg(feature = "extensions")] +use crate::pglite::base::install_missing_extension_archives; +use crate::pglite::base::{InstallOutcome, install_into}; +use crate::pglite::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use crate::pglite::extensions::Extension; +use crate::pglite::postgres_mod::{ + ProtocolPumpOutcome, ProtocolStream, StartupProtocolResponse, startup_error_response_output, +}; +use crate::pglite::timing; +use crate::pglite::wire::{ + FrontendFrameKind, FrontendFrameReader, classify_frontend_message, error_response, + response_contains_error, simple_query_message, startup_config_for_message, startup_parameter, +}; + +static PROTOCOL_STATS: ProtocolStats = ProtocolStats::new(); + +#[doc(hidden)] +#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ProtocolStatsSnapshot { + pub frontend_reads: u64, + pub frontend_bytes: u64, + pub frontend_messages: u64, + pub startup_messages: u64, + pub protocol_messages: u64, + pub simple_query_messages: u64, + pub parse_messages: u64, + pub bind_messages: u64, + pub execute_messages: u64, + pub sync_messages: u64, + pub flush_messages: u64, + pub copy_data_messages: u64, + pub protocol_batches: u64, + pub protocol_batch_bytes: u64, + pub backend_send_calls: u64, + pub backend_send_bytes: u64, + pub response_writes: u64, + pub response_bytes: u64, + pub socket_flushes: u64, + pub copy_guard_rejections: u64, + pub streaming_copy_handoffs: u64, +} + +struct ProtocolStats { + enabled: AtomicBool, + frontend_reads: AtomicU64, + frontend_bytes: AtomicU64, + frontend_messages: AtomicU64, + startup_messages: AtomicU64, + protocol_messages: AtomicU64, + simple_query_messages: AtomicU64, + parse_messages: AtomicU64, + bind_messages: AtomicU64, + execute_messages: AtomicU64, + sync_messages: AtomicU64, + flush_messages: AtomicU64, + copy_data_messages: AtomicU64, + protocol_batches: AtomicU64, + protocol_batch_bytes: AtomicU64, + backend_send_calls: AtomicU64, + backend_send_bytes: AtomicU64, + response_writes: AtomicU64, + response_bytes: AtomicU64, + socket_flushes: AtomicU64, + copy_guard_rejections: AtomicU64, + streaming_copy_handoffs: AtomicU64, +} + +impl ProtocolStats { + const fn new() -> Self { + Self { + enabled: AtomicBool::new(false), + frontend_reads: AtomicU64::new(0), + frontend_bytes: AtomicU64::new(0), + frontend_messages: AtomicU64::new(0), + startup_messages: AtomicU64::new(0), + protocol_messages: AtomicU64::new(0), + simple_query_messages: AtomicU64::new(0), + parse_messages: AtomicU64::new(0), + bind_messages: AtomicU64::new(0), + execute_messages: AtomicU64::new(0), + sync_messages: AtomicU64::new(0), + flush_messages: AtomicU64::new(0), + copy_data_messages: AtomicU64::new(0), + protocol_batches: AtomicU64::new(0), + protocol_batch_bytes: AtomicU64::new(0), + backend_send_calls: AtomicU64::new(0), + backend_send_bytes: AtomicU64::new(0), + response_writes: AtomicU64::new(0), + response_bytes: AtomicU64::new(0), + socket_flushes: AtomicU64::new(0), + copy_guard_rejections: AtomicU64::new(0), + streaming_copy_handoffs: AtomicU64::new(0), + } + } + + fn reset(&self) { + self.enabled.store(true, Ordering::Relaxed); + self.frontend_reads.store(0, Ordering::Relaxed); + self.frontend_bytes.store(0, Ordering::Relaxed); + self.frontend_messages.store(0, Ordering::Relaxed); + self.startup_messages.store(0, Ordering::Relaxed); + self.protocol_messages.store(0, Ordering::Relaxed); + self.simple_query_messages.store(0, Ordering::Relaxed); + self.parse_messages.store(0, Ordering::Relaxed); + self.bind_messages.store(0, Ordering::Relaxed); + self.execute_messages.store(0, Ordering::Relaxed); + self.sync_messages.store(0, Ordering::Relaxed); + self.flush_messages.store(0, Ordering::Relaxed); + self.copy_data_messages.store(0, Ordering::Relaxed); + self.protocol_batches.store(0, Ordering::Relaxed); + self.protocol_batch_bytes.store(0, Ordering::Relaxed); + self.backend_send_calls.store(0, Ordering::Relaxed); + self.backend_send_bytes.store(0, Ordering::Relaxed); + self.response_writes.store(0, Ordering::Relaxed); + self.response_bytes.store(0, Ordering::Relaxed); + self.socket_flushes.store(0, Ordering::Relaxed); + self.copy_guard_rejections.store(0, Ordering::Relaxed); + self.streaming_copy_handoffs.store(0, Ordering::Relaxed); + } -const SSL_REQUEST_CODE: i32 = 80_877_103; -const GSSENC_REQUEST_CODE: i32 = 80_877_104; -const CANCEL_REQUEST_CODE: i32 = 80_877_102; -const PROTOCOL_3: i32 = 196_608; -const MAX_FRONTEND_MESSAGE: usize = 64 * 1024 * 1024; + fn snapshot(&self) -> ProtocolStatsSnapshot { + ProtocolStatsSnapshot { + frontend_reads: self.frontend_reads.load(Ordering::Relaxed), + frontend_bytes: self.frontend_bytes.load(Ordering::Relaxed), + frontend_messages: self.frontend_messages.load(Ordering::Relaxed), + startup_messages: self.startup_messages.load(Ordering::Relaxed), + protocol_messages: self.protocol_messages.load(Ordering::Relaxed), + simple_query_messages: self.simple_query_messages.load(Ordering::Relaxed), + parse_messages: self.parse_messages.load(Ordering::Relaxed), + bind_messages: self.bind_messages.load(Ordering::Relaxed), + execute_messages: self.execute_messages.load(Ordering::Relaxed), + sync_messages: self.sync_messages.load(Ordering::Relaxed), + flush_messages: self.flush_messages.load(Ordering::Relaxed), + copy_data_messages: self.copy_data_messages.load(Ordering::Relaxed), + protocol_batches: self.protocol_batches.load(Ordering::Relaxed), + protocol_batch_bytes: self.protocol_batch_bytes.load(Ordering::Relaxed), + backend_send_calls: self.backend_send_calls.load(Ordering::Relaxed), + backend_send_bytes: self.backend_send_bytes.load(Ordering::Relaxed), + response_writes: self.response_writes.load(Ordering::Relaxed), + response_bytes: self.response_bytes.load(Ordering::Relaxed), + socket_flushes: self.socket_flushes.load(Ordering::Relaxed), + copy_guard_rejections: self.copy_guard_rejections.load(Ordering::Relaxed), + streaming_copy_handoffs: self.streaming_copy_handoffs.load(Ordering::Relaxed), + } + } + + fn add(counter: &AtomicU64, value: u64) { + if PROTOCOL_STATS.enabled.load(Ordering::Relaxed) { + counter.fetch_add(value, Ordering::Relaxed); + } + } +} + +#[doc(hidden)] +pub fn reset_protocol_stats() { + PROTOCOL_STATS.reset(); +} + +#[doc(hidden)] +pub fn disable_protocol_stats() { + PROTOCOL_STATS.enabled.store(false, Ordering::Relaxed); +} + +#[doc(hidden)] +pub fn protocol_stats_snapshot() -> ProtocolStatsSnapshot { + PROTOCOL_STATS.snapshot() +} /// Blocking PostgreSQL socket proxy for the embedded PGlite runtime. /// /// The proxy intentionally runs each accepted connection on one blocking thread -/// and does not call Wasmtime from an async runtime. That avoids the nested -/// runtime panic that can happen when an async wrapper blocks inside Wasmtime. +/// and does not call into the WASIX backend from an async runtime. That avoids +/// nested runtime panics when an async wrapper blocks inside the embedded engine. #[derive(Debug, Clone)] pub struct PgliteProxy { root: Arc, + prepared_root: Option>, + postgres_config: Arc, + startup_config: Arc, + #[cfg(feature = "extensions")] + extensions: Arc>, } impl PgliteProxy { @@ -37,9 +202,36 @@ impl PgliteProxy { pub fn new(root: impl Into) -> Self { Self { root: Arc::new(root.into()), + prepared_root: None, + postgres_config: Arc::new(PostgresConfig::default()), + startup_config: Arc::new(StartupConfig::default()), + #[cfg(feature = "extensions")] + extensions: Arc::new(Vec::new()), } } + pub(crate) fn with_prepared_root(mut self, outcome: InstallOutcome) -> Self { + self.prepared_root = Some(Arc::new(outcome)); + self + } + + pub(crate) fn with_postgres_config(mut self, postgres_config: PostgresConfig) -> Self { + self.postgres_config = Arc::new(postgres_config); + self + } + + pub(crate) fn with_startup_config(mut self, startup_config: StartupConfig) -> Self { + self.startup_config = Arc::new(startup_config); + self + } + + /// Enable bundled extensions in the proxy backend before accepting clients. + #[cfg(feature = "extensions")] + pub(crate) fn with_extensions(mut self, extensions: Vec) -> Self { + self.extensions = Arc::new(extensions); + self + } + /// Return the root directory used for runtime installation and cluster data. pub fn root(&self) -> &Path { &self.root @@ -56,10 +248,9 @@ impl PgliteProxy { /// Serve an existing TCP listener forever. Connections are handled one at a time. pub fn serve_tcp_listener(&self, listener: TcpListener) -> Result<()> { - let mut backend = WireBackend::open(&self.root)?; for stream in listener.incoming() { let stream = stream.context("accept TCP proxy connection")?; - self.handle_stream(stream, &mut backend)?; + self.handle_stream(stream)?; } Ok(()) } @@ -70,38 +261,21 @@ impl PgliteProxy { shutdown: Arc, ready: Option>>, ) -> Result<()> { - listener - .set_nonblocking(true) - .context("configure TCP proxy listener as nonblocking")?; - - let mut backend = match WireBackend::open(&self.root) { - Ok(backend) => { - if let Some(ready) = ready { - let _ = ready.send(Ok(())); - } - backend - } - Err(err) => { - let message = format!("{err:#}"); - if let Some(ready) = ready { - let _ = ready.send(Err(anyhow!(message.clone()))); - } - return Err(anyhow!(message)); - } - }; + if let Some(ready) = ready { + let _ = ready.send(Ok(())); + } while !shutdown.load(Ordering::SeqCst) { - match listener.accept() { - Ok((stream, _)) => { - stream - .set_nonblocking(false) - .context("configure TCP proxy stream as blocking")?; - self.handle_stream(stream, &mut backend)?; - } - Err(err) if err.kind() == ErrorKind::WouldBlock => { - thread::sleep(Duration::from_millis(10)); - } - Err(err) => return Err(err).context("accept TCP proxy connection"), + let (stream, _) = { + let _phase = timing::phase("proxy.accept_wait"); + listener.accept().context("accept TCP proxy connection")? + }; + if shutdown.load(Ordering::SeqCst) { + break; } + stream + .set_nonblocking(false) + .context("configure TCP proxy stream as blocking")?; + self.handle_stream(stream)?; } Ok(()) @@ -114,10 +288,9 @@ impl PgliteProxy { /// Accept and handle `count` TCP connections using one embedded backend. pub fn accept_tcp_connections(&self, listener: &TcpListener, count: usize) -> Result<()> { - let mut backend = WireBackend::open(&self.root)?; for _ in 0..count { let (stream, _) = listener.accept().context("accept TCP proxy connection")?; - self.handle_stream(stream, &mut backend)?; + self.handle_stream(stream)?; } Ok(()) } @@ -138,10 +311,9 @@ impl PgliteProxy { /// Serve an existing Unix-domain listener forever. Connections are handled one at a time. #[cfg(unix)] pub fn serve_unix_listener(&self, listener: UnixListener) -> Result<()> { - let mut backend = WireBackend::open(&self.root)?; for stream in listener.incoming() { let stream = stream.context("accept Unix proxy connection")?; - self.handle_stream(stream, &mut backend)?; + self.handle_stream(stream)?; } Ok(()) } @@ -153,38 +325,21 @@ impl PgliteProxy { shutdown: Arc, ready: Option>>, ) -> Result<()> { - listener - .set_nonblocking(true) - .context("configure Unix proxy listener as nonblocking")?; - - let mut backend = match WireBackend::open(&self.root) { - Ok(backend) => { - if let Some(ready) = ready { - let _ = ready.send(Ok(())); - } - backend - } - Err(err) => { - let message = format!("{err:#}"); - if let Some(ready) = ready { - let _ = ready.send(Err(anyhow!(message.clone()))); - } - return Err(anyhow!(message)); - } - }; + if let Some(ready) = ready { + let _ = ready.send(Ok(())); + } while !shutdown.load(Ordering::SeqCst) { - match listener.accept() { - Ok((stream, _)) => { - stream - .set_nonblocking(false) - .context("configure Unix proxy stream as blocking")?; - self.handle_stream(stream, &mut backend)?; - } - Err(err) if err.kind() == ErrorKind::WouldBlock => { - thread::sleep(Duration::from_millis(10)); - } - Err(err) => return Err(err).context("accept Unix proxy connection"), + let (stream, _) = { + let _phase = timing::phase("proxy.accept_wait"); + listener.accept().context("accept Unix proxy connection")? + }; + if shutdown.load(Ordering::SeqCst) { + break; } + stream + .set_nonblocking(false) + .context("configure Unix proxy stream as blocking")?; + self.handle_stream(stream)?; } Ok(()) @@ -199,190 +354,581 @@ impl PgliteProxy { /// Accept and handle `count` Unix-domain socket connections using one embedded backend. #[cfg(unix)] pub fn accept_unix_connections(&self, listener: &UnixListener, count: usize) -> Result<()> { - let mut backend = WireBackend::open(&self.root)?; for _ in 0..count { let (stream, _) = listener.accept().context("accept Unix proxy connection")?; - self.handle_stream(stream, &mut backend)?; + self.handle_stream(stream)?; } Ok(()) } - fn handle_stream(&self, mut stream: S, backend: &mut WireBackend) -> Result<()> + fn handle_stream(&self, mut stream: S) -> Result<()> where - S: Read + Write, + S: CloneProtocolStream, { - let mut reader = FrontendMessageReader::default(); + let _phase = timing::phase("proxy.handle_stream"); + let mut backend = None::; + let mut reader = FrontendFrameReader::default(); let mut buffer = [0u8; 64 * 1024]; let mut protocol_batch = Vec::new(); loop { - let read = stream.read(&mut buffer).context("read frontend socket")?; + let read = { + let _phase = timing::phase("proxy.stream_read"); + stream.read(&mut buffer).context("read frontend socket")? + }; if read == 0 { - flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + flush_protocol_batch_if_started( + &mut protocol_batch, + backend.as_mut(), + &mut stream, + )?; break; } + ProtocolStats::add(&PROTOCOL_STATS.frontend_reads, 1); + ProtocolStats::add(&PROTOCOL_STATS.frontend_bytes, read as u64); let mut close_after_flush = false; - let messages = reader.push(&buffer[..read])?; - for message in messages { - match classify_frontend_message(&message)? { - FrontendMessageKind::SslOrGssRequest => { - flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; - stream.write_all(b"N").context("write SSL refusal")?; + let messages = { + let _phase = timing::phase("proxy.frontend_parse"); + reader.push(&buffer[..read])? + }; + let message_count = messages.len(); + ProtocolStats::add(&PROTOCOL_STATS.frontend_messages, message_count as u64); + let mut message_index = 0usize; + while message_index < message_count { + let message = &messages[message_index]; + match classify_frontend_message(message)? { + FrontendFrameKind::SslOrGssRequest => { + flush_protocol_batch_if_started( + &mut protocol_batch, + backend.as_mut(), + &mut stream, + )?; + { + let _phase = timing::phase("proxy.startup_response_write"); + if !write_frontend(&mut stream, b"N", "write SSL refusal")? { + close_after_flush = true; + } + } } - FrontendMessageKind::CancelRequest => { - flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + FrontendFrameKind::CancelRequest => { + flush_protocol_batch_if_started( + &mut protocol_batch, + backend.as_mut(), + &mut stream, + )?; close_after_flush = true; } - FrontendMessageKind::Terminate => { - flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + FrontendFrameKind::Terminate => { + flush_protocol_batch_if_started( + &mut protocol_batch, + backend.as_mut(), + &mut stream, + )?; close_after_flush = true; } - FrontendMessageKind::Startup => { - flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; - stream - .write_all(&startup_response()) - .context("write startup response")?; + FrontendFrameKind::Startup => { + ProtocolStats::add(&PROTOCOL_STATS.startup_messages, 1); + if backend.is_some() { + bail!("received a second startup packet on one proxy connection"); + } + flush_protocol_batch_if_started( + &mut protocol_batch, + backend.as_mut(), + &mut stream, + )?; + let connection_startup_config = + startup_config_for_message(&self.startup_config, message)?; + let opened_result = { + let _phase = timing::phase("proxy.backend_open"); + WireBackend::open( + &self.root, + self.prepared_root.as_deref(), + &self.postgres_config, + &connection_startup_config, + self.extensions(), + ) + }; + let mut opened = match opened_result { + Ok(opened) => opened, + Err(err) => { + let response = startup_error_response_output(&err) + .map_or_else(|| backend_open_error_response(&err), Vec::from); + let _ = write_frontend( + &mut stream, + &response, + "write startup backend-open failure", + )?; + close_after_flush = true; + break; + } + }; + let response = { + let _phase = timing::phase("proxy.startup_response_backend"); + opened.startup(message)? + }; + let response_accepted = + response.accepted && !response_contains_error(&response.output); + if response_accepted { + #[cfg(feature = "extensions")] + { + // Use the serving backend for idempotent extension setup; a separate + // setup backend adds a full Postgres startup and can force WAL recovery. + let _phase = timing::phase("proxy.startup_extension_setup"); + opened.enable_extensions(self.extensions())?; + } + if let Some(user) = startup_parameter(message, "user")? + && user != "postgres" + { + let role_response = opened.set_role(user)?; + if response_contains_error(&role_response) { + let _ = write_frontend( + &mut stream, + &role_response, + "write startup role rejection", + )?; + opened.close(); + close_after_flush = true; + break; + } + } + } + { + let _phase = timing::phase("proxy.startup_response_write"); + if !write_frontend( + &mut stream, + &response.output, + "write startup response", + )? { + opened.close(); + close_after_flush = true; + break; + } + } + if response_accepted { + if opened.supports_protocol_pump() { + opened.attach_protocol_stream( + stream + .try_clone_for_protocol() + .context("clone frontend socket for protocol pump")?, + )?; + } + backend = Some(opened); + } else { + opened.close(); + close_after_flush = true; + } } - FrontendMessageKind::Protocol => { - let flush_after = should_flush_protocol_batch(&message); - protocol_batch.extend_from_slice(&message); + FrontendFrameKind::Protocol => { + record_protocol_message(message); + let is_last_message_in_read = message_index + 1 == message_count; + let flush_after = + should_flush_protocol_batch(message, is_last_message_in_read); + protocol_batch.extend_from_slice(message); if flush_after { - flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + let streamed = { + let backend = backend.as_mut().ok_or_else(|| { + anyhow!("frontend protocol message arrived before startup") + })?; + let continuation = ContinuationPrefix::from_reader( + &messages, + message_index + 1, + &reader, + ); + flush_protocol_batch( + &mut protocol_batch, + backend, + &mut stream, + continuation, + )? == FlushOutcome::Streamed + }; + if streamed { + if let Some(mut opened) = backend.take() { + opened.close(); + } + return Ok(()); + } } } } + message_index += 1; + } + { + let _phase = timing::phase("proxy.stream_flush"); + ProtocolStats::add(&PROTOCOL_STATS.socket_flushes, 1); + if let Err(err) = stream.flush().context("flush frontend socket") { + if close_after_flush + && err + .downcast_ref::() + .is_some_and(is_connection_closed_error) + { + break; + } + return Err(err); + } } - stream.flush().context("flush frontend socket")?; if close_after_flush { break; } } - backend.rollback_connection_state(); + { + let _phase = timing::phase("proxy.connection_cleanup"); + if let Some(mut backend) = backend { + backend.rollback_connection_state(); + backend.close(); + } + } Ok(()) } + + #[cfg(feature = "extensions")] + fn extensions(&self) -> &[Extension] { + self.extensions.as_slice() + } + + #[cfg(not(feature = "extensions"))] + fn extensions(&self) -> &[()] { + &[] + } } -struct WireBackend { - pg: PostgresMod, - transport: Transport, +trait ProtocolReadiness { + fn read_ready(&mut self) -> io::Result; } -impl WireBackend { - fn open(root: &Path) -> Result { - let outcome = install_into(root)?; - let mut pg = PostgresMod::new(outcome.paths)?; - pg.ensure_cluster()?; - let transport = Transport::prepare(&mut pg)?; - Ok(Self { pg, transport }) +impl ProtocolReadiness for TcpStream { + fn read_ready(&mut self) -> io::Result { + socket_read_ready(self, TcpStream::peek) } +} - fn send(&mut self, message: &[u8]) -> Result> { - self.transport.send(&mut self.pg, message, None) +#[cfg(unix)] +impl ProtocolReadiness for UnixStream { + fn read_ready(&mut self) -> io::Result { + Ok(true) } +} - fn rollback_connection_state(&mut self) { - let _ = self.send(&simple_query_message("ROLLBACK")); +impl ProtocolStream for TcpStream { + fn read_ready(&mut self) -> io::Result { + ProtocolReadiness::read_ready(self) } } -#[derive(Default)] -struct FrontendMessageReader { - buffer: Vec, +trait CloneProtocolStream: Read + Write + Send + ProtocolStream + Sized + 'static { + fn try_clone_for_protocol(&self) -> io::Result; } -impl FrontendMessageReader { - fn push(&mut self, input: &[u8]) -> Result>> { - self.buffer.extend_from_slice(input); - let mut messages = Vec::new(); +impl CloneProtocolStream for TcpStream { + fn try_clone_for_protocol(&self) -> io::Result { + self.try_clone() + } +} - loop { - let Some(message_len) = frontend_message_len(&self.buffer)? else { - break; - }; - let message = self.buffer.drain(..message_len).collect(); - messages.push(message); - } +fn socket_read_ready( + stream: &mut S, + peek: impl FnOnce(&S, &mut [u8]) -> io::Result, +) -> io::Result +where + S: SetNonblocking, +{ + stream.set_nonblocking(true)?; + let mut byte = [0u8; 1]; + let result = match peek(stream, &mut byte) { + Ok(read) => Ok(read > 0), + Err(err) if err.kind() == io::ErrorKind::WouldBlock => Ok(false), + Err(err) => Err(err), + }; + let restore = stream.set_nonblocking(false); + match (result, restore) { + (Ok(value), Ok(())) => Ok(value), + (Err(err), _) => Err(err), + (Ok(_), Err(err)) => Err(err), + } +} - Ok(messages) +trait SetNonblocking { + fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()>; +} + +impl SetNonblocking for TcpStream { + fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { + TcpStream::set_nonblocking(self, nonblocking) } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum FrontendMessageKind { - Protocol, - Startup, - SslOrGssRequest, - CancelRequest, - Terminate, +#[cfg(unix)] +impl SetNonblocking for UnixStream { + fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { + UnixStream::set_nonblocking(self, nonblocking) + } +} + +#[cfg(unix)] +impl ProtocolStream for UnixStream { + fn read_ready(&mut self) -> io::Result { + ProtocolReadiness::read_ready(self) + } } -fn frontend_message_len(buffer: &[u8]) -> Result> { - if buffer.len() < 4 { - return Ok(None); +#[cfg(unix)] +impl CloneProtocolStream for UnixStream { + fn try_clone_for_protocol(&self) -> io::Result { + self.try_clone() } +} - if buffer[0] == 0 { - let len = i32::from_be_bytes(buffer[0..4].try_into().unwrap()); - if len < 8 { - bail!("invalid startup packet length {len}"); - } - let len = len as usize; - if len > MAX_FRONTEND_MESSAGE { - bail!("startup packet length {len} exceeds limit"); +struct ContinuationPrefix<'a> { + messages: &'a [Vec], + first_unhandled_message: usize, + pending: &'a [u8], +} + +impl<'a> ContinuationPrefix<'a> { + fn empty() -> Self { + Self { + messages: &[], + first_unhandled_message: 0, + pending: &[], } - return Ok((buffer.len() >= len).then_some(len)); } - if buffer.len() < 5 { - return Ok(None); + fn from_reader( + messages: &'a [Vec], + first_unhandled_message: usize, + reader: &'a FrontendFrameReader, + ) -> Self { + Self { + messages, + first_unhandled_message, + pending: reader.pending(), + } } - let len = i32::from_be_bytes(buffer[1..5].try_into().unwrap()); - if len < 4 { - bail!("invalid frontend message length {len}"); + + fn into_vec(self) -> Vec { + let len = self + .messages + .iter() + .skip(self.first_unhandled_message) + .map(Vec::len) + .sum::() + + self.pending.len(); + if len == 0 { + return Vec::new(); + } + let mut prefix = Vec::with_capacity(len); + for message in self.messages.iter().skip(self.first_unhandled_message) { + prefix.extend_from_slice(message); + } + prefix.extend_from_slice(self.pending); + prefix } - let total = 1usize - .checked_add(len as usize) - .ok_or_else(|| anyhow!("frontend message length overflow"))?; - if total > MAX_FRONTEND_MESSAGE { - bail!("frontend message length {total} exceeds limit"); +} + +fn record_protocol_message(message: &[u8]) { + ProtocolStats::add(&PROTOCOL_STATS.protocol_messages, 1); + match message.first() { + Some(b'Q') => ProtocolStats::add(&PROTOCOL_STATS.simple_query_messages, 1), + Some(b'P') => ProtocolStats::add(&PROTOCOL_STATS.parse_messages, 1), + Some(b'B') => ProtocolStats::add(&PROTOCOL_STATS.bind_messages, 1), + Some(b'E') => ProtocolStats::add(&PROTOCOL_STATS.execute_messages, 1), + Some(b'S') => ProtocolStats::add(&PROTOCOL_STATS.sync_messages, 1), + Some(b'H') => ProtocolStats::add(&PROTOCOL_STATS.flush_messages, 1), + Some(b'd' | b'c' | b'f') => ProtocolStats::add(&PROTOCOL_STATS.copy_data_messages, 1), + _ => {} } - Ok((buffer.len() >= total).then_some(total)) } -fn classify_frontend_message(message: &[u8]) -> Result { - if message.is_empty() { - bail!("empty frontend message"); +struct WireBackend { + session: BackendSession, +} + +impl WireBackend { + fn installed_outcome( + root: &Path, + prepared_root: Option<&InstallOutcome>, + ) -> Result { + let _phase = timing::phase("proxy.backend_install"); + match prepared_root { + Some(outcome) => Ok(outcome.clone()), + None => install_into(root), + } + } + + #[cfg(feature = "extensions")] + fn open( + root: &Path, + prepared_root: Option<&InstallOutcome>, + postgres_config: &PostgresConfig, + startup_config: &StartupConfig, + extensions: &[Extension], + ) -> Result { + let outcome = Self::installed_outcome(root, prepared_root)?; + { + let _phase = timing::phase("proxy.extension_install"); + install_missing_extension_archives(&outcome, extensions)?; + } + Self::open_prepared(&outcome, postgres_config, startup_config, extensions) + } + + #[cfg(feature = "extensions")] + fn open_prepared( + outcome: &InstallOutcome, + postgres_config: &PostgresConfig, + startup_config: &StartupConfig, + extensions: &[Extension], + ) -> Result { + let session = BackendSession::open_with_extension_preload( + outcome.clone(), + postgres_config.clone(), + startup_config.clone(), + BackendOpenKind::Proxy, + extensions, + )?; + Ok(Self { session }) + } + + #[cfg(not(feature = "extensions"))] + fn open( + root: &Path, + prepared_root: Option<&InstallOutcome>, + postgres_config: &PostgresConfig, + startup_config: &StartupConfig, + _extensions: &[()], + ) -> Result { + let outcome = Self::installed_outcome(root, prepared_root)?; + let session = BackendSession::open( + outcome, + postgres_config.clone(), + startup_config.clone(), + BackendOpenKind::Proxy, + )?; + Ok(Self { session }) + } + + fn startup(&mut self, message: &[u8]) -> Result { + self.session.startup_with_packet(message) + } + + #[cfg(feature = "extensions")] + fn enable_extensions(&mut self, extensions: &[Extension]) -> Result<()> { + let _phase = timing::phase("proxy.extension_enable"); + self.session.enable_extensions(extensions) + } + + fn send(&mut self, message: &[u8]) -> Result> { + let _phase = timing::phase("proxy.backend_send"); + ProtocolStats::add(&PROTOCOL_STATS.backend_send_calls, 1); + ProtocolStats::add(&PROTOCOL_STATS.backend_send_bytes, message.len() as u64); + self.session.send_buffered(message, None) + } + + fn supports_protocol_pump(&self) -> bool { + self.session.supports_protocol_pump() + } + + fn attach_protocol_stream(&mut self, stream: S) -> Result<()> + where + S: ProtocolStream + 'static, + { + let _phase = timing::phase("proxy.backend_attach_protocol_stream"); + self.session.attach_protocol_stream(stream) + } + + fn send_with_protocol_pump( + &mut self, + message: &[u8], + continuation_prefix: ContinuationPrefix<'_>, + ) -> Result { + let _phase = timing::phase("proxy.backend_send"); + ProtocolStats::add(&PROTOCOL_STATS.backend_send_calls, 1); + ProtocolStats::add(&PROTOCOL_STATS.backend_send_bytes, message.len() as u64); + self.session + .send_with_protocol_pump(message, || continuation_prefix.into_vec()) + } + + fn set_role(&mut self, user: &str) -> Result> { + let sql = format!( + "SET ROLE {}", + crate::pglite::templating::quote_identifier(user) + ); + self.send(&simple_query_message(&sql)) } - if message[0] == 0 { - if message.len() < 8 { - bail!("startup/control packet is too short"); + fn rollback_connection_state(&mut self) { + let _ = self.reset_session_state(); + } + + fn reset_session_state(&mut self) -> Result<()> { + let _phase = timing::phase("proxy.reset_session_state"); + for sql in ["ROLLBACK", "DISCARD ALL"] { + let response = self.send(&simple_query_message(sql))?; + if response.first() == Some(&b'E') { + bail!("reset proxy backend session state failed while running {sql}"); + } } - let code = i32::from_be_bytes(message[4..8].try_into().unwrap()); - return Ok(match code { - SSL_REQUEST_CODE | GSSENC_REQUEST_CODE => FrontendMessageKind::SslOrGssRequest, - CANCEL_REQUEST_CODE => FrontendMessageKind::CancelRequest, - PROTOCOL_3 => FrontendMessageKind::Startup, - other => bail!("unsupported startup/control packet code {other}"), - }); + Ok(()) + } + + fn close(&mut self) { + let _phase = timing::phase("proxy.backend_shutdown"); + let _ = self.session.shutdown(); } +} - if message[0] == b'X' { - return Ok(FrontendMessageKind::Terminate); +fn should_flush_protocol_batch(message: &[u8], is_last_message_in_read: bool) -> bool { + match message.first() { + // Simple query and explicit Flush are client-visible boundaries. Keep + // them immediate so COPY guards and flush semantics stay obvious. + Some(b'Q' | b'H') => true, + // COPY frames belong to PostgreSQL's COPY subprotocol. Keep them as + // immediate flush boundaries so the backend-owned protocol pump can + // hand over to streaming at the exact CopyInResponse/CopyOutResponse + // boundary and protocol mistakes fail close to source. + Some(b'd' | b'c' | b'f') => true, + // Sync is also a protocol boundary, but pipelined extended-query + // clients often put several Bind/Execute/Sync groups into one socket + // read. Batching only those bytes already read avoids extra WASIX host + // crossings without waiting for future network input. + Some(b'S') => is_last_message_in_read, + _ => false, } +} - Ok(FrontendMessageKind::Protocol) +fn backend_open_error_response(err: &anyhow::Error) -> Vec { + let error = format!("{err:#}"); + error_response( + "FATAL", + "XX000", + &format!("could not start embedded Postgres backend: {error}"), + ) } -fn should_flush_protocol_batch(message: &[u8]) -> bool { - matches!(message.first(), Some(b'Q' | b'S' | b'H')) +fn is_connection_closed_error(err: &io::Error) -> bool { + matches!( + err.kind(), + io::ErrorKind::BrokenPipe + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::ConnectionReset + | io::ErrorKind::UnexpectedEof + ) } -fn flush_protocol_batch( +fn write_frontend(stream: &mut S, bytes: &[u8], context: &'static str) -> Result +where + S: Write, +{ + match stream.write_all(bytes) { + Ok(()) => Ok(true), + Err(err) if is_connection_closed_error(&err) => Ok(false), + Err(err) => Err(err).context(context), + } +} + +fn flush_protocol_batch_if_started( protocol_batch: &mut Vec, - backend: &mut WireBackend, + backend: Option<&mut WireBackend>, stream: &mut S, ) -> Result<()> where @@ -391,67 +937,71 @@ where if protocol_batch.is_empty() { return Ok(()); } - - let response = backend.send(protocol_batch)?; - protocol_batch.clear(); - if !response.is_empty() { - stream - .write_all(&response) - .context("write backend response")?; + let backend = + backend.ok_or_else(|| anyhow!("frontend protocol message arrived before startup"))?; + match flush_protocol_batch(protocol_batch, backend, stream, ContinuationPrefix::empty())? { + FlushOutcome::Continue => Ok(()), + FlushOutcome::Streamed => { + bail!("protocol stream was consumed while flushing control packet") + } } - - Ok(()) } -fn startup_response() -> Vec { - let mut response = Vec::new(); - push_authentication_ok(&mut response); - push_parameter_status(&mut response, "server_version", "17.5"); - push_parameter_status(&mut response, "server_encoding", "UTF8"); - push_parameter_status(&mut response, "client_encoding", "UTF8"); - push_parameter_status(&mut response, "DateStyle", "ISO, MDY"); - push_parameter_status(&mut response, "integer_datetimes", "on"); - push_backend_key_data(&mut response, 0, 0); - push_ready_for_query(&mut response, b'I'); - response -} - -fn push_authentication_ok(out: &mut Vec) { - out.push(b'R'); - out.extend_from_slice(&8_i32.to_be_bytes()); - out.extend_from_slice(&0_i32.to_be_bytes()); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FlushOutcome { + Continue, + Streamed, } -fn push_parameter_status(out: &mut Vec, key: &str, value: &str) { - out.push(b'S'); - let len = 4 + key.len() + 1 + value.len() + 1; - out.extend_from_slice(&(len as i32).to_be_bytes()); - out.extend_from_slice(key.as_bytes()); - out.push(0); - out.extend_from_slice(value.as_bytes()); - out.push(0); -} +fn flush_protocol_batch( + protocol_batch: &mut Vec, + backend: &mut WireBackend, + stream: &mut S, + continuation_prefix: ContinuationPrefix<'_>, +) -> Result +where + S: Write, +{ + if protocol_batch.is_empty() { + return Ok(FlushOutcome::Continue); + } -fn push_backend_key_data(out: &mut Vec, process_id: i32, secret_key: i32) { - out.push(b'K'); - out.extend_from_slice(&12_i32.to_be_bytes()); - out.extend_from_slice(&process_id.to_be_bytes()); - out.extend_from_slice(&secret_key.to_be_bytes()); + let outcome = { + let _phase = timing::phase("proxy.protocol_batch"); + ProtocolStats::add(&PROTOCOL_STATS.protocol_batches, 1); + ProtocolStats::add( + &PROTOCOL_STATS.protocol_batch_bytes, + protocol_batch.len() as u64, + ); + backend.send_with_protocol_pump(protocol_batch, continuation_prefix)? + }; + protocol_batch.clear(); + match outcome { + ProtocolPumpOutcome::Buffered(response) => { + write_backend_response(stream, &response)?; + Ok(FlushOutcome::Continue) + } + ProtocolPumpOutcome::Streamed => { + ProtocolStats::add(&PROTOCOL_STATS.streaming_copy_handoffs, 1); + Ok(FlushOutcome::Streamed) + } + } } -fn push_ready_for_query(out: &mut Vec, status: u8) { - out.push(b'Z'); - out.extend_from_slice(&5_i32.to_be_bytes()); - out.push(status); -} +fn write_backend_response(stream: &mut S, response: &[u8]) -> Result<()> +where + S: Write, +{ + if !response.is_empty() { + let _phase = timing::phase("proxy.response_write"); + ProtocolStats::add(&PROTOCOL_STATS.response_writes, 1); + ProtocolStats::add(&PROTOCOL_STATS.response_bytes, response.len() as u64); + stream + .write_all(response) + .context("write backend response")?; + } -fn simple_query_message(sql: &str) -> Vec { - let mut message = Vec::with_capacity(sql.len() + 6); - message.push(b'Q'); - message.extend_from_slice(&((sql.len() + 5) as i32).to_be_bytes()); - message.extend_from_slice(sql.as_bytes()); - message.push(0); - message + Ok(()) } #[cfg(test)] @@ -459,67 +1009,81 @@ mod tests { use super::*; #[test] - fn frontend_reader_buffers_split_messages() -> Result<()> { - let query = b"Q\0\0\0\rSELECT 1\0"; - let mut reader = FrontendMessageReader::default(); - assert!(reader.push(&query[..3])?.is_empty()); - let messages = reader.push(&query[3..])?; - assert_eq!(messages, vec![query.to_vec()]); - Ok(()) + fn protocol_batch_flushes_on_client_boundaries() { + assert!(should_flush_protocol_batch(b"Q\0\0\0\rSELECT 1\0", false)); + assert!(should_flush_protocol_batch(b"Q\0\0\0\rSELECT 1\0", true)); + assert!(!should_flush_protocol_batch(b"S\0\0\0\x04", false)); + assert!(should_flush_protocol_batch(b"S\0\0\0\x04", true)); + assert!(should_flush_protocol_batch(b"H\0\0\0\x04", false)); + assert!(should_flush_protocol_batch(b"H\0\0\0\x04", true)); + assert!(!should_flush_protocol_batch(b"P\0\0\0\x04", true)); + assert!(!should_flush_protocol_batch(b"B\0\0\0\x04", true)); + assert!(!should_flush_protocol_batch(b"D\0\0\0\x04", true)); + assert!(!should_flush_protocol_batch(b"E\0\0\0\x04", true)); } #[test] - fn frontend_reader_splits_batched_messages() -> Result<()> { - let mut batch = Vec::new(); - batch.extend_from_slice(b"Q\0\0\0\rSELECT 1\0"); - batch.extend_from_slice(b"X\0\0\0\x04"); - - let mut reader = FrontendMessageReader::default(); - let messages = reader.push(&batch)?; - assert_eq!(messages.len(), 2); - assert_eq!( - classify_frontend_message(&messages[0])?, - FrontendMessageKind::Protocol - ); - assert_eq!( - classify_frontend_message(&messages[1])?, - FrontendMessageKind::Terminate - ); - Ok(()) + fn response_error_detection_scans_backend_messages() { + let mut response = Vec::new(); + push_parameter_status(&mut response, "TimeZone", "UTC"); + response.push(b'E'); + response.extend_from_slice(&6_i32.to_be_bytes()); + response.extend_from_slice(b"S\0"); + push_ready_for_query(&mut response, b'I'); + + assert!(response_contains_error(&response)); + assert!(!response_contains_error(&backend_ready_response())); } #[test] - fn classify_ssl_request() -> Result<()> { - let mut message = Vec::new(); - message.extend_from_slice(&8_i32.to_be_bytes()); - message.extend_from_slice(&SSL_REQUEST_CODE.to_be_bytes()); - assert_eq!( - classify_frontend_message(&message)?, - FrontendMessageKind::SslOrGssRequest + fn backend_open_error_fallback_never_guesses_postgres_sqlstate() { + let missing_text = + backend_open_error_response(&anyhow!("database \"app_db\" does not exist")); + assert!(missing_text.windows(7).any(|window| window == b"CXX000\0")); + assert!(!missing_text.windows(7).any(|window| window == b"C3D000\0")); + + let missing_sqlstate = + backend_open_error_response(&anyhow!("Postgres startup failed with 3D000")); + assert!( + missing_sqlstate + .windows(7) + .any(|window| window == b"CXX000\0") + ); + assert!( + !missing_sqlstate + .windows(7) + .any(|window| window == b"C3D000\0") ); - Ok(()) - } - #[test] - fn classify_startup_request() -> Result<()> { - let mut message = Vec::new(); - message.extend_from_slice(&8_i32.to_be_bytes()); - message.extend_from_slice(&PROTOCOL_3.to_be_bytes()); - assert_eq!( - classify_frontend_message(&message)?, - FrontendMessageKind::Startup + let runtime = + backend_open_error_response(&anyhow!("runtime failed while opening database root")); + assert!(runtime.windows(7).any(|window| window == b"CXX000\0")); + assert!( + !runtime.windows(7).any(|window| window == b"C3D000\0"), + "runtime failures must not be reported as missing databases" ); - Ok(()) } - #[test] - fn protocol_batch_flushes_on_client_boundaries() { - assert!(should_flush_protocol_batch(b"Q\0\0\0\rSELECT 1\0")); - assert!(should_flush_protocol_batch(b"S\0\0\0\x04")); - assert!(should_flush_protocol_batch(b"H\0\0\0\x04")); - assert!(!should_flush_protocol_batch(b"P\0\0\0\x04")); - assert!(!should_flush_protocol_batch(b"B\0\0\0\x04")); - assert!(!should_flush_protocol_batch(b"D\0\0\0\x04")); - assert!(!should_flush_protocol_batch(b"E\0\0\0\x04")); + fn backend_ready_response() -> Vec { + let mut response = Vec::new(); + push_parameter_status(&mut response, "TimeZone", "UTC"); + push_ready_for_query(&mut response, b'I'); + response + } + + fn push_parameter_status(out: &mut Vec, key: &str, value: &str) { + out.push(b'S'); + let len = 4 + key.len() + 1 + value.len() + 1; + out.extend_from_slice(&(len as i32).to_be_bytes()); + out.extend_from_slice(key.as_bytes()); + out.push(0); + out.extend_from_slice(value.as_bytes()); + out.push(0); + } + + fn push_ready_for_query(out: &mut Vec, status: u8) { + out.push(b'Z'); + out.extend_from_slice(&5_i32.to_be_bytes()); + out.push(status); } } diff --git a/src/pglite/server.rs b/src/pglite/server.rs index 7b6a05bf..852f1673 100644 --- a/src/pglite/server.rs +++ b/src/pglite/server.rs @@ -1,6 +1,6 @@ -use std::net::{SocketAddr, TcpListener}; +use std::net::{SocketAddr, TcpListener, TcpStream}; #[cfg(unix)] -use std::os::unix::net::UnixListener; +use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::{ Arc, @@ -12,8 +12,15 @@ use std::thread::{self, JoinHandle}; use anyhow::{Context, Result, anyhow}; use tempfile::TempDir; -use crate::pglite::base::{install_into, install_temporary_from_template}; +use crate::pglite::base::{PreparedRoot, RootLock, RootPlan, RootSource, RootTarget, prepare_root}; +use crate::pglite::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use crate::pglite::extensions::{Extension, resolve_extension_set}; +use crate::pglite::interface::DebugLevel; +#[cfg(feature = "extensions")] +use crate::pglite::pg_dump::{PgDumpOptions, dump_server_sql}; use crate::pglite::proxy::PgliteProxy; +use crate::pglite::timing; /// A supervised local PostgreSQL socket backed by one embedded PGlite runtime. /// @@ -25,7 +32,9 @@ use crate::pglite::proxy::PgliteProxy; pub struct PgliteServer { root: PathBuf, _temp_dir: Option, + _root_lock: Option, endpoint: ServerEndpoint, + startup_config: StartupConfig, shutdown: Arc, handle: Option>>, } @@ -75,13 +84,15 @@ impl PgliteServer { /// Return a PostgreSQL connection URI for the local server. pub fn connection_uri(&self) -> String { match &self.endpoint { - ServerEndpoint::Tcp(addr) => tcp_connection_uri(*addr), + ServerEndpoint::Tcp(addr) => tcp_connection_uri(*addr, &self.startup_config), #[cfg(unix)] ServerEndpoint::Unix(path) => { let host = path.parent().unwrap_or_else(|| Path::new("/tmp")); let port = parse_unix_socket_port(path).unwrap_or(5432); format!( - "postgresql://postgres@/template1?host={}&port={}&sslmode=disable", + "postgresql://{}@/{}?host={}&port={}&sslmode=disable", + self.startup_config.username, + self.startup_config.database, percent_encode_query_value(&host.display().to_string()), port ) @@ -89,6 +100,26 @@ impl PgliteServer { } } + /// Alias for [`connection_uri`](Self::connection_uri). + pub fn database_url(&self) -> String { + self.connection_uri() + } + + /// Run the bundled WASIX `pg_dump` against this server and return SQL text. + #[cfg(feature = "extensions")] + pub fn dump_sql(&self, options: PgDumpOptions) -> Result { + let addr = self + .tcp_addr() + .context("pg_dump currently requires a TCP PgliteServer endpoint")?; + dump_server_sql(addr, &options) + } + + /// Run the bundled WASIX `pg_dump` and return UTF-8 SQL bytes. + #[cfg(feature = "extensions")] + pub fn dump_bytes(&self, options: PgDumpOptions) -> Result> { + Ok(self.dump_sql(options)?.into_bytes()) + } + /// Request shutdown and wait for the listener thread to exit. /// /// Close database clients before calling this method. The current proxy owns @@ -100,7 +131,12 @@ impl PgliteServer { fn stop(&mut self) -> Result<()> { self.shutdown.store(true, Ordering::SeqCst); + { + let _phase = timing::phase("server.shutdown_wake"); + wake_listener(&self.endpoint); + } if let Some(handle) = self.handle.take() { + let _phase = timing::phase("server.thread_join"); handle .join() .map_err(|_| anyhow!("pglite server thread panicked"))??; @@ -111,7 +147,9 @@ impl PgliteServer { impl Drop for PgliteServer { fn drop(&mut self) { - self.shutdown.store(true, Ordering::SeqCst); + if let Err(err) = self.stop() { + tracing::warn!("pglite server shutdown during drop failed: {err:#}"); + } } } @@ -120,6 +158,10 @@ impl Drop for PgliteServer { pub struct PgliteServerBuilder { root: ServerRoot, endpoint: ServerEndpointConfig, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + #[cfg(feature = "extensions")] + extensions: Vec, } #[derive(Debug, Clone)] @@ -142,6 +184,10 @@ impl Default for PgliteServerBuilder { template_cache: true, }, endpoint: ServerEndpointConfig::Tcp(SocketAddr::from(([127, 0, 0, 1], 0))), + postgres_config: PostgresConfig::default(), + startup_config: StartupConfig::default(), + #[cfg(feature = "extensions")] + extensions: Vec::new(), } } } @@ -168,6 +214,10 @@ impl PgliteServerBuilder { } /// Serve a temporary database initialized without the template cache. + /// + /// This is a compatibility alias for the pre-template-cache public API. + /// Fresh initdb uses the bundled split WASIX `initdb` module; cached + /// temporary databases remain the production fast path. pub fn fresh_temporary(mut self) -> Self { self.root = ServerRoot::Temporary { template_cache: false, @@ -188,27 +238,134 @@ impl PgliteServerBuilder { self } + /// Set a PostgreSQL startup GUC for the embedded backend used by this + /// server. + pub fn postgres_config(mut self, name: impl Into, value: impl Into) -> Self { + self.postgres_config.insert(name, value); + self + } + + /// Set multiple PostgreSQL startup GUCs for the embedded backend used by + /// this server. + pub fn postgres_configs(mut self, settings: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { + for (name, value) in settings { + self.postgres_config.insert(name, value); + } + self + } + + /// Default user encoded in [`PgliteServer::database_url`]. + pub fn username(mut self, username: impl Into) -> Self { + self.startup_config.username = username.into(); + self + } + + /// Default database encoded in [`PgliteServer::database_url`]. + pub fn database(mut self, database: impl Into) -> Self { + self.startup_config.database = database.into(); + self + } + + /// Enable PostgreSQL debug logging level `0..=5` for server backends. + pub fn debug_level(mut self, level: DebugLevel) -> Self { + self.startup_config.debug_level = Some(level); + self + } + + /// Use lower durability settings for ephemeral or cacheable local + /// workloads. + pub fn relaxed_durability(mut self, enabled: bool) -> Self { + self.startup_config.relaxed_durability = enabled; + self + } + + /// Append an advanced PostgreSQL startup argument for server backends. + pub fn startup_arg(mut self, arg: impl Into) -> Self { + self.startup_config.extra_args.push(arg.into()); + self + } + + /// Append advanced PostgreSQL startup arguments for server backends. + pub fn startup_args(mut self, args: impl IntoIterator>) -> Self { + self.startup_config + .extra_args + .extend(args.into_iter().map(Into::into)); + self + } + + /// Enable a bundled Postgres extension before serving connections. + #[cfg(feature = "extensions")] + pub fn extension(mut self, extension: Extension) -> Self { + self.extensions.push(extension); + self + } + + /// Enable bundled Postgres extensions before serving connections. + #[cfg(feature = "extensions")] + pub fn extensions(mut self, extensions: impl IntoIterator) -> Self { + self.extensions.extend(extensions); + self + } + /// Install the runtime if needed, initialize the cluster, and start serving. pub fn start(self) -> Result { - let (root, temp_dir) = match self.root { - ServerRoot::Path(root) => { - install_into(&root)?; - (root, None) - } - ServerRoot::Temporary { template_cache } => { - if template_cache { - let (root, temp_dir) = prepare_cached_temporary_root()?; - (root, Some(temp_dir)) - } else { - let temp_dir = TempDir::new().context("create temporary pglite directory")?; - install_into(temp_dir.path())?; - (temp_dir.path().to_path_buf(), Some(temp_dir)) + self.postgres_config.validate()?; + self.startup_config.validate()?; + #[cfg(feature = "extensions")] + let extensions = resolve_extension_set(&self.extensions)?; + let postgres_config = self.postgres_config.clone(); + let startup_config = self.startup_config.clone(); + + let prepared_root = { + let _phase = timing::phase("server.root_prepare"); + match self.root { + ServerRoot::Path(root) => { + let _phase = timing::phase("server.root_prepare.path"); + let plan = RootPlan::new(RootTarget::Path(root), RootSource::Template); + #[cfg(feature = "extensions")] + let plan = plan.with_extensions(extensions.clone(), postgres_config.clone()); + prepare_root(plan)? + } + ServerRoot::Temporary { template_cache } => { + let source = if template_cache { + RootSource::Template + } else { + RootSource::FreshInitdb + }; + let phase = if template_cache { + "server.root_prepare.temporary_cached" + } else { + "server.root_prepare.temporary_fresh" + }; + let _phase = timing::phase(phase); + let plan = RootPlan::new(RootTarget::Temporary, source); + #[cfg(feature = "extensions")] + let plan = plan.with_extensions(extensions.clone(), postgres_config.clone()); + run_blocking("pglite-template-cache", move || prepare_root(plan))? } } }; + let PreparedRoot { + root, + temp_dir, + root_lock, + outcome, + } = prepared_root; let shutdown = Arc::new(AtomicBool::new(false)); - let proxy = PgliteProxy::new(root.clone()); + let proxy = { + let _phase = timing::phase("server.proxy_create"); + PgliteProxy::new(root.clone()).with_prepared_root(outcome) + }; + let proxy = proxy + .with_postgres_config(postgres_config) + .with_startup_config(startup_config.clone()); + #[cfg(feature = "extensions")] + let proxy = proxy.with_extensions(extensions); let (endpoint, handle) = match self.endpoint { ServerEndpointConfig::Tcp(addr) => start_tcp(proxy, addr, shutdown.clone())?, @@ -219,7 +376,9 @@ impl PgliteServerBuilder { Ok(PgliteServer { root, _temp_dir: temp_dir, + _root_lock: root_lock, endpoint, + startup_config, shutdown, handle: Some(handle), }) @@ -231,50 +390,63 @@ fn start_tcp( addr: SocketAddr, shutdown: Arc, ) -> Result<(ServerEndpoint, JoinHandle>)> { - let listener = TcpListener::bind(addr).context("bind PGlite TCP server")?; - let addr = listener.local_addr().context("read PGlite TCP address")?; + let listener = { + let _phase = timing::phase("server.tcp_bind"); + TcpListener::bind(addr).context("bind PGlite TCP server")? + }; + let addr = { + let _phase = timing::phase("server.tcp_local_addr"); + listener.local_addr().context("read PGlite TCP address")? + }; let (ready_tx, ready_rx) = sync_channel(1); - let handle = thread::spawn(move || { - proxy.serve_tcp_listener_until_ready(listener, shutdown, Some(ready_tx)) - }); - wait_until_ready(&ready_rx)?; + let recorder = timing::current_recorder(); + let handle = { + let _phase = timing::phase("server.thread_spawn"); + thread::spawn(move || { + timing::with_recorder(recorder, || { + proxy.serve_tcp_listener_until_ready(listener, shutdown, Some(ready_tx)) + }) + }) + }; + { + let _phase = timing::phase("server.wait_ready"); + wait_until_ready(&ready_rx)?; + } Ok((ServerEndpoint::Tcp(addr), handle)) } -fn tcp_connection_uri(addr: SocketAddr) -> String { +fn tcp_connection_uri(addr: SocketAddr, startup: &StartupConfig) -> String { match addr { SocketAddr::V4(addr) => { format!( - "postgresql://postgres@{}:{}/template1?sslmode=disable", + "postgresql://{}@{}:{}/{}?sslmode=disable", + startup.username, addr.ip(), - addr.port() + addr.port(), + startup.database ) } SocketAddr::V6(addr) => { format!( - "postgresql://postgres@[{}]:{}/template1?sslmode=disable", + "postgresql://{}@[{}]:{}/{}?sslmode=disable", + startup.username, addr.ip(), - addr.port() + addr.port(), + startup.database ) } } } -fn prepare_cached_temporary_root() -> Result<(PathBuf, TempDir)> { - run_blocking("pglite-template-cache", || { - let (temp_dir, _outcome) = install_temporary_from_template()?; - Ok((temp_dir.path().to_path_buf(), temp_dir)) - }) -} - fn run_blocking(name: &'static str, f: F) -> Result where T: Send + 'static, F: FnOnce() -> Result + Send + 'static, { + let recorder = timing::current_recorder(); thread::Builder::new() .name(name.to_string()) - .spawn(f) + .spawn(move || timing::with_recorder(recorder, f)) .with_context(|| format!("spawn {name} worker"))? .join() .map_err(|_| anyhow!("{name} worker panicked"))? @@ -286,23 +458,38 @@ fn start_unix( path: PathBuf, shutdown: Arc, ) -> Result<(ServerEndpoint, JoinHandle>)> { - if path.exists() { - std::fs::remove_file(&path) - .with_context(|| format!("remove stale socket {}", path.display()))?; - } - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("create socket directory {}", parent.display()))?; + { + let _phase = timing::phase("server.unix_prepare_path"); + if path.exists() { + std::fs::remove_file(&path) + .with_context(|| format!("remove stale socket {}", path.display()))?; + } + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("create socket directory {}", parent.display()))?; + } } - let listener = UnixListener::bind(&path) - .with_context(|| format!("bind PGlite Unix socket {}", path.display()))?; + let listener = { + let _phase = timing::phase("server.unix_bind"); + UnixListener::bind(&path) + .with_context(|| format!("bind PGlite Unix socket {}", path.display()))? + }; let endpoint = ServerEndpoint::Unix(path); let (ready_tx, ready_rx) = sync_channel(1); - let handle = thread::spawn(move || { - proxy.serve_unix_listener_until_ready(listener, shutdown, Some(ready_tx)) - }); - wait_until_ready(&ready_rx)?; + let recorder = timing::current_recorder(); + let handle = { + let _phase = timing::phase("server.thread_spawn"); + thread::spawn(move || { + timing::with_recorder(recorder, || { + proxy.serve_unix_listener_until_ready(listener, shutdown, Some(ready_tx)) + }) + }) + }; + { + let _phase = timing::phase("server.wait_ready"); + wait_until_ready(&ready_rx)?; + } Ok((endpoint, handle)) } @@ -312,6 +499,18 @@ fn wait_until_ready(ready_rx: &Receiver>) -> Result<()> { .context("PGlite server thread exited before reporting readiness")? } +fn wake_listener(endpoint: &ServerEndpoint) { + match endpoint { + ServerEndpoint::Tcp(addr) => { + let _ = TcpStream::connect(addr); + } + #[cfg(unix)] + ServerEndpoint::Unix(path) => { + let _ = UnixStream::connect(path); + } + } +} + #[cfg(unix)] fn parse_unix_socket_port(path: &Path) -> Option { let name = path.file_name()?.to_str()?; diff --git a/src/pglite/sync_host_fs.rs b/src/pglite/sync_host_fs.rs new file mode 100644 index 00000000..cc757df1 --- /dev/null +++ b/src/pglite/sync_host_fs.rs @@ -0,0 +1,454 @@ +use std::fs; +use std::future::Future; +use std::io::{self, Write}; +use std::path::{Component, Path, PathBuf}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; +use wasmer_wasix::virtual_fs::{ + self, DirEntry, FileType, FsError, Metadata, OpenOptions, OpenOptionsConfig, ReadDir, + VirtualFile, +}; + +#[derive(Debug, Clone)] +pub(crate) struct SyncHostFileSystem { + root: PathBuf, +} + +impl SyncHostFileSystem { + pub(crate) fn new(root: impl Into) -> virtual_fs::Result { + let root = root.into(); + if !root.exists() { + return Err(FsError::InvalidInput); + } + let root = dunce::canonicalize(root).map_err(FsError::from)?; + Ok(Self { root }) + } + + fn prepare_path(&self, path: &Path) -> virtual_fs::Result { + let path = normalize_path(path); + + if matches!(path.components().next(), Some(Component::Prefix(..))) { + return Err(FsError::InvalidInput); + } + + if self.root != Path::new("/") && path.starts_with(&self.root) { + return Err(FsError::InvalidInput); + } + + let path = path.strip_prefix("/").unwrap_or(&path); + let path = self.root.join(path); + + debug_assert!(path.starts_with(&self.root)); + Ok(path) + } +} + +impl virtual_fs::FileSystem for SyncHostFileSystem { + fn readlink(&self, path: &Path) -> virtual_fs::Result { + fs::read_link(self.prepare_path(path)?).map_err(FsError::from) + } + + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + let path = self.prepare_path(path)?; + let mut data = fs::read_dir(path)? + .map(|entry| { + let entry = entry?; + let path = entry + .path() + .strip_prefix(&self.root) + .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))? + .to_owned(); + let path = Path::new("/").join(path); + Ok(DirEntry { + path, + metadata: entry + .metadata() + .map(metadata_from_std) + .map_err(FsError::from), + }) + }) + .collect::, io::Error>>()?; + data.sort_by(|a, b| a.path.file_name().cmp(&b.path.file_name())); + Ok(ReadDir::new(data)) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + let path = self.prepare_path(path)?; + if path.parent().is_none() { + return Err(FsError::BaseNotDirectory); + } + fs::create_dir(path).map_err(FsError::from) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + let path = self.prepare_path(path)?; + if path.parent().is_none() { + return Err(FsError::BaseNotDirectory); + } + if path.is_dir() + && fs::read_dir(&path) + .map(|mut entries| entries.next().is_some()) + .unwrap_or(false) + { + return Err(FsError::DirectoryNotEmpty); + } + fs::remove_dir(path).map_err(FsError::from) + } + + fn rename<'a>( + &'a self, + from: &'a Path, + to: &'a Path, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let norm_from = normalize_path(from); + let norm_to = normalize_path(to); + + if norm_from.parent().is_none() || norm_to.parent().is_none() { + return Err(FsError::BaseNotDirectory); + } + + let from = self.prepare_path(from)?; + let to = self.prepare_path(to)?; + + if !from.exists() { + return Err(FsError::EntryNotFound); + } + if !from.parent().is_some_and(Path::exists) || !to.parent().is_some_and(Path::exists) { + return Err(FsError::EntryNotFound); + } + + fs::rename(from, to).map_err(FsError::from) + }) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + fs::metadata(self.prepare_path(path)?) + .map(metadata_from_std) + .map_err(FsError::from) + } + + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { + fs::symlink_metadata(self.prepare_path(path)?) + .map(metadata_from_std) + .map_err(FsError::from) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + let path = self.prepare_path(path)?; + if path.parent().is_none() { + return Err(FsError::BaseNotDirectory); + } + fs::remove_file(path).map_err(FsError::from) + } + + fn new_open_options(&self) -> OpenOptions<'_> { + OpenOptions::new(self) + } +} + +impl virtual_fs::FileOpener for SyncHostFileSystem { + fn open( + &self, + path: &Path, + conf: &OpenOptionsConfig, + ) -> virtual_fs::Result> { + let path = self.prepare_path(path)?; + let append = conf.append() && !conf.truncate(); + let file = fs::OpenOptions::new() + .read(conf.read()) + .write(conf.write()) + .create_new(conf.create_new()) + .create(conf.create()) + .append(append) + .truncate(conf.truncate()) + .open(&path) + .map_err(FsError::from)?; + + Ok(Box::new(SyncHostFile::new(file, path)) as Box) + } +} + +#[derive(Debug)] +struct SyncHostFile { + file: fs::File, + position: u64, + host_path: PathBuf, +} + +impl SyncHostFile { + fn new(file: fs::File, host_path: PathBuf) -> Self { + Self { + file, + position: 0, + host_path, + } + } + + fn metadata(&self) -> io::Result { + self.file.metadata() + } +} + +impl VirtualFile for SyncHostFile { + fn last_accessed(&self) -> u64 { + self.metadata() + .ok() + .and_then(|metadata| metadata.accessed().ok()) + .map(system_time_nanos) + .unwrap_or(0) + } + + fn last_modified(&self) -> u64 { + self.metadata() + .ok() + .and_then(|metadata| metadata.modified().ok()) + .map(system_time_nanos) + .unwrap_or(0) + } + + fn created_time(&self) -> u64 { + self.metadata() + .ok() + .and_then(|metadata| metadata.created().ok()) + .map(system_time_nanos) + .unwrap_or(0) + } + + fn size(&self) -> u64 { + self.metadata().map(|metadata| metadata.len()).unwrap_or(0) + } + + fn set_len(&mut self, new_size: u64) -> virtual_fs::Result<()> { + self.file.set_len(new_size).map_err(FsError::from) + } + + fn set_times(&mut self, atime: Option, mtime: Option) -> virtual_fs::Result<()> { + let atime = atime.map(nanos_to_file_time); + let mtime = mtime.map(nanos_to_file_time); + filetime::set_file_handle_times(&self.file, atime, mtime).map_err(|_| FsError::IOError) + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + fs::remove_file(&self.host_path).map_err(FsError::from) + } + + fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + let file = self.get_mut(); + Poll::Ready( + file.file + .metadata() + .map(|metadata| metadata.len().saturating_sub(file.position) as usize), + ) + } + + fn poll_write_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(8192)) + } +} + +impl AsyncRead for SyncHostFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let file = self.get_mut(); + let result = read_at(&file.file, buf.initialize_unfilled(), file.position).map(|read| { + file.position = file.position.saturating_add(read as u64); + buf.advance(read); + }); + Poll::Ready(result) + } +} + +impl AsyncWrite for SyncHostFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let file = self.get_mut(); + let result = write_at(&file.file, buf, file.position).inspect(|written| { + file.position = file.position.saturating_add(*written as u64); + }); + Poll::Ready(result) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(self.get_mut().file.flush()) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(self.get_mut().file.flush()) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + let file = self.get_mut(); + let mut total = 0usize; + for buf in bufs.iter().filter(|buf| !buf.is_empty()) { + match write_at(&file.file, buf, file.position) { + Ok(written) => { + file.position = file.position.saturating_add(written as u64); + total += written; + if written != buf.len() { + break; + } + } + Err(_) if total > 0 => break, + Err(err) => return Poll::Ready(Err(err)), + } + } + Poll::Ready(Ok(total)) + } + + fn is_write_vectored(&self) -> bool { + true + } +} + +impl AsyncSeek for SyncHostFile { + fn start_seek(self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + let file = self.get_mut(); + let current = file.position as i128; + let target = match position { + io::SeekFrom::Start(offset) => offset as i128, + io::SeekFrom::Current(delta) => current + delta as i128, + io::SeekFrom::End(delta) => file.file.metadata()?.len() as i128 + delta as i128, + }; + if target < 0 || target > u64::MAX as i128 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid seek before start or past u64::MAX", + )); + } + file.position = target as u64; + Ok(()) + } + + fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(self.get_mut().position)) + } +} + +#[cfg(unix)] +fn read_at(file: &fs::File, buf: &mut [u8], offset: u64) -> io::Result { + use std::os::unix::fs::FileExt; + file.read_at(buf, offset) +} + +#[cfg(windows)] +fn read_at(file: &fs::File, buf: &mut [u8], offset: u64) -> io::Result { + use std::os::windows::fs::FileExt; + file.seek_read(buf, offset) +} + +#[cfg(not(any(unix, windows)))] +fn read_at(file: &fs::File, buf: &mut [u8], offset: u64) -> io::Result { + use std::io::{Read as _, Seek as _}; + + let mut file = file.try_clone()?; + file.seek(io::SeekFrom::Start(offset))?; + file.read(buf) +} + +#[cfg(unix)] +fn write_at(file: &fs::File, buf: &[u8], offset: u64) -> io::Result { + use std::os::unix::fs::FileExt; + file.write_at(buf, offset) +} + +#[cfg(windows)] +fn write_at(file: &fs::File, buf: &[u8], offset: u64) -> io::Result { + use std::os::windows::fs::FileExt; + file.seek_write(buf, offset) +} + +#[cfg(not(any(unix, windows)))] +fn write_at(file: &fs::File, buf: &[u8], offset: u64) -> io::Result { + use std::io::{Seek as _, Write as _}; + + let mut file = file.try_clone()?; + file.seek(io::SeekFrom::Start(offset))?; + file.write(buf) +} + +fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut normalized = if let Some(Component::Prefix(prefix)) = components.peek().cloned() { + components.next(); + PathBuf::from(prefix.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(_) => unreachable!(), + Component::RootDir => normalized.push(component.as_os_str()), + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(part) => normalized.push(part), + } + } + + normalized +} + +fn metadata_from_std(metadata: fs::Metadata) -> Metadata { + let filetype = metadata.file_type(); + let (char_device, block_device, socket, fifo) = { + #[cfg(unix)] + { + use std::os::unix::fs::FileTypeExt; + ( + filetype.is_char_device(), + filetype.is_block_device(), + filetype.is_socket(), + filetype.is_fifo(), + ) + } + #[cfg(not(unix))] + { + (false, false, false, false) + } + }; + + Metadata { + ft: FileType { + dir: filetype.is_dir(), + file: filetype.is_file(), + symlink: filetype.is_symlink(), + char_device, + block_device, + socket, + fifo, + }, + accessed: metadata.accessed().map(system_time_nanos).unwrap_or(0), + created: metadata.created().map(system_time_nanos).unwrap_or(0), + modified: metadata.modified().map(system_time_nanos).unwrap_or(0), + len: metadata.len(), + } +} + +fn system_time_nanos(time: SystemTime) -> u64 { + time.duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos() as u64) + .unwrap_or(0) +} + +fn nanos_to_file_time(nanos: u64) -> filetime::FileTime { + filetime::FileTime::from_unix_time( + (nanos / 1_000_000_000) as i64, + (nanos % 1_000_000_000) as u32, + ) +} diff --git a/src/pglite/timing.rs b/src/pglite/timing.rs new file mode 100644 index 00000000..7513794c --- /dev/null +++ b/src/pglite/timing.rs @@ -0,0 +1,108 @@ +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use serde::Serialize; + +thread_local! { + static ACTIVE_RECORDER: RefCell>>>> = + const { RefCell::new(None) }; +} + +pub(crate) type PhaseRecorder = Arc>>; + +/// One measured runtime phase captured during a cold-start operation. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PhaseTiming { + pub name: &'static str, + pub elapsed_micros: u128, +} + +impl PhaseTiming { + pub fn elapsed(&self) -> Duration { + Duration::from_micros(self.elapsed_micros.min(u64::MAX as u128) as u64) + } +} + +pub(crate) struct PhaseGuard { + name: &'static str, + started: Option, + recorder: Option>>>, +} + +pub(crate) fn phase(name: &'static str) -> PhaseGuard { + let recorder = ACTIVE_RECORDER.with(|active| active.borrow().clone()); + let started = recorder.as_ref().map(|_| Instant::now()); + PhaseGuard { + name, + started, + recorder, + } +} + +#[doc(hidden)] +pub fn measure_phase(name: &'static str, operation: impl FnOnce() -> T) -> T { + let _phase = phase(name); + operation() +} + +#[doc(hidden)] +pub fn record_phase_timing(name: &'static str, elapsed: Duration) { + let recorder = ACTIVE_RECORDER.with(|active| active.borrow().clone()); + if let Some(recorder) = recorder { + recorder + .lock() + .expect("phase timing recorder poisoned") + .push(PhaseTiming { + name, + elapsed_micros: elapsed.as_micros(), + }); + } +} + +pub(crate) fn current_recorder() -> Option { + ACTIVE_RECORDER.with(|active| active.borrow().clone()) +} + +pub(crate) fn with_recorder( + recorder: Option, + operation: impl FnOnce() -> T, +) -> T { + let previous = ACTIVE_RECORDER.with(|active| active.replace(recorder)); + let result = operation(); + ACTIVE_RECORDER.with(|active| { + active.replace(previous); + }); + result +} + +/// Run `operation` while collecting internal cold-start phase timings. +/// +/// This is hidden from normal docs because the exact phase names are diagnostic +/// surface, not a compatibility contract. +#[doc(hidden)] +pub fn capture_phase_timings(operation: impl FnOnce() -> T) -> (T, Vec) { + let recorder = Arc::new(Mutex::new(Vec::new())); + let result = with_recorder(Some(recorder.clone()), operation); + + let timings = recorder + .lock() + .expect("phase timing recorder poisoned") + .clone(); + (result, timings) +} + +impl Drop for PhaseGuard { + fn drop(&mut self) { + if let (Some(recorder), Some(started)) = (&self.recorder, self.started) { + recorder + .lock() + .expect("phase timing recorder poisoned") + .push(PhaseTiming { + name: self.name, + elapsed_micros: started.elapsed().as_micros(), + }); + } + } +} diff --git a/src/pglite/transport.rs b/src/pglite/transport.rs index e504e8e9..8e6332ce 100644 --- a/src/pglite/transport.rs +++ b/src/pglite/transport.rs @@ -1,38 +1,16 @@ -use anyhow::{Context, Result, bail, ensure}; -use std::fs; -use std::thread; -use std::time::{Duration, Instant}; +use anyhow::{Result, bail}; use super::postgres_mod::PostgresMod; use crate::pglite::interface::DataTransferContainer; -/// Mirrors the TypeScript transport abstraction (CMA vs file-backed lock files). -/// Currently only the shared-memory CMA channel is implemented. +/// Protocol transport for the WASIX PGlite backend. pub enum Transport { - Cma { - buffer_addr: usize, - buffer_len: usize, - }, - #[allow(dead_code)] - File, + Wasix, } impl Transport { - pub fn from_postgres_mod(pg: &PostgresMod) -> Result { - if let (Some(addr), Some(len)) = (pg.buffer_addr(), pg.buffer_len()) { - Ok(Self::Cma { - buffer_addr: addr, - buffer_len: len, - }) - } else { - Ok(Self::File) - } - } - - pub fn prepare(pg: &mut PostgresMod) -> Result { - pg.use_wire(true)?; - pg.backend()?; - Self::from_postgres_mod(pg) + pub fn prepare(_pg: &mut PostgresMod) -> Result { + Ok(Self::Wasix) } pub fn send( @@ -41,90 +19,9 @@ impl Transport { payload: &[u8], requested: Option, ) -> Result> { - match self { - Transport::Cma { - buffer_addr, - buffer_len, - } => match requested { - Some(DataTransferContainer::File) => { - bail!("file transport is not implemented yet") - } - _ => send_cma(pg, *buffer_addr, *buffer_len, payload), - }, - Transport::File => send_file(pg, payload), - } - } -} - -fn send_cma( - pg: &mut PostgresMod, - buffer_addr: usize, - buffer_len: usize, - payload: &[u8], -) -> Result> { - ensure!( - payload.len() <= buffer_len, - "payload of {} bytes exceeds CMA buffer ({} bytes)", - payload.len(), - buffer_len - ); - - pg.interactive_write(payload.len() as i32)?; - if !payload.is_empty() { - pg.write_memory(buffer_addr, payload)?; - } - pg.interactive_one()?; - - let available = pg.interactive_read()?; - if available <= 0 { - return Ok(Vec::new()); - } - - let response_len = available as usize; - let response_addr = buffer_addr + payload.len() + 1; - ensure!( - response_addr + response_len <= buffer_addr + buffer_len, - "response range [{}..{}) exceeds CMA buffer [{}..{})", - response_addr, - response_addr + response_len, - buffer_addr, - buffer_addr + buffer_len - ); - - let mut response = vec![0; response_len]; - pg.read_memory(response_addr, &mut response)?; - pg.interactive_write(0)?; - - Ok(response) -} - -fn send_file(pg: &mut PostgresMod, payload: &[u8]) -> Result> { - let base = pg.paths().pgroot.join("pglite/base"); - let lock_in = base.join(".s.PGSQL.5432.lck.in"); - let in_path = base.join(".s.PGSQL.5432.in"); - let out_path = base.join(".s.PGSQL.5432.out"); - - if out_path.exists() { - let _ = fs::remove_file(&out_path); - } - - fs::write(&lock_in, payload) - .with_context(|| format!("write payload to {}", lock_in.display()))?; - fs::rename(&lock_in, &in_path) - .with_context(|| format!("rename {} -> {}", lock_in.display(), in_path.display()))?; - - let start = Instant::now(); - let timeout = Duration::from_secs(5); - loop { - if out_path.exists() { - let bytes = fs::read(&out_path) - .with_context(|| format!("read response from {}", out_path.display()))?; - let _ = fs::remove_file(&out_path); - return Ok(bytes); - } - if start.elapsed() > timeout { - bail!("file transport timed out waiting for response"); + if matches!(requested, Some(DataTransferContainer::File)) { + bail!("file transport is not implemented for the WASIX backend") } - thread::sleep(Duration::from_millis(2)); + pg.send_protocol(payload) } } diff --git a/src/pglite/types.rs b/src/pglite/types.rs index 34585c9b..e5df4061 100644 --- a/src/pglite/types.rs +++ b/src/pglite/types.rs @@ -35,6 +35,23 @@ const_oid!(JSONB = 3802); pub static DEFAULT_PARSERS: LazyLock = LazyLock::new(build_default_parsers); pub static DEFAULT_SERIALIZERS: LazyLock = LazyLock::new(build_default_serializers); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArrayTypeInfo { + pub element_oid: i32, + pub array_oid: i32, + pub delimiter: char, +} + +impl ArrayTypeInfo { + pub const fn new(element_oid: i32, array_oid: i32, delimiter: char) -> Self { + Self { + element_oid, + array_oid, + delimiter, + } + } +} + pub struct ParserLookup<'a> { defaults: &'a ParserMap, overrides: &'a ParserMap, @@ -61,14 +78,10 @@ impl<'a> ParserLookup<'a> { } } -fn array_delimiter(typarray: i32) -> char { - if typarray == 1020 { ';' } else { ',' } -} - pub fn serialize_array_value( value: &Value, element_serializer: Option, - typarray: i32, + delimiter: char, ) -> Result { match value { Value::Array(items) => { @@ -76,7 +89,6 @@ pub fn serialize_array_value( return Ok("{}".to_string()); } - let delimiter = array_delimiter(typarray); let mut parts = Vec::with_capacity(items.len()); for item in items { match item { @@ -85,7 +97,7 @@ pub fn serialize_array_value( parts.push(serialize_array_value( item, element_serializer.clone(), - typarray, + delimiter, )?); } _ => { @@ -124,121 +136,124 @@ fn value_to_string(value: &Value) -> String { } } -#[derive(Default)] -struct ArrayParserState { - index: usize, - last: usize, - quoted: bool, - buffer: String, - prev: Option, -} - pub fn parse_array_text( text: &str, element_parser: Option, element_type_id: i32, - typarray: i32, + delimiter: char, ) -> Value { - let mut state = ArrayParserState::default(); - let result = parse_array_loop( + let Some(start) = text.find('{') else { + return Value::Array(Vec::new()); + }; + let mut parser = ArrayTextParser { text, - &mut state, - element_parser.as_ref(), + index: start, + element_parser: element_parser.as_ref(), element_type_id, - typarray, - ); - match result { - Value::Array(outer) => { - if let Some(Value::Array(inner)) = outer.into_iter().next() { - Value::Array(inner) - } else { - Value::Array(Vec::new()) - } - } - _ => Value::Array(Vec::new()), - } + delimiter, + }; + parser.parse_array() } -fn parse_array_loop( - text: &str, - state: &mut ArrayParserState, - element_parser: Option<&TypeParser>, +struct ArrayTextParser<'a> { + text: &'a str, + index: usize, + element_parser: Option<&'a TypeParser>, element_type_id: i32, - typarray: i32, -) -> Value { - let delimiter = array_delimiter(typarray); - let bytes = text.as_bytes(); - let mut values: Vec = Vec::new(); - - while state.index < bytes.len() { - let ch = bytes[state.index] as char; - if state.quoted { - if ch == '\\' { - state.index += 1; - if state.index < bytes.len() { - state.buffer.push(bytes[state.index] as char); + delimiter: char, +} + +impl ArrayTextParser<'_> { + fn parse_array(&mut self) -> Value { + if self.peek() != Some('{') { + return Value::Array(Vec::new()); + } + self.advance(); + + let mut values = Vec::new(); + loop { + match self.peek() { + Some('}') => { + self.advance(); + return Value::Array(values); } - } else if ch == '"' { - let value = apply_element_parser(&state.buffer, element_parser, element_type_id); - values.push(value); - state.buffer.clear(); - if state.index + 1 < bytes.len() && bytes[state.index + 1] as char == '"' { - state.index += 1; - state.quoted = true; - } else { - state.quoted = false; + Some('{') => values.push(self.parse_array()), + Some('"') => values.push(self.parse_quoted()), + Some(_) => values.push(self.parse_unquoted()), + None => return Value::Array(values), + } + + match self.peek() { + Some(ch) if ch == self.delimiter => { + self.advance(); } - state.last = state.index + 1; - } else { - state.buffer.push(ch); + Some('}') => {} + Some(_) | None => {} } - } else if ch == '"' { - state.quoted = true; - state.buffer.clear(); - state.last = state.index + 1; - } else if ch == '{' { - state.last = state.index + 1; - state.index += 1; - values.push(parse_array_loop( - text, - state, - element_parser, - element_type_id, - typarray, - )); - } else if ch == '}' { - state.quoted = false; - if state.last < state.index && state.prev != Some('}') && state.prev != Some('"') { - let slice = &text[state.last..state.index]; - if !slice.is_empty() { - values.push(apply_element_parser(slice, element_parser, element_type_id)); + } + } + + fn parse_quoted(&mut self) -> Value { + self.advance(); + let mut value = String::new(); + + while let Some(ch) = self.peek() { + self.advance(); + match ch { + '\\' => { + if let Some(escaped) = self.peek() { + self.advance(); + value.push(escaped); + } + } + '"' => { + return apply_element_parser( + &value, + self.element_parser, + self.element_type_id, + true, + ); } + other => value.push(other), } - state.last = state.index + 1; - break; - } else if ch == delimiter && state.prev != Some('}') && state.prev != Some('"') { - let slice = &text[state.last..state.index]; - values.push(apply_element_parser(slice, element_parser, element_type_id)); - state.last = state.index + 1; } - state.prev = Some(ch); - state.index += 1; + + apply_element_parser(&value, self.element_parser, self.element_type_id, true) } - if state.last < state.index { - let slice = &text[state.last..state.index]; - if !slice.is_empty() { - values.push(apply_element_parser(slice, element_parser, element_type_id)); + fn parse_unquoted(&mut self) -> Value { + let start = self.index; + while let Some(ch) = self.peek() { + if ch == self.delimiter || ch == '}' { + break; + } + self.advance(); } + + let slice = self.text[start..self.index].trim(); + apply_element_parser(slice, self.element_parser, self.element_type_id, false) + } + + fn peek(&self) -> Option { + self.text[self.index..].chars().next() } - Value::Array(values) + fn advance(&mut self) { + if let Some(ch) = self.peek() { + self.index += ch.len_utf8(); + } + } } -fn apply_element_parser(slice: &str, parser: Option<&TypeParser>, element_type_id: i32) -> Value { +fn apply_element_parser( + slice: &str, + parser: Option<&TypeParser>, + element_type_id: i32, + quoted: bool, +) -> Value { if let Some(p) = parser { p(slice, element_type_id) - } else if slice.eq_ignore_ascii_case("NULL") { + } else if !quoted && slice.eq_ignore_ascii_case("NULL") { Value::Null } else { Value::String(slice.to_string()) @@ -282,6 +297,7 @@ fn build_default_parsers() -> ParserMap { ); map.insert(DATE, Arc::new(|value: &str, _| json!(value.to_string()))); + register_builtin_array_parsers(&mut map); map } @@ -314,9 +330,133 @@ fn build_default_serializers() -> SerializerMap { ); map.insert(DATE, Arc::new(|value: &Value| serialize_string(value))); + register_builtin_array_serializers(&mut map); map } +pub fn register_array_type( + parsers: &mut ParserMap, + serializers: &mut SerializerMap, + info: ArrayTypeInfo, +) { + register_array_parser(parsers, info); + register_array_serializer(serializers, info); +} + +fn register_array_parser(parsers: &mut ParserMap, info: ArrayTypeInfo) { + let element_parser = parsers.get(&info.element_oid).cloned(); + let element_oid = info.element_oid; + let delimiter = info.delimiter; + let array_parser: TypeParser = Arc::new(move |text: &str, _| { + parse_array_text(text, element_parser.clone(), element_oid, delimiter) + }); + parsers.insert(info.array_oid, array_parser); +} + +fn register_array_serializer(serializers: &mut SerializerMap, info: ArrayTypeInfo) { + let element_serializer = serializers.get(&info.element_oid).cloned(); + let delimiter = info.delimiter; + let array_serializer: Serializer = Arc::new(move |value: &Value| { + serialize_array_value(value, element_serializer.clone(), delimiter) + }); + serializers.insert(info.array_oid, array_serializer); +} + +fn register_builtin_array_parsers(parsers: &mut ParserMap) { + for info in BUILTIN_ARRAY_TYPES { + register_array_parser(parsers, *info); + } +} + +fn register_builtin_array_serializers(serializers: &mut SerializerMap) { + for info in BUILTIN_ARRAY_TYPES { + register_array_serializer(serializers, *info); + } +} + +// Generated from PostgreSQL's built-in pg_type.dat OID assignments for the +// default PGlite/Postgres 17 catalog. Keep this list to built-in types only: +// extension and runtime-created custom arrays are discovered through the direct +// client type cache when they are actually used. +const BUILTIN_ARRAY_TYPES: &[ArrayTypeInfo] = &[ + ArrayTypeInfo::new(16, 1000, ','), + ArrayTypeInfo::new(17, 1001, ','), + ArrayTypeInfo::new(18, 1002, ','), + ArrayTypeInfo::new(19, 1003, ','), + ArrayTypeInfo::new(20, 1016, ','), + ArrayTypeInfo::new(21, 1005, ','), + ArrayTypeInfo::new(22, 1006, ','), + ArrayTypeInfo::new(23, 1007, ','), + ArrayTypeInfo::new(24, 1008, ','), + ArrayTypeInfo::new(25, 1009, ','), + ArrayTypeInfo::new(26, 1028, ','), + ArrayTypeInfo::new(27, 1010, ','), + ArrayTypeInfo::new(28, 1011, ','), + ArrayTypeInfo::new(29, 1012, ','), + ArrayTypeInfo::new(30, 1013, ','), + ArrayTypeInfo::new(114, 199, ','), + ArrayTypeInfo::new(142, 143, ','), + ArrayTypeInfo::new(600, 1017, ','), + ArrayTypeInfo::new(601, 1018, ','), + ArrayTypeInfo::new(602, 1019, ','), + ArrayTypeInfo::new(603, 1020, ';'), + ArrayTypeInfo::new(604, 1027, ','), + ArrayTypeInfo::new(628, 629, ','), + ArrayTypeInfo::new(700, 1021, ','), + ArrayTypeInfo::new(701, 1022, ','), + ArrayTypeInfo::new(718, 719, ','), + ArrayTypeInfo::new(790, 791, ','), + ArrayTypeInfo::new(829, 1040, ','), + ArrayTypeInfo::new(869, 1041, ','), + ArrayTypeInfo::new(650, 651, ','), + ArrayTypeInfo::new(774, 775, ','), + ArrayTypeInfo::new(1033, 1034, ','), + ArrayTypeInfo::new(1042, 1014, ','), + ArrayTypeInfo::new(1043, 1015, ','), + ArrayTypeInfo::new(1082, 1182, ','), + ArrayTypeInfo::new(1083, 1183, ','), + ArrayTypeInfo::new(1114, 1115, ','), + ArrayTypeInfo::new(1184, 1185, ','), + ArrayTypeInfo::new(1186, 1187, ','), + ArrayTypeInfo::new(1266, 1270, ','), + ArrayTypeInfo::new(1560, 1561, ','), + ArrayTypeInfo::new(1562, 1563, ','), + ArrayTypeInfo::new(1700, 1231, ','), + ArrayTypeInfo::new(1790, 2201, ','), + ArrayTypeInfo::new(2202, 2207, ','), + ArrayTypeInfo::new(2203, 2208, ','), + ArrayTypeInfo::new(2204, 2209, ','), + ArrayTypeInfo::new(2205, 2210, ','), + ArrayTypeInfo::new(4191, 4192, ','), + ArrayTypeInfo::new(2206, 2211, ','), + ArrayTypeInfo::new(4096, 4097, ','), + ArrayTypeInfo::new(4089, 4090, ','), + ArrayTypeInfo::new(2950, 2951, ','), + ArrayTypeInfo::new(3220, 3221, ','), + ArrayTypeInfo::new(3614, 3643, ','), + ArrayTypeInfo::new(3642, 3644, ','), + ArrayTypeInfo::new(3615, 3645, ','), + ArrayTypeInfo::new(3734, 3735, ','), + ArrayTypeInfo::new(3769, 3770, ','), + ArrayTypeInfo::new(3802, 3807, ','), + ArrayTypeInfo::new(4072, 4073, ','), + ArrayTypeInfo::new(2970, 2949, ','), + ArrayTypeInfo::new(5038, 5039, ','), + ArrayTypeInfo::new(3904, 3905, ','), + ArrayTypeInfo::new(3906, 3907, ','), + ArrayTypeInfo::new(3908, 3909, ','), + ArrayTypeInfo::new(3910, 3911, ','), + ArrayTypeInfo::new(3912, 3913, ','), + ArrayTypeInfo::new(3926, 3927, ','), + ArrayTypeInfo::new(4451, 6150, ','), + ArrayTypeInfo::new(4532, 6151, ','), + ArrayTypeInfo::new(4533, 6152, ','), + ArrayTypeInfo::new(4534, 6153, ','), + ArrayTypeInfo::new(4535, 6155, ','), + ArrayTypeInfo::new(4536, 6157, ','), + ArrayTypeInfo::new(2275, 1263, ','), +]; + fn parse_int(value: &str) -> Value { match value.parse::() { Ok(int) => json!(int), @@ -410,3 +550,29 @@ fn serialize_bytea(value: &Value) -> Result { _ => Err(anyhow!("unsupported value for bytea serialization")), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_multidimensional_arrays_without_separator_artifacts() { + let parser: TypeParser = Arc::new(|value, _| parse_float(value)); + let parsed = parse_array_text("{{1.5,2.5},{3.5,4.5}}", Some(parser), FLOAT8, ','); + assert_eq!(parsed, json!([[1.5, 2.5], [3.5, 4.5]])); + } + + #[test] + fn parses_quoted_array_values_and_unquoted_nulls() { + let parsed = parse_array_text( + r#"{"comma,value","quote \" value",NULL,"NULL",""}"#, + None, + TEXT, + ',', + ); + assert_eq!( + parsed, + json!(["comma,value", "quote \" value", null, "NULL", ""]) + ); + } +} diff --git a/src/pglite/wire.rs b/src/pglite/wire.rs new file mode 100644 index 00000000..db45b7c5 --- /dev/null +++ b/src/pglite/wire.rs @@ -0,0 +1,260 @@ +use anyhow::{Context, Result, anyhow, bail}; + +use crate::pglite::config::StartupConfig; + +pub(crate) const SSL_REQUEST_CODE: i32 = 80_877_103; +pub(crate) const GSSENC_REQUEST_CODE: i32 = 80_877_104; +pub(crate) const CANCEL_REQUEST_CODE: i32 = 80_877_102; +pub(crate) const PROTOCOL_3: i32 = 196_608; +pub(crate) const MAX_FRONTEND_MESSAGE: usize = 128 * 1024 * 1024; + +#[derive(Default)] +pub(crate) struct FrontendFrameReader { + buffer: Vec, +} + +impl FrontendFrameReader { + pub(crate) fn push(&mut self, input: &[u8]) -> Result>> { + self.buffer.extend_from_slice(input); + let mut messages = Vec::new(); + + loop { + let Some(message_len) = frontend_message_len_if_complete(&self.buffer)? else { + break; + }; + messages.push(self.buffer.drain(..message_len).collect()); + } + + Ok(messages) + } + + pub(crate) fn pending(&self) -> &[u8] { + &self.buffer + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FrontendFrameKind { + Protocol, + Startup, + SslOrGssRequest, + CancelRequest, + Terminate, +} + +pub(crate) fn frontend_message_len_if_complete(buffer: &[u8]) -> Result> { + if buffer.len() < 4 { + return Ok(None); + } + + if buffer[0] == 0 { + let len = i32::from_be_bytes(buffer[0..4].try_into().unwrap()); + if len < 8 { + bail!("invalid startup packet length {len}"); + } + let len = len as usize; + if len > MAX_FRONTEND_MESSAGE { + bail!("startup/control packet length {len} exceeds limit"); + } + return Ok((buffer.len() >= len).then_some(len)); + } + + if buffer.len() < 5 { + return Ok(None); + } + let len = i32::from_be_bytes(buffer[1..5].try_into().unwrap()); + if len < 4 { + bail!("invalid frontend message length {len}"); + } + let total = 1usize + .checked_add(len as usize) + .ok_or_else(|| anyhow!("frontend message length overflow"))?; + if total > MAX_FRONTEND_MESSAGE { + bail!("frontend message length {total} exceeds limit"); + } + Ok((buffer.len() >= total).then_some(total)) +} + +pub(crate) fn raw_protocol_message_len(buffer: &[u8]) -> Result { + if buffer.len() < 5 { + bail!("raw protocol stream input contains an incomplete frontend message header"); + } + let len = i32::from_be_bytes(buffer[1..5].try_into().unwrap()); + if len < 4 { + bail!("raw protocol stream input contains invalid frontend message length {len}"); + } + let total = 1usize + .checked_add(len as usize) + .ok_or_else(|| anyhow!("raw protocol stream frontend message length overflow"))?; + if total > MAX_FRONTEND_MESSAGE { + bail!("raw protocol stream frontend message length {total} exceeds limit"); + } + if buffer.len() < total { + bail!( + "raw protocol stream input contains incomplete frontend message: expected {total} bytes, got {}", + buffer.len() + ); + } + Ok(total) +} + +pub(crate) fn classify_frontend_message(message: &[u8]) -> Result { + if message.is_empty() { + bail!("empty frontend message"); + } + + if message[0] == 0 { + if message.len() < 8 { + bail!("startup/control packet is too short"); + } + let code = i32::from_be_bytes(message[4..8].try_into().unwrap()); + return Ok(match code { + SSL_REQUEST_CODE | GSSENC_REQUEST_CODE => FrontendFrameKind::SslOrGssRequest, + CANCEL_REQUEST_CODE => FrontendFrameKind::CancelRequest, + PROTOCOL_3 => FrontendFrameKind::Startup, + other => bail!("unsupported startup/control packet code {other}"), + }); + } + + if message[0] == b'X' { + return Ok(FrontendFrameKind::Terminate); + } + + Ok(FrontendFrameKind::Protocol) +} + +pub(crate) fn startup_parameter<'a>(message: &'a [u8], wanted: &str) -> Result> { + if message.len() < 8 { + bail!("startup packet is too short"); + } + let mut cursor = 8usize; + while cursor < message.len() { + if message[cursor] == 0 { + break; + } + let key_end = message[cursor..] + .iter() + .position(|byte| *byte == 0) + .map(|offset| cursor + offset) + .ok_or_else(|| anyhow!("startup parameter key is not nul-terminated"))?; + let key = std::str::from_utf8(&message[cursor..key_end]) + .context("startup parameter key is not UTF-8")?; + cursor = key_end + 1; + + let value_end = message[cursor..] + .iter() + .position(|byte| *byte == 0) + .map(|offset| cursor + offset) + .ok_or_else(|| anyhow!("startup parameter value is not nul-terminated"))?; + let value = std::str::from_utf8(&message[cursor..value_end]) + .context("startup parameter value is not UTF-8")?; + cursor = value_end + 1; + if key == wanted { + return Ok(Some(value)); + } + } + Ok(None) +} + +pub(crate) fn startup_config_for_message( + base: &StartupConfig, + message: &[u8], +) -> Result { + let mut config = base.clone(); + if let Some(user) = startup_parameter(message, "user")? { + config.username = user.to_owned(); + } + if let Some(database) = startup_parameter(message, "database")? { + config.database = database.to_owned(); + } + config.validate()?; + Ok(config) +} + +pub(crate) fn response_contains_error(response: &[u8]) -> bool { + response_contains_tag(response, b'E') +} + +pub(crate) fn response_contains_tag(response: &[u8], expected: u8) -> bool { + let mut cursor = 0usize; + while cursor + 5 <= response.len() { + let tag = response[cursor]; + let len = i32::from_be_bytes(response[cursor + 1..cursor + 5].try_into().unwrap()); + if len < 4 { + return false; + } + let total = 1usize.saturating_add(len as usize); + if cursor + total > response.len() { + return false; + } + if tag == expected { + return true; + } + cursor += total; + } + false +} + +pub(crate) fn error_response(severity: &str, code: &str, message: &str) -> Vec { + let mut body = Vec::new(); + push_error_field(&mut body, b'S', severity); + push_error_field(&mut body, b'V', severity); + push_error_field(&mut body, b'C', code); + push_error_field(&mut body, b'M', message); + body.push(0); + + let mut response = Vec::with_capacity(body.len() + 5); + response.push(b'E'); + response.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); + response.extend_from_slice(&body); + response +} + +pub(crate) fn simple_query_message(sql: &str) -> Vec { + let mut message = Vec::with_capacity(sql.len() + 6); + message.push(b'Q'); + message.extend_from_slice(&((sql.len() + 5) as i32).to_be_bytes()); + message.extend_from_slice(sql.as_bytes()); + message.push(0); + message +} + +fn push_error_field(body: &mut Vec, tag: u8, value: &str) { + body.push(tag); + body.extend_from_slice(value.as_bytes()); + body.push(0); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_reader_buffers_split_messages() -> Result<()> { + let query = b"Q\0\0\0\rSELECT 1\0"; + let mut reader = FrontendFrameReader::default(); + assert!(reader.push(&query[..3])?.is_empty()); + assert_eq!(reader.push(&query[3..])?, vec![query.to_vec()]); + Ok(()) + } + + #[test] + fn classifies_startup_and_control_packets() -> Result<()> { + let mut startup = Vec::new(); + startup.extend_from_slice(&8_i32.to_be_bytes()); + startup.extend_from_slice(&PROTOCOL_3.to_be_bytes()); + assert_eq!( + classify_frontend_message(&startup)?, + FrontendFrameKind::Startup + ); + + let mut ssl = Vec::new(); + ssl.extend_from_slice(&8_i32.to_be_bytes()); + ssl.extend_from_slice(&SSL_REQUEST_CODE.to_be_bytes()); + assert_eq!( + classify_frontend_message(&ssl)?, + FrontendFrameKind::SslOrGssRequest + ); + Ok(()) + } +} diff --git a/tests/cli_smoke.rs b/tests/cli_smoke.rs new file mode 100644 index 00000000..d38fb260 --- /dev/null +++ b/tests/cli_smoke.rs @@ -0,0 +1,82 @@ +#![cfg(feature = "extensions")] + +use anyhow::{Context, Result}; +use pglite_oxide::{Pglite, capture_phase_timings}; +use sqlx::{Connection, Row}; +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; +use tokio::time::{Duration, timeout}; + +mod support; +use support::{ChildGuard, TestTrace, trace_step}; + +fn direct_open_diagnostic() -> String { + let (result, phases) = capture_phase_timings(|| Pglite::builder().temporary().open()); + let outcome = match result { + Ok(mut pg) => match pg.close() { + Ok(()) => "direct temporary Pglite open succeeded".to_owned(), + Err(err) => format!("direct temporary Pglite open succeeded, close failed: {err:#}"), + }, + Err(err) => format!("direct temporary Pglite open failed: {err:#}"), + }; + format!("{outcome}\nphases:\n{phases:#?}") +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn pglite_proxy_print_uri_accepts_sqlx_connection() -> Result<()> { + let _trace = TestTrace::new("pglite_proxy_print_uri_accepts_sqlx_connection"); + let process = Command::new(env!("CARGO_BIN_EXE_pglite-proxy")) + .args(["--temporary", "--tcp", "127.0.0.1:0", "--print-uri"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("spawn pglite-proxy")?; + let mut child = ChildGuard::new(process, "pglite-proxy")?; + + let stdout = child + .child_mut() + .stdout + .take() + .context("pglite-proxy stdout pipe")?; + let mut reader = BufReader::new(stdout); + let mut uri = String::new(); + let bytes = reader + .read_line(&mut uri) + .context("read pglite-proxy printed URI")?; + if bytes == 0 { + let stderr = child.collect_stderr(); + anyhow::bail!("pglite-proxy exited before printing URI\n\nstderr:\n{stderr}"); + } + let uri = uri.trim(); + assert!( + uri.starts_with("postgresql://") || uri.starts_with("postgres://"), + "unexpected URI: {uri}" + ); + trace_step("pglite_proxy printed URI"); + + let mut conn = match timeout(Duration::from_secs(30), sqlx::PgConnection::connect(uri)).await { + Ok(Ok(conn)) => conn, + Ok(Err(err)) => { + let stderr = child.collect_stderr(); + let direct = direct_open_diagnostic(); + anyhow::bail!( + "connect to pglite-proxy failed: {err:#}\n\nstderr:\n{stderr}\n\ndirect backend diagnostic:\n{direct}" + ); + } + Err(err) => { + let stderr = child.collect_stderr(); + let direct = direct_open_diagnostic(); + anyhow::bail!( + "timed out connecting to pglite-proxy: {err}\n\nstderr:\n{stderr}\n\ndirect backend diagnostic:\n{direct}" + ); + } + }; + let row = sqlx::query("SELECT $1::int4 + 1 AS answer") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("answer")?, 42); + + conn.close().await?; + Ok(()) +} diff --git a/tests/client_compat.rs b/tests/client_compat.rs index a26f2a84..96587bff 100644 --- a/tests/client_compat.rs +++ b/tests/client_compat.rs @@ -1,9 +1,18 @@ +#![cfg(feature = "extensions")] + use anyhow::{Context, Result}; -use pglite_oxide::PgliteServer; -use sqlx::{Connection, Row}; +use pglite_oxide::{Pglite, PgliteServer}; +use sqlx::{Connection, Executor, Row}; +use std::io::{Read, Write}; +use std::net::TcpStream; +#[cfg(unix)] +use std::os::unix::net::UnixStream; use tokio::time::{Duration, timeout}; use tokio_postgres::NoTls; +const SSL_REQUEST_CODE: i32 = 80_877_103; +const CANCEL_REQUEST_CODE: i32 = 80_877_102; + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_extended_query_works() -> Result<()> { let server = PgliteServer::temporary_tcp()?; @@ -36,6 +45,518 @@ async fn tokio_postgres_extended_query_works() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_extended_query_errors_recover_after_sync() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) + .await + .context("connect with tokio-postgres")?; + let connection_task = tokio::spawn(connection); + + let parse_err = client + .query_one("SELECT missing FROM missing_table WHERE id = $1", &[&7_i32]) + .await + .expect_err("undefined table should fail during extended-query parse"); + assert_eq!( + parse_err.code().map(|code| code.code()), + Some("42P01"), + "undefined table should preserve SQLSTATE" + ); + let row = client + .query_one("SELECT 11::int4 AS recovered_after_parse", &[]) + .await + .context("query after parse error")?; + assert_eq!(row.get::<_, i32>("recovered_after_parse"), 11); + + let execute_err = client + .query_one("SELECT 10 / $1::int4 AS impossible", &[&0_i32]) + .await + .expect_err("division by zero should fail during extended-query execute"); + assert_eq!( + execute_err.code().map(|code| code.code()), + Some("22012"), + "execute error should preserve SQLSTATE" + ); + let row = client + .query_one("SELECT 12::int4 AS recovered_after_execute", &[]) + .await + .context("query after execute error")?; + assert_eq!(row.get::<_, i32>("recovered_after_execute"), 12); + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_bind_errors_recover_after_sync() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + let invalid_int = sqlx::query("SELECT $1::int4 AS value") + .bind("not_an_int") + .fetch_one(&mut conn) + .await + .expect_err("invalid int text should fail while binding a typed parameter"); + assert_sqlx_code(&invalid_int, "22P02"); + let row = sqlx::query("SELECT 31::int4 AS recovered_after_invalid_bind") + .fetch_one(&mut conn) + .await + .context("query after invalid bind value")?; + assert_eq!(row.try_get::("recovered_after_invalid_bind")?, 31); + + let wrong_param_count = sqlx::query("SELECT $1::int4 + $2::int4 AS value") + .bind(1_i32) + .fetch_one(&mut conn) + .await + .expect_err("missing parameter should fail during extended-query bind"); + assert_sqlx_code(&wrong_param_count, "08P01"); + let row = sqlx::query("SELECT 32::int4 AS recovered_after_param_count") + .fetch_one(&mut conn) + .await + .context("query after wrong parameter count")?; + assert_eq!(row.try_get::("recovered_after_param_count")?, 32); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_pipelined_extended_queries_keep_ready_state() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) + .await + .context("connect with tokio-postgres")?; + let connection_task = tokio::spawn(connection); + + let first = client.query_one("SELECT $1::int4 AS value", &[&10_i32]); + let second = client.query_one("SELECT $1::int4 + 1 AS value", &[&41_i32]); + let (first, second) = tokio::try_join!(first, second).context("run pipelined queries")?; + + assert_eq!(first.get::<_, i32>("value"), 10); + assert_eq!(second.get::<_, i32>("value"), 42); + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_mixed_pipelined_success_error_success_recovers() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) + .await + .context("connect with tokio-postgres")?; + let connection_task = tokio::spawn(connection); + + let first = client.query_one("SELECT $1::int4 AS value", &[&1_i32]); + let second = client.query_one("SELECT 10 / $1::int4 AS value", &[&0_i32]); + let third = client.query_one("SELECT $1::int4 AS value", &[&3_i32]); + let (first, second, third) = tokio::join!(first, second, third); + + assert_eq!(first?.get::<_, i32>("value"), 1); + let second = second.expect_err("middle pipelined query should fail"); + assert_eq!(second.code().map(|code| code.code()), Some("22012")); + assert_eq!(third?.get::<_, i32>("value"), 3); + + let row = client + .query_one("SELECT 4::int4 AS recovered_after_pipeline", &[]) + .await?; + assert_eq!(row.get::<_, i32>("recovered_after_pipeline"), 4); + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_protocol_bind_errors_are_synchronized() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = TcpStream::connect(addr).context("connect raw protocol socket")?; + stream.set_read_timeout(Some(Duration::from_secs(120)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + + stream + .write_all(&startup_message()) + .context("write startup message")?; + let startup = read_until_ready(&mut stream).context("read startup response")?; + assert!( + startup.iter().any(|msg| msg.tag == b'R'), + "startup should include AuthenticationOk" + ); + assert_eq!(startup.last().map(|msg| msg.tag), Some(b'Z')); + + stream + .write_all( + &[ + parse_statement("typed_int", "SELECT $1::int4 AS value"), + sync(), + ] + .concat(), + ) + .context("write Parse + Sync")?; + let parsed = read_until_ready(&mut stream).context("read Parse response")?; + assert_message_tags_ignoring_parameter_status(&parsed, b"1Z"); + + stream + .write_all( + &[ + bind_statement("", "typed_int", &["not_an_int"]), + describe_portal(""), + execute_portal(""), + sync(), + ] + .concat(), + ) + .context("write invalid Bind batch")?; + let invalid_bind = read_until_ready(&mut stream).context("read invalid Bind response")?; + assert_eq!(invalid_bind.last().map(|msg| msg.tag), Some(b'Z')); + assert!( + invalid_bind.iter().all(|msg| msg.tag != b'2'), + "invalid Bind must not emit BindComplete" + ); + assert_eq!(first_error_code(&invalid_bind).as_deref(), Some("22P02")); + + stream + .write_all(&[bind_statement("", "typed_int", &[]), sync()].concat()) + .context("write wrong parameter count Bind batch")?; + let wrong_count = read_until_ready(&mut stream).context("read wrong Bind response")?; + assert_eq!(wrong_count.last().map(|msg| msg.tag), Some(b'Z')); + assert!( + wrong_count.iter().all(|msg| msg.tag != b'2'), + "wrong parameter count must not emit BindComplete" + ); + assert_eq!(first_error_code(&wrong_count).as_deref(), Some("08P01")); + + stream + .write_all(&query_message("SELECT 33::int4 AS recovered")) + .context("write recovery query")?; + let recovered = read_until_ready(&mut stream).context("read recovery query")?; + assert!( + recovered.iter().any(|msg| msg.tag == b'T') + && recovered.iter().any(|msg| msg.tag == b'D') + && recovered.iter().any(|msg| msg.tag == b'C') + && recovered.last().is_some_and(|msg| msg.tag == b'Z'), + "connection should recover after raw Bind errors" + ); + + Ok(()) + }) + .await??; + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_protocol_handles_partial_reads_and_pipelined_simple_queries() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = TcpStream::connect(addr).context("connect raw protocol socket")?; + stream.set_read_timeout(Some(Duration::from_secs(120)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + + write_in_small_chunks(&mut stream, &startup_message(), 3)?; + let startup = read_until_ready(&mut stream).context("read startup response")?; + assert_eq!(startup.last().map(|msg| msg.tag), Some(b'Z')); + + write_in_small_chunks( + &mut stream, + &query_message( + "CREATE TABLE partial_items(value text); + INSERT INTO partial_items(value) VALUES ('alpha'), ('beta'); + SELECT count(*)::int4 AS count FROM partial_items", + ), + 5, + )?; + let first = read_until_ready(&mut stream).context("read split simple-query response")?; + assert!( + first.iter().any(|msg| msg.tag == b'D'), + "split query should return a DataRow" + ); + assert_eq!(first.last().map(|msg| msg.tag), Some(b'Z')); + + write_in_small_chunks( + &mut stream, + &[ + query_message("SELECT value FROM partial_items WHERE value = 'alpha'"), + query_message("SELECT value FROM partial_items WHERE value = 'beta'"), + ] + .concat(), + 4, + )?; + let first_pipelined = + read_until_ready(&mut stream).context("read first pipelined query")?; + let second_pipelined = + read_until_ready(&mut stream).context("read second pipelined query")?; + assert!( + first_pipelined.iter().any(|msg| msg.tag == b'D') + && second_pipelined.iter().any(|msg| msg.tag == b'D'), + "both pipelined simple queries should return rows" + ); + Ok(()) + }) + .await??; + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_copy_from_stdin_streams_through_backend_copy_state() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = TcpStream::connect(addr).context("connect raw protocol socket")?; + stream.set_read_timeout(Some(Duration::from_secs(120)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + + stream.write_all(&startup_message())?; + let startup = read_until_ready(&mut stream).context("read startup response")?; + assert_eq!(startup.last().map(|msg| msg.tag), Some(b'Z')); + + stream.write_all(&query_message("CREATE TABLE copy_items(value text)"))?; + let created = read_until_ready(&mut stream).context("read create table response")?; + assert!(created.iter().any(|msg| msg.tag == b'C')); + + write_in_small_chunks( + &mut stream, + &query_message("COPY copy_items(value) FROM STDIN"), + 3, + )?; + let copy_start = read_until_copy_in_or_ready(&mut stream).context("read COPY response")?; + assert_eq!( + copy_start.first().map(|msg| msg.tag), + Some(b'G'), + "server COPY must enter PostgreSQL's real CopyInResponse state" + ); + stream.write_all(©_data(b"alpha\nbeta\n"))?; + stream.write_all(©_done())?; + stream.flush()?; + let copied = read_until_ready(&mut stream).context("read COPY completion")?; + assert!(copied.iter().any(|msg| msg.tag == b'C')); + + stream.write_all(&query_message( + "SELECT count(*)::int4 AS copied FROM copy_items", + ))?; + let recovered = read_until_ready(&mut stream).context("read recovery query")?; + assert!( + recovered.iter().any(|msg| msg.tag == b'D'), + "connection should remain usable after COPY FROM STDIN" + ); + + Ok(()) + }) + .await??; + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_extended_copy_from_stdin_uses_backend_protocol_pump() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server + .tcp_addr() + .context("temporary TCP server should expose addr")?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = TcpStream::connect(addr).context("connect raw TCP client")?; + stream.set_read_timeout(Some(Duration::from_secs(120)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + stream.write_all(&startup_message())?; + read_until_ready(&mut stream).context("read startup")?; + + stream.write_all(&query_message("CREATE TABLE copy_ext_items(value text)"))?; + read_until_ready(&mut stream).context("create copy_ext_items")?; + + let mut extended = Vec::new(); + extended.extend_from_slice(&parse_statement( + "copy_ext_stmt", + "COPY copy_ext_items(value) FROM STDIN", + )); + extended.extend_from_slice(&bind_statement("", "copy_ext_stmt", &[])); + extended.extend_from_slice(&execute_portal("")); + extended.extend_from_slice(&sync()); + stream.write_all(&extended)?; + stream.flush()?; + + let copy_start = + read_until_copy_in_or_ready(&mut stream).context("read extended COPY response")?; + assert!( + copy_start.iter().any(|msg| msg.tag == b'G'), + "extended COPY must enter PostgreSQL's real CopyInResponse state" + ); + + stream.write_all(©_data(b"gamma\ndelta\n"))?; + stream.write_all(©_done())?; + stream.write_all(&sync())?; + stream.flush()?; + read_until_ready(&mut stream).context("read extended COPY completion")?; + + stream.write_all(&query_message( + "SELECT count(*)::int4 AS copied FROM copy_ext_items", + ))?; + let copied = read_until_ready(&mut stream).context("query extended COPY count")?; + assert!( + copied.iter().any(|msg| msg.tag == b'D'), + "connection should remain usable after extended COPY FROM STDIN" + ); + Ok(()) + }) + .await??; + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_copy_variants_and_copyfail_are_backend_owned() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = TcpStream::connect(addr).context("connect raw TCP client")?; + stream.set_read_timeout(Some(Duration::from_secs(120)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + stream.write_all(&startup_message())?; + read_until_ready(&mut stream).context("read startup")?; + + stream.write_all(&query_message( + "CREATE TABLE copy_csv_items(value text); + CREATE TABLE copy_binary_items(value int4); + CREATE TABLE copy_abort_items(value text)", + ))?; + read_until_ready(&mut stream).context("create copy variant tables")?; + + stream.write_all(&query_message( + "COPY copy_csv_items(value) FROM STDIN WITH (FORMAT csv)", + ))?; + let csv_start = read_until_copy_in_or_ready(&mut stream).context("read csv COPY start")?; + assert_eq!(csv_start.first().map(|msg| msg.tag), Some(b'G')); + stream.write_all(©_data(b"alpha\nbeta\n"))?; + stream.write_all(©_done())?; + read_until_ready(&mut stream).context("finish csv COPY")?; + + stream.write_all(&query_message( + "COPY copy_binary_items(value) FROM STDIN WITH (FORMAT binary)", + ))?; + let binary_start = + read_until_copy_in_or_ready(&mut stream).context("read binary COPY start")?; + assert_eq!(binary_start.first().map(|msg| msg.tag), Some(b'G')); + stream.write_all(©_data(&binary_copy_one_int4(42)))?; + stream.write_all(©_done())?; + read_until_ready(&mut stream).context("finish binary COPY")?; + + stream.write_all(&query_message("COPY copy_abort_items(value) FROM STDIN"))?; + let abort_start = + read_until_copy_in_or_ready(&mut stream).context("read abort COPY start")?; + assert_eq!(abort_start.first().map(|msg| msg.tag), Some(b'G')); + stream.write_all(©_data(b"will_abort\n"))?; + stream.write_all(©_fail("client aborted copy"))?; + let abort = read_until_ready(&mut stream).context("read CopyFail response")?; + assert_eq!(first_error_code(&abort).as_deref(), Some("57014")); + + stream.write_all(&query_message( + "SELECT \ + (SELECT count(*)::int4 FROM copy_csv_items) AS csv_count, \ + (SELECT coalesce(sum(value), 0)::int4 FROM copy_binary_items) AS binary_sum, \ + (SELECT count(*)::int4 FROM copy_abort_items) AS abort_count", + ))?; + let recovered = read_until_ready(&mut stream).context("query COPY variant counts")?; + assert!( + recovered.iter().any(|msg| msg.tag == b'D'), + "connection should remain usable after CSV, binary, and CopyFail paths" + ); + + Ok(()) + }) + .await??; + + server.shutdown()?; + Ok(()) +} + +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_unix_socket_copy_uses_same_protocol_path() -> Result<()> { + let dir = tempfile::TempDir::new().context("create Unix socket tempdir")?; + let socket_path = dir.path().join("pglite.sock"); + let server = PgliteServer::builder() + .temporary() + .unix(&socket_path) + .start()?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = UnixStream::connect(socket_path).context("connect raw Unix client")?; + stream.write_all(&startup_message())?; + read_until_ready(&mut stream).context("read Unix startup")?; + + stream.write_all(&query_message("CREATE TABLE unix_copy_items(value text)"))?; + read_until_ready(&mut stream).context("create unix_copy_items")?; + + stream.write_all(&query_message("COPY unix_copy_items(value) FROM STDIN"))?; + let copy_start = + read_until_copy_in_or_ready(&mut stream).context("read Unix COPY start")?; + assert_eq!(copy_start.first().map(|msg| msg.tag), Some(b'G')); + stream.write_all(©_data(b"one\ntwo\n"))?; + stream.write_all(©_done())?; + read_until_ready(&mut stream).context("finish Unix COPY")?; + + stream.write_all(&query_message( + "SELECT count(*)::int4 AS copied FROM unix_copy_items", + ))?; + let copied = read_until_ready(&mut stream).context("query Unix COPY count")?; + assert!(copied.iter().any(|msg| msg.tag == b'D')); + Ok(()) + }) + .await??; + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn raw_wire_disconnect_during_extended_query_does_not_poison_backend() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut stream = TcpStream::connect(addr).context("connect raw protocol socket")?; + stream.set_read_timeout(Some(Duration::from_secs(120)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + stream.write_all(&startup_message())?; + let startup = read_until_ready(&mut stream).context("read startup response")?; + assert_eq!(startup.last().map(|msg| msg.tag), Some(b'Z')); + + stream.write_all(&parse_statement("will_disconnect", "SELECT $1::int4"))?; + drop(stream); + Ok(()) + }) + .await??; + + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + let row = sqlx::query("SELECT 41::int4 + 1 AS answer") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("answer")?, 42); + conn.close().await?; + server.shutdown()?; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_query_works() -> Result<()> { let server = PgliteServer::temporary_tcp()?; @@ -68,9 +589,713 @@ async fn sqlx_query_works() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_prepared_statement_reuse_works() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) + .await + .context("connect with tokio-postgres")?; + let connection_task = tokio::spawn(connection); + + let statement = client.prepare("SELECT $1::int4 + $2::int4 AS sum").await?; + for value in [1_i32, 10, 40] { + let row = client.query_one(&statement, &[&value, &2_i32]).await?; + assert_eq!(row.get::<_, i32>("sum"), value + 2); + } + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_transaction_error_recovers_after_rollback() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + conn.execute("BEGIN").await?; + let err = sqlx::query("SELECT 10 / $1::int4 AS impossible") + .bind(0_i32) + .fetch_one(&mut conn) + .await + .expect_err("transaction query should fail"); + assert_sqlx_code(&err, "22012"); + + let aborted = sqlx::query("SELECT 1::int4 AS still_aborted") + .fetch_one(&mut conn) + .await + .expect_err("transaction should stay aborted until rollback"); + assert_sqlx_code(&aborted, "25P02"); + + conn.execute("ROLLBACK").await?; + let row = sqlx::query("SELECT 42::int4 AS recovered") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("recovered")?, 42); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_transaction_commit_and_rollback_preserve_state() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + conn.execute("CREATE TABLE sqlx_tx_items(id int PRIMARY KEY, value text)") + .await?; + + { + let mut tx = conn.begin().await?; + sqlx::query("INSERT INTO sqlx_tx_items(id, value) VALUES ($1, $2)") + .bind(1_i32) + .bind("committed") + .execute(&mut *tx) + .await?; + tx.commit().await?; + } + + { + let mut tx = conn.begin().await?; + sqlx::query("INSERT INTO sqlx_tx_items(id, value) VALUES ($1, $2)") + .bind(2_i32) + .bind("rolled back") + .execute(&mut *tx) + .await?; + tx.rollback().await?; + } + + let row = sqlx::query( + "SELECT count(*)::int4 AS count, string_agg(value, ',' ORDER BY id) AS values \ + FROM sqlx_tx_items", + ) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("count")?, 1); + assert_eq!(row.try_get::("values")?, "committed"); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_transaction_commit_rollback_and_error_recovery() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let (mut client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) + .await + .context("connect with tokio-postgres")?; + let connection_task = tokio::spawn(connection); + + client + .batch_execute("CREATE TABLE tokio_tx_items(id int PRIMARY KEY, value text)") + .await?; + + { + let tx = client.transaction().await?; + tx.execute( + "INSERT INTO tokio_tx_items(id, value) VALUES ($1, $2)", + &[&1_i32, &"committed"], + ) + .await?; + tx.commit().await?; + } + + { + let tx = client.transaction().await?; + tx.execute( + "INSERT INTO tokio_tx_items(id, value) VALUES ($1, $2)", + &[&2_i32, &"rolled back"], + ) + .await?; + tx.rollback().await?; + } + + { + let tx = client.transaction().await?; + tx.execute( + "INSERT INTO tokio_tx_items(id, value) VALUES ($1, $2)", + &[&3_i32, &"before failure"], + ) + .await?; + let err = tx + .query_one("SELECT 10 / $1::int4 AS impossible", &[&0_i32]) + .await + .expect_err("transaction query should fail"); + assert_eq!(err.code().map(|code| code.code()), Some("22012")); + let aborted = tx + .query_one("SELECT 1::int4 AS still_aborted", &[]) + .await + .expect_err("transaction should stay aborted until rollback"); + assert_eq!(aborted.code().map(|code| code.code()), Some("25P02")); + tx.rollback().await?; + } + + let row = client + .query_one( + "SELECT count(*)::int4 AS count, string_agg(value, ',' ORDER BY id) AS values \ + FROM tokio_tx_items", + &[], + ) + .await?; + assert_eq!(row.get::<_, i32>("count"), 1); + assert_eq!(row.get::<_, String>("values"), "committed"); + + let row = client + .query_one("SELECT 43::int4 AS recovered_after_tx_error", &[]) + .await?; + assert_eq!(row.get::<_, i32>("recovered_after_tx_error"), 43); + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_extended_query_errors_recover_after_sync() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + let parse_err = sqlx::query("SELECT missing FROM missing_table WHERE id = $1") + .bind(7_i32) + .fetch_one(&mut conn) + .await + .expect_err("undefined table should fail during extended-query parse"); + assert_sqlx_code(&parse_err, "42P01"); + let row = sqlx::query("SELECT 21::int4 AS recovered_after_parse") + .fetch_one(&mut conn) + .await + .context("query after parse error")?; + assert_eq!(row.try_get::("recovered_after_parse")?, 21); + + let execute_err = sqlx::query("SELECT 10 / $1::int4 AS impossible") + .bind(0_i32) + .fetch_one(&mut conn) + .await + .expect_err("division by zero should fail during extended-query execute"); + assert_sqlx_code(&execute_err, "22012"); + let row = sqlx::query("SELECT 22::int4 AS recovered_after_execute") + .fetch_one(&mut conn) + .await + .context("query after execute error")?; + assert_eq!(row.try_get::("recovered_after_execute")?, 22); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_simple_query_timezone_errors_recover() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + let timezone = sqlx::query("SELECT current_setting('TimeZone') AS timezone") + .fetch_one(&mut conn) + .await + .context("read default timezone")?; + assert_eq!(timezone.try_get::("timezone")?, "UTC"); + + conn.execute("SET TIME ZONE 'America/New_York'") + .await + .context("set named timezone")?; + let row = sqlx::query( + "SELECT current_setting('TimeZone') AS timezone, \ + count(*)::int4 AS matching_zones \ + FROM pg_timezone_names \ + WHERE name = 'America/New_York' \ + GROUP BY 1", + ) + .fetch_one(&mut conn) + .await + .context("query timezone catalog")?; + assert_eq!(row.try_get::("timezone")?, "America/New_York"); + assert_eq!(row.try_get::("matching_zones")?, 1); + + conn.execute("SET TIME ZONE 'Missing/Zone'") + .await + .expect_err("invalid timezone should fail"); + let row = sqlx::query("SELECT 24::int4 AS recovered_after_timezone_error") + .fetch_one(&mut conn) + .await + .context("query after invalid timezone")?; + assert_eq!(row.try_get::("recovered_after_timezone_error")?, 24); + + conn.close().await?; + + let mut next_conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("reconnect with SQLx")?; + let row = sqlx::query("SELECT current_setting('TimeZone') AS timezone") + .fetch_one(&mut next_conn) + .await + .context("read timezone after connection cleanup")?; + assert_eq!(row.try_get::("timezone")?, "UTC"); + next_conn.close().await?; + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_server_startup_postgres_config_uses_real_guc_handling() -> Result<()> { + let server = PgliteServer::builder() + .temporary() + .postgres_config("synchronous_commit", "off") + .postgres_config("work_mem", "8MB") + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + let row = sqlx::query( + "SELECT current_setting('synchronous_commit') AS sync_commit, \ + current_setting('work_mem') AS work_mem", + ) + .fetch_one(&mut conn) + .await + .context("read configured startup GUCs")?; + assert_eq!(row.try_get::("sync_commit")?, "off"); + assert_eq!(row.try_get::("work_mem")?, "8MB"); + + conn.execute("BEGIN").await?; + conn.execute("SET LOCAL synchronous_commit = on").await?; + let row = sqlx::query("SELECT current_setting('synchronous_commit') AS sync_commit") + .fetch_one(&mut conn) + .await + .context("read SET LOCAL startup GUC override")?; + assert_eq!(row.try_get::("sync_commit")?, "on"); + conn.execute("COMMIT").await?; + let row = sqlx::query("SELECT current_setting('synchronous_commit') AS sync_commit") + .fetch_one(&mut conn) + .await + .context("read startup GUC after SET LOCAL scope")?; + assert_eq!(row.try_get::("sync_commit")?, "off"); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_server_relaxed_durability_is_idempotent_and_user_config_wins() -> Result<()> { + let server = PgliteServer::builder() + .temporary() + .relaxed_durability(true) + .relaxed_durability(false) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("connect to disabled relaxed durability server")?; + let row = sqlx::query("SELECT current_setting('synchronous_commit') AS sync_commit") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("sync_commit")?, "on"); + conn.close().await?; + server.shutdown()?; + + let server = PgliteServer::builder() + .temporary() + .relaxed_durability(true) + .postgres_config("synchronous_commit", "on") + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("connect to overridden relaxed durability server")?; + let row = sqlx::query("SELECT current_setting('synchronous_commit') AS sync_commit") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("sync_commit")?, "on"); + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_server_startup_identity_can_select_existing_user_and_database() -> Result<()> { + let root = tempfile::TempDir::new()?; + let seed_root = root.path().to_path_buf(); + let seed_task_root = seed_root.clone(); + tokio::task::spawn_blocking(move || -> Result<()> { + let mut admin = Pglite::builder().path(seed_task_root).open()?; + admin.exec("CREATE ROLE server_user LOGIN", None)?; + admin.exec("CREATE DATABASE server_db OWNER server_user", None)?; + admin.close()?; + Ok(()) + }) + .await + .context("join startup identity seed task")??; + + let server = PgliteServer::builder() + .path(seed_root) + .username("server_user") + .database("server_db") + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; + + let row = + sqlx::query("SELECT current_user AS current_user, current_database() AS current_database") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("current_user")?, "server_user"); + assert_eq!(row.try_get::("current_database")?, "server_db"); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_startup_options_are_forwarded_to_postgres() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + let mut config = tokio_postgres::Config::new(); + config + .host(addr.ip().to_string()) + .port(addr.port()) + .user("postgres") + .dbname("template1") + .options("-c synchronous_commit=off -c work_mem=8MB"); + let (client, connection) = config + .connect(NoTls) + .await + .context("connect with tokio-postgres startup options")?; + let connection_task = tokio::spawn(connection); + + let row = client + .query_one( + "SELECT current_setting('synchronous_commit'), current_setting('work_mem')", + &[], + ) + .await + .context("read startup option GUCs")?; + assert_eq!(row.get::<_, String>(0), "off"); + assert_eq!(row.get::<_, String>(1), "8MB"); + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn postgres_control_packets_are_handled_safely() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + let ssl_response = tokio::task::spawn_blocking(move || -> Result { + let mut stream = TcpStream::connect(addr).context("connect raw SSLRequest socket")?; + stream + .write_all(&startup_control_packet(SSL_REQUEST_CODE, &[])) + .context("write SSLRequest")?; + let mut response = [0u8; 1]; + stream + .read_exact(&mut response) + .context("read SSLRequest response")?; + Ok(response[0]) + }) + .await??; + assert_eq!(ssl_response, b'N'); + + let cancel_closed = tokio::task::spawn_blocking(move || -> Result { + let mut stream = TcpStream::connect(addr).context("connect raw CancelRequest socket")?; + stream + .write_all(&startup_control_packet( + CANCEL_REQUEST_CODE, + &[0, 0, 0, 1, 0, 0, 0, 2], + )) + .context("write CancelRequest")?; + let mut response = [0u8; 1]; + let read = stream + .read(&mut response) + .context("read CancelRequest close")?; + Ok(read == 0) + }) + .await??; + assert!( + cancel_closed, + "CancelRequest should close without backend panic" + ); + + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn postgres_startup_identity_is_delegated_to_postgres() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let addr = server.tcp_addr().context("server should use TCP")?; + + let bad_user = tokio::task::spawn_blocking(move || -> Result> { + let mut stream = TcpStream::connect(addr).context("connect raw startup socket")?; + stream + .write_all(&startup_message_with(&[ + ("user", "alice"), + ("database", "template1"), + ])) + .context("write unknown user startup message")?; + read_startup_error_sqlstate(&mut stream).context("read unknown user response") + }) + .await??; + assert_eq!(bad_user.as_deref(), Some("22023")); + + let bad_database = tokio::task::spawn_blocking(move || -> Result> { + let mut stream = TcpStream::connect(addr).context("connect raw startup socket")?; + stream + .write_all(&startup_message_with(&[ + ("user", "postgres"), + ("database", "no_such_database"), + ])) + .context("write unknown database startup message")?; + read_startup_error_sqlstate(&mut stream).context("read unknown database response") + }) + .await??; + assert_eq!(bad_database.as_deref(), Some("3D000")); + + server.shutdown()?; + Ok(()) +} + async fn wait_for_tokio_postgres( connection_task: tokio::task::JoinHandle>, ) -> Result<()> { timeout(Duration::from_secs(5), connection_task).await???; Ok(()) } + +fn assert_sqlx_code(err: &sqlx::Error, expected: &str) { + let code = err + .as_database_error() + .and_then(|db| db.code()) + .map(|code| code.into_owned()); + assert_eq!(code.as_deref(), Some(expected)); +} + +fn startup_control_packet(code: i32, tail: &[u8]) -> Vec { + let len = 8 + tail.len() as i32; + let mut packet = Vec::with_capacity(len as usize); + packet.extend_from_slice(&len.to_be_bytes()); + packet.extend_from_slice(&code.to_be_bytes()); + packet.extend_from_slice(tail); + packet +} + +#[derive(Debug)] +struct RawBackendMessage { + tag: u8, + body: Vec, +} + +fn startup_message() -> Vec { + startup_message_with(&[ + ("user", "postgres"), + ("database", "template1"), + ("client_encoding", "UTF8"), + ]) +} + +fn startup_message_with(params: &[(&str, &str)]) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&3_i16.to_be_bytes()); + body.extend_from_slice(&0_i16.to_be_bytes()); + for (key, value) in params { + add_cstring(&mut body, key); + add_cstring(&mut body, value); + } + add_cstring(&mut body, ""); + + let mut packet = Vec::with_capacity(body.len() + 4); + packet.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); + packet.extend_from_slice(&body); + packet +} + +fn query_message(sql: &str) -> Vec { + let mut body = Vec::new(); + add_cstring(&mut body, sql); + tagged_message(b'Q', body) +} + +fn parse_statement(name: &str, sql: &str) -> Vec { + let mut body = Vec::new(); + add_cstring(&mut body, name); + add_cstring(&mut body, sql); + body.extend_from_slice(&0_i16.to_be_bytes()); + tagged_message(b'P', body) +} + +fn bind_statement(portal: &str, statement: &str, values: &[&str]) -> Vec { + let mut body = Vec::new(); + add_cstring(&mut body, portal); + add_cstring(&mut body, statement); + body.extend_from_slice(&0_i16.to_be_bytes()); + body.extend_from_slice(&(values.len() as i16).to_be_bytes()); + for value in values { + body.extend_from_slice(&(value.len() as i32).to_be_bytes()); + body.extend_from_slice(value.as_bytes()); + } + body.extend_from_slice(&0_i16.to_be_bytes()); + tagged_message(b'B', body) +} + +fn describe_portal(portal: &str) -> Vec { + let mut body = Vec::new(); + body.push(b'P'); + add_cstring(&mut body, portal); + tagged_message(b'D', body) +} + +fn execute_portal(portal: &str) -> Vec { + let mut body = Vec::new(); + add_cstring(&mut body, portal); + body.extend_from_slice(&0_i32.to_be_bytes()); + tagged_message(b'E', body) +} + +fn sync() -> Vec { + tagged_message(b'S', Vec::new()) +} + +fn copy_data(bytes: &[u8]) -> Vec { + tagged_message(b'd', bytes.to_vec()) +} + +fn copy_done() -> Vec { + tagged_message(b'c', Vec::new()) +} + +fn copy_fail(message: &str) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(message.as_bytes()); + body.push(0); + tagged_message(b'f', body) +} + +fn binary_copy_one_int4(value: i32) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(b"PGCOPY\n\xff\r\n\0"); + data.extend_from_slice(&0_i32.to_be_bytes()); + data.extend_from_slice(&0_i32.to_be_bytes()); + data.extend_from_slice(&1_i16.to_be_bytes()); + data.extend_from_slice(&4_i32.to_be_bytes()); + data.extend_from_slice(&value.to_be_bytes()); + data.extend_from_slice(&(-1_i16).to_be_bytes()); + data +} + +fn tagged_message(tag: u8, body: Vec) -> Vec { + let mut packet = Vec::with_capacity(body.len() + 5); + packet.push(tag); + packet.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); + packet.extend_from_slice(&body); + packet +} + +fn add_cstring(buffer: &mut Vec, value: &str) { + buffer.extend_from_slice(value.as_bytes()); + buffer.push(0); +} + +fn read_until_ready(stream: &mut impl Read) -> Result> { + let mut messages = Vec::new(); + loop { + let message = read_one_message(stream)?; + let ready = message.tag == b'Z'; + messages.push(message); + if ready { + return Ok(messages); + } + } +} + +fn read_until_copy_in_or_ready(stream: &mut impl Read) -> Result> { + let mut messages = Vec::new(); + loop { + let message = read_one_message(stream)?; + let done = matches!(message.tag, b'G' | b'Z'); + messages.push(message); + if done { + return Ok(messages); + } + } +} + +fn write_in_small_chunks(stream: &mut impl Write, bytes: &[u8], chunk_size: usize) -> Result<()> { + for chunk in bytes.chunks(chunk_size.max(1)) { + stream.write_all(chunk).context("write protocol chunk")?; + } + stream.flush().context("flush protocol chunks") +} + +fn read_one_message(stream: &mut impl Read) -> Result { + let mut header = [0u8; 5]; + stream + .read_exact(&mut header) + .context("read backend message header")?; + let len = i32::from_be_bytes([header[1], header[2], header[3], header[4]]); + anyhow::ensure!(len >= 4, "invalid backend message length {len}"); + let body_len = (len - 4) as usize; + let mut body = vec![0u8; body_len]; + stream + .read_exact(&mut body) + .context("read backend message body")?; + Ok(RawBackendMessage { + tag: header[0], + body, + }) +} + +fn read_startup_error_sqlstate(stream: &mut impl Read) -> Result> { + loop { + let message = read_one_message(stream)?; + if message.tag == b'E' { + return Ok(error_sqlstate(&message)); + } + } +} + +fn assert_message_tags_ignoring_parameter_status(messages: &[RawBackendMessage], expected: &[u8]) { + let actual = messages + .iter() + .filter_map(|msg| (msg.tag != b'S').then_some(msg.tag)) + .collect::>(); + assert_eq!(actual, expected); +} + +fn first_error_code(messages: &[RawBackendMessage]) -> Option { + messages + .iter() + .find(|msg| msg.tag == b'E') + .and_then(error_sqlstate) +} + +fn error_sqlstate(message: &RawBackendMessage) -> Option { + let mut cursor = 0usize; + while cursor < message.body.len() { + let field = message.body[cursor]; + cursor += 1; + if field == 0 { + break; + } + let end = message.body[cursor..] + .iter() + .position(|byte| *byte == 0) + .map(|offset| cursor + offset)?; + if field == b'C' { + return Some(String::from_utf8_lossy(&message.body[cursor..end]).into_owned()); + } + cursor = end + 1; + } + None +} diff --git a/tests/extensions_smoke.rs b/tests/extensions_smoke.rs new file mode 100644 index 00000000..b90012ca --- /dev/null +++ b/tests/extensions_smoke.rs @@ -0,0 +1,789 @@ +#![cfg(feature = "extensions")] + +use anyhow::Result; +use pglite_oxide::{Pglite, PgliteError, PgliteServer, extensions}; +use serde_json::json; +use sqlx::{Connection, Row}; +use std::path::{Path, PathBuf}; + +struct TestTrace { + name: &'static str, +} + +impl TestTrace { + fn new(name: &'static str) -> Self { + eprintln!("extensions_smoke::{name} start"); + Self { name } + } +} + +impl Drop for TestTrace { + fn drop(&mut self) { + eprintln!("extensions_smoke::{} end", self.name); + } +} + +fn trace_expected(label: &str) { + eprintln!("extensions_smoke::expected_sql_error exercising {label}"); +} + +fn first_f64(result: &pglite_oxide::Results, column: &str) -> f64 { + result.rows[0][column].as_f64().expect("floating result") +} + +fn assert_pglite_code(err: &anyhow::Error, expected_code: &str, message_contains: &str) { + let pg_err = err + .downcast_ref::() + .expect("error should preserve Postgres fields"); + assert_eq!(pg_err.database_error().code.as_deref(), Some(expected_code)); + assert!( + pg_err.database_error().message.contains(message_contains), + "expected error message to contain {message_contains:?}, got {:?}", + pg_err.database_error().message + ); +} + +fn assert_sqlx_code(err: &sqlx::Error, expected_code: &str) { + assert_eq!( + err.as_database_error().and_then(|db| db.code()).as_deref(), + Some(expected_code) + ); +} + +fn assert_only_requested_extension_assets_are_materialized( + root: &Path, + requested: &str, + unrequested: &str, +) { + let runtime = root.join("tmp/pglite"); + assert!( + runtime + .join(format!("lib/postgresql/{requested}.so")) + .is_file(), + "requested extension side module should be materialized in the upper runtime layer" + ); + assert!( + runtime + .join(format!("share/postgresql/extension/{requested}.control")) + .is_file(), + "requested extension control file should be materialized in the upper runtime layer" + ); + assert!( + !runtime + .join(format!("lib/postgresql/{unrequested}.so")) + .exists(), + "unrequested extension side module should not be materialized" + ); + let lib_files = relative_files(&runtime.join("lib/postgresql")); + assert_eq!( + lib_files, + vec![PathBuf::from(format!("{requested}.so"))], + "upper runtime library layer should contain only the requested extension side module" + ); + let share_files = relative_files(&runtime.join("share/postgresql")); + assert!( + share_files.iter().all(|path| { + path.parent() == Some(Path::new("extension")) + && path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| { + name == format!("{requested}.control") + || name == format!("{requested}.sql") + || name.starts_with(&format!("{requested}--")) + }) + }), + "upper runtime share layer should contain only requested extension metadata, got {share_files:?}" + ); + assert!( + !runtime.join("bin").exists(), + "core binaries should stay in the lower cached runtime" + ); + assert!( + !runtime.join("lib/postgresql/plpgsql.so").exists(), + "core runtime side modules should stay in the lower cached runtime" + ); + assert!( + !runtime.join("share/postgresql/postgres.bki").exists(), + "core catalog files should stay in the lower cached runtime" + ); +} + +fn relative_files(root: &Path) -> Vec { + fn walk(base: &Path, current: &Path, files: &mut Vec) { + if !current.exists() { + return; + } + for entry in std::fs::read_dir(current).expect("read runtime test directory") { + let entry = entry.expect("read runtime test directory entry"); + let path = entry.path(); + if path.is_dir() { + walk(base, &path, files); + } else if path.is_file() { + files.push( + path.strip_prefix(base) + .expect("relative file") + .to_path_buf(), + ); + } + } + } + + let mut files = Vec::new(); + walk(root, root, &mut files); + files.sort(); + files +} + +#[test] +fn vector_extension_direct_smoke() -> Result<()> { + let _trace = TestTrace::new("vector_extension_direct_smoke"); + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + + db.exec("CREATE TEMP TABLE oxide_vec (embedding vector(3))", None)?; + db.exec("INSERT INTO oxide_vec VALUES ('[1,2,3]')", None)?; + let result = db.query( + "SELECT embedding <-> '[1,2,4]'::vector AS distance FROM oxide_vec", + &[], + None, + )?; + assert_eq!(first_f64(&result, "distance"), 1.0); + + let version = db.query( + "SELECT extversion, n.nspname AS schema_name \ + FROM pg_extension e \ + JOIN pg_namespace n ON n.oid = e.extnamespace \ + WHERE e.extname = 'vector'", + &[], + None, + )?; + let extversion = version.rows[0]["extversion"] + .as_str() + .expect("vector extversion"); + assert!(!extversion.is_empty()); + assert_eq!(version.rows[0]["schema_name"], json!("pg_catalog")); + + trace_expected("vector_direct division-by-zero"); + let err = db + .query( + "SELECT 10 / $1::int4 AS impossible_after_vector", + &[serde_json::json!(0)], + None, + ) + .expect_err("division by zero after vector load should fail"); + assert_pglite_code(&err, "22012", "division by zero"); + let recovered = db.query("SELECT 13::int AS recovered_after_vector_error", &[], None)?; + assert_eq!(recovered.rows[0]["recovered_after_vector_error"], json!(13)); + + trace_expected("vector_direct invalid-vector-literal"); + let invalid_vector = db + .query( + "SELECT $1::vector AS embedding", + &[json!("[hello,1]")], + None, + ) + .expect_err("invalid vector literal should fail inside the vector extension"); + assert_pglite_code( + &invalid_vector, + "22P02", + "invalid input syntax for type vector", + ); + let recovered = db.query( + "SELECT 15::int AS recovered_after_invalid_vector", + &[], + None, + )?; + assert_eq!( + recovered.rows[0]["recovered_after_invalid_vector"], + json!(15) + ); + + trace_expected("vector_direct dimension-mismatch"); + let dimension_mismatch = db + .query( + "SELECT $1::vector <-> $2::vector AS distance", + &[json!("[1,2]"), json!("[3]")], + None, + ) + .expect_err("vector distance should reject mismatched dimensions"); + assert_pglite_code(&dimension_mismatch, "22000", "different vector dimensions"); + let recovered = db.query( + "SELECT 16::int AS recovered_after_dimension_mismatch", + &[], + None, + )?; + assert_eq!( + recovered.rows[0]["recovered_after_dimension_mismatch"], + json!(16) + ); + + db.close()?; + Ok(()) +} + +#[test] +fn pure_mountfs_materializes_only_requested_extension_assets() -> Result<()> { + let _trace = TestTrace::new("pure_mountfs_materializes_only_requested_extension_assets"); + let root = tempfile::TempDir::new()?; + { + let mut db = Pglite::builder() + .path(root.path()) + .extension(extensions::VECTOR) + .open()?; + let result = db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + assert_eq!(first_f64(&result, "distance"), 1.0); + db.close()?; + } + + assert_only_requested_extension_assets_are_materialized(root.path(), "vector", "pg_trgm"); + Ok(()) +} + +#[test] +fn vector_extension_ports_pgvector_core_type_cases() -> Result<()> { + let _trace = TestTrace::new("vector_extension_ports_pgvector_core_type_cases"); + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + + let valid = db.query( + "SELECT \ + '[1,2,3]'::vector::text AS vector_text, \ + vector_dims('[1,2,3]'::vector)::int AS dims, \ + l2_distance('[0,0]'::vector, '[3,4]'::vector)::float8 AS distance", + &[], + None, + )?; + assert_eq!(valid.rows[0]["vector_text"], json!("[1,2,3]")); + assert_eq!(valid.rows[0]["dims"], json!(3)); + assert_eq!(first_f64(&valid, "distance"), 5.0); + + for (sql, code, message) in [ + ( + "SELECT '[hello,1]'::vector", + "22P02", + "invalid input syntax for type vector", + ), + ("SELECT '[NaN,1]'::vector", "22000", "NaN not allowed"), + ( + "SELECT '[1,2,3]'::vector(2)", + "22000", + "expected 2 dimensions, not 3", + ), + ( + "SELECT '[1,2]'::vector <-> '[3]'::vector", + "22000", + "different vector dimensions", + ), + ] { + trace_expected(&format!("vector_core_type_cases {sql}")); + let err = match db.query(sql, &[], None) { + Ok(_) => panic!("{sql} should fail"), + Err(err) => err, + }; + assert_pglite_code(&err, code, message); + let recovered = db.query("SELECT 17::int AS recovered", &[], None)?; + assert_eq!(recovered.rows[0]["recovered"], json!(17)); + } + + db.close()?; + Ok(()) +} + +#[test] +fn vector_extension_direct_transaction_commit_rollback_and_error_recovery() -> Result<()> { + let _trace = + TestTrace::new("vector_extension_direct_transaction_commit_rollback_and_error_recovery"); + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.exec( + "CREATE TABLE vector_tx_items(id int PRIMARY KEY, embedding vector(3))", + None, + )?; + + db.transaction(|tx| { + tx.query( + "INSERT INTO vector_tx_items(id, embedding) VALUES ($1, $2::vector) \ + RETURNING embedding <-> '[1,2,4]'::vector AS distance", + &[json!(1), json!("[1,2,3]")], + None, + )?; + Ok::<_, anyhow::Error>(()) + })?; + + let rollback: anyhow::Result<()> = db.transaction(|tx| { + tx.query( + "INSERT INTO vector_tx_items(id, embedding) VALUES ($1, $2::vector)", + &[json!(2), json!("[9,9,9]")], + None, + )?; + Err(anyhow::anyhow!("force vector rollback")) + }); + assert!(rollback.is_err()); + + trace_expected("vector_direct_transaction invalid-vector-literal"); + let failed: anyhow::Result<()> = db.transaction(|tx| { + tx.query( + "INSERT INTO vector_tx_items(id, embedding) VALUES ($1, $2::vector)", + &[json!(3), json!("[3,3,3]")], + None, + )?; + tx.query( + "SELECT $1::vector AS embedding", + &[json!("[hello,1]")], + None, + )?; + Ok(()) + }); + let failed = failed.expect_err("invalid vector should fail inside transaction"); + assert_pglite_code(&failed, "22P02", "invalid input syntax for type vector"); + + let result = db.query( + "SELECT count(*)::int AS count, \ + min(embedding <-> '[1,2,4]'::vector)::float8 AS distance \ + FROM vector_tx_items", + &[], + None, + )?; + assert_eq!(result.rows[0]["count"], json!(1)); + assert_eq!(first_f64(&result, "distance"), 1.0); + + let recovered = db.query("SELECT 44::int AS recovered_after_vector_tx", &[], None)?; + assert_eq!(recovered.rows[0]["recovered_after_vector_tx"], json!(44)); + + db.close()?; + Ok(()) +} + +#[test] +fn vector_extension_install_is_demand_driven_idempotent_and_persistent() -> Result<()> { + let _trace = + TestTrace::new("vector_extension_install_is_demand_driven_idempotent_and_persistent"); + let root = tempfile::TempDir::new()?; + { + let mut db = Pglite::builder().path(root.path()).open()?; + assert!( + !db.paths() + .pgroot + .join("pglite") + .join("lib/postgresql/vector.so") + .exists(), + "vector side module should not be installed before it is requested" + ); + + db.enable_extension(extensions::VECTOR)?; + db.enable_extension(extensions::VECTOR)?; + assert!( + db.paths() + .pgroot + .join("pglite") + .join("lib/postgresql/vector.so") + .exists(), + "vector side module should be installed after enable_extension" + ); + + let installed = db.query( + "SELECT count(*)::int AS count FROM pg_extension WHERE extname = 'vector'", + &[], + None, + )?; + assert_eq!(installed.rows[0]["count"], json!(1)); + db.close()?; + } + + { + let mut reopened = Pglite::builder().path(root.path()).open()?; + let result = reopened.query("SELECT '[1,2,3]'::vector::text AS value", &[], None)?; + assert_eq!(result.rows[0]["value"], json!("[1,2,3]")); + reopened.close()?; + } + + Ok(()) +} + +#[test] +fn pg_trgm_extension_direct_smoke() -> Result<()> { + let _trace = TestTrace::new("pg_trgm_extension_direct_smoke"); + let mut db = Pglite::builder() + .temporary() + .extension(extensions::PG_TRGM) + .open()?; + + let result = db.query( + "SELECT similarity('postgres', 'postgrex') AS score", + &[], + None, + )?; + assert!(first_f64(&result, "score") > 0.5); + + let installed = db.query( + "SELECT count(*)::int AS count, max(n.nspname) AS schema_name \ + FROM pg_extension e \ + JOIN pg_namespace n ON n.oid = e.extnamespace \ + WHERE e.extname = 'pg_trgm'", + &[], + None, + )?; + assert_eq!(installed.rows[0]["count"], json!(1)); + assert_eq!(installed.rows[0]["schema_name"], json!("pg_catalog")); + + db.close()?; + Ok(()) +} + +#[test] +fn hstore_extension_direct_smoke() -> Result<()> { + let _trace = TestTrace::new("hstore_extension_direct_smoke"); + let mut db = Pglite::builder() + .temporary() + .extension(extensions::HSTORE) + .open()?; + + db.exec( + "CREATE TEMP TABLE oxide_hstore (id serial PRIMARY KEY, data hstore)", + None, + )?; + db.exec( + "INSERT INTO oxide_hstore (data) VALUES ('\"name\"=>\"test1\"'), ('\"name\"=>\"test2\"')", + None, + )?; + let result = db.query( + "SELECT data::jsonb AS data FROM oxide_hstore WHERE data -> 'name' = 'test1'", + &[], + None, + )?; + assert_eq!(result.rows[0]["data"], json!({"name": "test1"})); + + let installed = db.query( + "SELECT count(*)::int AS count, max(n.nspname) AS schema_name \ + FROM pg_extension e \ + JOIN pg_namespace n ON n.oid = e.extnamespace \ + WHERE e.extname = 'hstore'", + &[], + None, + )?; + assert_eq!(installed.rows[0]["count"], json!(1)); + assert_eq!(installed.rows[0]["schema_name"], json!("pg_catalog")); + + db.close()?; + Ok(()) +} + +#[test] +fn hstore_extension_reopens_cleanly() -> Result<()> { + let _trace = TestTrace::new("hstore_extension_reopens_cleanly"); + let root = tempfile::TempDir::new()?; + { + let mut db = Pglite::builder() + .path(root.path()) + .extension(extensions::HSTORE) + .open()?; + db.exec("CREATE TABLE oxide_hstore_restart (data hstore)", None)?; + db.exec( + "INSERT INTO oxide_hstore_restart VALUES ('\"name\"=>\"persisted\"')", + None, + )?; + db.close()?; + } + + { + let mut reopened = Pglite::builder().path(root.path()).open()?; + let result = reopened.query( + "SELECT data -> 'name' AS name FROM oxide_hstore_restart", + &[], + None, + )?; + assert_eq!(result.rows[0]["name"], json!("persisted")); + reopened.close()?; + } + + Ok(()) +} + +#[test] +fn multiple_extension_set_direct_smoke() -> Result<()> { + let _trace = TestTrace::new("multiple_extension_set_direct_smoke"); + let mut db = Pglite::builder() + .temporary() + .extensions([extensions::VECTOR, extensions::PG_TRGM]) + .open()?; + + let result = db.query( + "SELECT \ + '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance, \ + similarity('postgres', 'postgrex') AS score", + &[], + None, + )?; + assert_eq!(first_f64(&result, "distance"), 1.0); + assert!(first_f64(&result, "score") > 0.5); + + let installed = db.query( + "SELECT count(*)::int AS count \ + FROM pg_extension \ + WHERE extname IN ('vector', 'pg_trgm')", + &[], + None, + )?; + assert_eq!(installed.rows[0]["count"], json!(2)); + + db.close()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn pg_trgm_extension_server_sqlx_smoke() -> Result<()> { + let _trace = TestTrace::new("pg_trgm_extension_server_sqlx_smoke"); + let server = PgliteServer::builder() + .temporary() + .extension(extensions::PG_TRGM) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + + let row = sqlx::query("SELECT similarity('postgres', 'postgrex')::float8 AS score") + .fetch_one(&mut conn) + .await?; + assert!(row.try_get::("score")? > 0.5); + + let row = + sqlx::query("SELECT count(*)::int4 AS count FROM pg_extension WHERE extname = 'pg_trgm'") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("count")?, 1); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn hstore_extension_server_sqlx_smoke() -> Result<()> { + let _trace = TestTrace::new("hstore_extension_server_sqlx_smoke"); + let server = PgliteServer::builder() + .temporary() + .extension(extensions::HSTORE) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + + sqlx::query("CREATE TEMP TABLE oxide_hstore_sqlx (data hstore)") + .execute(&mut conn) + .await?; + sqlx::query("INSERT INTO oxide_hstore_sqlx VALUES ('\"name\"=>\"test1\"')") + .execute(&mut conn) + .await?; + let row = sqlx::query( + "SELECT data -> 'name' AS name, \ + (SELECT count(*)::int4 FROM pg_extension WHERE extname = 'hstore') AS count \ + FROM oxide_hstore_sqlx", + ) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("name")?, "test1"); + assert_eq!(row.try_get::("count")?, 1); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn vector_extension_server_sqlx_smoke() -> Result<()> { + let _trace = TestTrace::new("vector_extension_server_sqlx_smoke"); + let server = PgliteServer::builder() + .temporary() + .extension(extensions::VECTOR) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + + sqlx::query("CREATE TABLE oxide_vec_server (embedding vector(3))") + .execute(&mut conn) + .await?; + sqlx::query("INSERT INTO oxide_vec_server VALUES ('[1,2,3]')") + .execute(&mut conn) + .await?; + let row = + sqlx::query("SELECT embedding <-> '[1,2,4]'::vector AS distance FROM oxide_vec_server") + .fetch_one(&mut conn) + .await?; + + assert_eq!(row.try_get::("distance")?, 1.0); + + trace_expected("vector_server_sqlx division-by-zero"); + let err = sqlx::query("SELECT 10 / $1::int4 AS impossible_after_vector") + .bind(0_i32) + .fetch_one(&mut conn) + .await + .expect_err("division by zero after vector load should fail"); + assert_sqlx_code(&err, "22012"); + let row = sqlx::query("SELECT 14::int4 AS recovered_after_vector_error") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("recovered_after_vector_error")?, 14); + + trace_expected("vector_server_sqlx invalid-vector-literal"); + let err = sqlx::query("SELECT $1::text::vector AS embedding") + .bind("[hello,1]") + .fetch_one(&mut conn) + .await + .expect_err("invalid vector input through SQLx should fail in the vector extension"); + assert_sqlx_code(&err, "22P02"); + let row = sqlx::query("SELECT 18::int4 AS recovered_after_invalid_vector") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("recovered_after_invalid_vector")?, 18); + + trace_expected("vector_server_sqlx dimension-mismatch"); + let err = sqlx::query("SELECT $1::text::vector <-> $2::text::vector AS distance") + .bind("[1,2]") + .bind("[3]") + .fetch_one(&mut conn) + .await + .expect_err("vector distance should reject mismatched dimensions through SQLx"); + assert_sqlx_code(&err, "22000"); + let row = sqlx::query("SELECT 19::int4 AS recovered_after_dimension_mismatch") + .fetch_one(&mut conn) + .await?; + assert_eq!( + row.try_get::("recovered_after_dimension_mismatch")?, + 19 + ); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn vector_extension_server_sqlx_transaction_commit_rollback_and_error_recovery() -> Result<()> +{ + let _trace = TestTrace::new( + "vector_extension_server_sqlx_transaction_commit_rollback_and_error_recovery", + ); + let server = PgliteServer::builder() + .temporary() + .extension(extensions::VECTOR) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + + sqlx::query("CREATE TABLE vector_server_tx_items(id int PRIMARY KEY, embedding vector(3))") + .execute(&mut conn) + .await?; + + { + let mut tx = conn.begin().await?; + sqlx::query( + "INSERT INTO vector_server_tx_items(id, embedding) VALUES ($1, $2::text::vector)", + ) + .bind(1_i32) + .bind("[1,2,3]") + .execute(&mut *tx) + .await?; + let row = sqlx::query( + "SELECT embedding <-> '[1,2,4]'::vector AS distance \ + FROM vector_server_tx_items WHERE id = 1", + ) + .fetch_one(&mut *tx) + .await?; + assert_eq!(row.try_get::("distance")?, 1.0); + tx.commit().await?; + } + + { + let mut tx = conn.begin().await?; + sqlx::query( + "INSERT INTO vector_server_tx_items(id, embedding) VALUES ($1, $2::text::vector)", + ) + .bind(2_i32) + .bind("[9,9,9]") + .execute(&mut *tx) + .await?; + tx.rollback().await?; + } + + { + let mut tx = conn.begin().await?; + sqlx::query( + "INSERT INTO vector_server_tx_items(id, embedding) VALUES ($1, $2::text::vector)", + ) + .bind(3_i32) + .bind("[3,3,3]") + .execute(&mut *tx) + .await?; + trace_expected("vector_server_sqlx_transaction invalid-vector-literal"); + let err = sqlx::query("SELECT $1::text::vector AS embedding") + .bind("[hello,1]") + .fetch_one(&mut *tx) + .await + .expect_err("invalid vector should fail inside SQLx transaction"); + assert_sqlx_code(&err, "22P02"); + trace_expected("vector_server_sqlx_transaction still-aborted"); + let aborted = sqlx::query("SELECT 1::int4 AS still_aborted") + .fetch_one(&mut *tx) + .await + .expect_err("transaction should stay aborted after vector failure"); + assert_sqlx_code(&aborted, "25P02"); + tx.rollback().await?; + } + + let row = sqlx::query( + "SELECT count(*)::int4 AS count, \ + min(embedding <-> '[1,2,4]'::vector)::float8 AS distance \ + FROM vector_server_tx_items", + ) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("count")?, 1); + assert_eq!(row.try_get::("distance")?, 1.0); + + let row = sqlx::query("SELECT 45::int4 AS recovered_after_vector_tx") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("recovered_after_vector_tx")?, 45); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn multiple_extension_set_server_sqlx_smoke() -> Result<()> { + let _trace = TestTrace::new("multiple_extension_set_server_sqlx_smoke"); + let server = PgliteServer::builder() + .temporary() + .extensions([extensions::VECTOR, extensions::PG_TRGM]) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + + let row = sqlx::query( + "SELECT \ + '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance, \ + similarity('postgres', 'postgrex')::float8 AS score", + ) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("distance")?, 1.0); + assert!(row.try_get::("score")? > 0.5); + + let row = sqlx::query( + "SELECT count(*)::int4 AS count \ + FROM pg_extension \ + WHERE extname IN ('vector', 'pg_trgm')", + ) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("count")?, 2); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} diff --git a/tests/performance_smoke.rs b/tests/performance_smoke.rs new file mode 100644 index 00000000..faedef6f --- /dev/null +++ b/tests/performance_smoke.rs @@ -0,0 +1,274 @@ +#![cfg(feature = "extensions")] + +use anyhow::Result; +use pglite_oxide::PgliteServer; +use pglite_oxide::extensions; +use pglite_oxide::{Pglite, capture_phase_timings}; +use serde_json::json; +use std::time::Instant; + +fn first_int(result: &pglite_oxide::Results, column: &str) -> i64 { + result.rows[0][column].as_i64().expect("integer result") +} + +fn phase_elapsed_micros(phases: &[pglite_oxide::PhaseTiming], name: &str) -> Option { + phases + .iter() + .find(|phase| phase.name == name) + .map(|phase| phase.elapsed_micros) +} + +fn assert_startup_xlog_fast_if_instrumented(phases: &[pglite_oxide::PhaseTiming], context: &str) { + let Some(startup_xlog) = phase_elapsed_micros(phases, "postgres.backend.c.startup_xlog") else { + eprintln!( + "{context}: C backend timing is not present; rebuild assets with \ + PGLITE_OXIDE_WASIX_BACKEND_TIMING=1 to assert StartupXLOG directly" + ); + return; + }; + assert!( + startup_xlog < 200_000, + "{context} should not require slow StartupXLOG recovery; \ + saw {startup_xlog}us in phases: {phases:#?}" + ); +} + +#[test] +fn preload_runtime_then_open_smoke() -> Result<()> { + let preload_started = Instant::now(); + Pglite::preload()?; + let preload_elapsed = preload_started.elapsed(); + + let open_started = Instant::now(); + let mut db = Pglite::builder().temporary().open()?; + let open_elapsed = open_started.elapsed(); + + let result = db.query("SELECT $1::int + 1 AS answer", &[json!(41)], None)?; + assert_eq!(first_int(&result, "answer"), 42); + db.close()?; + + eprintln!( + "preload_runtime_then_open_smoke preload_ms={} open_ms={}", + preload_elapsed.as_millis(), + open_elapsed.as_millis() + ); + Ok(()) +} + +#[test] +fn scalar_open_does_not_scan_array_catalog() -> Result<()> { + let (result, phases) = capture_phase_timings(|| { + let mut db = Pglite::builder().temporary().open()?; + let result = db.query("SELECT $1::int + 1 AS answer", &[json!(41)], None)?; + assert_eq!(first_int(&result, "answer"), 42); + db.close() + }); + result?; + + assert!( + !phases + .iter() + .any(|phase| phase.name == "pglite.array_type_catalog_query"), + "scalar open/query should not scan pg_type for array mappings: {phases:#?}" + ); + Ok(()) +} + +#[test] +fn preload_reuses_process_aot_module_cache() -> Result<()> { + let (first, first_phases) = capture_phase_timings(Pglite::preload); + first?; + let (second, second_phases) = capture_phase_timings(Pglite::preload); + second?; + + let first_deserialized = first_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + let second_deserialized = second_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + + if first_deserialized { + assert!( + !second_deserialized, + "second preload should reuse the process module cache instead of deserializing again" + ); + } + Ok(()) +} + +#[test] +fn shared_runtime_does_not_share_database_state_between_instances() -> Result<()> { + Pglite::preload()?; + + let mut first = Pglite::builder().temporary().open()?; + first.exec( + "CREATE TABLE process_cache_isolation(value int); \ + INSERT INTO process_cache_isolation VALUES (42);", + None, + )?; + + let mut second = Pglite::builder().temporary().open()?; + let missing = second + .query("SELECT value FROM process_cache_isolation", &[], None) + .expect_err("temporary database state must not leak across instances"); + assert!( + missing.to_string().contains("process_cache_isolation") + || missing.to_string().contains("does not exist"), + "unexpected isolation error: {missing:#}" + ); + + first.close()?; + second.close()?; + Ok(()) +} + +#[test] +fn persistent_direct_close_avoids_startup_xlog_recovery() -> Result<()> { + let root = tempfile::TempDir::new()?; + { + let mut db = Pglite::builder().path(root.path()).open()?; + db.exec( + "CREATE TABLE clean_shutdown(value int); \ + INSERT INTO clean_shutdown VALUES (42);", + None, + )?; + db.close()?; + } + + let (result, phases) = capture_phase_timings(|| -> Result<()> { + let mut db = Pglite::open(root.path())?; + let row = db.query("SELECT value FROM clean_shutdown", &[], None)?; + assert_eq!(first_int(&row, "value"), 42); + db.close() + }); + result?; + + assert_startup_xlog_fast_if_instrumented(&phases, "persistent direct close"); + Ok(()) +} + +#[cfg(feature = "extensions")] +#[test] +fn preload_extensions_reuses_extension_side_module_cache() -> Result<()> { + let (first, first_phases) = + capture_phase_timings(|| Pglite::preload_extensions([extensions::VECTOR])); + first?; + let (second, second_phases) = + capture_phase_timings(|| Pglite::preload_extensions([extensions::VECTOR])); + second?; + + let first_deserialized = first_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + let second_deserialized = second_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + + if first_deserialized { + assert!( + !second_deserialized, + "second extension preload should reuse the process side-module cache" + ); + } + Ok(()) +} + +#[cfg(feature = "extensions")] +#[test] +fn persistent_extension_server_reopen_uses_single_clean_backend() -> Result<()> { + Pglite::preload_extensions([extensions::VECTOR])?; + let root = tempfile::TempDir::new()?; + + { + let mut db = Pglite::builder() + .path(root.path()) + .extension(extensions::VECTOR) + .open()?; + db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + db.close()?; + } + + let (result, phases) = capture_phase_timings(|| -> Result<()> { + let server = PgliteServer::builder() + .path(root.path()) + .extension(extensions::VECTOR) + .start()?; + let url = server.database_url(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + runtime.block_on(async { + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(1) + .connect(&url) + .await?; + let (distance,): (f64,) = + sqlx::query_as("SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector") + .fetch_one(&pool) + .await?; + assert_eq!(distance, 1.0); + pool.close().await; + Ok::<_, anyhow::Error>(()) + })?; + server.shutdown() + }); + result?; + + let backend_starts = phases + .iter() + .filter(|phase| phase.name == "postgres.backend_start") + .count(); + assert_eq!( + backend_starts, 1, + "extension server startup should not use a second setup backend: {phases:#?}" + ); + assert_startup_xlog_fast_if_instrumented(&phases, "extension server reopen"); + Ok(()) +} + +#[cfg(feature = "extensions")] +#[test] +fn cached_extension_template_opens_without_startup_xlog_recovery() -> Result<()> { + Pglite::preload_extensions([extensions::VECTOR])?; + + { + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + db.close()?; + } + + let (result, phases) = capture_phase_timings(|| -> Result<()> { + let mut db = Pglite::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + db.close() + }); + result?; + + assert!( + !phases + .iter() + .any(|phase| phase.name == "pgdata.extension_template_build"), + "second extension open should reuse the cached extension template: {phases:#?}" + ); + assert_startup_xlog_fast_if_instrumented(&phases, "cached extension template"); + Ok(()) +} diff --git a/tests/postgres_regression.rs b/tests/postgres_regression.rs new file mode 100644 index 00000000..15eca888 --- /dev/null +++ b/tests/postgres_regression.rs @@ -0,0 +1,615 @@ +#![cfg(feature = "extensions")] + +use anyhow::{Context, Result, anyhow}; +use pglite_oxide::{Pglite, QueryOptions}; +use serde_json::{Map, Value, json}; + +struct TestTrace { + name: &'static str, +} + +impl TestTrace { + fn new(name: &'static str) -> Self { + eprintln!("postgres_regression::{name} start"); + Self { name } + } +} + +impl Drop for TestTrace { + fn drop(&mut self) { + eprintln!("postgres_regression::{} end", self.name); + } +} + +fn first_row(result: &pglite_oxide::Results) -> Result<&Map> { + result + .rows + .first() + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("expected at least one object row")) +} + +fn single_column_strings(result: &pglite_oxide::Results, column: &str) -> Result> { + result + .rows + .iter() + .map(|row| { + row.get(column) + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or_else(|| anyhow!("expected string column {column} in row {row:?}")) + }) + .collect() +} + +#[test] +fn datatypes_cover_pglite_basic_surface() -> Result<()> { + let _trace = TestTrace::new("datatypes_cover_pglite_basic_surface"); + let mut db = Pglite::builder().temporary().open()?; + + db.exec( + "CREATE TABLE regression_types ( + id serial PRIMARY KEY, + text_col text NOT NULL, + small_col smallint, + int_col integer, + big_col bigint, + numeric_col numeric(12,2), + real_col real, + double_col double precision, + bool_col boolean, + date_col date, + ts_col timestamp, + tstz_col timestamptz, + json_col json, + jsonb_col jsonb, + bytea_col bytea, + text_arr text[], + int_arr integer[], + nested_float double precision[][], + nullable_col integer + )", + None, + )?; + + db.query( + "INSERT INTO regression_types ( + text_col, + small_col, + int_col, + big_col, + numeric_col, + real_col, + double_col, + bool_col, + date_col, + ts_col, + tstz_col, + json_col, + jsonb_col, + bytea_col, + text_arr, + int_arr, + nested_float, + nullable_col + ) VALUES ( + $1::text, + $2::int2, + $3::int4, + $4::int8, + $5::numeric, + $6::float4, + $7::float8, + $8::bool, + $9::date, + $10::timestamp, + $11::timestamptz, + $12::json, + $13::jsonb, + $14::bytea, + $15::text[], + $16::int4[], + $17::float8[][], + $18::int4 + )", + &[ + json!("hello, \"postgres\""), + json!(7), + json!(42), + json!(9_007_199_254_740_i64), + json!(1234.5), + json!(1.25), + json!(2.5), + json!(true), + json!("2021-01-02"), + json!("2021-01-02 03:04:05"), + json!("2021-01-02 03:04:05+00"), + json!({"kind": "json", "items": [1, 2, 3]}), + json!({"kind": "jsonb", "nested": {"ok": true}}), + json!([0, 1, 2, 255]), + json!(["alpha", "beta,gamma", "quote \" value"]), + json!([1, 2, 3]), + json!([[1.5, 2.5], [3.5, 4.5]]), + Value::Null, + ], + None, + )?; + + let result = db.query( + "SELECT + text_col, + small_col, + int_col, + big_col, + numeric_col, + real_col, + double_col, + bool_col, + date_col::text AS date_text, + ts_col::text AS timestamp_text, + to_char(tstz_col AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS timestamptz_utc, + json_col, + jsonb_col, + bytea_col, + text_arr, + int_arr, + nested_float, + nullable_col + FROM regression_types", + &[], + None, + )?; + let row = first_row(&result)?; + + assert_eq!(row.get("text_col"), Some(&json!("hello, \"postgres\""))); + assert_eq!(row.get("small_col"), Some(&json!(7))); + assert_eq!(row.get("int_col"), Some(&json!(42))); + assert_eq!(row.get("big_col"), Some(&json!(9_007_199_254_740_i64))); + assert_eq!(row.get("numeric_col"), Some(&json!(1234.5))); + assert_eq!(row.get("real_col"), Some(&json!(1.25))); + assert_eq!(row.get("double_col"), Some(&json!(2.5))); + assert_eq!(row.get("bool_col"), Some(&json!(true))); + assert_eq!(row.get("date_text"), Some(&json!("2021-01-02"))); + assert_eq!( + row.get("timestamp_text"), + Some(&json!("2021-01-02 03:04:05")) + ); + assert_eq!( + row.get("timestamptz_utc"), + Some(&json!("2021-01-02 03:04:05")) + ); + assert_eq!( + row.get("json_col") + .and_then(|value| value.get("items")) + .and_then(Value::as_array) + .map(Vec::len), + Some(3) + ); + assert_eq!( + row.get("jsonb_col") + .and_then(|value| value.get("nested")) + .and_then(|value| value.get("ok")), + Some(&json!(true)) + ); + assert_eq!(row.get("bytea_col"), Some(&json!([0, 1, 2, 255]))); + assert_eq!( + row.get("text_arr"), + Some(&json!(["alpha", "beta,gamma", "quote \" value"])) + ); + assert_eq!(row.get("int_arr"), Some(&json!([1, 2, 3]))); + assert_eq!( + row.get("nested_float"), + Some(&json!([[1.5, 2.5], [3.5, 4.5]])) + ); + assert_eq!(row.get("nullable_col"), Some(&Value::Null)); + + let field_oids: Vec<(&str, i32)> = result + .fields + .iter() + .map(|field| (field.name.as_str(), field.data_type_id)) + .collect(); + assert!( + field_oids.contains(&("jsonb_col", 3802)), + "jsonb field should preserve PostgreSQL type OID: {field_oids:?}" + ); + assert!( + field_oids.contains(&("bytea_col", 17)), + "bytea field should preserve PostgreSQL type OID: {field_oids:?}" + ); + assert!( + field_oids.contains(&("text_arr", 1009)), + "text[] field should preserve PostgreSQL type OID: {field_oids:?}" + ); + + Ok(()) +} + +#[test] +fn ddl_schema_view_trigger_and_rollback_behave_like_postgres() -> Result<()> { + let _trace = TestTrace::new("ddl_schema_view_trigger_and_rollback_behave_like_postgres"); + let mut db = Pglite::builder().temporary().open()?; + + db.exec( + "CREATE SCHEMA reg; + CREATE TABLE reg.accounts ( + id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + email text NOT NULL UNIQUE, + balance numeric(12,2) NOT NULL DEFAULT 0 CHECK (balance >= 0), + status text NOT NULL DEFAULT 'open' + ); + ALTER TABLE reg.accounts ADD COLUMN tags text[] NOT NULL DEFAULT ARRAY[]::text[]; + ALTER TABLE reg.accounts RENAME COLUMN email TO login; + CREATE TABLE reg.account_audit ( + account_id integer NOT NULL, + action text NOT NULL + ); + CREATE FUNCTION reg.audit_account_insert() RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO reg.account_audit(account_id, action) + VALUES (NEW.id, 'insert'); + RETURN NEW; + END + $$; + CREATE TRIGGER account_insert_audit + AFTER INSERT ON reg.accounts + FOR EACH ROW EXECUTE FUNCTION reg.audit_account_insert(); + INSERT INTO reg.accounts(login, balance, tags) + VALUES ('one@example.com', 12.50, ARRAY['seed', 'ddl']); + CREATE VIEW reg.open_accounts AS + SELECT id, login, balance, tags + FROM reg.accounts + WHERE status = 'open';", + None, + )?; + + let view_result = db.query( + "SELECT login, balance, tags FROM reg.open_accounts", + &[], + None, + )?; + let view_row = first_row(&view_result)?; + assert_eq!(view_row.get("login"), Some(&json!("one@example.com"))); + assert_eq!(view_row.get("balance"), Some(&json!(12.5))); + assert_eq!(view_row.get("tags"), Some(&json!(["seed", "ddl"]))); + + let audit_result = db.query( + "SELECT count(*)::int AS audit_count FROM reg.account_audit", + &[], + None, + )?; + assert_eq!( + first_row(&audit_result)?.get("audit_count"), + Some(&json!(1)) + ); + + let constraint_error = db + .exec( + "INSERT INTO reg.accounts(login, balance) + VALUES ('bad@example.com', -1)", + None, + ) + .expect_err("check constraint should reject negative balance"); + eprintln!("postgres_regression::ddl_schema expected check-constraint error returned"); + let pg_error = constraint_error + .downcast_ref::() + .context("constraint error should preserve PostgreSQL fields")?; + assert_eq!(pg_error.database_error().code.as_deref(), Some("23514")); + + db.exec( + "BEGIN; + CREATE TABLE reg.rolled_back(id integer); + INSERT INTO reg.rolled_back VALUES (1); + ROLLBACK;", + None, + )?; + let regclass = db.query( + "SELECT to_regclass('reg.rolled_back')::text AS rolled_back_table", + &[], + None, + )?; + assert_eq!( + first_row(®class)?.get("rolled_back_table"), + Some(&Value::Null) + ); + + db.exec("ALTER TABLE reg.accounts RENAME TO customers", None)?; + let rename_result = db.query( + "SELECT + to_regclass('reg.accounts')::text AS old_name, + to_regclass('reg.customers')::text AS new_name", + &[], + None, + )?; + let rename_row = first_row(&rename_result)?; + assert_eq!(rename_row.get("old_name"), Some(&Value::Null)); + assert_eq!(rename_row.get("new_name"), Some(&json!("reg.customers"))); + + Ok(()) +} + +#[test] +fn transactions_savepoints_and_error_recovery_match_postgres() -> Result<()> { + let _trace = TestTrace::new("transactions_savepoints_and_error_recovery_match_postgres"); + let mut db = Pglite::builder().temporary().open()?; + db.exec( + "CREATE TABLE tx_items ( + id integer PRIMARY KEY, + value text NOT NULL + )", + None, + )?; + + db.exec("BEGIN", None)?; + db.exec( + "INSERT INTO tx_items VALUES (1, 'committed-before-savepoint')", + None, + )?; + db.exec("SAVEPOINT before_second", None)?; + db.exec( + "INSERT INTO tx_items VALUES (2, 'rolled-back-to-savepoint')", + None, + )?; + db.exec("ROLLBACK TO SAVEPOINT before_second", None)?; + db.exec( + "INSERT INTO tx_items VALUES (3, 'committed-after-savepoint')", + None, + )?; + db.exec("COMMIT", None)?; + + let ids = db.query( + "SELECT array_agg(id ORDER BY id) AS ids FROM tx_items", + &[], + None, + )?; + assert_eq!(first_row(&ids)?.get("ids"), Some(&json!([1, 3]))); + + db.exec("BEGIN", None)?; + db.exec("SAVEPOINT duplicate_guard", None)?; + let duplicate = db + .exec("INSERT INTO tx_items VALUES (1, 'duplicate')", None) + .expect_err("duplicate primary key should fail inside savepoint"); + eprintln!("postgres_regression::transactions expected duplicate-key error returned"); + let pg_error = duplicate + .downcast_ref::() + .context("duplicate error should preserve PostgreSQL fields")?; + assert_eq!(pg_error.database_error().code.as_deref(), Some("23505")); + db.exec("ROLLBACK TO SAVEPOINT duplicate_guard", None)?; + db.exec( + "INSERT INTO tx_items VALUES (4, 'recovered-after-savepoint-error')", + None, + )?; + db.exec("COMMIT", None)?; + + let values = db.query("SELECT value FROM tx_items ORDER BY id", &[], None)?; + assert_eq!( + single_column_strings(&values, "value")?, + vec![ + "committed-before-savepoint", + "committed-after-savepoint", + "recovered-after-savepoint-error", + ] + ); + + let after_error = db.query("SELECT 99::int AS recovered", &[], None)?; + assert_eq!(first_row(&after_error)?.get("recovered"), Some(&json!(99))); + + Ok(()) +} + +#[test] +fn expected_sql_error_recovery_stays_inside_protocol_loop() -> Result<()> { + let _trace = TestTrace::new("expected_sql_error_recovery_stays_inside_protocol_loop"); + let mut db = Pglite::builder().temporary().open()?; + db.exec( + "CREATE TABLE error_recovery ( + id integer PRIMARY KEY, + value integer NOT NULL CHECK (value > 0) + ); + INSERT INTO error_recovery VALUES (1, 1);", + None, + )?; + + for (label, sql, code) in [ + ( + "check-constraint", + "INSERT INTO error_recovery VALUES (2, -1)", + "23514", + ), + ( + "duplicate-key", + "INSERT INTO error_recovery VALUES (1, 2)", + "23505", + ), + ] { + eprintln!("postgres_regression::expected_sql_error exercising {label}"); + let err = db.exec(sql, None).expect_err(label); + let pg_error = err + .downcast_ref::() + .with_context(|| format!("{label} should preserve PostgreSQL fields"))?; + assert_eq!(pg_error.database_error().code.as_deref(), Some(code)); + let recovered = db.query( + "SELECT count(*)::int AS rows FROM error_recovery", + &[], + None, + )?; + assert_eq!(first_row(&recovered)?.get("rows"), Some(&json!(1))); + } + + Ok(()) +} + +#[test] +fn planner_uses_indexes_for_selective_queries_and_updates() -> Result<()> { + let _trace = TestTrace::new("planner_uses_indexes_for_selective_queries_and_updates"); + let mut db = Pglite::builder().temporary().open()?; + db.exec( + "CREATE TABLE plan_items ( + id integer PRIMARY KEY, + category integer NOT NULL, + name text NOT NULL, + active boolean NOT NULL, + score integer NOT NULL + ); + INSERT INTO plan_items(id, category, name, active, score) + SELECT + i, + i % 17, + 'item-' || lpad(i::text, 4, '0'), + (i % 3 = 0), + i % 101 + FROM generate_series(1, 2000) AS s(i); + CREATE INDEX plan_items_category_idx ON plan_items(category); + CREATE INDEX plan_items_lower_name_idx ON plan_items((lower(name))); + CREATE INDEX plan_items_active_score_idx ON plan_items(score) WHERE active; + ANALYZE plan_items; + SET enable_seqscan = off;", + None, + )?; + + let category_plan = explain_text( + &mut db, + "EXPLAIN (COSTS OFF) + SELECT id FROM plan_items WHERE category = 7", + )?; + assert!( + category_plan.contains("plan_items_category_idx"), + "category query should use category index:\n{category_plan}" + ); + + let expression_plan = explain_text( + &mut db, + "EXPLAIN (COSTS OFF) + SELECT id FROM plan_items WHERE lower(name) = 'item-0042'", + )?; + assert!( + expression_plan.contains("plan_items_lower_name_idx"), + "expression query should use expression index:\n{expression_plan}" + ); + + let partial_plan = explain_text( + &mut db, + "EXPLAIN (COSTS OFF) + SELECT id FROM plan_items WHERE active AND score = 42", + )?; + assert!( + partial_plan.contains("plan_items_active_score_idx"), + "partial-index query should use partial index:\n{partial_plan}" + ); + + db.exec( + "UPDATE plan_items + SET score = score + 1000 + WHERE category = 7", + None, + )?; + let updated = db.query( + "SELECT count(*)::int AS updated_count + FROM plan_items + WHERE category = 7 AND score >= 1000", + &[], + None, + )?; + assert_eq!(first_row(&updated)?.get("updated_count"), Some(&json!(118))); + + db.exec( + "DELETE FROM plan_items + WHERE active AND score = 42", + None, + )?; + let deleted = db.query( + "SELECT count(*)::int AS remaining + FROM plan_items + WHERE active AND score = 42", + &[], + None, + )?; + assert_eq!(first_row(&deleted)?.get("remaining"), Some(&json!(0))); + + Ok(()) +} + +#[test] +fn direct_blob_copy_round_trips_csv_with_pglite_dev_blob_surface() -> Result<()> { + let _trace = TestTrace::new("direct_blob_copy_round_trips_csv_with_pglite_dev_blob_surface"); + let mut db = Pglite::builder().temporary().open()?; + db.exec( + "CREATE TABLE blob_items ( + id integer PRIMARY KEY, + note text NOT NULL + ); + INSERT INTO blob_items(id, note) VALUES + (1, 'alpha'), + (2, 'comma,value'), + (3, 'quote \" value'), + (4, E'line\nbreak');", + None, + )?; + + let copy_out = db.exec( + "COPY blob_items TO '/dev/blob' WITH (FORMAT csv, HEADER true)", + None, + )?; + let csv = copy_out + .last() + .and_then(|result| result.blob.as_ref()) + .context("COPY TO /dev/blob should return blob bytes")?; + let csv_text = std::str::from_utf8(csv).context("COPY CSV should be UTF-8")?; + assert!( + csv_text.starts_with("id,note\n"), + "CSV should include header: {csv_text:?}" + ); + assert!( + csv_text.contains("2,\"comma,value\""), + "CSV should quote comma-containing fields: {csv_text:?}" + ); + assert!( + csv_text.contains("3,\"quote \"\" value\""), + "CSV should quote embedded quote fields: {csv_text:?}" + ); + + db.exec( + "CREATE TABLE blob_items_copy ( + id integer PRIMARY KEY, + note text NOT NULL + )", + None, + )?; + let copy_options = QueryOptions { + blob: Some(csv.clone()), + ..Default::default() + }; + let copy_in = db.exec( + "COPY blob_items_copy FROM '/dev/blob' WITH (FORMAT csv, HEADER true)", + Some(©_options), + )?; + assert_eq!( + copy_in.last().and_then(|result| result.affected_rows), + Some(4) + ); + + let copied = db.query( + "SELECT jsonb_agg(jsonb_build_array(id, note) ORDER BY id) AS rows + FROM blob_items_copy", + &[], + None, + )?; + assert_eq!( + first_row(&copied)?.get("rows"), + Some(&json!([ + [1, "alpha"], + [2, "comma,value"], + [3, "quote \" value"], + [4, "line\nbreak"] + ])) + ); + + Ok(()) +} + +fn explain_text(db: &mut Pglite, sql: &str) -> Result { + let result = db.query(sql, &[], None)?; + let lines = single_column_strings(&result, "QUERY PLAN")?; + Ok(lines.join("\n")) +} diff --git a/tests/proxy_smoke.rs b/tests/proxy_smoke.rs index ebf8118c..368ecc8f 100644 --- a/tests/proxy_smoke.rs +++ b/tests/proxy_smoke.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "extensions")] + use anyhow::{Result, anyhow, bail, ensure}; use pglite_oxide::PgliteProxy; use std::io::{Read, Write}; diff --git a/tests/runtime_smoke.rs b/tests/runtime_smoke.rs index 6799d9a0..e3c75c0d 100644 --- a/tests/runtime_smoke.rs +++ b/tests/runtime_smoke.rs @@ -1,9 +1,17 @@ +#![cfg(feature = "extensions")] + use pglite_oxide::{ - Pglite, PgliteError, QueryOptions, QueryTemplate, RowMode, format_query, quote_identifier, + DataDirArchiveFormat, ExecProtocolOptions, Pglite, PgliteError, PgliteServer, QueryOptions, + QueryTemplate, RowMode, format_query, quote_identifier, }; use serde_json::{Value, json}; +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; +mod support; +use support::{ChildGuard, TestTrace, trace_step}; + fn first_row(result: &pglite_oxide::Results) -> anyhow::Result<&serde_json::Map> { result .rows @@ -12,8 +20,701 @@ fn first_row(result: &pglite_oxide::Results) -> anyhow::Result<&serde_json::Map< .ok_or_else(|| anyhow::anyhow!("expected first row object")) } +fn assert_file_missing_or_without(path: &std::path::Path, needle: &str) -> anyhow::Result<()> { + match std::fs::read_to_string(path) { + Ok(contents) => { + assert!( + !contents.contains(needle), + "{} still contained stale marker {needle:?}: {contents:?}", + path.display() + ); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + Ok(()) +} + +fn raw_query_message(sql: &str) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(sql.as_bytes()); + body.push(0); + raw_tagged_message(b'Q', &body) +} + +fn raw_tagged_message(tag: u8, body: &[u8]) -> Vec { + let mut packet = Vec::with_capacity(body.len() + 5); + packet.push(tag); + packet.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); + packet.extend_from_slice(body); + packet +} + +fn raw_message_tags(mut bytes: &[u8]) -> Vec { + let mut tags = Vec::new(); + while bytes.len() >= 5 { + let tag = bytes[0]; + let len = i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]); + if len < 4 { + break; + } + let total = 1 + len as usize; + if bytes.len() < total { + break; + } + tags.push(tag); + bytes = &bytes[total..]; + } + tags +} + +fn raw_message_tags_ignoring_parameter_status(bytes: &[u8]) -> Vec { + raw_message_tags(bytes) + .into_iter() + .filter(|tag| *tag != b'S') + .collect() +} + +fn raw_backend_message_name(message: &pglite_oxide::BackendMessage) -> &'static str { + match message { + pglite_oxide::BackendMessage::RowDescription(_) => "rowDescription", + pglite_oxide::BackendMessage::DataRow(_) => "dataRow", + pglite_oxide::BackendMessage::CommandComplete(_) => "commandComplete", + pglite_oxide::BackendMessage::ReadyForQuery(_) => "readyForQuery", + pglite_oxide::BackendMessage::Error(_) => "error", + pglite_oxide::BackendMessage::ParseComplete { .. } => "parseComplete", + pglite_oxide::BackendMessage::BindComplete { .. } => "bindComplete", + _ => "other", + } +} + +fn assert_core_runtime_assets_stay_in_lower_mount(root: &std::path::Path) { + let runtime = root.join("tmp/pglite"); + assert!( + runtime.join(".pglite-oxide-mountfs-runtime").is_file(), + "expected shared runtime overlay marker under {}", + runtime.display() + ); + assert!( + !runtime.join("bin").exists(), + "core binaries should be served from the lower cached runtime, not linked into {}", + runtime.display() + ); + assert!( + !runtime.join("lib").exists(), + "core runtime libraries should stay in the lower cached runtime" + ); + assert!( + !runtime.join("share").exists(), + "core catalog, timezone, and extension metadata should stay in the lower cached runtime" + ); +} + +#[test] +fn template_cache_false_runs_split_initdb() -> anyhow::Result<()> { + let mut db = Pglite::builder().temporary().template_cache(false).open()?; + let result = db.query("SELECT 1 AS value", &[], None)?; + assert_eq!(first_row(&result)?["value"], json!(1)); + Ok(()) +} + +#[test] +fn direct_transaction_commit_rollback_and_error_recovery() -> anyhow::Result<()> { + let mut pg = Pglite::builder().temporary().open()?; + pg.exec( + "CREATE TABLE direct_tx_items(id int PRIMARY KEY, value text)", + None, + )?; + + let committed = pg.transaction(|tx| { + let inserted = tx.query( + "INSERT INTO direct_tx_items(id, value) VALUES ($1, $2) RETURNING value", + &[json!(1), json!("committed")], + None, + )?; + assert_eq!( + first_row(&inserted)?.get("value"), + Some(&json!("committed")) + ); + Ok::<_, anyhow::Error>("commit-result") + })?; + assert_eq!(committed, "commit-result"); + + let rollback: anyhow::Result<()> = pg.transaction(|tx| { + tx.query( + "INSERT INTO direct_tx_items(id, value) VALUES ($1, $2)", + &[json!(2), json!("rolled back")], + None, + )?; + Err(anyhow::anyhow!("force rollback")) + }); + assert!(rollback.is_err()); + + let failed: anyhow::Result<()> = pg.transaction(|tx| { + tx.query( + "INSERT INTO direct_tx_items(id, value) VALUES ($1, $2)", + &[json!(3), json!("before failure")], + None, + )?; + tx.query("SELECT 10 / $1::int4 AS impossible", &[json!(0)], None)?; + Ok(()) + }); + let failed = failed.expect_err("transaction should return the SQL failure"); + let pg_err = failed + .downcast_ref::() + .expect("transaction SQL error should preserve Postgres fields"); + assert_eq!(pg_err.database_error().code.as_deref(), Some("22012")); + + let count = pg.query( + "SELECT count(*)::int AS count, string_agg(value, ',' ORDER BY id) AS values \ + FROM direct_tx_items", + &[], + None, + )?; + assert_eq!(first_row(&count)?.get("count"), Some(&json!(1))); + assert_eq!(first_row(&count)?.get("values"), Some(&json!("committed"))); + + let recovered = pg.query("SELECT 42::int AS recovered_after_tx_error", &[], None)?; + assert_eq!( + first_row(&recovered)?.get("recovered_after_tx_error"), + Some(&json!(42)) + ); + + pg.close()?; + Ok(()) +} + +#[test] +fn direct_startup_postgres_config_uses_real_guc_handling() -> anyhow::Result<()> { + let mut pg = Pglite::builder() + .temporary() + .postgres_config("synchronous_commit", "off") + .postgres_config("work_mem", "8MB") + .open()?; + + let result = pg.query( + "SELECT current_setting('synchronous_commit') AS sync_commit, \ + current_setting('work_mem') AS work_mem", + &[], + None, + )?; + let row = first_row(&result)?; + assert_eq!(row.get("sync_commit"), Some(&json!("off"))); + assert_eq!(row.get("work_mem"), Some(&json!("8MB"))); + + pg.exec("BEGIN", None)?; + pg.exec("SET LOCAL synchronous_commit = on", None)?; + let local = pg.query( + "SELECT current_setting('synchronous_commit') AS sync_commit", + &[], + None, + )?; + assert_eq!( + first_row(&local)?.get("sync_commit"), + Some(&json!("on")), + "SET LOCAL should still be handled by PostgreSQL itself" + ); + pg.exec("COMMIT", None)?; + + let after_commit = pg.query( + "SELECT current_setting('synchronous_commit') AS sync_commit", + &[], + None, + )?; + assert_eq!( + first_row(&after_commit)?.get("sync_commit"), + Some(&json!("off")), + "startup GUC should remain the session default after SET LOCAL scope ends" + ); + + Ok(()) +} + +#[test] +fn invalid_postgres_config_is_rejected_before_backend_startup() -> anyhow::Result<()> { + let err = match Pglite::builder() + .temporary() + .postgres_config("bad=name", "off") + .open() + { + Ok(_) => anyhow::bail!("invalid startup config name should fail before opening"), + Err(err) => err, + }; + assert!( + format!("{err:#}").contains("Postgres config name"), + "unexpected error: {err:#}" + ); + Ok(()) +} + +#[test] +fn direct_startup_identity_can_select_existing_user_and_database() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + { + let mut db = Pglite::builder().path(root.path()).open()?; + db.exec("CREATE ROLE test_user LOGIN", None)?; + db.exec("CREATE DATABASE test_db OWNER test_user", None)?; + db.close()?; + } + + let mut db = Pglite::builder() + .path(root.path()) + .username("test_user") + .database("test_db") + .open()?; + let result = db.query( + "SELECT current_user, current_database(), current_setting('synchronous_commit') AS sync_commit", + &[], + None, + )?; + let row = first_row(&result)?; + assert_eq!(row.get("current_user"), Some(&json!("test_user"))); + assert_eq!(row.get("current_database"), Some(&json!("test_db"))); + db.close()?; + Ok(()) +} + +#[test] +fn relaxed_durability_uses_postgres_guc() -> anyhow::Result<()> { + let mut db = Pglite::builder() + .temporary() + .relaxed_durability(true) + .open()?; + let result = db.query( + "SELECT current_setting('synchronous_commit') AS sync_commit", + &[], + None, + )?; + assert_eq!(first_row(&result)?.get("sync_commit"), Some(&json!("off"))); + db.close()?; + Ok(()) +} + +#[test] +fn relaxed_durability_is_idempotent_and_user_config_wins() -> anyhow::Result<()> { + let mut disabled = Pglite::builder() + .temporary() + .relaxed_durability(true) + .relaxed_durability(false) + .open()?; + let result = disabled.query( + "SELECT current_setting('synchronous_commit') AS sync_commit", + &[], + None, + )?; + assert_eq!(first_row(&result)?.get("sync_commit"), Some(&json!("on"))); + disabled.close()?; + + let mut overridden = Pglite::builder() + .temporary() + .relaxed_durability(true) + .postgres_config("synchronous_commit", "on") + .open()?; + let result = overridden.query( + "SELECT current_setting('synchronous_commit') AS sync_commit", + &[], + None, + )?; + assert_eq!(first_row(&result)?.get("sync_commit"), Some(&json!("on"))); + overridden.close()?; + Ok(()) +} + +#[test] +fn startup_args_are_passed_to_postgres() -> anyhow::Result<()> { + let mut db = Pglite::builder() + .temporary() + .startup_args(["-c", "application_name=pglite-oxide-test"]) + .open()?; + let result = db.query( + "SELECT current_setting('application_name') AS app", + &[], + None, + )?; + assert_eq!( + first_row(&result)?.get("app"), + Some(&json!("pglite-oxide-test")) + ); + db.close()?; + Ok(()) +} + +#[test] +fn data_dir_dump_load_and_clone_round_trip() -> anyhow::Result<()> { + let mut source = Pglite::builder().temporary().open()?; + source.exec( + "CREATE TABLE data_dir_items(id serial PRIMARY KEY, value text); + INSERT INTO data_dir_items(value) VALUES ('alpha'), ('beta');", + None, + )?; + + let expected = source.query( + "SELECT id, value FROM data_dir_items ORDER BY id", + &[], + None, + )?; + let archive = source.dump_data_dir_with_format(DataDirArchiveFormat::Tar)?; + + let mut loaded = Pglite::builder() + .temporary() + .load_data_dir_archive(archive) + .open()?; + let loaded_rows = loaded.query( + "SELECT id, value FROM data_dir_items ORDER BY id", + &[], + None, + )?; + assert_eq!(loaded_rows.rows, expected.rows); + + let mut cloned = source.try_clone()?; + cloned.exec( + "INSERT INTO data_dir_items(value) VALUES ('clone-only')", + None, + )?; + let source_count = source.query( + "SELECT count(*)::int AS count FROM data_dir_items", + &[], + None, + )?; + let clone_count = cloned.query( + "SELECT count(*)::int AS count FROM data_dir_items", + &[], + None, + )?; + assert_eq!(first_row(&source_count)?.get("count"), Some(&json!(2))); + assert_eq!(first_row(&clone_count)?.get("count"), Some(&json!(3))); + + cloned.close()?; + loaded.close()?; + source.close()?; + Ok(()) +} + +#[test] +fn direct_raw_protocol_api_matches_pglite_exec_protocol_cases() -> anyhow::Result<()> { + let mut db = Pglite::builder().temporary().open()?; + + let simple = db.exec_protocol( + &raw_query_message("SELECT 1"), + ExecProtocolOptions::default(), + )?; + assert_eq!( + raw_message_tags_ignoring_parameter_status(&simple.data), + vec![b'T', b'D', b'C', b'Z'] + ); + assert_eq!( + simple + .messages + .iter() + .filter(|message| { + !matches!(message, pglite_oxide::BackendMessage::ParameterStatus(_)) + }) + .map(raw_backend_message_name) + .collect::>(), + vec![ + "rowDescription", + "dataRow", + "commandComplete", + "readyForQuery" + ] + ); + + let no_throw = db.exec_protocol( + &raw_query_message("invalid sql"), + ExecProtocolOptions { + throw_on_error: false, + ..ExecProtocolOptions::default() + }, + )?; + assert_eq!( + raw_message_tags_ignoring_parameter_status(&no_throw.data), + vec![b'E', b'Z'] + ); + + let err = db + .exec_protocol( + &raw_query_message("invalid sql"), + ExecProtocolOptions::default(), + ) + .expect_err("throw_on_error should return the Postgres error"); + assert!( + err.downcast_ref::().is_some(), + "unexpected raw protocol error: {err:#}" + ); + + let mut streamed = Vec::new(); + db.exec_protocol_raw_stream( + &raw_query_message("SELECT 2"), + ExecProtocolOptions::default(), + |chunk| { + streamed.extend_from_slice(chunk); + Ok(()) + }, + )?; + assert_eq!( + raw_message_tags_ignoring_parameter_status(&streamed), + vec![b'T', b'D', b'C', b'Z'] + ); + + let mut pipelined = raw_query_message("SELECT 3"); + pipelined.extend_from_slice(&raw_query_message("SELECT 4")); + let mut chunks = Vec::new(); + db.exec_protocol_raw_stream(&pipelined, ExecProtocolOptions::default(), |chunk| { + chunks.push(raw_message_tags_ignoring_parameter_status(chunk)); + Ok(()) + })?; + assert_eq!( + chunks, + vec![vec![b'T', b'D', b'C', b'Z'], vec![b'T', b'D', b'C', b'Z']] + ); + + db.close()?; + Ok(()) +} + +#[cfg(debug_assertions)] +#[test] +fn direct_protocol_bridge_guest_allocations_are_freed() -> anyhow::Result<()> { + let mut db = Pglite::builder().temporary().open()?; + let (allocations_before, frees_before) = db.guest_bridge_allocation_counts(); + assert_eq!( + allocations_before, frees_before, + "bridge allocations must be balanced before stress loop" + ); + + for _ in 0..128 { + let mut output = Vec::new(); + db.exec_protocol_raw_stream( + &raw_query_message("SELECT repeat('x', 4096)"), + ExecProtocolOptions::default(), + |chunk| { + output.extend_from_slice(chunk); + Ok(()) + }, + )?; + assert_eq!( + raw_message_tags_ignoring_parameter_status(&output), + vec![b'T', b'D', b'C', b'Z'] + ); + } + + let (allocations_after, frees_after) = db.guest_bridge_allocation_counts(); + assert_eq!( + allocations_after, frees_after, + "each Rust-owned guest bridge allocation must be freed" + ); + assert!( + allocations_after > allocations_before, + "stress loop should exercise bridge allocations" + ); + + db.close()?; + Ok(()) +} + +#[test] +fn pure_mountfs_serves_core_runtime_assets_from_lower_cache() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + { + let mut pg = Pglite::builder().path(root.path()).open()?; + let result = pg.query( + "SELECT count(*)::int AS utc_zones \ + FROM pg_timezone_names \ + WHERE name = 'UTC'", + &[], + None, + )?; + assert_eq!(first_row(&result)?.get("utc_zones"), Some(&json!(1))); + pg.close()?; + } + + assert_core_runtime_assets_stay_in_lower_mount(root.path()); + Ok(()) +} + +#[test] +fn server_drop_without_explicit_shutdown_releases_root() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + { + let server = PgliteServer::builder().path(root.path()).start()?; + assert!(server.tcp_addr().is_some()); + } + + let mut db = Pglite::builder().path(root.path()).open()?; + let result = db.query("SELECT 1 AS value", &[], None)?; + assert_eq!(first_row(&result)?.get("value"), Some(&json!(1))); + db.close()?; + Ok(()) +} + +#[test] +fn persistent_template_survives_restart_and_stale_state_files() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + { + let mut pg = Pglite::builder().path(root.path()).open()?; + pg.exec("CREATE TABLE template_restart(value TEXT)", None)?; + pg.query( + "INSERT INTO template_restart(value) VALUES ($1)", + &[json!("boot-single-ok")], + None, + )?; + pg.close()?; + } + + let pgdata = root.path().join("tmp/pglite/base"); + std::fs::write( + pgdata.join("postmaster.pid"), + b"stale pid from interrupted run", + )?; + std::fs::write( + pgdata.join("postmaster.opts"), + b"stale opts from interrupted run", + )?; + + let mut reopened = Pglite::builder().path(root.path()).open()?; + let result = reopened.query("SELECT value FROM template_restart", &[], None)?; + assert_eq!( + first_row(&result)?.get("value"), + Some(&json!("boot-single-ok")) + ); + assert_file_missing_or_without(&pgdata.join("postmaster.pid"), "stale pid")?; + assert_file_missing_or_without(&pgdata.join("postmaster.opts"), "stale opts")?; + reopened.close()?; + Ok(()) +} + +#[test] +fn persistent_template_recovers_interrupted_pgdata_without_marker() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + let pgdata = root.path().join("tmp/pglite/base"); + std::fs::create_dir_all(&pgdata)?; + std::fs::write(pgdata.join("postmaster.pid"), b"interrupted pid")?; + std::fs::write(pgdata.join("partial-bootstrap.sql"), b"interrupted initdb")?; + + let mut pg = Pglite::builder().path(root.path()).open()?; + let result = pg.query("SELECT 1::int AS one", &[], None)?; + assert_eq!(first_row(&result)?.get("one"), Some(&json!(1))); + assert!(pgdata.join("PG_VERSION").exists()); + assert!(!pgdata.join("partial-bootstrap.sql").exists()); + assert_file_missing_or_without(&pgdata.join("postmaster.pid"), "interrupted pid")?; + pg.close()?; + Ok(()) +} + +#[test] +fn persistent_template_recovers_interrupted_pgdata_with_incomplete_markers() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + let pgdata = root.path().join("tmp/pglite/base"); + std::fs::create_dir_all(&pgdata)?; + std::fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + std::fs::write(pgdata.join("partial-bootstrap.sql"), b"interrupted initdb")?; + + let mut pg = Pglite::builder().path(root.path()).open()?; + let result = pg.query("SELECT 2::int AS two", &[], None)?; + assert_eq!(first_row(&result)?.get("two"), Some(&json!(2))); + assert!(pgdata.join("PG_VERSION").exists()); + assert!(pgdata.join("global/pg_control").exists()); + assert!(!pgdata.join("partial-bootstrap.sql").exists()); + pg.close()?; + Ok(()) +} + +#[test] +fn persistent_root_lock_rejects_second_direct_open() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + let mut first = Pglite::builder().path(root.path()).open()?; + let err = match Pglite::builder().path(root.path()).open() { + Ok(_) => anyhow::bail!("second open must fail while the root lock is held"), + Err(err) => err, + }; + assert!(format!("{err:#}").contains("PGlite root is already in use")); + + first.close()?; + + let mut reopened = Pglite::builder().path(root.path()).open()?; + let result = reopened.query("SELECT 1::int AS one", &[], None)?; + assert_eq!(first_row(&result)?.get("one"), Some(&json!(1))); + reopened.close()?; + Ok(()) +} + +#[test] +fn persistent_root_lock_rejects_second_server_open() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + let server = PgliteServer::builder().path(root.path()).start()?; + let err = match PgliteServer::builder().path(root.path()).start() { + Ok(_) => anyhow::bail!("second server must fail while the root lock is held"), + Err(err) => err, + }; + assert!(format!("{err:#}").contains("PGlite root is already in use")); + server.shutdown()?; + Ok(()) +} + +#[test] +fn persistent_root_lock_rejects_direct_open_while_server_runs() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + let server = PgliteServer::builder().path(root.path()).start()?; + let err = match Pglite::builder().path(root.path()).open() { + Ok(_) => anyhow::bail!("direct open must fail while the server owns the root lock"), + Err(err) => err, + }; + assert!(format!("{err:#}").contains("PGlite root is already in use")); + server.shutdown()?; + + let mut reopened = Pglite::builder().path(root.path()).open()?; + let result = reopened.query("SELECT 1::int AS one", &[], None)?; + assert_eq!(first_row(&result)?.get("one"), Some(&json!(1))); + reopened.close()?; + Ok(()) +} + +#[test] +fn persistent_root_lock_rejects_cross_process_open() -> anyhow::Result<()> { + let root = tempfile::TempDir::new()?; + let child = Command::new(env!("CARGO_BIN_EXE_pglite-proxy")) + .arg("--root") + .arg(root.path()) + .args(["--tcp", "127.0.0.1:0", "--print-uri"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + let mut child = ChildGuard::new(child, "pglite-proxy")?; + + let stdout = child + .child_mut() + .stdout + .take() + .ok_or_else(|| anyhow::anyhow!("missing pglite-proxy stdout"))?; + let mut line = String::new(); + let read = BufReader::new(stdout).read_line(&mut line)?; + if read == 0 { + let stderr = child.collect_stderr(); + anyhow::bail!("pglite-proxy exited before printing URI\n\nstderr:\n{stderr}"); + } + assert!(line.starts_with("postgresql://"), "{line:?}"); + + let err = match Pglite::builder().path(root.path()).open() { + Ok(mut db) => { + let close = db.close(); + let stderr = child.collect_stderr(); + anyhow::bail!( + "direct open unexpectedly succeeded while another process owns the root lock; close={close:?}\n\nstderr:\n{stderr}" + ); + } + Err(err) => err, + }; + let message = format!("{err:#}"); + if !message.contains("PGlite root is already in use") { + let stderr = child.collect_stderr(); + anyhow::bail!("unexpected cross-process root-lock error: {message}\n\nstderr:\n{stderr}"); + } + Ok(()) +} + #[test] fn runtime_smoke() -> anyhow::Result<()> { + let _trace = TestTrace::new("runtime_smoke"); let mut pg = Pglite::builder().temporary().open()?; assert!(pg.paths().pgdata.join("PG_VERSION").exists()); @@ -31,6 +732,49 @@ fn runtime_smoke() -> anyhow::Result<()> { "expected PostgreSQL 17+, got {version_num}" ); + let identity = pg.query( + "SELECT current_user AS current_user, \ + session_user AS session_user, \ + current_database() AS database_name, \ + current_setting('TimeZone') AS timezone, \ + current_setting('search_path') AS search_path", + &[], + None, + )?; + let identity_row = first_row(&identity)?; + assert_eq!(identity_row.get("current_user"), Some(&json!("postgres"))); + assert_eq!(identity_row.get("session_user"), Some(&json!("postgres"))); + assert_eq!(identity_row.get("database_name"), Some(&json!("template1"))); + assert_eq!(identity_row.get("timezone"), Some(&json!("UTC"))); + assert_eq!(identity_row.get("search_path"), Some(&json!("public"))); + + pg.exec("SET TIME ZONE 'UTC'", None)?; + let timezone_catalog = pg.query( + "SELECT count(*)::int AS ny_zones, \ + EXTRACT(HOUR FROM TIMESTAMPTZ '2024-07-01 12:00:00+00' \ + AT TIME ZONE 'America/New_York')::int AS ny_summer_hour, \ + EXTRACT(HOUR FROM TIMESTAMPTZ '2024-01-01 12:00:00+00' \ + AT TIME ZONE 'America/New_York')::int AS ny_winter_hour \ + FROM pg_timezone_names \ + WHERE name = 'America/New_York'", + &[], + None, + )?; + let timezone_row = first_row(&timezone_catalog)?; + assert_eq!(timezone_row.get("ny_zones"), Some(&json!(1))); + assert_eq!(timezone_row.get("ny_summer_hour"), Some(&json!(8))); + assert_eq!(timezone_row.get("ny_winter_hour"), Some(&json!(7))); + + trace_step("runtime_smoke expected-error invalid-timezone"); + pg.exec("SET TIME ZONE 'Missing/Zone'", None) + .expect_err("invalid timezone should fail"); + let after_timezone_error = pg.query("SELECT 25::int AS recovered", &[], None)?; + assert_eq!( + first_row(&after_timezone_error)?.get("recovered"), + Some(&json!(25)) + ); + pg.exec("SET TIME ZONE 'UTC'", None)?; + pg.exec("CREATE TABLE items(value TEXT)", None)?; // COPY FROM '/dev/blob' @@ -66,6 +810,25 @@ fn runtime_smoke() -> anyhow::Result<()> { pg.unlisten(handle)?; + let quoted_events = Arc::new(Mutex::new(Vec::new())); + let quoted_events_clone = Arc::clone("ed_events); + let quoted_channel = "Case Sensitive \"Channel\""; + let quoted_handle = pg.listen(quoted_channel, move |payload| { + quoted_events_clone + .lock() + .expect("lock poisoning") + .push(payload.to_string()); + })?; + pg.exec( + "NOTIFY \"Case Sensitive \"\"Channel\"\"\", 'quoted listener'", + None, + )?; + let recorded = quoted_events.lock().unwrap(); + assert_eq!(recorded.as_slice(), ["quoted listener"]); + drop(recorded); + pg.unlisten(quoted_handle)?; + pg.unlisten_channel(quoted_channel)?; + let formatted = format_query(&mut pg, "SELECT $1::int", &[json!(42)])?; assert_eq!(formatted, "SELECT '42'::int"); @@ -121,6 +884,37 @@ fn runtime_smoke() -> anyhow::Result<()> { )?; assert_eq!(array_result.rows.first(), Some(&json!([1, "two"]))); + pg.exec( + "CREATE TYPE mood AS ENUM ('sad', 'happy'); \ + CREATE TABLE mood_items(moods mood[])", + None, + )?; + let mood_result = pg.query( + "INSERT INTO mood_items(moods) VALUES ($1) RETURNING moods", + &[json!(["sad", "happy"])], + None, + )?; + assert_eq!( + first_row(&mood_result)?.get("moods"), + Some(&json!(["sad", "happy"])) + ); + + pg.exec( + "CREATE TYPE weather AS ENUM ('rain', 'sun'); \ + CREATE TABLE weather_items(values weather[])", + None, + )?; + pg.refresh_array_types()?; + let weather_result = pg.query( + "INSERT INTO weather_items(values) VALUES ($1) RETURNING values", + &[json!(["rain", "sun"])], + None, + )?; + assert_eq!( + first_row(&weather_result)?.get("values"), + Some(&json!(["rain", "sun"])) + ); + pg.exec("CREATE TABLE tx_items(value TEXT)", None)?; pg.transaction(|tx| { tx.query( @@ -138,26 +932,68 @@ fn runtime_smoke() -> anyhow::Result<()> { let count = pg.query("SELECT count(*)::int AS count FROM tx_items", &[], None)?; assert_eq!(first_row(&count)?.get("count"), Some(&json!(1))); - let err = pg + trace_step("runtime_smoke expected-error syntax"); + let syntax_err = pg + .exec("SELECT +", None) + .expect_err("syntax error should fail"); + let syntax_pg_err = syntax_err + .downcast_ref::() + .expect("syntax error should preserve Postgres error fields"); + assert_eq!(syntax_pg_err.query(), "SELECT +"); + assert_eq!( + syntax_pg_err.database_error().code.as_deref(), + Some("42601") + ); + + trace_step("runtime_smoke expected-error missing-table"); + let missing_err = pg .query( "SELECT * FROM missing_table WHERE id = $1", &[json!(7)], None, ) .expect_err("missing table should fail"); - if let Some(pg_err) = err.downcast_ref::() { - assert_eq!(pg_err.query(), "SELECT * FROM missing_table WHERE id = $1"); - assert_eq!(pg_err.params(), &[json!(7)]); - assert_eq!(pg_err.database_error().code.as_deref(), Some("42P01")); - } else { - let message = format!("{err:#}"); - assert!( - message.contains( - "failed to execute extended query: SELECT * FROM missing_table WHERE id = $1" - ), - "{message}" - ); - } + let missing_pg_err = missing_err + .downcast_ref::() + .expect("extended query error should preserve Postgres error fields"); + assert_eq!( + missing_pg_err.query(), + "SELECT * FROM missing_table WHERE id = $1" + ); + assert_eq!(missing_pg_err.params(), &[json!(7)]); + assert_eq!( + missing_pg_err.database_error().code.as_deref(), + Some("42P01") + ); + + trace_step("runtime_smoke expected-error invalid-bind"); + let invalid_bind = pg + .query("SELECT $1::int4 AS value", &[json!("not_an_int")], None) + .expect_err("invalid typed parameter should fail during extended-query bind"); + let invalid_bind_pg_err = invalid_bind + .downcast_ref::() + .expect("bind error should preserve Postgres error fields"); + assert_eq!(invalid_bind_pg_err.query(), "SELECT $1::int4 AS value"); + assert_eq!(invalid_bind_pg_err.params(), &[json!("not_an_int")]); + assert_eq!( + invalid_bind_pg_err.database_error().code.as_deref(), + Some("22P02") + ); + + trace_step("runtime_smoke expected-error wrong-param-count"); + let wrong_param_count = pg + .query("SELECT $1::int4 + $2::int4 AS value", &[json!(1)], None) + .expect_err("missing parameter should fail during extended-query bind"); + let wrong_param_count_pg_err = wrong_param_count + .downcast_ref::() + .expect("parameter count error should preserve Postgres error fields"); + assert_eq!( + wrong_param_count_pg_err.database_error().code.as_deref(), + Some("08P01") + ); + + let after_error = pg.query("SELECT 99::int AS recovered", &[], None)?; + assert_eq!(first_row(&after_error)?.get("recovered"), Some(&json!(99))); pg.close()?; assert!(pg.is_closed()); diff --git a/tests/support/mod.rs b/tests/support/mod.rs new file mode 100644 index 00000000..be69852a --- /dev/null +++ b/tests/support/mod.rs @@ -0,0 +1,67 @@ +use anyhow::Context; +use std::io::{BufReader, Read}; +use std::process::Child; +use std::thread::JoinHandle; + +pub(crate) struct TestTrace { + name: &'static str, +} + +impl TestTrace { + pub(crate) fn new(name: &'static str) -> Self { + eprintln!("{name} start"); + Self { name } + } +} + +impl Drop for TestTrace { + fn drop(&mut self) { + eprintln!("{} end", self.name); + } +} + +pub(crate) fn trace_step(label: &str) { + eprintln!("{label}"); +} + +pub(crate) struct ChildGuard { + child: Child, + stderr: Option>, +} + +impl ChildGuard { + pub(crate) fn new(mut child: Child, name: &'static str) -> anyhow::Result { + let stderr = child + .stderr + .take() + .with_context(|| format!("{name} stderr pipe"))?; + let stderr = std::thread::spawn(move || { + let mut output = String::new(); + let _ = BufReader::new(stderr).read_to_string(&mut output); + output + }); + Ok(Self { + child, + stderr: Some(stderr), + }) + } + + pub(crate) fn child_mut(&mut self) -> &mut Child { + &mut self.child + } + + pub(crate) fn collect_stderr(&mut self) -> String { + let _ = self.child.kill(); + let _ = self.child.wait(); + self.stderr + .take() + .and_then(|reader| reader.join().ok()) + .unwrap_or_default() + } +} + +impl Drop for ChildGuard { + fn drop(&mut self) { + let _ = self.collect_stderr(); + } +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..880a2bfd --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "xtask" +version = "0.0.0" +edition = "2024" +rust-version = "1.92" +publish = false + +[features] +wasix-runner = ["dep:wasmer", "dep:wasmer-wasix", "dep:webc"] +template-runner = ["wasix-runner", "wasmer/llvm", "wasmer-wasix/host-fs"] +aot-serializer = [ + "template-runner", + "wasmer/wasmer-artifact-create", +] + +[dependencies] +anyhow = "1" +async-trait = "0.1" +directories = "6" +futures-util = "0.3" +pglite-oxide = { path = "..", features = ["extensions"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +sqlx = { version = "0.8", default-features = false, features = [ + "postgres", + "runtime-tokio", +] } +tar = "0.4" +tokio = { version = "1", features = ["rt-multi-thread"] } +tokio-postgres = "0.7" +toml = "0.9" +walkdir = "2" +wasmer = { version = "7.2.0-alpha.2", default-features = false, features = [ + "sys", +], optional = true } +wasmer-types = "7.2.0-alpha.2" +wasmer-wasix = { version = "0.702.0-alpha.2", default-features = false, features = [ + "sys-minimal", + "sys-poll", + "sys-thread", + "time", +], optional = true } +wasmparser = "0.247.0" +webc = { version = "11.0.0", optional = true } +zstd = "0.13" diff --git a/xtask/src/extension_catalog.rs b/xtask/src/extension_catalog.rs new file mode 100644 index 00000000..287d560a --- /dev/null +++ b/xtask/src/extension_catalog.rs @@ -0,0 +1,1890 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, anyhow, bail, ensure}; +use serde::{Deserialize, Serialize}; + +const CATALOG_PATH: &str = "assets/generated/extensions.catalog.json"; +const BUILD_PLAN_PATH: &str = "assets/generated/extensions.build-plan.json"; +const CONTRIB_BUILD_PLAN_PATH: &str = "assets/generated/contrib-build.tsv"; +const PGXS_BUILD_PLAN_PATH: &str = "assets/generated/pgxs-build.tsv"; +const PROMOTION_CONFIG_PATH: &str = "assets/extensions.promoted.toml"; +const SMOKE_CONFIG_PATH: &str = "assets/extensions.smoke.toml"; +const PGLITE_REPL_EXTENSIONS: &str = "assets/checkouts/pglite/docs/repl/allExtensions.ts"; +const PGLITE_DOCS_EXTENSIONS: &str = "assets/checkouts/pglite/docs/extensions/extensions.data.ts"; +const PGLITE_PACKAGE_JSON: &str = "assets/checkouts/pglite/packages/pglite/package.json"; +const PGLITE_CONTRIB_SRC: &str = "assets/checkouts/pglite/packages/pglite/src/contrib"; +const PGLITE_TESTS: &str = "assets/checkouts/pglite/packages/pglite/tests"; +const PGLITE_POSTGIS_TESTS: &str = "assets/checkouts/pglite/packages/pglite-postgis/tests"; +const POSTGRES_CONTRIB: &str = "assets/checkouts/postgres-pglite/contrib"; +const POSTGRES_OTHER_EXTENSIONS: &str = "assets/checkouts/postgres-pglite/pglite/other_extensions"; +const PGVECTOR_CHECKOUT: &str = "assets/checkouts/pgvector"; +const EXTERNAL_EXTENSION_CHECKOUT_ROOT: &str = "assets/checkouts"; +const ASSET_MANIFEST: &str = "target/pglite-oxide/assets/manifest.json"; + +pub(crate) fn extensions(args: Vec) -> Result<()> { + match args.first().map(String::as_str) { + Some("discover") => { + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + let text = serde_json::to_string_pretty(&catalog).context("serialize catalog")?; + if args.iter().any(|arg| arg == "--write") { + write_catalog(&text)?; + } else { + println!("{text}"); + } + Ok(()) + } + Some("generate") => { + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + let text = serde_json::to_string_pretty(&catalog).context("serialize catalog")?; + write_catalog(&text)?; + write_build_plan_files(&catalog)?; + write_generated_extension_api(&catalog)?; + Ok(()) + } + Some("build-plan") => { + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + if args.iter().any(|arg| arg == "--write") { + write_build_plan_files(&catalog) + } else if args.iter().any(|arg| arg == "--check") { + check_build_plan_file(true) + } else { + let plan = build_plan(&catalog)?; + println!( + "{}", + serde_json::to_string_pretty(&plan) + .context("serialize extension build plan")? + ); + Ok(()) + } + } + Some("check") => { + check_catalog_file(true)?; + check_build_plan_file(true) + } + Some(other) => bail!("unknown extensions subcommand: {other}"), + None => { + bail!( + "usage: cargo run -p xtask -- extensions [--write|--check]" + ) + } + } +} + +pub(crate) fn check_catalog_file(strict: bool) -> Result<()> { + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + let expected = serde_json::to_string_pretty(&catalog).context("serialize extension catalog")?; + let path = Path::new(CATALOG_PATH); + if !path.exists() { + if strict { + bail!( + "generated extension catalog is missing at {}; run `cargo run -p xtask -- extensions discover --write`", + path.display() + ); + } + eprintln!( + "warning: generated extension catalog is missing at {}", + path.display() + ); + return Ok(()); + } + let actual = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + if !extension_catalog_text_matches_source_control(&actual, &expected)? { + if strict { + bail!( + "generated extension catalog is stale at {}; run `cargo run -p xtask -- extensions discover --write`", + path.display() + ); + } + eprintln!( + "warning: generated extension catalog is stale at {}", + path.display() + ); + } + Ok(()) +} + +pub(crate) fn check_build_plan_file(strict: bool) -> Result<()> { + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + let expected = build_plan_texts(&catalog)?; + for (path, text, command) in [ + ( + BUILD_PLAN_PATH, + expected.json.as_str(), + "cargo run -p xtask -- extensions build-plan --write", + ), + ( + CONTRIB_BUILD_PLAN_PATH, + expected.contrib_tsv.as_str(), + "cargo run -p xtask -- extensions build-plan --write", + ), + ( + PGXS_BUILD_PLAN_PATH, + expected.pgxs_tsv.as_str(), + "cargo run -p xtask -- extensions build-plan --write", + ), + ] { + let path = Path::new(path); + if !path.exists() { + if strict { + bail!( + "generated extension build plan is missing at {}; run `{command}`", + path.display() + ); + } + eprintln!( + "warning: generated extension build plan is missing at {}", + path.display() + ); + continue; + } + let actual = + fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let matches = if path == Path::new(BUILD_PLAN_PATH) { + extension_build_plan_text_matches_source_control(&actual, text)? + } else { + actual.trim_end() == text.trim_end() + }; + if !matches { + if strict { + bail!( + "generated extension build plan is stale at {}; run `{command}`", + path.display() + ); + } + eprintln!( + "warning: generated extension build plan is stale at {}", + path.display() + ); + } + } + Ok(()) +} + +fn extension_catalog_text_matches_source_control(actual: &str, expected: &str) -> Result { + let actual: serde_json::Value = + serde_json::from_str(actual).context("parse generated extension catalog")?; + let expected: serde_json::Value = + serde_json::from_str(expected).context("parse expected extension catalog")?; + Ok(normalize_extension_catalog_for_source_control(actual) + == normalize_extension_catalog_for_source_control(expected)) +} + +fn extension_build_plan_text_matches_source_control(actual: &str, expected: &str) -> Result { + let actual: serde_json::Value = + serde_json::from_str(actual).context("parse generated extension build plan")?; + let expected: serde_json::Value = + serde_json::from_str(expected).context("parse expected extension build plan")?; + Ok(normalize_generated_inputs_for_source_control(actual) + == normalize_generated_inputs_for_source_control(expected)) +} + +fn normalize_extension_catalog_for_source_control(value: serde_json::Value) -> serde_json::Value { + let mut value = normalize_generated_inputs_for_source_control(value); + if let Some(extensions) = value + .get_mut("extensions") + .and_then(serde_json::Value::as_array_mut) + { + for extension in extensions { + if let Some(promotion) = extension + .get_mut("promotion") + .and_then(serde_json::Value::as_object_mut) + { + promotion.remove("packaged"); + promotion.remove("promoted"); + promotion.remove("module-sha256"); + } + } + } + value +} + +fn normalize_generated_inputs_for_source_control( + mut value: serde_json::Value, +) -> serde_json::Value { + if let Some(inputs) = value + .get_mut("generated-from") + .and_then(serde_json::Value::as_array_mut) + { + inputs.retain(|input| { + input.get("name").and_then(serde_json::Value::as_str) != Some("asset-manifest-evidence") + }); + } + value +} + +pub(crate) fn manifest_metadata_by_sql_name() -> Result> +{ + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + Ok(catalog + .extensions + .into_iter() + .map(|extension| { + ( + extension.sql_name.clone(), + ManifestExtensionMetadata { + source_kind: extension.source_kind, + control_files: extension.control_file.into_iter().collect(), + dependencies: extension.dependencies, + native_dependencies: extension.native_dependencies, + load_order: extension.load_order, + lifecycle: ManifestExtensionLifecycle { + create_extension: extension.lifecycle.create_extension, + create_schema: extension.lifecycle.create_schema, + load_sql: extension.lifecycle.load_sql, + post_create_sql: extension.lifecycle.post_create_sql, + startup_config: extension.lifecycle.startup_config, + preload_required: extension.lifecycle.preload_required, + restart_required: extension.lifecycle.restart_required, + shared_memory_required: extension.lifecycle.shared_memory_required, + }, + smoke_status: ManifestExtensionSmokeStatus { + promoted: extension.promotion.promoted, + direct: extension.smoke.direct, + server: extension.smoke.server, + restart: extension.smoke.restart, + dump_restore: extension.smoke.dump_restore, + }, + }, + ) + }) + .collect()) +} + +pub(crate) fn promoted_build_specs() -> Result> { + let catalog = discover_catalog()?; + validate_catalog(&catalog)?; + build_specs(&catalog) +} + +pub(crate) fn build_plan_contrib_path() -> &'static str { + CONTRIB_BUILD_PLAN_PATH +} + +pub(crate) fn build_plan_pgxs_path() -> &'static str { + PGXS_BUILD_PLAN_PATH +} + +fn build_specs(catalog: &ExtensionCatalog) -> Result> { + let mut specs = Vec::new(); + for extension in catalog + .extensions + .iter() + .filter(|extension| extension.promotion.requested) + { + let archive = extension + .promotion + .archive + .clone() + .unwrap_or_else(|| format!("extensions/{}.tar.zst", extension.sql_name)); + specs.push(PromotedExtensionBuildSpec { + id: extension.id.clone(), + display_name: extension.display_name.clone(), + sql_name: extension.sql_name.clone(), + source_kind: extension.source_kind.clone(), + build_kind: build_kind(extension).to_owned(), + source_dir: extension_source_dir(extension), + make_args: pgxs_make_args(extension), + contrib_dir: (extension.source_kind == "postgres-contrib") + .then(|| extension_contrib_dir_name(&extension.id)), + module_file: extension.native_module_file.clone(), + archive, + control_file: extension.control_file.clone(), + stable: extension.promotion.stable, + dependencies: extension.dependencies.clone(), + native_dependencies: extension.native_dependencies.clone(), + load_order: extension.load_order.clone(), + lifecycle: extension.lifecycle.clone(), + smoke: extension.smoke.clone(), + tests: extension.tests.clone(), + }); + } + specs.sort_by(|left, right| left.sql_name.cmp(&right.sql_name)); + Ok(specs) +} + +#[derive(Debug, Clone)] +pub(crate) struct PromotedExtensionBuildSpec { + pub(crate) id: String, + pub(crate) display_name: String, + pub(crate) sql_name: String, + pub(crate) source_kind: String, + pub(crate) build_kind: String, + pub(crate) source_dir: String, + pub(crate) make_args: Vec, + pub(crate) contrib_dir: Option, + pub(crate) module_file: Option, + pub(crate) archive: String, + pub(crate) control_file: Option, + pub(crate) stable: bool, + pub(crate) dependencies: Vec, + pub(crate) native_dependencies: Vec, + pub(crate) load_order: Vec, + pub(crate) lifecycle: ExtensionLifecycle, + pub(crate) smoke: ExtensionSmokeEvidence, + pub(crate) tests: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct ManifestExtensionMetadata { + pub(crate) source_kind: String, + pub(crate) control_files: Vec, + pub(crate) dependencies: Vec, + pub(crate) native_dependencies: Vec, + pub(crate) load_order: Vec, + pub(crate) lifecycle: ManifestExtensionLifecycle, + pub(crate) smoke_status: ManifestExtensionSmokeStatus, +} + +#[derive(Debug, Clone)] +pub(crate) struct ManifestExtensionLifecycle { + pub(crate) create_extension: bool, + pub(crate) create_schema: Option, + pub(crate) load_sql: Vec, + pub(crate) post_create_sql: Vec, + pub(crate) startup_config: Vec, + pub(crate) preload_required: bool, + pub(crate) restart_required: bool, + pub(crate) shared_memory_required: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct ManifestExtensionSmokeStatus { + pub(crate) promoted: bool, + pub(crate) direct: String, + pub(crate) server: String, + pub(crate) restart: String, + pub(crate) dump_restore: String, +} + +fn write_catalog(text: &str) -> Result<()> { + let path = Path::new(CATALOG_PATH); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; + } + fs::write(path, format!("{text}\n")).with_context(|| format!("write {}", path.display())) +} + +fn write_build_plan_files(catalog: &ExtensionCatalog) -> Result<()> { + let texts = build_plan_texts(catalog)?; + for (path, text) in [ + (BUILD_PLAN_PATH, texts.json), + (CONTRIB_BUILD_PLAN_PATH, texts.contrib_tsv), + (PGXS_BUILD_PLAN_PATH, texts.pgxs_tsv), + ] { + let path = Path::new(path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; + } + fs::write(path, text).with_context(|| format!("write {}", path.display()))?; + } + Ok(()) +} + +fn build_plan_texts(catalog: &ExtensionCatalog) -> Result { + let plan = build_plan(catalog)?; + let json = + serde_json::to_string_pretty(&plan).context("serialize extension build plan")? + "\n"; + let mut contrib_tsv = "# id\tsql_name\tcontrib_dir\tmodule_file\tarchive\tstable\n".to_owned(); + let mut pgxs_tsv = + "# id\tsql_name\tsource_dir\tmodule_file\tarchive\tstable\tmake_args\n".to_owned(); + for extension in &plan.extensions { + match extension.build_kind.as_str() { + "postgres-contrib" => { + let contrib_dir = extension.contrib_dir.as_deref().ok_or_else(|| { + anyhow!("contrib extension {} has no contrib_dir", extension.id) + })?; + contrib_tsv.push_str(&format!( + "{}\t{}\t{}\t{}\t{}\t{}\n", + extension.id, + extension.sql_name, + contrib_dir, + extension.module_file.as_deref().unwrap_or("-"), + extension.archive, + extension.stable + )); + } + "pgxs-external" => { + pgxs_tsv.push_str(&format!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\n", + extension.id, + extension.sql_name, + extension.source_dir, + extension.module_file.as_deref().unwrap_or("-"), + extension.archive, + extension.stable, + shell_words(&extension.make_args) + )); + } + "postgis" => {} + other => bail!( + "extension {} has unsupported build kind {other}", + extension.id + ), + } + } + Ok(BuildPlanTexts { + json, + contrib_tsv, + pgxs_tsv, + }) +} + +fn build_plan(catalog: &ExtensionCatalog) -> Result { + let specs = build_specs(catalog)?; + Ok(ExtensionBuildPlan { + format_version: 1, + generated_from: vec![ + CatalogInput { + name: "extension-catalog".to_owned(), + path: CATALOG_PATH.to_owned(), + }, + CatalogInput { + name: "promotion-config".to_owned(), + path: PROMOTION_CONFIG_PATH.to_owned(), + }, + CatalogInput { + name: "asset-manifest-evidence".to_owned(), + path: ASSET_MANIFEST.to_owned(), + }, + ], + extensions: specs + .into_iter() + .map(|spec| ExtensionBuildPlanEntry { + id: spec.id, + sql_name: spec.sql_name, + display_name: spec.display_name, + source_kind: spec.source_kind, + build_kind: spec.build_kind, + source_dir: spec.source_dir, + make_args: spec.make_args, + contrib_dir: spec.contrib_dir, + module_file: spec.module_file, + archive: spec.archive, + control_file: spec.control_file, + stable: spec.stable, + dependencies: spec.dependencies, + native_dependencies: spec.native_dependencies, + load_order: spec.load_order, + lifecycle: spec.lifecycle, + smoke: spec.smoke, + tests: spec.tests, + }) + .collect(), + }) +} + +fn write_generated_extension_api(catalog: &ExtensionCatalog) -> Result<()> { + let promoted = promoted_extensions(catalog); + let candidates = packaged_extensions(catalog); + let mut text = String::new(); + text.push_str("// @generated by `cargo run -p xtask -- extensions generate`\n\n"); + text.push_str("use super::{Extension, ExtensionSetup};\n\n"); + text.push_str("const EMPTY_SQL_NAMES: &[&str] = &[];\n"); + text.push_str("const EMPTY_SQL: &[&str] = &[];\n\n"); + + for extension in &candidates { + let prefix = extension.rust_constant.as_str(); + let candidate_const = format!("CANDIDATE_{prefix}"); + let dependencies = api_dependencies(extension); + if dependencies.is_empty() { + text.push_str(&format!( + "const {candidate_const}_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES;\n" + )); + } else { + text.push_str(&format!( + "const {candidate_const}_DEPENDENCIES: &[&str] = &{};\n", + rust_string_array(&dependencies) + )); + } + if extension.lifecycle.load_sql.is_empty() { + text.push_str(&format!( + "const {candidate_const}_LOAD_SQL: &[&str] = EMPTY_SQL;\n" + )); + } else { + text.push_str(&format!( + "const {candidate_const}_LOAD_SQL: &[&str] = &{};\n", + rust_string_array(&extension.lifecycle.load_sql) + )); + } + if extension.lifecycle.post_create_sql.is_empty() { + text.push_str(&format!( + "const {candidate_const}_POST_CREATE_SQL: &[&str] = EMPTY_SQL;\n" + )); + } else { + text.push_str(&format!( + "const {candidate_const}_POST_CREATE_SQL: &[&str] = &{};\n", + rust_string_array(&extension.lifecycle.post_create_sql) + )); + } + text.push('\n'); + let archive = extension + .promotion + .archive + .as_deref() + .ok_or_else(|| anyhow!("packaged extension {} is missing archive", extension.id))?; + text.push_str(&format!( + "pub(crate) const {candidate_const}: Extension = Extension::new(\n {:?},\n {:?},\n {:?},\n {},\n {},\n {candidate_const}_DEPENDENCIES,\n ExtensionSetup::new(\n {},\n {},\n {candidate_const}_LOAD_SQL,\n {candidate_const}_POST_CREATE_SQL,\n ),\n);\n\n", + extension.display_name, + extension.sql_name, + archive, + option_string_literal(extension.native_module_file.as_deref()), + option_string_literal( + extension + .native_module_file + .as_ref() + .map(|_| format!("extension:{}", extension.sql_name)) + .as_deref() + ), + extension.lifecycle.create_extension, + option_string_literal(extension.lifecycle.create_schema.as_deref()), + )); + } + + for extension in &promoted { + let prefix = extension.rust_constant.as_str(); + text.push_str(&format!( + "pub const {prefix}: Extension = CANDIDATE_{prefix};\n" + )); + } + if !promoted.is_empty() { + text.push('\n'); + } + + let all = promoted + .iter() + .map(|extension| extension.rust_constant.as_str()) + .collect::>() + .join(", "); + text.push_str(&format!("pub const ALL: &[Extension] = &[{all}];\n")); + let candidates_all = candidates + .iter() + .map(|extension| format!("CANDIDATE_{}", extension.rust_constant)) + .collect::>() + .join(", "); + text.push_str(&format!( + "pub(crate) const CANDIDATES: &[Extension] = &[{candidates_all}];\n" + )); + + fs::write("src/pglite/generated_extensions.rs", text) + .context("write src/pglite/generated_extensions.rs") +} + +fn promoted_extensions(catalog: &ExtensionCatalog) -> Vec<&ExtensionCatalogEntry> { + catalog + .extensions + .iter() + .filter(|extension| extension.promotion.promoted) + .collect() +} + +fn packaged_extensions(catalog: &ExtensionCatalog) -> Vec<&ExtensionCatalogEntry> { + catalog + .extensions + .iter() + .filter(|extension| { + extension.promotion.requested + && extension.promotion.packaged + && extension.promotion.archive.is_some() + }) + .collect() +} + +fn rust_string_array(values: &[String]) -> String { + let items = values + .iter() + .map(|value| format!("{value:?}")) + .collect::>() + .join(", "); + format!("[{items}]") +} + +fn option_string_literal(value: Option<&str>) -> String { + value + .map(|value| format!("Some({value:?})")) + .unwrap_or_else(|| "None".to_owned()) +} + +fn discover_catalog() -> Result { + let repl = parse_repl_exports(Path::new(PGLITE_REPL_EXTENSIONS))?; + let docs = parse_docs_catalog(Path::new(PGLITE_DOCS_EXTENSIONS))?; + let package_exports = parse_package_exports(Path::new(PGLITE_PACKAGE_JSON))?; + let promotion_requests = parse_promotion_config(Path::new(PROMOTION_CONFIG_PATH))?; + let smoke_evidence = parse_smoke_config(Path::new(SMOKE_CONFIG_PATH))?; + let packaged = parse_packaged_manifest(Path::new(ASSET_MANIFEST))?; + let submodules = parse_other_extension_submodules(Path::new(POSTGRES_OTHER_EXTENSIONS))?; + + let mut ids = BTreeSet::new(); + ids.extend(repl.keys().cloned()); + ids.extend(docs.keys().filter(|id| id.as_str() != "live").cloned()); + + let mut entries = Vec::new(); + for id in ids { + let repl_export = repl.get(&id); + let docs_entry = docs.get(&id); + let sql_name = discover_sql_name(&id, repl_export, docs_entry)?; + let control_file = discover_control_file(&id, &sql_name, repl_export); + let control = control_file + .as_ref() + .filter(|path| path.is_file()) + .map(|path| parse_control_file(path)) + .transpose()?; + let tests = discover_test_paths(&id); + let lifecycle = classify_lifecycle(&id, control.as_ref()); + let dependencies = discover_dependencies(&id, control.as_ref()); + let source_kind = classify_source_kind(&id, repl_export, docs_entry); + let native_module_file = + discover_native_module_file(&id, &sql_name, source_kind, control.as_ref())?; + let request = promotion_requests + .get(id.as_str()) + .or_else(|| promotion_requests.get(sql_name.as_str())); + let asset = packaged.get(sql_name.as_str()); + let archive = asset + .and_then(|asset| asset.archive.clone()) + .or_else(|| request.and_then(|request| request.archive.clone())) + .or_else(|| request.map(|_| format!("extensions/{sql_name}.tar.zst"))); + let requested = request.map(|request| request.build).unwrap_or(false); + let stable = request.map(|request| request.stable).unwrap_or(false); + let blocker = request.and_then(|request| request.blocker.clone()); + let packaged = asset.is_some(); + let asset_stable = asset.map(|asset| asset.stable).unwrap_or(false); + let smoke = smoke_evidence + .get(id.as_str()) + .or_else(|| smoke_evidence.get(sql_name.as_str())) + .cloned() + .unwrap_or_default(); + let promotion = PromotionStatus { + configured: request.is_some(), + requested, + packaged, + promoted: requested + && stable + && packaged + && asset_stable + && smoke.direct == "passed" + && smoke.server == "passed" + && smoke.restart == "passed", + stable, + archive, + module_sha256: asset.and_then(|asset| asset.module_sha256.clone()), + blocker, + }; + let mut notes = Vec::new(); + if id == "live" { + notes.push("PGlite plugin, not a SQL extension".to_owned()); + } + if control_file.as_ref().is_none_or(|path| !path.is_file()) + && lifecycle.create_extension + && source_kind != "pglite-plugin" + { + notes.push("control file unavailable in current checkout; source submodule may not be initialized".to_owned()); + } + if let Some(submodule) = submodules.get(&id) { + notes.push(format!( + "postgres-pglite submodule {} pinned at {}", + submodule.url, submodule.commit + )); + } + if let Some(blocker) = &promotion.blocker { + notes.push(format!("promotion blocker: {blocker}")); + } + + entries.push(ExtensionCatalogEntry { + id: id.clone(), + sql_name, + rust_constant: rust_constant_name(&id), + display_name: docs_entry + .map(|entry| entry.name.clone()) + .unwrap_or_else(|| id.clone()), + source_kind: source_kind.to_owned(), + pglite_import_name: repl_export + .map(|entry| entry.import_name.clone()) + .or_else(|| docs_entry.map(|entry| entry.import_name.clone())) + .unwrap_or_else(|| id.clone()), + pglite_import_path: repl_export + .map(|entry| entry.import_path.clone()) + .or_else(|| docs_entry.map(|entry| entry.import_path.clone())), + package_export: package_exports.get(&id).cloned().or_else(|| { + (source_kind == "postgres-contrib") + .then(|| package_exports.get("*").map(|_| format!("./contrib/{id}"))) + .flatten() + }), + tags: docs_entry + .map(|entry| entry.tags.clone()) + .unwrap_or_default(), + bundle_size: docs_entry.and_then(|entry| entry.size), + control_file: control_file + .filter(|path| path.is_file()) + .map(|path| normalize_path(&path)), + control, + dependencies, + native_dependencies: Vec::new(), + load_order: known_load_order(&id), + lifecycle, + smoke, + tests, + native_module_file, + promotion, + notes, + }); + } + + entries.sort_by(|left, right| left.id.cmp(&right.id)); + + Ok(ExtensionCatalog { + format_version: 1, + generated_from: vec![ + CatalogInput { + name: "pglite-repl-exports".to_owned(), + path: PGLITE_REPL_EXTENSIONS.to_owned(), + }, + CatalogInput { + name: "pglite-docs-catalog".to_owned(), + path: PGLITE_DOCS_EXTENSIONS.to_owned(), + }, + CatalogInput { + name: "pglite-package-exports".to_owned(), + path: PGLITE_PACKAGE_JSON.to_owned(), + }, + CatalogInput { + name: "pglite-contrib-modules".to_owned(), + path: PGLITE_CONTRIB_SRC.to_owned(), + }, + CatalogInput { + name: "postgres-contrib".to_owned(), + path: POSTGRES_CONTRIB.to_owned(), + }, + CatalogInput { + name: "postgres-pglite-other-extensions".to_owned(), + path: POSTGRES_OTHER_EXTENSIONS.to_owned(), + }, + CatalogInput { + name: "extension-promotion-config".to_owned(), + path: PROMOTION_CONFIG_PATH.to_owned(), + }, + CatalogInput { + name: "extension-smoke-evidence".to_owned(), + path: SMOKE_CONFIG_PATH.to_owned(), + }, + CatalogInput { + name: "asset-manifest-evidence".to_owned(), + path: ASSET_MANIFEST.to_owned(), + }, + ], + extensions: entries, + }) +} + +fn validate_catalog(catalog: &ExtensionCatalog) -> Result<()> { + ensure!( + catalog.format_version == 1, + "extension catalog format must be 1" + ); + let mut ids = BTreeSet::new(); + let mut sql_names = BTreeSet::new(); + for extension in &catalog.extensions { + ensure!( + ids.insert(extension.id.as_str()), + "duplicate extension id {}", + extension.id + ); + ensure!( + extension.id != "live", + "live must not be included in SQL extension catalog" + ); + ensure!( + extension.promotion.configured, + "{} is missing from {}; every discovered SQL extension must be explicitly build-requested or blocked", + extension.id, + PROMOTION_CONFIG_PATH + ); + ensure!( + extension.promotion.requested || extension.promotion.blocker.is_some(), + "{} is not build-requested and has no blocker in {}", + extension.id, + PROMOTION_CONFIG_PATH + ); + ensure!( + sql_names.insert(extension.sql_name.as_str()), + "duplicate SQL extension name {}", + extension.sql_name + ); + ensure!( + !extension.promotion.promoted || extension.promotion.stable, + "{} cannot be promoted without stable=true", + extension.id + ); + if extension.promotion.requested { + ensure!( + extension.promotion.archive.is_some(), + "requested extension {} must resolve to an archive path", + extension.id + ); + ensure!( + extension.source_kind != "pglite-plugin", + "requested extension {} is not a SQL extension", + extension.id + ); + ensure!( + extension.lifecycle.create_extension || !extension.lifecycle.load_sql.is_empty(), + "requested extension {} must declare a lifecycle operation", + extension.id + ); + } + if extension.promotion.promoted { + ensure!( + !extension.tests.is_empty(), + "promoted extension {} must have a smoke test source", + extension.id + ); + ensure!( + extension.lifecycle.create_extension || !extension.lifecycle.load_sql.is_empty(), + "promoted extension {} must declare a lifecycle operation", + extension.id + ); + } + for dependency in &extension.dependencies { + if runtime_provided_sql_extensions().contains(&dependency.as_str()) { + continue; + } + ensure!( + catalog + .extensions + .iter() + .any(|candidate| candidate.sql_name == *dependency + || candidate.id == *dependency), + "{} depends on unknown extension {}", + extension.id, + dependency + ); + if extension.promotion.promoted { + ensure!( + catalog.extensions.iter().any(|candidate| { + candidate.promotion.promoted + && (candidate.sql_name == *dependency || candidate.id == *dependency) + }), + "promoted extension {} depends on unpromoted extension {}", + extension.id, + dependency + ); + } + if extension.promotion.requested { + ensure!( + catalog.extensions.iter().any(|candidate| { + candidate.promotion.requested + && (candidate.sql_name == *dependency || candidate.id == *dependency) + }), + "requested extension {} depends on unrequested extension {}", + extension.id, + dependency + ); + } + } + } + + for required in [ + "vector", "pg_trgm", "hstore", "pgcrypto", "pgtap", "postgis", + ] { + ensure!( + catalog + .extensions + .iter() + .any(|extension| extension.id == required || extension.sql_name == required), + "extension catalog is missing required PGlite extension {required}" + ); + } + Ok(()) +} + +fn api_dependencies(extension: &ExtensionCatalogEntry) -> Vec { + extension + .dependencies + .iter() + .filter(|dependency| !runtime_provided_sql_extensions().contains(&dependency.as_str())) + .cloned() + .collect() +} + +fn runtime_provided_sql_extensions() -> &'static [&'static str] { + &["plpgsql"] +} + +fn parse_repl_exports(path: &Path) -> Result> { + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let mut exports = BTreeMap::new(); + for line in text.lines() { + let line = line.trim(); + let Some(rest) = line.strip_prefix("export { ") else { + continue; + }; + let Some((name, module_part)) = rest.split_once(" } from ") else { + continue; + }; + let import_path = strip_quoted(module_part.trim()) + .ok_or_else(|| anyhow!("could not parse export module from {line:?}"))?; + exports.insert( + name.to_owned(), + ReplExport { + import_name: name.to_owned(), + import_path: import_path.to_owned(), + }, + ); + } + Ok(exports) +} + +fn parse_docs_catalog(path: &Path) -> Result> { + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let mut entries = BTreeMap::new(); + let mut current = DocsCatalogEntryBuilder::default(); + let mut in_entry = false; + for line in text.lines() { + let trimmed = line.trim(); + if trimmed == "{" { + in_entry = true; + current = DocsCatalogEntryBuilder::default(); + continue; + } + if !in_entry { + continue; + } + if trimmed == "}," || trimmed == "}" { + if let Some(entry) = std::mem::take(&mut current).finish() { + entries.insert(entry.import_name.clone(), entry); + } + in_entry = false; + continue; + } + if let Some(value) = parse_string_field(trimmed, "name") { + current.name = Some(value.to_owned()); + } else if let Some(value) = parse_string_field(trimmed, "importPath") { + current.import_path = Some(value.to_owned()); + } else if let Some(value) = parse_string_field(trimmed, "importName") { + current.import_name = Some(value.to_owned()); + } else if let Some(value) = parse_u64_field(trimmed, "size") { + current.size = Some(value); + } else if let Some(tags) = parse_tags_field(trimmed) { + current.tags = tags; + } + } + Ok(entries) +} + +fn parse_package_exports(path: &Path) -> Result> { + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let json: serde_json::Value = + serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))?; + let mut exports = BTreeMap::new(); + let Some(map) = json.get("exports").and_then(|value| value.as_object()) else { + return Ok(exports); + }; + for key in map.keys() { + let Some(name) = key.strip_prefix("./") else { + continue; + }; + if name == "contrib" { + continue; + } + if let Some(name) = name.strip_prefix("contrib/") { + exports.insert(name.to_owned(), key.clone()); + } else { + exports.insert(name.to_owned(), key.clone()); + } + } + Ok(exports) +} + +fn parse_promotion_config(path: &Path) -> Result> { + if !path.exists() { + return Ok(BTreeMap::new()); + } + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let config: PromotionConfig = + toml::from_str(&text).with_context(|| format!("parse {}", path.display()))?; + ensure!( + config.format_version == 1, + "{} format_version must be 1", + path.display() + ); + let mut requests = BTreeMap::new(); + for request in config.extensions { + ensure!(!request.id.is_empty(), "promotion request has empty id"); + ensure!( + requests.insert(request.id.clone(), request).is_none(), + "duplicate promotion request" + ); + } + Ok(requests) +} + +fn parse_smoke_config(path: &Path) -> Result> { + if !path.exists() { + return Ok(BTreeMap::new()); + } + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let config: SmokeConfig = + toml::from_str(&text).with_context(|| format!("parse {}", path.display()))?; + ensure!( + config.format_version == 1, + "{} format_version must be 1", + path.display() + ); + let mut evidence = BTreeMap::new(); + for mut extension in config.extensions { + ensure!(!extension.id.is_empty(), "smoke evidence has empty id"); + normalize_smoke_statuses(&mut extension); + ensure_valid_smoke_status(&extension.direct, &extension.id, "direct")?; + ensure_valid_smoke_status(&extension.server, &extension.id, "server")?; + ensure_valid_smoke_status(&extension.restart, &extension.id, "restart")?; + ensure_valid_smoke_status(&extension.dump_restore, &extension.id, "dump-restore")?; + ensure!( + evidence + .insert( + extension.id.clone(), + ExtensionSmokeEvidence::from(extension) + ) + .is_none(), + "duplicate smoke evidence" + ); + } + Ok(evidence) +} + +fn normalize_smoke_statuses(extension: &mut SmokeConfigExtension) { + if extension.direct.is_empty() { + extension.direct = "not-run".to_owned(); + } + if extension.server.is_empty() { + extension.server = "not-run".to_owned(); + } + if extension.restart.is_empty() { + extension.restart = "not-run".to_owned(); + } + if extension.dump_restore.is_empty() { + extension.dump_restore = "not-run".to_owned(); + } +} + +fn ensure_valid_smoke_status(status: &str, id: &str, field: &str) -> Result<()> { + ensure!( + matches!(status, "passed" | "failed" | "not-run" | "blocked"), + "extension {id} has invalid smoke status for {field}: {status}" + ); + Ok(()) +} + +fn parse_packaged_manifest(path: &Path) -> Result> { + if !path.exists() { + return Ok(BTreeMap::new()); + } + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let manifest: AssetManifest = + serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))?; + Ok(manifest + .extensions + .into_iter() + .map(|extension| { + ( + extension.sql_name, + PackagedExtension { + archive: Some(extension.archive), + module_sha256: Some(extension.module_sha256), + stable: extension.stable, + }, + ) + }) + .collect()) +} + +fn parse_other_extension_submodules(path: &Path) -> Result> { + let gitmodules = path + .parent() + .and_then(Path::parent) + .map(|root| root.join(".gitmodules")) + .ok_or_else(|| anyhow!("could not resolve postgres-pglite .gitmodules"))?; + let gitmodules_text = fs::read_to_string(&gitmodules) + .with_context(|| format!("read {}", gitmodules.display()))?; + let mut urls = BTreeMap::new(); + let mut current: Option = None; + for line in gitmodules_text.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("[submodule ") { + current = trimmed + .split('"') + .nth(1) + .and_then(|value| value.strip_prefix("pglite/other_extensions/")) + .map(str::to_owned); + } else if let Some(url) = trimmed.strip_prefix("url = ") + && let Some(name) = ¤t + { + urls.insert(name.clone(), url.to_owned()); + } + } + + let status = std::process::Command::new("git") + .args([ + "-C", + "assets/checkouts/postgres-pglite", + "submodule", + "status", + ]) + .output() + .context("read postgres-pglite submodule status")?; + let status_text = String::from_utf8(status.stdout).context("submodule status utf8")?; + let mut pins = BTreeMap::new(); + for line in status_text.lines() { + let trimmed = line.trim_start_matches(['-', '+', ' ']); + let mut parts = trimmed.split_whitespace(); + let Some(commit) = parts.next() else { + continue; + }; + let Some(path) = parts.next() else { + continue; + }; + let Some(name) = path.strip_prefix("pglite/other_extensions/") else { + continue; + }; + if let Some(url) = urls.get(name) { + pins.insert( + name.to_owned(), + SubmodulePin { + url: url.clone(), + commit: commit.to_owned(), + }, + ); + } + } + Ok(pins) +} + +fn discover_sql_name( + id: &str, + repl_export: Option<&ReplExport>, + _docs_entry: Option<&DocsCatalogEntry>, +) -> Result { + if id == "uuid_ossp" { + return Ok("uuid-ossp".to_owned()); + } + if id == "vector" { + return Ok("vector".to_owned()); + } + let control_file = discover_control_file(id, id, repl_export); + if let Some(path) = control_file + && path.is_file() + && let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) + { + return Ok(stem.to_owned()); + } + Ok(id.to_owned()) +} + +fn discover_control_file( + id: &str, + sql_name: &str, + repl_export: Option<&ReplExport>, +) -> Option { + let candidates = if repl_export + .map(|entry| entry.import_path.contains("/contrib/")) + .unwrap_or(false) + { + let dashed_id = id.replace('_', "-"); + vec![ + Path::new(POSTGRES_CONTRIB) + .join(id) + .join(format!("{sql_name}.control")), + Path::new(POSTGRES_CONTRIB) + .join(id) + .join(format!("{id}.control")), + Path::new(POSTGRES_CONTRIB) + .join(id) + .join(format!("{dashed_id}.control")), + Path::new(POSTGRES_CONTRIB) + .join(&dashed_id) + .join(format!("{sql_name}.control")), + Path::new(POSTGRES_CONTRIB) + .join(&dashed_id) + .join(format!("{dashed_id}.control")), + ] + } else { + vec![ + Path::new(&extension_source_dir_for( + id, + classify_source_kind(id, repl_export, None), + )) + .join(format!("{sql_name}.control")), + ] + }; + candidates.into_iter().find(|path| path.is_file()) +} + +fn parse_control_file(path: &Path) -> Result { + let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; + let mut control = ControlMetadata::default(); + for line in text.lines() { + let line = line.split('#').next().unwrap_or_default().trim(); + if line.is_empty() { + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + let value = strip_quoted(value.trim()) + .unwrap_or_else(|| value.trim().trim_matches('"')) + .to_owned(); + match key { + "default_version" => control.default_version = Some(value), + "module_pathname" => control.module_pathname = Some(value), + "requires" => { + control.requires = value + .split(',') + .map(|item| item.trim().trim_matches('"').to_owned()) + .filter(|item| !item.is_empty()) + .collect(); + } + "relocatable" => control.relocatable = Some(value), + "schema" => control.schema = Some(value), + _ => {} + } + } + Ok(control) +} + +fn discover_dependencies(id: &str, control: Option<&ControlMetadata>) -> Vec { + let mut dependencies = BTreeSet::new(); + if let Some(control) = control { + dependencies.extend(control.requires.iter().cloned()); + } + if id == "earthdistance" { + dependencies.insert("cube".to_owned()); + } + dependencies.into_iter().collect() +} + +fn discover_native_module_file( + id: &str, + sql_name: &str, + source_kind: &str, + control: Option<&ControlMetadata>, +) -> Result> { + if let Some(module_pathname) = control.and_then(|control| control.module_pathname.as_deref()) { + return Ok(module_pathname_to_file(module_pathname)); + } + + let source_dir = extension_source_dir_for(id, source_kind); + if source_dir.is_empty() { + return Ok(None); + } + let makefile = Path::new(&source_dir).join("Makefile"); + if !makefile.is_file() { + return Ok(None); + } + discover_native_module_file_from_makefile(&makefile, sql_name) +} + +fn module_pathname_to_file(module_pathname: &str) -> Option { + let value = module_pathname + .strip_prefix("$libdir/") + .or_else(|| module_pathname.strip_prefix("${libdir}/")) + .unwrap_or(module_pathname) + .trim(); + if value.is_empty() { + return None; + } + let file = Path::new(value) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(value); + if file.ends_with(".so") { + Some(file.to_owned()) + } else { + Some(format!("{file}.so")) + } +} + +fn discover_native_module_file_from_makefile( + makefile: &Path, + sql_name: &str, +) -> Result> { + let text = + fs::read_to_string(makefile).with_context(|| format!("read {}", makefile.display()))?; + let mut variables = BTreeMap::new(); + for line in text.lines() { + let line = line.split('#').next().unwrap_or_default().trim(); + if line.is_empty() || line.starts_with("ifeq") || line.starts_with("ifneq") { + continue; + } + let Some((key, value)) = parse_make_assignment(line) else { + continue; + }; + variables.insert(key.to_owned(), value.to_owned()); + } + + for key in ["MODULE_big", "MODULES"] { + let Some(value) = variables.get(key) else { + continue; + }; + let expanded = expand_make_value(value, &variables, 0); + let modules = expanded + .split_whitespace() + .filter(|item| !item.is_empty()) + .collect::>(); + if modules.is_empty() { + continue; + } + let selected = modules + .iter() + .copied() + .find(|module| *module == sql_name) + .unwrap_or(modules[0]); + return Ok(Some(if selected.ends_with(".so") { + selected.to_owned() + } else { + format!("{selected}.so") + })); + } + Ok(None) +} + +fn parse_make_assignment(line: &str) -> Option<(&str, &str)> { + for operator in [":=", "?=", "="] { + if let Some((key, value)) = line.split_once(operator) { + let key = key.trim(); + if key + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_') + { + return Some((key, value.trim())); + } + } + } + None +} + +fn expand_make_value(value: &str, variables: &BTreeMap, depth: usize) -> String { + if depth > 8 { + return value.to_owned(); + } + let mut out = String::new(); + let mut rest = value; + while let Some(index) = rest.find('$') { + out.push_str(&rest[..index]); + rest = &rest[index..]; + let Some(open) = rest.chars().nth(1) else { + out.push('$'); + rest = &rest[1..]; + continue; + }; + let close = match open { + '(' => ')', + '{' => '}', + _ => { + out.push('$'); + rest = &rest[1..]; + continue; + } + }; + let Some(close_index) = rest.find(close) else { + out.push('$'); + rest = &rest[1..]; + continue; + }; + let key = &rest[2..close_index]; + if let Some(replacement) = variables.get(key) { + out.push_str(&expand_make_value(replacement, variables, depth + 1)); + } else { + out.push_str(&rest[..=close_index]); + } + rest = &rest[close_index + 1..]; + } + out.push_str(rest); + out +} + +fn classify_lifecycle(id: &str, control: Option<&ControlMetadata>) -> ExtensionLifecycle { + let mut lifecycle = ExtensionLifecycle { + create_extension: id != "auto_explain", + create_schema: Some( + control + .and_then(|control| control.schema.clone()) + .unwrap_or_else(|| "pg_catalog".to_owned()), + ), + load_sql: Vec::new(), + post_create_sql: Vec::new(), + startup_config: Vec::new(), + preload_required: false, + restart_required: false, + shared_memory_required: false, + }; + match id { + "auto_explain" => { + lifecycle.create_extension = false; + lifecycle.create_schema = None; + lifecycle.load_sql = vec![ + "LOAD 'auto_explain';".to_owned(), + "SET auto_explain.log_min_duration = '0';".to_owned(), + "SET auto_explain.log_analyze = 'true';".to_owned(), + "SET auto_explain.log_level = 'NOTICE';".to_owned(), + ]; + } + "age" => { + lifecycle.load_sql.push("LOAD 'age';".to_owned()); + lifecycle + .post_create_sql + .push("SET search_path = ag_catalog, \"$user\", public;".to_owned()); + } + _ => {} + } + lifecycle +} + +fn classify_source_kind( + id: &str, + repl_export: Option<&ReplExport>, + docs_entry: Option<&DocsCatalogEntry>, +) -> &'static str { + if id == "live" + || docs_entry + .map(|entry| entry.tags.iter().any(|tag| tag == "pglite plugin")) + .unwrap_or(false) + { + "pglite-plugin" + } else if repl_export + .map(|entry| entry.import_path.contains("/contrib/")) + .unwrap_or(false) + { + "postgres-contrib" + } else if id == "postgis" { + "postgis" + } else { + "pglite-other-extension" + } +} + +fn discover_test_paths(id: &str) -> Vec { + let mut paths = Vec::new(); + let mut test_names = vec![id.to_owned()]; + if id == "vector" { + test_names.push("pgvector".to_owned()); + } + for test_name in test_names { + for candidate in [ + Path::new(PGLITE_TESTS) + .join("contrib") + .join(format!("{test_name}.test.js")), + Path::new(PGLITE_TESTS) + .join("contrib") + .join(format!("{test_name}.test.ts")), + Path::new(PGLITE_TESTS).join(format!("{test_name}.test.js")), + Path::new(PGLITE_TESTS).join(format!("{test_name}.test.ts")), + Path::new(PGLITE_POSTGIS_TESTS).join(format!("{test_name}.test.ts")), + Path::new(PGLITE_POSTGIS_TESTS).join(format!("{test_name}.test.js")), + ] { + if candidate.is_file() { + paths.push(normalize_path(&candidate)); + } + } + } + paths.sort(); + paths.dedup(); + paths +} + +fn known_load_order(id: &str) -> Vec { + match id { + "postgis" => vec![ + "lib/postgresql/postgis-3.so".to_owned(), + "lib/postgresql/postgis_topology-3.so".to_owned(), + "lib/postgresql/postgis_raster-3.so".to_owned(), + ], + _ => Vec::new(), + } +} + +fn build_kind(extension: &ExtensionCatalogEntry) -> &'static str { + match extension.source_kind.as_str() { + "postgres-contrib" => "postgres-contrib", + "pglite-other-extension" => "pgxs-external", + "postgis" => "postgis", + _ => "unsupported", + } +} + +fn extension_source_dir(extension: &ExtensionCatalogEntry) -> String { + extension_source_dir_for(&extension.id, &extension.source_kind) +} + +fn pgxs_make_args(extension: &ExtensionCatalogEntry) -> Vec { + match extension.id.as_str() { + // AGE's graphid SQL is target-ABI sensitive. wasm32/WASIX has a 4-byte + // Datum, so AGE must generate pass-by-reference graphid SQL. + "age" => vec!["SIZEOF_DATUM=4".to_owned()], + _ => Vec::new(), + } +} + +fn extension_source_dir_for(id: &str, source_kind: &str) -> String { + match source_kind { + "postgres-contrib" => Path::new(POSTGRES_CONTRIB) + .join(extension_contrib_dir_name(id)) + .to_string_lossy() + .replace('\\', "/"), + "pglite-other-extension" if id == "vector" => PGVECTOR_CHECKOUT.to_owned(), + "pglite-other-extension" | "postgis" => Path::new(EXTERNAL_EXTENSION_CHECKOUT_ROOT) + .join(id) + .to_string_lossy() + .replace('\\', "/"), + _ => String::new(), + } +} + +fn extension_contrib_dir_name(id: &str) -> String { + match id { + "uuid_ossp" => "uuid-ossp".to_owned(), + other => other.to_owned(), + } +} + +fn parse_string_field<'a>(line: &'a str, field: &str) -> Option<&'a str> { + let rest = line.strip_prefix(&format!("{field}: "))?; + strip_quoted(rest.trim_end_matches(',').trim()) +} + +fn parse_u64_field(line: &str, field: &str) -> Option { + let rest = line.strip_prefix(&format!("{field}: "))?; + rest.trim_end_matches(',').trim().parse().ok() +} + +fn parse_tags_field(line: &str) -> Option> { + let rest = line.strip_prefix("tags: ")?; + let rest = rest.trim().trim_end_matches(',').trim(); + let rest = rest.strip_prefix('[')?.strip_suffix(']')?; + Some( + rest.split(',') + .filter_map(|item| strip_quoted(item.trim()).map(str::to_owned)) + .collect(), + ) +} + +fn strip_quoted(value: &str) -> Option<&str> { + let value = value.trim().trim_end_matches(','); + if value.len() < 2 { + return None; + } + let quote = value.as_bytes()[0] as char; + if quote != '\'' && quote != '"' { + return None; + } + value + .strip_prefix(quote) + .and_then(|value| value.strip_suffix(quote)) +} + +fn rust_constant_name(id: &str) -> String { + id.chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() { + ch.to_ascii_uppercase() + } else { + '_' + } + }) + .collect() +} + +fn normalize_path(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +fn shell_words(words: &[String]) -> String { + if words.is_empty() { + "-".to_owned() + } else { + words.join(" ") + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct ExtensionCatalog { + format_version: u32, + generated_from: Vec, + extensions: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct CatalogInput { + name: String, + path: String, +} + +struct BuildPlanTexts { + json: String, + contrib_tsv: String, + pgxs_tsv: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct ExtensionBuildPlan { + format_version: u32, + generated_from: Vec, + extensions: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct ExtensionBuildPlanEntry { + id: String, + sql_name: String, + display_name: String, + source_kind: String, + build_kind: String, + source_dir: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + make_args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + contrib_dir: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_file: Option, + archive: String, + #[serde(skip_serializing_if = "Option::is_none")] + control_file: Option, + stable: bool, + dependencies: Vec, + native_dependencies: Vec, + load_order: Vec, + lifecycle: ExtensionLifecycle, + smoke: ExtensionSmokeEvidence, + tests: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct ExtensionCatalogEntry { + id: String, + sql_name: String, + rust_constant: String, + display_name: String, + source_kind: String, + pglite_import_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pglite_import_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + package_export: Option, + tags: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + bundle_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + control_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + control: Option, + dependencies: Vec, + native_dependencies: Vec, + load_order: Vec, + lifecycle: ExtensionLifecycle, + smoke: ExtensionSmokeEvidence, + tests: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + native_module_file: Option, + promotion: PromotionStatus, + notes: Vec, +} + +#[derive(Debug, Clone)] +struct ReplExport { + import_name: String, + import_path: String, +} + +#[derive(Debug, Clone)] +struct DocsCatalogEntry { + name: String, + import_path: String, + import_name: String, + tags: Vec, + size: Option, +} + +#[derive(Debug, Default)] +struct DocsCatalogEntryBuilder { + name: Option, + import_path: Option, + import_name: Option, + tags: Vec, + size: Option, +} + +impl DocsCatalogEntryBuilder { + fn finish(self) -> Option { + Some(DocsCatalogEntry { + name: self.name?, + import_path: self.import_path?, + import_name: self.import_name?, + tags: self.tags, + size: self.size, + }) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct ControlMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + default_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_pathname: Option, + requires: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + relocatable: Option, + #[serde(skip_serializing_if = "Option::is_none")] + schema: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct ExtensionLifecycle { + pub(crate) create_extension: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) create_schema: Option, + pub(crate) load_sql: Vec, + pub(crate) post_create_sql: Vec, + pub(crate) startup_config: Vec, + pub(crate) preload_required: bool, + pub(crate) restart_required: bool, + pub(crate) shared_memory_required: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct ExtensionSmokeEvidence { + direct: String, + server: String, + restart: String, + dump_restore: String, +} + +impl Default for ExtensionSmokeEvidence { + fn default() -> Self { + Self { + direct: "not-run".to_owned(), + server: "not-run".to_owned(), + restart: "not-run".to_owned(), + dump_restore: "not-run".to_owned(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +struct PromotionStatus { + configured: bool, + requested: bool, + packaged: bool, + promoted: bool, + stable: bool, + #[serde(skip_serializing_if = "Option::is_none")] + archive: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_sha256: Option, + #[serde(skip_serializing_if = "Option::is_none")] + blocker: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct PromotionConfig { + format_version: u32, + #[serde(default)] + extensions: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct PromotionRequest { + id: String, + #[serde(default = "default_true")] + build: bool, + #[serde(default)] + stable: bool, + #[serde(default)] + archive: Option, + #[serde(default)] + blocker: Option, +} + +fn default_true() -> bool { + true +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct SmokeConfig { + format_version: u32, + #[serde(default)] + extensions: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct SmokeConfigExtension { + id: String, + #[serde(default)] + direct: String, + #[serde(default)] + server: String, + #[serde(default)] + restart: String, + #[serde(default)] + dump_restore: String, +} + +impl From for ExtensionSmokeEvidence { + fn from(value: SmokeConfigExtension) -> Self { + Self { + direct: value.direct, + server: value.server, + restart: value.restart, + dump_restore: value.dump_restore, + } + } +} + +#[derive(Debug, Clone)] +struct PackagedExtension { + archive: Option, + module_sha256: Option, + stable: bool, +} + +#[derive(Debug)] +struct SubmodulePin { + url: String, + commit: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct AssetManifest { + #[serde(default)] + extensions: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct AssetManifestExtension { + sql_name: String, + archive: String, + #[serde(default)] + module_sha256: String, + #[serde(default)] + stable: bool, +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..823c0008 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,10065 @@ +use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::env; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use anyhow::{Context, Result, anyhow, bail, ensure}; +use directories::ProjectDirs; +use futures_util::future::try_join_all; +use pglite_oxide::{ + Pglite, PgliteServer, PhaseTiming, ProtocolStatsSnapshot, capture_phase_timings, + disable_protocol_stats, extensions, fs_trace_snapshot, measure_phase, protocol_stats_snapshot, + record_phase_timing, reset_fs_trace, reset_protocol_stats, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use sqlx::postgres::{PgConnectOptions, PgSslMode}; +use sqlx::{Connection, Executor, Row}; +use walkdir::WalkDir; +use wasmparser::{Dylink0Subsection, ExternalKind, KnownCustom, Parser, Payload, TypeRef}; +use zstd::stream::write::Encoder as ZstdEncoder; + +mod extension_catalog; + +const POSTGRES_PGLITE_SOURCE: &str = "postgres-pglite"; +const POSTGRES_PGLITE_PATH: &str = "assets/checkouts/postgres-pglite"; +const PGLITE_BUILD_SOURCE: &str = "pglite-build"; +const PGLITE_BUILD_PATH: &str = "assets/checkouts/pglite-build"; +const WASIX_BUILD_ROOT: &str = "assets/wasix-build"; +const WASIX_DOCKER_BUILD_DIR: &str = "assets/wasix-build/work/docker-pglite"; +const WASIX_PATCHED_SOURCE_DIR: &str = "assets/wasix-build/work/postgres-pglite-wasix-src"; +const WASIX_BUILD_MANIFEST_PATH: &str = "assets/wasix-build/build/outputs.json"; +const WASIX_PATCH_PATH: &str = "assets/wasix-build/patches/postgres-pglite-wasix-dl.patch"; +const WASIX_BRIDGE_PATH: &str = "assets/wasix-build/wasix_shim/pglite_wasix_bridge.c"; +const DEFAULT_ASSET_BUILD_PROFILE: &str = "release-o3"; +const VALIDATE_XTASK_ENV: &str = "PGLITE_OXIDE_XTASK"; +const PGVECTOR_BUILD_DIR: &str = "assets/checkouts/pgvector"; +const POSTGRES_OTHER_EXTENSIONS: &str = "assets/checkouts/postgres-pglite/pglite/other_extensions"; +const PGLITE_BENCHMARK_SQL_DIR: &str = "assets/checkouts/pglite/packages/benchmark/src"; +const EXPECTED_POSTGRES_PGLITE_BRANCH: &str = "REL_17_5-pglite"; +const EXPECTED_PGLITE_BUILD_BRANCH: &str = "portable"; +const ASSET_INPUT_FINGERPRINT_PATH: &str = "assets/generated/asset-inputs.sha256"; +const GENERATED_ASSETS_DIR: &str = "target/pglite-oxide/assets"; +const ASSET_CRATE_PAYLOAD_DIR: &str = "crates/assets/payload"; +const RELEASE_STAGE_DIR: &str = "target/pglite-oxide/release"; +const LEGACY_STATIC_WASI_ARCHIVE: &str = concat!("assets/", "pglite-", "wasi.tar.zst"); + +#[cfg(feature = "template-runner")] +#[derive(Debug, Default)] +struct LocalOnlyPackageLoader; + +#[cfg(feature = "template-runner")] +#[derive(Debug, Clone)] +struct TailCaptureFile { + inner: std::sync::Arc>, + limit: usize, +} + +#[cfg(feature = "template-runner")] +#[derive(Debug, Default)] +struct TailCaptureState { + bytes: std::collections::VecDeque, +} + +#[cfg(feature = "template-runner")] +#[derive(Debug, Clone)] +struct TailCaptureHandle { + inner: std::sync::Arc>, +} + +#[cfg(feature = "template-runner")] +impl TailCaptureFile { + fn new(limit: usize) -> (Self, TailCaptureHandle) { + let inner = std::sync::Arc::new(std::sync::Mutex::new(TailCaptureState::default())); + ( + Self { + inner: inner.clone(), + limit, + }, + TailCaptureHandle { inner }, + ) + } + + fn push_tail(&self, bytes: &[u8]) { + let Ok(mut state) = self.inner.lock() else { + return; + }; + for byte in bytes { + state.bytes.push_back(*byte); + while state.bytes.len() > self.limit { + state.bytes.pop_front(); + } + } + } +} + +#[cfg(feature = "template-runner")] +impl TailCaptureHandle { + fn text(&self) -> String { + let Ok(state) = self.inner.lock() else { + return "