diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..830dc542 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,4 @@ +[profile.default] +slow-timeout = { period = "10s", terminate-after = 6, grace-period = "0s" } +status-level = "fail" +final-status-level = "fail" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..42c43678 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,27 @@ +name: Bug report +description: Report incorrect behavior in pglite-oxide. +title: "bug: " +labels: ["bug"] +body: + - type: textarea + id: summary + attributes: + label: Summary + description: What happened, and what did you expect? + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction + description: Minimal Rust code or commands that reproduce the issue. + render: rust + validations: + required: true + - type: input + id: versions + attributes: + label: Versions + description: pglite-oxide, Rust, OS, and architecture. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0086358d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..22162a28 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,19 @@ +name: Feature request +description: Suggest a focused API, runtime, or packaging improvement. +title: "feat: " +labels: ["enhancement"] +body: + - type: textarea + id: use_case + attributes: + label: Use case + description: What are you trying to build? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: The API or behavior you want. + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..162d77a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + groups: + cargo-patch: + update-types: [patch] + cargo-minor: + update-types: [minor] + cargo-major: + update-types: [major] + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..d326a4f8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Summary + +## Release Intent + +- [ ] Package/API/runtime change: PR title uses `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, or a breaking `!`. +- [ ] Docs/CI/repository-only change: no release intended. + +## Verification + +- [ ] `cargo fmt --all --check` +- [ ] `cargo clippy --all-targets -- -D warnings` +- [ ] `cargo test --doc` +- [ ] `cargo test --test runtime_smoke -- --nocapture` diff --git a/.github/scripts/check-conventional-commit.sh b/.github/scripts/check-conventional-commit.sh new file mode 100755 index 00000000..afd0b3b6 --- /dev/null +++ b/.github/scripts/check-conventional-commit.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +subject="${1:-}" + +if [[ -z "${subject}" ]]; then + echo "expected a non-empty commit subject or PR title" >&2 + exit 1 +fi + +pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9][a-z0-9._/-]*\))?(!)?: .+' + +if [[ ! "${subject}" =~ ${pattern} ]]; then + cat >&2 <(optional-scope)!: + +Allowed types: + build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test + +Received: + ${subject} +EOF + exit 1 +fi diff --git a/.github/scripts/check-release-intent.sh b/.github/scripts/check-release-intent.sh new file mode 100755 index 00000000..9c24b60e --- /dev/null +++ b/.github/scripts/check-release-intent.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +subject="${1:-}" +base_ref="${2:-origin/main}" +head_ref="${3:-HEAD}" +head_branch="${4:-}" + +if [[ -z "${subject}" ]]; then + echo "expected a non-empty PR title or commit subject" >&2 + exit 1 +fi + +release_pattern='^((feat|fix|perf|refactor|revert)(\([a-z0-9][a-z0-9._/-]*\))?(!)?|[a-z]+(\([a-z0-9][a-z0-9._/-]*\))?!): .+' +release_pr_pattern='^chore\(release\): .+' + +affected_files=() + +while IFS= read -r file; do + [[ -z "${file}" ]] && continue + + case "${file}" in + Cargo.toml | Cargo.lock | build.rs | src/* | assets/* | examples/* | benches/*) + affected_files+=("${file}") + ;; + esac +done < <(git diff --name-only "${base_ref}...${head_ref}" --) + +if (( ${#affected_files[@]} == 0 )); then + exit 0 +fi + +if [[ "${subject}" =~ ${release_pattern} ]]; then + exit 0 +fi + +if [[ "${subject}" =~ ${release_pr_pattern} && "${head_branch}" == release-plz-* ]]; then + exit 0 +fi + +cat >&2 <&2 +exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2a141db8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + checks: + name: Rust checks + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - name: Format + run: cargo fmt --all --check + - name: Check default features + run: cargo check --all-targets --locked + - name: Check no default features + run: cargo check --no-default-features --all-targets --locked + - name: Clippy + run: cargo clippy --all-targets --locked -- -D warnings + - name: Unit and binary tests + run: cargo test --lib --bins --locked + - name: Doctests + run: cargo test --doc --locked + - name: Package + run: cargo package --locked + + runtime-smoke: + name: Embedded Postgres smoke + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92 + - uses: Swatinem/rust-cache@v2 + - name: Runtime smoke + run: cargo test --test runtime_smoke --locked -- --nocapture + - name: Proxy smoke + run: cargo test --test proxy_smoke --locked -- --nocapture + - name: Client compatibility + run: cargo test --test client_compat --locked -- --nocapture + + supply-chain: + name: Supply chain + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: EmbarkStudios/cargo-deny-action@v2 diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 00000000..48a0b89b --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,41 @@ +name: Conventional Commits + +on: + pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review] + push: + branches: [main] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: conventional-commits-${{ github.ref }} + cancel-in-progress: true + +jobs: + conventional: + name: Validate commit and PR title + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Check PR title + if: github.event_name == 'pull_request' + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: ./.github/scripts/check-conventional-commit.sh "$PR_TITLE" + - name: Check release intent for package changes + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: ./.github/scripts/check-release-intent.sh "$PR_TITLE" "$BASE_SHA" "$HEAD_SHA" "$HEAD_BRANCH" + - name: Check HEAD commit subject + run: ./.github/scripts/check-conventional-commit.sh "$(git log -1 --pretty=%s)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ca4560d3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,128 @@ +name: Release + +on: + workflow_dispatch: + inputs: + operation: + description: Prepare a release PR, dry-run publish, or publish from main + required: true + type: choice + default: prepare-release-pr + options: + - prepare-release-pr + - publish-dry-run + - publish + +permissions: + contents: write + id-token: write + pull-requests: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + 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' }} + steps: + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92 + + - name: Create or update release PR + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish: + name: Publish release + runs-on: ubuntu-latest + timeout-minutes: 90 + if: ${{ github.repository == 'f0rr0/pglite-oxide' && inputs.operation != 'prepare-release-pr' }} + environment: ${{ inputs.operation == 'publish' && 'crates-io' || 'release-dry-run' }} + steps: + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92 + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Format + run: cargo fmt --all --check + + - name: Check default features + run: cargo check --all-targets --locked + + - name: Check no default features + run: cargo check --no-default-features --all-targets --locked + + - name: Clippy + run: cargo clippy --all-targets --locked -- -D warnings + + - name: Unit and binary tests + run: cargo test --lib --bins --locked + + - name: Doctests + run: cargo test --doc --locked + + - name: Runtime smoke + run: cargo test --test runtime_smoke --locked -- --nocapture + + - name: Proxy smoke + run: cargo test --test proxy_smoke --locked -- --nocapture + + - name: Client compatibility + run: cargo test --test client_compat --locked -- --nocapture + + - name: Supply-chain policy + uses: EmbarkStudios/cargo-deny-action@v2 + + - name: Inspect package contents + run: cargo package --locked --list + + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + dry_run: ${{ inputs.operation == 'publish-dry-run' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ab8c1dab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) +and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- 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 SQLx and `tokio-postgres` compatibility coverage, runtime/proxy smoke + tests, CI, cargo-deny policy checks, Conventional Commit validation, and + documented runtime asset provenance. +- Improved the blocking proxy/server path for extended-protocol clients, + readiness handling, and socket mode behavior. + +## [0.1.0] - 2026-04-24 + +- Initial repository release. + +[Unreleased]: https://github.com/f0rr0/pglite-oxide/compare/0.1.0...HEAD +[0.1.0]: https://github.com/f0rr0/pglite-oxide/releases/tag/0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8c0b9dbe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +## Local Checks + +Run the same gates as CI before opening a PR: + +```sh +cargo fmt --all --check +cargo check --all-targets +cargo check --no-default-features --all-targets +cargo clippy --all-targets -- -D warnings +cargo deny check +cargo test --lib --bins +cargo test --doc +cargo test --test runtime_smoke -- --nocapture +cargo test --test proxy_smoke -- --nocapture +cargo test --test client_compat -- --nocapture +cargo package --locked --allow-dirty +``` + +The runtime smoke starts embedded Postgres and is intentionally slower than unit tests. + +## Assets + +Bundled runtime assets must stay aligned with `docs/ASSETS.md`. If the WASI runtime +changes, update the asset metadata in `Cargo.toml` and run the full local checks. + +## Releases + +Releases are manual and must be dispatched from `main` through the GitHub +Actions `Release` workflow. release-plz owns version bumps, changelog updates, +tags, GitHub releases, and crates.io publishing. See `docs/RELEASE.md` for the +release-intent, Trusted Publishing, and manual workflow details. diff --git a/Cargo.lock b/Cargo.lock index 6cc11d30..fb097398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,20 +4,11 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" dependencies = [ - "gimli 0.28.1", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli 0.32.3", + "gimli", ] [[package]] @@ -27,25 +18,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] -name = "ahash" -version = "0.8.12" +name = "aho-corasick" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", + "memchr", ] [[package]] -name = "aho-corasick" -version = "1.1.3" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ambient-authority" @@ -64,9 +49,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -86,46 +71,31 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.76" +name = "atoi" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ - "addr2line 0.25.1", - "cfg-if", - "libc", - "miniz_oxide", - "object 0.37.3", - "rustc-demangle", - "windows-link", + "num-traits", ] [[package]] -name = "base64" -version = "0.21.7" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "bincode" -version = "1.3.3" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -136,11 +106,23 @@ 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 = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] [[package]] name = "byteorder" @@ -150,15 +132,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cap-fs-ext" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" dependencies = [ "cap-primitives", "cap-std", @@ -168,21 +150,21 @@ dependencies = [ [[package]] name = "cap-net-ext" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" dependencies = [ "cap-primitives", "cap-std", - "rustix 1.1.2", + "rustix 1.1.4", "smallvec", ] [[package]] name = "cap-primitives" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" dependencies = [ "ambient-authority", "fs-set-times", @@ -190,7 +172,7 @@ dependencies = [ "io-lifetimes", "ipnet", "maybe-owned", - "rustix 1.1.2", + "rustix 1.1.4", "rustix-linux-procfs", "windows-sys 0.59.0", "winx", @@ -198,45 +180,45 @@ dependencies = [ [[package]] name = "cap-rand" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" dependencies = [ "ambient-authority", - "rand", + "rand 0.8.5", ] [[package]] name = "cap-std" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" dependencies = [ "cap-primitives", "io-extras", "io-lifetimes", - "rustix 1.1.2", + "rustix 1.1.4", ] [[package]] name = "cap-time-ext" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" dependencies = [ "ambient-authority", "cap-primitives", "iana-time-zone", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "winx", ] [[package]] name = "cc" -version = "1.2.39" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -246,9 +228,50 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +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 = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "core-foundation-sys" @@ -274,75 +297,127 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[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.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57d4f3ffc28bbd6ef1ca7b50b20126717232f97487efe027d135d9d87eb29c" +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.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f7d0ac7fd53f2c29db3ff9a063f6ff5a8be2abaa8f6942aceb6e1521e70df7" +checksum = "96d9315d98d6e0a64454d4c83be2ee0e8055c3f80c3b2d7bcad7079f281a06ff" dependencies = [ "bumpalo", + "cranelift-assembler-x64", "cranelift-bforest", + "cranelift-bitset", "cranelift-codegen-meta", "cranelift-codegen-shared", "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli 0.28.1", - "hashbrown 0.14.5", + "gimli", + "hashbrown 0.16.1", + "libm", "log", + "pulley-interpreter", "regalloc2", + "rustc-hash", + "serde", "smallvec", "target-lexicon", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40bf21460a600178956cb7fd900a7408c6587fbb988a8063f7215361801a1da" +checksum = "d89c00a88081c55e3087c45bebc77e0cc973de2d7b44ef6a943c7122647b89f5" dependencies = [ + "cranelift-assembler-x64-meta", "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", ] [[package]] name = "cranelift-codegen-shared" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d792ecc1243b7ebec4a7f77d9ed428ef27456eeb1f8c780587a6f5c38841be19" +checksum = "879f77c497a1eb6273482aa1ac3b23cb8563ff04edb39ed5dfcfd28c8deff8f5" [[package]] name = "cranelift-control" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea2808043df964b73ad7582e09afbbe06a31f3fb9db834d53e74b4e16facaeb" +checksum = "498dc1f17a6910c88316d49c7176d8fa97cf10c30859c32a266040449317f963" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1930946836da6f514da87625cd1a0331f3908e0de454628c24a0b97b130c4d4" +checksum = "c2acba797f6a46042ce82aaf7680d0c3567fe2001e238db9df649fd104a2727f" dependencies = [ + "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5482a5fcdf98f2f31b21093643bdcfe9030866b8be6481117022e7f52baa0f2b" +checksum = "4dca3df1d107d98d88f159ad1d5eaa2d5cdb678b3d5bcfadc6fc83d8ebb448ea" dependencies = [ "cranelift-codegen", "log", @@ -352,15 +427,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6e1869b6053383bdb356900e42e33555b4c9ebee05699469b7c53cdafc82ea" +checksum = "f62dd18116d88bed649871feceda79dad7b59cc685ea8998c2b3e64d0e689602" [[package]] name = "cranelift-native" -version = "0.106.2" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91446e8045f1c4bc164b7bba68e2419c623904580d4b730877a663c6da38964" +checksum = "f843b80360d7fdf61a6124642af7597f6d55724cf521210c34af8a1c66daca6e" dependencies = [ "cranelift-codegen", "libc", @@ -368,21 +443,26 @@ dependencies = [ ] [[package]] -name = "cranelift-wasm" -version = "0.106.2" +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" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b17979b862d3b0d52de6ae3294ffe4d86c36027b56ad0443a7c8c8f921d14f" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "itertools", - "log", - "smallvec", - "wasmparser 0.201.0", - "wasmtime-types", + "crc-catalog", ] +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -411,6 +491,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -419,82 +508,84 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] -name = "debugid" -version = "0.8.0" +name = "crypto-common" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ - "uuid", + "hybrid-array", ] [[package]] -name = "digest" -version = "0.10.7" +name = "ctutils" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "block-buffer", - "crypto-common", + "cmov", ] [[package]] -name = "directories" -version = "5.0.1" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "dirs-sys 0.4.1", + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", ] [[package]] -name = "directories-next" -version = "2.0.0" +name = "digest" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ - "cfg-if", - "dirs-sys-next", + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", ] [[package]] -name = "dirs" -version = "4.0.0" +name = "directories" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys 0.3.7", + "dirs-sys", ] [[package]] -name = "dirs-sys" -version = "0.3.7" +name = "directories-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ - "libc", - "redox_users", - "winapi", + "cfg-if", + "dirs-sys-next", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", - "windows-sys 0.48.0", + "redox_users 0.5.2", + "windows-sys 0.59.0", ] [[package]] @@ -504,7 +595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -519,11 +610,32 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "encoding_rs" @@ -547,14 +659,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] name = "fallible-iterator" -version = "0.3.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" @@ -569,7 +703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.59.0", ] @@ -587,20 +721,38 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -617,19 +769,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.59.0", ] [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -638,9 +789,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -648,78 +799,54 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] -name = "futures-executor" -version = "0.3.31" +name = "futures-intrusive" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", - "futures-task", - "futures-util", + "lock_api", + "parking_lot", ] [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "fxprof-processed-profile" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" -dependencies = [ - "bitflags", - "debugid", - "fxhash", - "serde", - "serde_json", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -732,9 +859,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -743,68 +870,144 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", ] [[package]] name = "gimli" -version = "0.28.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] [[package]] -name = "gimli" -version = "0.32.3" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "ahash", + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" dependencies = [ - "ahash", + "foldhash 0.2.0", ] [[package]] -name = "hashbrown" -version = "0.16.0" +name = "hashlink" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "iana-time-zone" -version = "0.1.64" +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -826,9 +1029,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -839,9 +1042,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -852,11 +1055,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -867,42 +1069,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -912,9 +1110,9 @@ dependencies = [ [[package]] name = "id-arena" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" @@ -939,12 +1137,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -965,57 +1163,26 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itertools" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "ittapi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" -dependencies = [ - "anyhow", - "ittapi-sys", - "log", -] - -[[package]] -name = "ittapi-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" -dependencies = [ - "cc", -] +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -1023,26 +1190,20 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "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" @@ -1057,19 +1218,25 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall", + "redox_syscall 0.7.4", ] [[package]] @@ -1080,31 +1247,30 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lzma-sys" @@ -1118,23 +1284,14 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "mach2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "maybe-owned" version = "0.3.4" @@ -1148,14 +1305,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.2", ] [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memfd" @@ -1163,16 +1330,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.2", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", + "rustix 1.1.4", ] [[package]] @@ -1182,54 +1340,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "nu-ansi-term" -version = "0.50.1" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "windows-sys 0.52.0", + "autocfg", ] [[package]] -name = "object" -version = "0.32.2" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "crc32fast", - "hashbrown 0.14.5", - "indexmap", - "memchr", + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", ] [[package]] name = "object" -version = "0.37.3" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ + "crc32fast", + "hashbrown 0.17.0", + "indexmap", "memchr", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "option-ext" @@ -1237,11 +1405,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1249,23 +1423,17 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1277,34 +1445,48 @@ name = "pglite-oxide" version = "0.1.0" dependencies = [ "anyhow", - "cap-std", "directories", "flate2", - "getrandom 0.2.16", - "md-5", - "once_cell", - "serial_test", + "getrandom 0.4.2", + "hex", + "regex", + "serde", + "serde_json", + "sqlx", "tar", "tempfile", "tokio", + "tokio-postgres", "tracing", - "tracing-subscriber", "wasmtime", "wasmtime-wasi", "xz2", ] [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "phf" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "phf_shared" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" @@ -1312,11 +1494,52 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[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 = "postgres-protocol" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac 0.13.0", + "md-5 0.11.0", + "memchr", + "rand 0.10.1", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1330,29 +1553,53 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] -name = "psm" -version = "0.1.26" +name = "pulley-interpreter" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +checksum = "df866b7fd522992ccc6682e58b2741cc7972b163b661db24c4328f4c914cb09d" dependencies = [ - "cc", + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "44.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7dfa8354acc622b3857e1bb1a4e4315d3bc1a44ad31d5653c3e87c0da9306d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1363,6 +1610,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -1371,7 +1624,18 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[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]] @@ -1381,7 +1645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1390,9 +1654,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.11.0" @@ -1415,9 +1685,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -1428,29 +1707,53 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "regalloc2" -version = "0.9.3" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ - "hashbrown 0.13.2", + "allocator-api2", + "bumpalo", + "hashbrown 0.17.0", "log", "rustc-hash", - "slice-group-by", "smallvec", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1459,9 +1762,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-demangle" @@ -1471,9 +1774,9 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -1490,15 +1793,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.1", + "linux-raw-sys 0.12.1", + "windows-sys 0.59.0", ] [[package]] @@ -1508,7 +1811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" dependencies = [ "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", ] [[package]] @@ -1517,44 +1820,27 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -1562,18 +1848,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1582,140 +1868,237 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "serial_test" -version = "3.2.0" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "futures", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] -name = "serial_test_derive" -version = "3.2.0" +name = "sha2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] -name = "sha2" -version = "0.10.9" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "serde", ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ - "lazy_static", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "shellexpand" -version = "2.1.2" +name = "sqlx" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ - "dirs", + "sqlx-core", + "sqlx-macros", + "sqlx-postgres", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "sqlx-core" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] [[package]] -name = "signal-hook-registry" -version = "1.4.6" +name = "sqlx-macros" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ - "libc", + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", ] [[package]] -name = "slab" -version = "0.4.11" +name = "sqlx-macros-core" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-postgres", + "syn", + "tokio", + "url", +] [[package]] -name = "slice-group-by" -version = "0.3.1" +name = "sqlx-postgres" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5 0.10.6", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami 1.6.1", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "socket2" -version = "0.6.0" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "libc", - "windows-sys 0.59.0", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] -name = "sptr" -version = "0.3.2" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1751,9 +2134,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -1762,21 +2145,30 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.1", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", ] [[package]] @@ -1785,7 +2177,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -1800,101 +2201,163 @@ dependencies = [ ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "cfg-if", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.47.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", - "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-postgres" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.10.1", + "socket2", + "tokio", + "tokio-util", + "whoami 2.1.1", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" -version = "0.8.23" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow 0.7.15", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", + "winnow 1.0.2", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1902,9 +2365,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1913,60 +2376,45 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", ] [[package]] -name = "tracing-log" -version = "0.2.0" +name = "typenum" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] -name = "tracing-subscriber" -version = "0.3.20" +name = "unicode-bidi" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] -name = "typenum" -version = "1.18.0" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-ident" -version = "1.0.19" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] -name = "unicode-width" -version = "0.2.1" +name = "unicode-properties" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-xid" @@ -1976,9 +2424,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -1992,22 +2440,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "version_check" version = "0.9.5" @@ -2031,18 +2463,42 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasite" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" dependencies = [ - "wit-bindgen", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2051,25 +2507,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2077,345 +2519,345 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.201.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "leb128", + "leb128fmt", + "wasmparser 0.244.0", ] [[package]] name = "wasm-encoder" -version = "0.239.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" dependencies = [ "leb128fmt", - "wasmparser 0.239.0", + "wasmparser 0.246.2", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] name = "wasmparser" -version = "0.201.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", + "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "wasmparser" -version = "0.239.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" dependencies = [ "bitflags", + "hashbrown 0.16.1", "indexmap", "semver", + "serde", ] [[package]] name = "wasmprinter" -version = "0.201.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67e66da702706ba08729a78e3c0079085f6bfcb1a62e4799e97bbf728c2c265" +checksum = "6e41f7493ba994b8a779430a4c25ff550fd5a40d291693af43a6ef48688f00e3" dependencies = [ "anyhow", - "wasmparser 0.201.0", + "termcolor", + "wasmparser 0.246.2", ] [[package]] name = "wasmtime" -version = "19.0.2" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e300c0e3f19dc9064e3b17ce661088646c70dbdde36aab46470ed68ba58db7d" +checksum = "fca3f777dfb4db45915f95eeb25cac7f2eeb268797a27e5eb78b072618135c7f" dependencies = [ - "addr2line 0.21.0", - "anyhow", + "addr2line", "async-trait", - "bincode", + "bitflags", "bumpalo", + "cc", "cfg-if", "encoding_rs", - "fxprof-processed-profile", - "gimli 0.28.1", - "indexmap", - "ittapi", "libc", "log", - "object 0.32.2", + "mach2", + "memfd", + "object", "once_cell", - "paste", + "postcard", + "pulley-interpreter", "rayon", - "rustix 0.38.44", + "rustix 1.1.4", "semver", "serde", "serde_derive", - "serde_json", + "smallvec", "target-lexicon", - "wasm-encoder 0.201.0", - "wasmparser 0.201.0", - "wasmtime-cache", - "wasmtime-component-macro", - "wasmtime-component-util", - "wasmtime-cranelift", + "wasmparser 0.246.2", "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-debug", - "wasmtime-jit-icache-coherence", - "wasmtime-runtime", - "wasmtime-slab", - "wasmtime-winch", - "wat", - "windows-sys 0.52.0", + "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", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-asm-macros" -version = "19.0.2" +name = "wasmtime-environ" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110aa598e02a136fb095ca70fa96367fc16bab55256a131e66f9b58f16c73daf" +checksum = "7c5ca1af838cec374931242d07af5d354aedf63f297f95b3625ac863e516ef67" dependencies = [ - "cfg-if", + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "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-cache" -version = "19.0.2" +name = "wasmtime-internal-cache" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e660537b0ac2fc76917fb0cc9d403d2448b6983a84e59c51f7fea7b7dae024" +checksum = "b2004f7c86ebeb116550655377cdf16dbf7b03ae5aa6b4b1c1458cfa23aaa306" dependencies = [ - "anyhow", "base64", - "bincode", "directories-next", "log", - "rustix 0.38.44", + "postcard", + "rustix 1.1.4", "serde", "serde_derive", - "sha2", + "sha2 0.10.9", "toml", - "windows-sys 0.52.0", + "wasmtime-environ", + "windows-sys 0.61.2", "zstd", ] [[package]] -name = "wasmtime-component-macro" -version = "19.0.2" +name = "wasmtime-internal-component-macro" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091f32ce586251ac4d07019388fb665b010d9518ffe47be1ddbabb162eed6007" +checksum = "58b31927f7b613d8fe019609744e226f6458d8aa5e6289e92fbbc60e521cd026" dependencies = [ "anyhow", "proc-macro2", "quote", "syn", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.246.2", ] [[package]] -name = "wasmtime-component-util" -version = "19.0.2" +name = "wasmtime-internal-component-util" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd17dc1ebc0b28fd24b6b9d07638f55b82ae908918ff08fd221f8b0fefa9125" +checksum = "dc29e3478928b93979831ba02a997ce7f707c673ce47180d643091cf4fa4f561" [[package]] -name = "wasmtime-cranelift" -version = "19.0.2" +name = "wasmtime-internal-core" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e923262451a4b5b39fe02f69f1338d56356db470e289ea1887346b9c7f592738" +checksum = "816a61a75275c6be435131fc625a4f5956daf24d9f9f59443e81cbef228929b3" dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "cranelift-wasm", - "gimli 0.28.1", - "log", - "object 0.32.2", - "target-lexicon", - "thiserror", - "wasmparser 0.201.0", - "wasmtime-cranelift-shared", - "wasmtime-environ", - "wasmtime-versioned-export-macros", + "hashbrown 0.16.1", + "libm", + "serde", ] [[package]] -name = "wasmtime-cranelift-shared" -version = "19.0.2" +name = "wasmtime-internal-cranelift" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "508898cbbea0df81a5d29cfc1c7c72431a1bc4c9e89fd9514b4c868474c05c7a" +checksum = "69ceb5e079877e7e4565c1e2d86d9db889175d55f7ca0001315576d08c71e634" dependencies = [ - "anyhow", + "cfg-if", "cranelift-codegen", "cranelift-control", - "cranelift-native", - "gimli 0.28.1", - "object 0.32.2", - "target-lexicon", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-environ" -version = "19.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7e3f2aa72dbb64c19708646e1ff97650f34e254598b82bad5578ea9c80edd30" -dependencies = [ - "anyhow", - "bincode", - "cpp_demangle", "cranelift-entity", - "gimli 0.28.1", - "indexmap", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", "log", - "object 0.32.2", - "rustc-demangle", - "serde", - "serde_derive", + "object", + "pulley-interpreter", + "smallvec", "target-lexicon", - "thiserror", - "wasm-encoder 0.201.0", - "wasmparser 0.201.0", - "wasmprinter", - "wasmtime-component-util", - "wasmtime-types", + "thiserror 2.0.18", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-fiber" -version = "19.0.2" +name = "wasmtime-internal-fiber" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9235b643527bcbac808216ed342e1fba324c95f14a62762acfa6f2e6ca5edbd6" +checksum = "e18f8bb05d25e0d4cca7278147c9f9e2f26f66886ef754b562bf729128f1e537" dependencies = [ - "anyhow", "cc", "cfg-if", - "rustix 0.38.44", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.52.0", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-jit-debug" -version = "19.0.2" +name = "wasmtime-internal-jit-debug" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92de34217bf7f0464262adf391a9950eba440f9dfc7d3b0e3209302875c6f65f" +checksum = "357f1070b31154ee463937b477ca0b2962bf450b40fc59799bef2f656b15da73" dependencies = [ - "object 0.32.2", - "once_cell", - "rustix 0.38.44", - "wasmtime-versioned-export-macros", + "cc", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-jit-icache-coherence" -version = "19.0.2" +name = "wasmtime-internal-jit-icache-coherence" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22ca2ef4d87b23d400660373453e274b2251bc2d674e3102497f690135e04b0" +checksum = "2fd683a94490bf755d016a09697b0955602c50106b1ded97d16983ab2ded9fed" dependencies = [ "cfg-if", "libc", - "windows-sys 0.52.0", + "wasmtime-internal-core", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-runtime" -version = "19.0.2" +name = "wasmtime-internal-unwinder" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1806ee242ca4fd183309b7406e4e83ae7739b7569f395d56700de7c7ef9f5eb8" +checksum = "4471746ce113c3c1862ce2c0674acb35399a4b3ed3ef4531dc087f333c74f064" dependencies = [ - "anyhow", - "cc", "cfg-if", - "encoding_rs", - "indexmap", - "libc", + "cranelift-codegen", "log", - "mach", - "memfd", - "memoffset", - "paste", - "psm", - "rustix 0.38.44", - "sptr", - "wasm-encoder 0.201.0", - "wasmtime-asm-macros", + "object", "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-debug", - "wasmtime-versioned-export-macros", - "wasmtime-wmemcheck", - "windows-sys 0.52.0", ] [[package]] -name = "wasmtime-slab" -version = "19.0.2" +name = "wasmtime-internal-versioned-export-macros" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c58bef9ce877fd06acb58f08d003af17cb05cc51225b455e999fbad8e584c0" +checksum = "d6af582ec18b674bf7a17775d6fbfbddfcc143f0edbd89c9c1778239c8aa92ed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "wasmtime-types" -version = "19.0.2" +name = "wasmtime-internal-winch" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cebe297aa063136d9d2e5b347c1528868aa43c2c8d0e1eb0eec144567e38fe0f" +checksum = "d31be8916bb60ea756d2f0ae1f634d9258442aa71e773c893e2f4cead30501b5" dependencies = [ - "cranelift-entity", - "serde", - "serde_derive", - "thiserror", - "wasmparser 0.201.0", + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", ] [[package]] -name = "wasmtime-versioned-export-macros" -version = "19.0.2" +name = "wasmtime-internal-wit-bindgen" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffaafa5c12355b1a9ee068e9295d50c4ca0a400c721950cdae4f5b54391a2da5" +checksum = "e2150e63d502ab2d64754e5abe8eb737ae674b7dd4ad53144fd16bbeceaf4a19" dependencies = [ - "proc-macro2", - "quote", - "syn", + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.246.2", ] [[package]] name = "wasmtime-wasi" -version = "19.0.2" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95961546319d4019625920756967a929879d1d46c4e5f89a74e9f4405655b0c" +checksum = "83f5109b4fd619b9796b9c9901de59d83e3575cd1226c1a36d1901371f43db28" dependencies = [ - "anyhow", "async-trait", "bitflags", "bytes", @@ -2428,119 +2870,106 @@ dependencies = [ "futures", "io-extras", "io-lifetimes", - "once_cell", - "rustix 0.38.44", + "rustix 1.1.4", "system-interface", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "url", "wasmtime", + "wasmtime-wasi-io", "wiggle", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-winch" -version = "19.0.2" +name = "wasmtime-wasi-io" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d618b4e90d3f259b1b77411ce573c9f74aade561957102132e169918aabdc863" +checksum = "74ebe14c586e98d2fdc32c76ca0005ef28348e98ed737e776d378b3b0cc2afd0" dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.28.1", - "object 0.32.2", - "target-lexicon", - "wasmparser 0.201.0", - "wasmtime-cranelift-shared", - "wasmtime-environ", - "winch-codegen", + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", ] [[package]] -name = "wasmtime-wit-bindgen" -version = "19.0.2" +name = "wast" +version = "35.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7a253c8505edd7493603e548bff3af937b0b7dbf2b498bd5ff2131b651af72" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" dependencies = [ - "anyhow", - "heck", - "indexmap", - "wit-parser", + "leb128", ] [[package]] -name = "wasmtime-wmemcheck" -version = "19.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a8c62e9df8322b2166d2a6f096fbec195ddb093748fd74170dcf25ef596769" - -[[package]] -name = "wast" -version = "35.0.2" +name = "web-sys" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ - "leb128", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "wast" -version = "239.0.0" +name = "whoami" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9139176fe8a2590e0fb174cdcaf373b224cb93c3dde08e4297c1361d2ba1ea5d" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "bumpalo", - "leb128fmt", - "memchr", - "unicode-width", - "wasm-encoder 0.239.0", + "libredox", + "wasite 0.1.0", ] [[package]] -name = "wat" -version = "1.239.0" +name = "whoami" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1c941927d34709f255558166f8901a2005f8ab4a9650432e9281b7cc6f3b75" +checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" dependencies = [ - "wast 239.0.0", + "libc", + "libredox", + "objc2-system-configuration", + "wasite 1.0.2", + "web-sys", ] [[package]] name = "wiggle" -version = "19.0.2" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899d3fe5fbacd02f114cacdaa1cca9040280c4153c71833a77b9609c60ccf72b" +checksum = "89cff414ef7dce0cc1cf8a033ff80d3f38e3987c37e3efeec7926ecb5ffaaae6" dependencies = [ - "anyhow", - "async-trait", "bitflags", - "thiserror", + "thiserror 2.0.18", "tracing", "wasmtime", + "wasmtime-environ", "wiggle-macro", ] [[package]] name = "wiggle-generate" -version = "19.0.2" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df5887f452cff44ffe1e1aba69b7fafe812deed38498446fa7a46b55e962cd5" +checksum = "ccf9dc7272b151a9616a2699e7f94ea1d4ae253b47b63a79fbc8f38e2cca5fa6" dependencies = [ - "anyhow", "heck", "proc-macro2", "quote", - "shellexpand", "syn", + "wasmtime-environ", "witx", ] [[package]] name = "wiggle-macro" -version = "19.0.2" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdb12de36507498abaa3a042f895a43ee00a2f6125b6901b9a27edf72bfdbe7" +checksum = "aa7c29fcf738630cba4e35f1805da5e42dde20ee9809ee9202b0648ae671602f" dependencies = [ "proc-macro2", "quote", @@ -2564,6 +2993,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2572,25 +3010,28 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "0.17.2" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15869abc9e3bb29c017c003dbe007a08e9910e8ff9023a962aa13c1b2ee6af" +checksum = "9339858ad222412200fd8b1af9e270712201aaec440c7618991443af3446481f" dependencies = [ - "anyhow", + "cranelift-assembler-x64", "cranelift-codegen", - "gimli 0.28.1", + "gimli", "regalloc2", "smallvec", "target-lexicon", - "wasmparser 0.201.0", + "thiserror 2.0.18", + "wasmparser 0.246.2", "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", ] [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -2601,9 +3042,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -2612,9 +3053,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -2623,24 +3064,24 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -2654,15 +3095,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -2678,14 +3110,14 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] @@ -2723,19 +3155,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "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]] @@ -2752,9 +3184,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2770,9 +3202,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2788,9 +3220,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -2800,9 +3232,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2818,9 +3250,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2836,9 +3268,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2854,9 +3286,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2872,18 +3304,21 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "winx" @@ -2897,17 +3332,106 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] [[package]] name = "wit-parser" -version = "0.201.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" dependencies = [ "anyhow", + "hashbrown 0.16.1", "id-arena", "indexmap", "log", @@ -2916,7 +3440,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.201.0", + "wasmparser 0.246.2", ] [[package]] @@ -2927,15 +3451,15 @@ checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" dependencies = [ "anyhow", "log", - "thiserror", - "wast 35.0.2", + "thiserror 1.0.69", + "wast", ] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" @@ -2944,7 +3468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix 1.1.4", ] [[package]] @@ -2958,11 +3482,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2970,9 +3493,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -2982,18 +3505,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", @@ -3023,9 +3546,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -3034,9 +3557,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3045,15 +3568,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 947b5e7c..233d948f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "pglite-oxide" version = "0.1.0" -edition = "2021" -rust-version = "1.74" +edition = "2024" +rust-version = "1.92" description = "Rust helpers for embedding the Electric SQL pglite WebAssembly PostgreSQL runtime" readme = "README.md" repository = "https://github.com/f0rr0/pglite-oxide" @@ -10,26 +10,82 @@ homepage = "https://github.com/f0rr0/pglite-oxide" documentation = "https://docs.rs/pglite-oxide" keywords = ["postgres", "pglite", "wasm", "database", "embedded"] categories = ["database-implementations", "wasm", "development-tools::testing"] -license = "MIT" -exclude = ["Cargo.toml.orig"] +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = ["crates-io"] +exclude = [ + ".config/**", + ".github/**", + "Cargo.toml.orig", + "assets/bin/pg_dump.wasm", + "assets/extensions/vector.tar.gz", + "assets/pglite.data", + "assets/pglite.wasi", + "docs/ALIGNMENT_DIFF.md", + "docs/ALIGNMENT_NOTES.md", + "docs/RUST_PORT_PLAN.md", + "docs/reviews/**", + "release-plz.toml", +] + +[features] +default = ["runtime-cache"] +runtime-cache = ["wasmtime/cache"] + +[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 = "c725235f22a4fd50fed363f4065edb151a716fa769cba66f2383b8b854e6bdb5" +pglite-wasi-sha256 = "a72b96adcd4ce40c51dd7201ee76a90f1b5799f633753b9cbb3c9af7b79f8da5" +pglite-data-sha256 = "791a44e2ad1d48830714fb54e8662a3372618883566a0af7fc9f6b8375ab82d1" +pglite-fs-manifest-sha256 = "880c9c058f416aad6ddc33fe0a1c84f6213b40c2b378e32587d25167d2f346f5" [dependencies] anyhow = "1" tar = "0.4" xz2 = "0.1" -directories = "5" -wasmtime = "19" -wasmtime-wasi = { version = "19", default-features = false, features = ["preview1"] } -cap-std = "3" -getrandom = "0.2" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "sync"] } +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" -tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } -once_cell = "1.19" flate2 = "1" -md-5 = "0.10" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +regex = "1" +tempfile = "3" +hex = "0.4" [dev-dependencies] -tempfile = "3" -anyhow = "1" -serial_test = "3" +sqlx = { version = "0.8", default-features = false, features = [ + "postgres", + "runtime-tokio", +] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } +tokio-postgres = "0.7" + +[[bin]] +name = "pglite-dump" +path = "src/bin/pglite_dump.rs" + +[[bin]] +name = "pglite-manifest-sync" +path = "src/bin/pglite_manifest_sync.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 9f95cccb..dd6fc865 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,234 @@ # pglite-oxide -pglite-oxide is the Rust companion to the [Electric SQL pglite](https://github.com/electric-sql/pglite) [WASM builds](https://github.com/electric-sql/pglite-build). It gives a consumer-level API for installing and running a self-contained PostgreSQL 17.x instance inside a WebAssembly guest, so you can embed "real Postgres" into CLI tools, desktop apps, server side functions, or tests without talking to a separate Postgres service. This document explains how to install the kit, the modes it supports, and the knobs you can turn to adapt it to your own runtime. +[![CI](https://github.com/f0rr0/pglite-oxide/actions/workflows/ci.yml/badge.svg)](https://github.com/f0rr0/pglite-oxide/actions/workflows/ci.yml) -## Installation Patterns +`pglite-oxide` embeds the [Electric SQL PGlite](https://github.com/electric-sql/pglite) +WASI PostgreSQL runtime in a Rust library. It installs the bundled runtime, starts +Postgres inside Wasmtime, and exposes a small synchronous API for executing SQL +without a separate database server. It can also expose the embedded backend over +a local PostgreSQL socket for Rust clients such as SQLx and `tokio-postgres`. -The crate ships with a prebuilt pglite-wasi.tar.xz. You have three options for provisioning it: +The crate currently targets PostgreSQL 17.x PGlite builds, Rust 1.92+, and +Wasmtime 44. -### Default initialization (recommended) +## Quick Start -```rust -let paths = pglite_oxide::install_and_init(("com", "example", "app"))?; -``` - -- Installs the WASM runtime into the operating system's data directory for that app ID (see directories::ProjectDirs). -- Runs pg_initdb inside the WASM guest. -- Returns PglitePaths { pgroot, pgdata } where pgroot holds the runtime and pgdata is the database cluster. - -### Specify an explicit mount root +```rust,no_run +use pglite_oxide::Pglite; +use serde_json::json; -```rust -let paths = pglite_oxide::install_and_init_in("/custom/location")?; -``` +fn main() -> anyhow::Result<()> { + let mut db = Pglite::builder().path("./.pglite").open()?; -Useful for portable binaries or integration tests where you want to keep the runtime under /tmp, inside your project tree, etc. + db.exec("CREATE TABLE IF NOT EXISTS items(value TEXT)", None)?; + db.query( + "INSERT INTO items(value) VALUES ($1)", + &[json!("alpha")], + None, + )?; -### Fine-grained control + let result = db.query("SELECT value FROM items", &[], None)?; + println!("{:?}", result.rows); -```rust -let paths = pglite_oxide::install_with_options( - PglitePaths::with_root("/opt/pglite"), - InstallOptions { ensure_cluster: false }, -)?; + db.close()?; + Ok(()) +} ``` -- You can skip initdb if you only want the runtime binaries. -- Later call ensure_cluster(&paths) manually once you want a database. - -All of the helpers detect existing installs: if tmp/pglite/base/PG_VERSION or /tmp/pglite/base/PG_VERSION already exists, the kit reuses them instead of unpacking another archive. - -## Runtime API ("interactive" module) +Use `Pglite::temporary()?` for an ephemeral database in tests; it clones a +process-local template cluster so repeated tests do not rerun `initdb`. -Most consumers only need the top-level functions in pglite_oxide::interactive: +## PostgreSQL Client Compatibility -| Function | Purpose | -|----------|---------| -| `prepare_default_mount()` | Returns MountInfo with the auto-detected runtime + cluster, creating them on demand. The example binary uses this. | -| `wasm_import(alias, path)` | Ensures the default session is initialized and returns the path to the WASM module (you can use this if you need to spawn your own Wasmtime instance). | -| `exec_interactive(PokeInput)` | Sends bytes over the WASM-backed Postgres wire protocol and returns the raw response. This performs the full startup handshake the first time it runs. | -| `poke(PokeInput) + interactive_one()` | Lower-level helpers for REPL-style usage; poke flashes SQL into the shared buffer and interactive_one ticks the host once. | -| `run_pg_dump(argv, env)` | Invokes the optional pg_dump shim if it exists in the runtime. Returns Ok(None) when the archive didn't include that artifact. | -| `default_mount()` | Gives you the same MountInfo the global runtime uses (good for exposing the socket path or the installation root). | -| `start_proxy(use_tcp)` | Launches the full socket proxy to /tmp/.s.PGSQL.5432 (Unix) or 0.0.0.0:5432 (TCP). This allows external clients like psql to connect to the WASM backend. | +Use `PgliteServer` when a library expects a PostgreSQL connection string. The +server owns one embedded backend, so configure downstream pools with a single +connection. -PokeInput accepts either &str (we append the null terminator) or raw bytes. +```rust,no_run +use pglite_oxide::PgliteServer; +use sqlx::{Connection, Row}; -### Under the hood - -The crate decides between two transports: - -- **CMA channel available** – the WASM exports a shared-memory channel (get_channel() >= 0). The kit writes requests directly into the CMA buffer and issues interactive_write(len). -- **File-based fallback** – when the CMA channel is missing, the kit uses the .in/.out lock files (/tmp/pglite/base/.s.PGSQL.5432{.in,.out}). The run_tests_quick helper only runs when we're in file mode. - -You normally don't need to reason about which transport is active; the helpers handle that automatically. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; -## Knobs & Environment + 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); -When we instantiate the WASI context (standard_wasi_builder), we pre-open three directories inside the guest: + conn.close().await?; + server.shutdown()?; + Ok(()) +} +``` -- `/tmp` -> the runtime root (pgroot) -- `/tmp/pglite/base` -> the cluster data directory (pgdata) -- `/dev` -> a shim directory for things like urandom +For app persistence, use `PgliteServer::builder().path("./.pglite").start()?`. +For Rust code that does not require a connection URI, prefer the direct +`Pglite` API because it avoids the socket/protocol compatibility layer. +For desktop app shape and state management notes, see +[`docs/TAURI.md`](docs/TAURI.md). -We also set the environment variables Postgres expects: +## Runtime API -| Environment variable | Default | -|---------------------|---------| -| `ENVIRONMENT` | wasm32_wasi_preview1 | -| `PREFIX` / `PGDATA` / `PGSYSCONFDIR` | /tmp/pglite / /tmp/pglite/base / /tmp/pglite | -| `PGUSER` / `PGDATABASE` | postgres / template1 | -| `MODE` / `REPL` | REACT / N | -| `PGCLIENTENCODING`, `LC_CTYPE`, `PG_COLOR`, `PATH`, `TZ`, `PGTZ` | Standard defaults | +`Pglite` is the main entry point. -Override them by modifying the WasiCtxBuilder before you call into interactive::wasm_import or interactive::exec_interactive. For advanced scenarios, grab the builder yourself: +- `Pglite::builder()` configures persistent, app-data, or temporary databases. +- `Pglite::open(path)` opens a persistent database rooted at `path`. +- `Pglite::temporary()` opens a cached ephemeral database for tests. +- `exec(sql, options)` runs simple SQL and returns zero or more result sets. +- `query(sql, params, options)` uses the extended protocol with JSON parameters. +- `describe_query(sql, options)` returns parameter and row metadata. +- `transaction(|tx| ...)` runs `BEGIN`/`COMMIT` with rollback on error. +- `listen`, `unlisten`, and `on_notification` support PostgreSQL notifications. +- `close()` shuts down the embedded backend. +- `PgliteServer` exposes a local PostgreSQL socket for existing client crates. -```rust -let mut builder = pglite_oxide::standard_wasi_builder(&paths)?; -builder.env("PGDATABASE", "custom_db"); -``` +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`. -Similarly, you can provide different runtime modules by passing Some(path) into wasm_import, or by placing alternate .wasi files under paths.pgroot/pglite/bin/. +## Query Options -## Embedding Patterns +`QueryOptions` controls result parsing and protocol behavior: -### Run ad-hoc SQL within Rust +```rust,no_run +use pglite_oxide::{Pglite, QueryOptions, RowMode}; -```rust -let response = interactive::exec_interactive(PokeInput::Str("SELECT 42;"))?; -println!("{}", interactive::hexc(&response, "<-", Some(4))); -``` +fn main() -> anyhow::Result<()> { + let mut db = Pglite::open("./.pglite")?; -You get the raw Postgres wire response. For higher-level decoding plug in a Postgres wire parser (e.g. tokio-postgres's frame decoder) or reuse the CMA buffer via poke + interactive_one. + let options = QueryOptions { + row_mode: Some(RowMode::Array), + ..QueryOptions::default() + }; -### Expose a real Postgres socket + let _rows = db.query("SELECT 1, 2", &[], Some(&options))?; -```rust -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // Start a Unix domain socket at /tmp/.s.PGSQL.5432 - interactive::start_proxy(false).await + Ok(()) } ``` -This gives external tools access to the WASM backend. The proxy forwards raw startup packets, handles CMA/file transport, and recovers from interactive_one traps by clearing the channel. +For `COPY ... FROM '/dev/blob'`, set `QueryOptions::blob` to the bytes to expose +through the guest `/dev/blob`. For `COPY ... TO '/dev/blob'`, read the returned +`Results::blob`. -### Ship pg_dump in your application +## SQL Templating Helpers -Call `run_pg_dump(&["pg_dump", "--schema-only"], &[("PGDATABASE", "mydb")])` to execute the embedded CLI. The helper returns Ok(None) if the asset isn't bundled. +```rust,no_run +use pglite_oxide::{Pglite, QueryTemplate, format_query, quote_identifier}; +use serde_json::json; -### Custom runtimes (CI builds, development) +fn main() -> anyhow::Result<()> { + let mut db = Pglite::open("./.pglite")?; -Place an alternate pglite.wasi somewhere and let `wasm_import("postgres", Some(custom_path))` resolve it. The crate's installer already honors tmp/pglite vs /tmp/pglite detection, so you can drop a development build into /tmp/pglite and the kit will reuse it. + let sql = format_query(&mut db, "SELECT $1::int", &[json!(42)])?; + assert_eq!(sql, "SELECT '42'::int"); -## Managing the Data Directory + let mut template = QueryTemplate::new(); + template.push_sql("SELECT * FROM "); + template.push_identifier("items"); + template.push_sql(" WHERE value = "); + template.push_param(json!("alpha")); + let built = template.build(); -PglitePaths exposes pgroot and pgdata. The kit writes a marker file (PG_VERSION) after initdb. You can safely remove the pgroot directory between runs to force a clean install: + assert_eq!(built.query, "SELECT * FROM \"items\" WHERE value = $1"); + assert_eq!(quote_identifier("a\"b"), "\"a\"\"b\""); -```rust -std::fs::remove_dir_all(paths.pgroot)?; + Ok(()) +} ``` -During testing, the smoke suite uses temporary directories (see tests/pglite_smoke.rs) and ensures we clean them up afterwards. - -### Runtime layout +## Runtime Notes -The unpacked runtime lives under `/pglite/` with familiar Postgres directories (`bin`, `lib`, `share`, `password`, `tmp`). Core extensions, including `plpgsql`, ship in `share/extension` so they are ready for immediate use inside the WASM guest. +The embedded backend uses the same shared-memory CMA protocol as upstream +PGlite. The host preopens: -## Example Binary (runtime_showcase) +- `/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` -Build and run: - -```bash -cargo run -p pglite-oxide --example runtime_showcase -``` +The first instance in a process can take a while because Wasmtime compiles the +large PGlite WASM module and the first temporary cluster runs `initdb`. Compiled +modules are cached inside the process so additional `Pglite` instances avoid the +same compile cost. `Pglite::temporary()` also clones a process-local template +cluster, so later temporary databases in the same test process only copy the +prepared filesystem. Use `Pglite::builder().fresh_temporary().open()?` when a +test needs to exercise fresh cluster initialization. -The example prints the mount root, socket path, whether it reused an existing install, then runs a simple SELECT 1; through exec_interactive and logs the hex-dump of the response. It finishes by invoking pg_dump --version. +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. -## Proxy Example (psql-friendly) +The default `runtime-cache` feature also enables Wasmtime's persistent compiled +module cache, so later processes can reuse native code for the same PGlite WASM +module. Disable it with `default-features = false` if you need to avoid global +cache writes. -Expose a Postgres-compatible socket for GUI tools or `psql` by running: +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: -```bash -cargo run -p pglite-oxide --example proxy_showcase +```toml +[profile.dev.package.wasmtime-internal-cache] +debug-assertions = false ``` -By default the example binds the canonical Unix socket `/tmp/.s.PGSQL.5432`. GUI tools that support sockets usually expose it as a “socket directory” field—point them at `/tmp`, with user `postgres` and database `template1`. The libpq-style connection URI looks like: +For larger downstream suites, prefer reusing one `Pglite` instance per test when +isolation allows it, and use `fresh_temporary` only for initialization-specific +coverage. -``` -postgresql://postgres@/template1?host=/tmp -``` - -Pass `--tcp` after the `--` delimiter to listen on `0.0.0.0:5432` instead: +`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`. -```bash -cargo run -p pglite-oxide --example proxy_showcase -- --tcp +```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' ``` -Clients can then use a standard URI such as `postgresql://postgres@127.0.0.1:5432/template1`. Use `--uds` to return to socket mode. Hit Ctrl+C when you want to tear the proxy down. +On Unix systems, the default proxy mode is `/tmp/.s.PGSQL.5432`: -## Proxy Lifecycles and Error Handling +```sh +cargo run --bin pglite-proxy +PGPASSWORD=postgres psql 'postgresql://postgres@/template1?host=/tmp' +``` -- The interactive session lazily performs the wire handshake on the first exec_interactive call. If you use poke + interactive_one without calling use_wire(true), it stays in the REPL (non-wire) mode. -- When interactive_one throws a WASM trap, the runtime recovers by: clear_error(), resets the CMA length (interactive_write(-1)), and can re-attempt the handshake. -- start_proxy runs run_tests_quick() only in file-transport mode. That helper feeds a handful of SQL statements via REPL to prove the backend responds before accepting real connections. +Runtime asset provenance is tracked in `docs/ASSETS.md`. +Release process details are tracked in `docs/RELEASE.md`. -## Summary of Key Types +## Development -- **PglitePaths** – descriptive struct with pgroot and pgdata. -- **MountInfo** – wraps PglitePaths and tracks the mount root, socket path, and whether an existing install was reused. -- **InteractiveRuntime** – owns the Wasmtime engine, store, and exports. Normally you access it through the global with_default_runtime. -- **Transport enum** – internal, describes CMA vs file transport (automatic). -- **PokeInput<'a>** – either Str(&'a str) or Bytes(&'a [u8]). +The required local gates are: -## Putting It Together +```sh +cargo fmt --all --check +cargo check --all-targets +cargo check --no-default-features --all-targets +cargo clippy --all-targets -- -D warnings +cargo deny check +cargo test --doc +cargo test --test runtime_smoke -- --nocapture +cargo test --test proxy_smoke -- --nocapture +cargo test --test client_compat -- --nocapture +cargo package --allow-dirty +``` -A minimal "headless Postgres in WASM" flow looks like: +Install the supply-chain gate with `cargo install cargo-deny --locked` if it is +not already available. -```rust -fn main() -> anyhow::Result<()> { - // Ensure the runtime and cluster exist (or reuse an existing /tmp/pglite) - let _mount = pglite_oxide::prepare_default_mount()?; +`tests/runtime_smoke.rs` starts the real WASM backend and is intentionally slower +than the protocol unit tests. - // Run SQL inside the WASM backend - let response = pglite_oxide::interactive::exec_interactive( - pglite_oxide::interactive::PokeInput::Str("SELECT current_database();") - )?; +## Utilities - println!( - "Postgres wire response:\n{}", - pglite_oxide::interactive::hexc(&response, "<-", Some(6)) - ); +Two maintenance binaries are included: - Ok(()) -} -``` +- `pglite-dump` expands the bundled filesystem manifest/runtime assets. +- `pglite-manifest-sync` syncs `assets/pglite_fs_manifest.json` from the + `pglite.js` bundle published on `electric-sql/pglite-build` `gh-pages`. +- `pglite-proxy` exposes a local PostgreSQL socket backed by the embedded runtime. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..575bddf6 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,13 @@ +# Third-Party Notices + +`pglite-oxide` Rust code is licensed under the MIT license in `LICENSE`. + +The bundled PGlite/PostgreSQL runtime assets derive from Electric SQL PGlite and +PostgreSQL: + +- PGlite: https://github.com/electric-sql/pglite +- PGlite PostgreSQL fork: https://github.com/electric-sql/postgres-pglite +- PostgreSQL license: https://www.postgresql.org/about/licence/ + +Those bundled assets are covered by their upstream Apache-2.0 and PostgreSQL +license terms. diff --git a/assets/pglite-wasi.tar.xz b/assets/pglite-wasi.tar.xz index 810893bc..a67551c9 100644 Binary files a/assets/pglite-wasi.tar.xz and b/assets/pglite-wasi.tar.xz differ diff --git a/assets/pglite.data b/assets/pglite.data new file mode 100644 index 00000000..806da31b Binary files /dev/null and b/assets/pglite.data differ diff --git a/assets/pglite.wasi b/assets/pglite.wasi new file mode 100644 index 00000000..a7f2d5cf Binary files /dev/null and b/assets/pglite.wasi differ diff --git a/assets/pglite_fs_manifest.json b/assets/pglite_fs_manifest.json new file mode 100644 index 00000000..7a296152 --- /dev/null +++ b/assets/pglite_fs_manifest.json @@ -0,0 +1,3457 @@ +[ + { + "path": "/home/web_user/.pgpass", + "start": 0, + "end": 204 + }, + { + "path": "/tmp/pglite/bin/initdb", + "start": 204, + "end": 204 + }, + { + "path": "/tmp/pglite/bin/postgres", + "start": 204, + "end": 204 + }, + { + "path": "/tmp/pglite/lib/postgresql/cyrillic_and_mic.so", + "start": 204, + "end": 20365 + }, + { + "path": "/tmp/pglite/lib/postgresql/dict_snowball.so", + "start": 20365, + "end": 1580915 + }, + { + "path": "/tmp/pglite/lib/postgresql/euc2004_sjis2004.so", + "start": 1580915, + "end": 1591990 + }, + { + "path": "/tmp/pglite/lib/postgresql/euc_cn_and_mic.so", + "start": 1591990, + "end": 1598856 + }, + { + "path": "/tmp/pglite/lib/postgresql/euc_jp_and_sjis.so", + "start": 1598856, + "end": 1622469 + }, + { + "path": "/tmp/pglite/lib/postgresql/euc_kr_and_mic.so", + "start": 1622469, + "end": 1629587 + }, + { + "path": "/tmp/pglite/lib/postgresql/euc_tw_and_big5.so", + "start": 1629587, + "end": 1651064 + }, + { + "path": "/tmp/pglite/lib/postgresql/latin2_and_win1250.so", + "start": 1651064, + "end": 1659835 + }, + { + "path": "/tmp/pglite/lib/postgresql/latin_and_mic.so", + "start": 1659835, + "end": 1667754 + }, + { + "path": "/tmp/pglite/lib/postgresql/libpqwalreceiver.so", + "start": 1667754, + "end": 2211970 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgoutput.so", + "start": 2211970, + "end": 2335393 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/config/install-sh", + "start": 2335393, + "end": 2349390 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/config/missing", + "start": 2349390, + "end": 2350738 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/src/Makefile.global", + "start": 2350738, + "end": 2386976 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/src/Makefile.port", + "start": 2386976, + "end": 2387528 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/src/Makefile.shlib", + "start": 2387528, + "end": 2402830 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/src/makefiles/pgxs.mk", + "start": 2402830, + "end": 2417741 + }, + { + "path": "/tmp/pglite/lib/postgresql/pgxs/src/nls-global.mk", + "start": 2417741, + "end": 2424609 + }, + { + "path": "/tmp/pglite/lib/postgresql/plpgsql.so", + "start": 2424609, + "end": 3203718 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_big5.so", + "start": 3203718, + "end": 3324635 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_cyrillic.so", + "start": 3324635, + "end": 3338178 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_euc2004.so", + "start": 3338178, + "end": 3549543 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_euc_cn.so", + "start": 3549543, + "end": 3630930 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_euc_jp.so", + "start": 3630930, + "end": 3788365 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_euc_kr.so", + "start": 3788365, + "end": 3897428 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_euc_tw.so", + "start": 3897428, + "end": 4103191 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_gb18030.so", + "start": 4103191, + "end": 4381413 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_gbk.so", + "start": 4381413, + "end": 4534095 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_iso8859.so", + "start": 4534095, + "end": 4568708 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_iso8859_1.so", + "start": 4568708, + "end": 4575376 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_johab.so", + "start": 4575376, + "end": 4743268 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_sjis.so", + "start": 4743268, + "end": 4831097 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_sjis2004.so", + "start": 4831097, + "end": 4964197 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_uhc.so", + "start": 4964197, + "end": 5137619 + }, + { + "path": "/tmp/pglite/lib/postgresql/utf8_and_win.so", + "start": 5137619, + "end": 5174344 + }, + { + "path": "/tmp/pglite/password", + "start": 5174344, + "end": 5174353 + }, + { + "path": "/tmp/pglite/share/postgresql/errcodes.txt", + "start": 5174353, + "end": 5207745 + }, + { + "path": "/tmp/pglite/share/postgresql/extension/plpgsql--1.0.sql", + "start": 5207745, + "end": 5208403 + }, + { + "path": "/tmp/pglite/share/postgresql/extension/plpgsql.control", + "start": 5208403, + "end": 5208596 + }, + { + "path": "/tmp/pglite/share/postgresql/information_schema.sql", + "start": 5208596, + "end": 5324119 + }, + { + "path": "/tmp/pglite/share/postgresql/pg_hba.conf.sample", + "start": 5324119, + "end": 5329744 + }, + { + "path": "/tmp/pglite/share/postgresql/pg_ident.conf.sample", + "start": 5329744, + "end": 5332384 + }, + { + "path": "/tmp/pglite/share/postgresql/pg_service.conf.sample", + "start": 5332384, + "end": 5332988 + }, + { + "path": "/tmp/pglite/share/postgresql/postgres.bki", + "start": 5332988, + "end": 6286256 + }, + { + "path": "/tmp/pglite/share/postgresql/postgresql.conf.sample", + "start": 6286256, + "end": 6316918 + }, + { + "path": "/tmp/pglite/share/postgresql/psqlrc.sample", + "start": 6316918, + "end": 6317196 + }, + { + "path": "/tmp/pglite/share/postgresql/snowball_create.sql", + "start": 6317196, + "end": 6361372 + }, + { + "path": "/tmp/pglite/share/postgresql/sql_features.txt", + "start": 6361372, + "end": 6397105 + }, + { + "path": "/tmp/pglite/share/postgresql/system_constraints.sql", + "start": 6397105, + "end": 6406000 + }, + { + "path": "/tmp/pglite/share/postgresql/system_functions.sql", + "start": 6406000, + "end": 6430303 + }, + { + "path": "/tmp/pglite/share/postgresql/system_views.sql", + "start": 6430303, + "end": 6481997 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Abidjan", + "start": 6481997, + "end": 6482127 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Accra", + "start": 6482127, + "end": 6482257 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Addis_Ababa", + "start": 6482257, + "end": 6482448 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Algiers", + "start": 6482448, + "end": 6482918 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Asmara", + "start": 6482918, + "end": 6483109 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Asmera", + "start": 6483109, + "end": 6483300 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Bamako", + "start": 6483300, + "end": 6483430 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Bangui", + "start": 6483430, + "end": 6483610 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Banjul", + "start": 6483610, + "end": 6483740 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Bissau", + "start": 6483740, + "end": 6483889 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Blantyre", + "start": 6483889, + "end": 6484020 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Brazzaville", + "start": 6484020, + "end": 6484200 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Bujumbura", + "start": 6484200, + "end": 6484331 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Cairo", + "start": 6484331, + "end": 6485640 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Casablanca", + "start": 6485640, + "end": 6487559 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Ceuta", + "start": 6487559, + "end": 6488121 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Conakry", + "start": 6488121, + "end": 6488251 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Dakar", + "start": 6488251, + "end": 6488381 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Dar_es_Salaam", + "start": 6488381, + "end": 6488572 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Djibouti", + "start": 6488572, + "end": 6488763 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Douala", + "start": 6488763, + "end": 6488943 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/El_Aaiun", + "start": 6488943, + "end": 6490773 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Freetown", + "start": 6490773, + "end": 6490903 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Gaborone", + "start": 6490903, + "end": 6491034 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Harare", + "start": 6491034, + "end": 6491165 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Johannesburg", + "start": 6491165, + "end": 6491355 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Juba", + "start": 6491355, + "end": 6491813 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Kampala", + "start": 6491813, + "end": 6492004 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Khartoum", + "start": 6492004, + "end": 6492462 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Kigali", + "start": 6492462, + "end": 6492593 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Kinshasa", + "start": 6492593, + "end": 6492773 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Lagos", + "start": 6492773, + "end": 6492953 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Libreville", + "start": 6492953, + "end": 6493133 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Lome", + "start": 6493133, + "end": 6493263 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Luanda", + "start": 6493263, + "end": 6493443 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Lubumbashi", + "start": 6493443, + "end": 6493574 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Lusaka", + "start": 6493574, + "end": 6493705 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Malabo", + "start": 6493705, + "end": 6493885 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Maputo", + "start": 6493885, + "end": 6494016 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Maseru", + "start": 6494016, + "end": 6494206 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Mbabane", + "start": 6494206, + "end": 6494396 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Mogadishu", + "start": 6494396, + "end": 6494587 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Monrovia", + "start": 6494587, + "end": 6494751 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Nairobi", + "start": 6494751, + "end": 6494942 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Ndjamena", + "start": 6494942, + "end": 6495102 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Niamey", + "start": 6495102, + "end": 6495282 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Nouakchott", + "start": 6495282, + "end": 6495412 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Ouagadougou", + "start": 6495412, + "end": 6495542 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Porto-Novo", + "start": 6495542, + "end": 6495722 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Sao_Tome", + "start": 6495722, + "end": 6495895 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Timbuktu", + "start": 6495895, + "end": 6496025 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Tripoli", + "start": 6496025, + "end": 6496456 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Tunis", + "start": 6496456, + "end": 6496905 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Africa/Windhoek", + "start": 6496905, + "end": 6497543 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Adak", + "start": 6497543, + "end": 6498512 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Anchorage", + "start": 6498512, + "end": 6499489 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Anguilla", + "start": 6499489, + "end": 6499666 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Antigua", + "start": 6499666, + "end": 6499843 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Araguaina", + "start": 6499843, + "end": 6500435 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Buenos_Aires", + "start": 6500435, + "end": 6501143 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Catamarca", + "start": 6501143, + "end": 6501851 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/ComodRivadavia", + "start": 6501851, + "end": 6502559 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Cordoba", + "start": 6502559, + "end": 6503267 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Jujuy", + "start": 6503267, + "end": 6503957 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/La_Rioja", + "start": 6503957, + "end": 6504674 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Mendoza", + "start": 6504674, + "end": 6505382 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Rio_Gallegos", + "start": 6505382, + "end": 6506090 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Salta", + "start": 6506090, + "end": 6506780 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/San_Juan", + "start": 6506780, + "end": 6507497 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/San_Luis", + "start": 6507497, + "end": 6508214 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Tucuman", + "start": 6508214, + "end": 6508940 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Argentina/Ushuaia", + "start": 6508940, + "end": 6509648 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Aruba", + "start": 6509648, + "end": 6509825 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Asuncion", + "start": 6509825, + "end": 6510910 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Atikokan", + "start": 6510910, + "end": 6511059 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Atka", + "start": 6511059, + "end": 6512028 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Bahia", + "start": 6512028, + "end": 6512710 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Bahia_Banderas", + "start": 6512710, + "end": 6513410 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Barbados", + "start": 6513410, + "end": 6513688 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Belem", + "start": 6513688, + "end": 6514082 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Belize", + "start": 6514082, + "end": 6515127 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Blanc-Sablon", + "start": 6515127, + "end": 6515304 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Boa_Vista", + "start": 6515304, + "end": 6515734 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Bogota", + "start": 6515734, + "end": 6515913 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Boise", + "start": 6515913, + "end": 6516912 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Buenos_Aires", + "start": 6516912, + "end": 6517620 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Cambridge_Bay", + "start": 6517620, + "end": 6518503 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Campo_Grande", + "start": 6518503, + "end": 6519455 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Cancun", + "start": 6519455, + "end": 6519993 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Caracas", + "start": 6519993, + "end": 6520183 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Catamarca", + "start": 6520183, + "end": 6520891 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Cayenne", + "start": 6520891, + "end": 6521042 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Cayman", + "start": 6521042, + "end": 6521191 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Chicago", + "start": 6521191, + "end": 6522945 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Chihuahua", + "start": 6522945, + "end": 6523636 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Ciudad_Juarez", + "start": 6523636, + "end": 6524354 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Coral_Harbour", + "start": 6524354, + "end": 6524503 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Cordoba", + "start": 6524503, + "end": 6525211 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Costa_Rica", + "start": 6525211, + "end": 6525443 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Coyhaique", + "start": 6525443, + "end": 6526805 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Creston", + "start": 6526805, + "end": 6527045 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Cuiaba", + "start": 6527045, + "end": 6527979 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Curacao", + "start": 6527979, + "end": 6528156 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Danmarkshavn", + "start": 6528156, + "end": 6528603 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Dawson", + "start": 6528603, + "end": 6529632 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Dawson_Creek", + "start": 6529632, + "end": 6530315 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Denver", + "start": 6530315, + "end": 6531357 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Detroit", + "start": 6531357, + "end": 6532256 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Dominica", + "start": 6532256, + "end": 6532433 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Edmonton", + "start": 6532433, + "end": 6533403 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Eirunepe", + "start": 6533403, + "end": 6533839 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/El_Salvador", + "start": 6533839, + "end": 6534015 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Ensenada", + "start": 6534015, + "end": 6535094 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Fort_Nelson", + "start": 6535094, + "end": 6536542 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Fort_Wayne", + "start": 6536542, + "end": 6537073 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Fortaleza", + "start": 6537073, + "end": 6537557 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Glace_Bay", + "start": 6537557, + "end": 6538437 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Godthab", + "start": 6538437, + "end": 6539402 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Goose_Bay", + "start": 6539402, + "end": 6540982 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Grand_Turk", + "start": 6540982, + "end": 6541835 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Grenada", + "start": 6541835, + "end": 6542012 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Guadeloupe", + "start": 6542012, + "end": 6542189 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Guatemala", + "start": 6542189, + "end": 6542401 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Guayaquil", + "start": 6542401, + "end": 6542580 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Guyana", + "start": 6542580, + "end": 6542761 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Halifax", + "start": 6542761, + "end": 6544433 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Havana", + "start": 6544433, + "end": 6545550 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Hermosillo", + "start": 6545550, + "end": 6545808 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Indianapolis", + "start": 6545808, + "end": 6546339 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Knox", + "start": 6546339, + "end": 6547355 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Marengo", + "start": 6547355, + "end": 6547922 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Petersburg", + "start": 6547922, + "end": 6548605 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Tell_City", + "start": 6548605, + "end": 6549127 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Vevay", + "start": 6549127, + "end": 6549496 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Vincennes", + "start": 6549496, + "end": 6550054 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indiana/Winamac", + "start": 6550054, + "end": 6550657 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Indianapolis", + "start": 6550657, + "end": 6551188 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Inuvik", + "start": 6551188, + "end": 6552005 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Iqaluit", + "start": 6552005, + "end": 6552860 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Jamaica", + "start": 6552860, + "end": 6553199 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Jujuy", + "start": 6553199, + "end": 6553889 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Juneau", + "start": 6553889, + "end": 6554855 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Kentucky/Louisville", + "start": 6554855, + "end": 6556097 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Kentucky/Monticello", + "start": 6556097, + "end": 6557069 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Knox_IN", + "start": 6557069, + "end": 6558085 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Kralendijk", + "start": 6558085, + "end": 6558262 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/La_Paz", + "start": 6558262, + "end": 6558432 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Lima", + "start": 6558432, + "end": 6558715 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Los_Angeles", + "start": 6558715, + "end": 6560009 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Louisville", + "start": 6560009, + "end": 6561251 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Lower_Princes", + "start": 6561251, + "end": 6561428 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Maceio", + "start": 6561428, + "end": 6561930 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Managua", + "start": 6561930, + "end": 6562225 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Manaus", + "start": 6562225, + "end": 6562637 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Marigot", + "start": 6562637, + "end": 6562814 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Martinique", + "start": 6562814, + "end": 6562992 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Matamoros", + "start": 6562992, + "end": 6563429 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Mazatlan", + "start": 6563429, + "end": 6564119 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Mendoza", + "start": 6564119, + "end": 6564827 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Menominee", + "start": 6564827, + "end": 6565744 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Merida", + "start": 6565744, + "end": 6566398 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Metlakatla", + "start": 6566398, + "end": 6566984 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Mexico_City", + "start": 6566984, + "end": 6567757 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Miquelon", + "start": 6567757, + "end": 6568307 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Moncton", + "start": 6568307, + "end": 6569800 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Monterrey", + "start": 6569800, + "end": 6570509 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Montevideo", + "start": 6570509, + "end": 6571478 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Montreal", + "start": 6571478, + "end": 6573195 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Montserrat", + "start": 6573195, + "end": 6573372 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Nassau", + "start": 6573372, + "end": 6575089 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/New_York", + "start": 6575089, + "end": 6576833 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Nipigon", + "start": 6576833, + "end": 6578550 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Nome", + "start": 6578550, + "end": 6579525 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Noronha", + "start": 6579525, + "end": 6580009 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/North_Dakota/Beulah", + "start": 6580009, + "end": 6581052 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/North_Dakota/Center", + "start": 6581052, + "end": 6582042 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/North_Dakota/New_Salem", + "start": 6582042, + "end": 6583032 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Nuuk", + "start": 6583032, + "end": 6583997 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Ojinaga", + "start": 6583997, + "end": 6584715 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Panama", + "start": 6584715, + "end": 6584864 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Pangnirtung", + "start": 6584864, + "end": 6585719 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Paramaribo", + "start": 6585719, + "end": 6585906 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Phoenix", + "start": 6585906, + "end": 6586146 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Port-au-Prince", + "start": 6586146, + "end": 6586711 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Port_of_Spain", + "start": 6586711, + "end": 6586888 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Porto_Acre", + "start": 6586888, + "end": 6587306 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Porto_Velho", + "start": 6587306, + "end": 6587700 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Puerto_Rico", + "start": 6587700, + "end": 6587877 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Punta_Arenas", + "start": 6587877, + "end": 6589095 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Rainy_River", + "start": 6589095, + "end": 6590389 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Rankin_Inlet", + "start": 6590389, + "end": 6591196 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Recife", + "start": 6591196, + "end": 6591680 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Regina", + "start": 6591680, + "end": 6592318 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Resolute", + "start": 6592318, + "end": 6593125 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Rio_Branco", + "start": 6593125, + "end": 6593543 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Rosario", + "start": 6593543, + "end": 6594251 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Santa_Isabel", + "start": 6594251, + "end": 6595330 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Santarem", + "start": 6595330, + "end": 6595739 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Santiago", + "start": 6595739, + "end": 6597093 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Santo_Domingo", + "start": 6597093, + "end": 6597410 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Sao_Paulo", + "start": 6597410, + "end": 6598362 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Scoresbysund", + "start": 6598362, + "end": 6599346 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Shiprock", + "start": 6599346, + "end": 6600388 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Sitka", + "start": 6600388, + "end": 6601344 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/St_Barthelemy", + "start": 6601344, + "end": 6601521 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/St_Johns", + "start": 6601521, + "end": 6603399 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/St_Kitts", + "start": 6603399, + "end": 6603576 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/St_Lucia", + "start": 6603576, + "end": 6603753 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/St_Thomas", + "start": 6603753, + "end": 6603930 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/St_Vincent", + "start": 6603930, + "end": 6604107 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Swift_Current", + "start": 6604107, + "end": 6604475 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Tegucigalpa", + "start": 6604475, + "end": 6604669 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Thule", + "start": 6604669, + "end": 6605124 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Thunder_Bay", + "start": 6605124, + "end": 6606841 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Tijuana", + "start": 6606841, + "end": 6607920 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Toronto", + "start": 6607920, + "end": 6609637 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Tortola", + "start": 6609637, + "end": 6609814 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Vancouver", + "start": 6609814, + "end": 6611144 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Virgin", + "start": 6611144, + "end": 6611321 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Whitehorse", + "start": 6611321, + "end": 6612350 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Winnipeg", + "start": 6612350, + "end": 6613644 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Yakutat", + "start": 6613644, + "end": 6614590 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/America/Yellowknife", + "start": 6614590, + "end": 6615560 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Casey", + "start": 6615560, + "end": 6615847 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Davis", + "start": 6615847, + "end": 6616044 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/DumontDUrville", + "start": 6616044, + "end": 6616198 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Macquarie", + "start": 6616198, + "end": 6617174 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Mawson", + "start": 6617174, + "end": 6617326 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/McMurdo", + "start": 6617326, + "end": 6618369 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Palmer", + "start": 6618369, + "end": 6619256 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Rothera", + "start": 6619256, + "end": 6619388 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/South_Pole", + "start": 6619388, + "end": 6620431 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Syowa", + "start": 6620431, + "end": 6620564 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Troll", + "start": 6620564, + "end": 6620722 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Antarctica/Vostok", + "start": 6620722, + "end": 6620892 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Arctic/Longyearbyen", + "start": 6620892, + "end": 6621597 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Aden", + "start": 6621597, + "end": 6621730 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Almaty", + "start": 6621730, + "end": 6622348 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Amman", + "start": 6622348, + "end": 6623276 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Anadyr", + "start": 6623276, + "end": 6624019 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Aqtau", + "start": 6624019, + "end": 6624625 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Aqtobe", + "start": 6624625, + "end": 6625240 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ashgabat", + "start": 6625240, + "end": 6625615 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ashkhabad", + "start": 6625615, + "end": 6625990 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Atyrau", + "start": 6625990, + "end": 6626606 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Baghdad", + "start": 6626606, + "end": 6627236 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Bahrain", + "start": 6627236, + "end": 6627388 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Baku", + "start": 6627388, + "end": 6628132 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Bangkok", + "start": 6628132, + "end": 6628284 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Barnaul", + "start": 6628284, + "end": 6629037 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Beirut", + "start": 6629037, + "end": 6629769 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Bishkek", + "start": 6629769, + "end": 6630387 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Brunei", + "start": 6630387, + "end": 6630707 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Calcutta", + "start": 6630707, + "end": 6630927 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Chita", + "start": 6630927, + "end": 6631677 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Choibalsan", + "start": 6631677, + "end": 6632271 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Chongqing", + "start": 6632271, + "end": 6632664 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Chungking", + "start": 6632664, + "end": 6633057 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Colombo", + "start": 6633057, + "end": 6633304 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Dacca", + "start": 6633304, + "end": 6633535 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Damascus", + "start": 6633535, + "end": 6634769 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Dhaka", + "start": 6634769, + "end": 6635000 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Dili", + "start": 6635000, + "end": 6635170 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Dubai", + "start": 6635170, + "end": 6635303 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Dushanbe", + "start": 6635303, + "end": 6635669 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Famagusta", + "start": 6635669, + "end": 6636609 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Gaza", + "start": 6636609, + "end": 6639559 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Harbin", + "start": 6639559, + "end": 6639952 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Hebron", + "start": 6639952, + "end": 6642920 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ho_Chi_Minh", + "start": 6642920, + "end": 6643156 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Hong_Kong", + "start": 6643156, + "end": 6643931 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Hovd", + "start": 6643931, + "end": 6644525 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Irkutsk", + "start": 6644525, + "end": 6645285 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Istanbul", + "start": 6645285, + "end": 6646485 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Jakarta", + "start": 6646485, + "end": 6646733 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Jayapura", + "start": 6646733, + "end": 6646904 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Jerusalem", + "start": 6646904, + "end": 6647978 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kabul", + "start": 6647978, + "end": 6648137 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kamchatka", + "start": 6648137, + "end": 6648864 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Karachi", + "start": 6648864, + "end": 6649130 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kashgar", + "start": 6649130, + "end": 6649263 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kathmandu", + "start": 6649263, + "end": 6649424 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Katmandu", + "start": 6649424, + "end": 6649585 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Khandyga", + "start": 6649585, + "end": 6650360 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kolkata", + "start": 6650360, + "end": 6650580 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Krasnoyarsk", + "start": 6650580, + "end": 6651321 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kuala_Lumpur", + "start": 6651321, + "end": 6651577 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kuching", + "start": 6651577, + "end": 6651897 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Kuwait", + "start": 6651897, + "end": 6652030 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Macao", + "start": 6652030, + "end": 6652821 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Macau", + "start": 6652821, + "end": 6653612 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Magadan", + "start": 6653612, + "end": 6654363 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Makassar", + "start": 6654363, + "end": 6654553 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Manila", + "start": 6654553, + "end": 6654827 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Muscat", + "start": 6654827, + "end": 6654960 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Nicosia", + "start": 6654960, + "end": 6655557 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Novokuznetsk", + "start": 6655557, + "end": 6656283 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Novosibirsk", + "start": 6656283, + "end": 6657036 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Omsk", + "start": 6657036, + "end": 6657777 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Oral", + "start": 6657777, + "end": 6658402 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Phnom_Penh", + "start": 6658402, + "end": 6658554 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Pontianak", + "start": 6658554, + "end": 6658801 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Pyongyang", + "start": 6658801, + "end": 6658984 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Qatar", + "start": 6658984, + "end": 6659136 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Qostanay", + "start": 6659136, + "end": 6659760 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Qyzylorda", + "start": 6659760, + "end": 6660384 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Rangoon", + "start": 6660384, + "end": 6660571 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Riyadh", + "start": 6660571, + "end": 6660704 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Saigon", + "start": 6660704, + "end": 6660940 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Sakhalin", + "start": 6660940, + "end": 6661695 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Samarkand", + "start": 6661695, + "end": 6662061 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Seoul", + "start": 6662061, + "end": 6662476 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Shanghai", + "start": 6662476, + "end": 6662869 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Singapore", + "start": 6662869, + "end": 6663125 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Srednekolymsk", + "start": 6663125, + "end": 6663867 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Taipei", + "start": 6663867, + "end": 6664378 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Tashkent", + "start": 6664378, + "end": 6664744 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Tbilisi", + "start": 6664744, + "end": 6665373 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Tehran", + "start": 6665373, + "end": 6666185 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Tel_Aviv", + "start": 6666185, + "end": 6667259 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Thimbu", + "start": 6667259, + "end": 6667413 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Thimphu", + "start": 6667413, + "end": 6667567 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Tokyo", + "start": 6667567, + "end": 6667780 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Tomsk", + "start": 6667780, + "end": 6668533 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ujung_Pandang", + "start": 6668533, + "end": 6668723 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ulaanbaatar", + "start": 6668723, + "end": 6669317 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ulan_Bator", + "start": 6669317, + "end": 6669911 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Urumqi", + "start": 6669911, + "end": 6670044 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Ust-Nera", + "start": 6670044, + "end": 6670815 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Vientiane", + "start": 6670815, + "end": 6670967 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Vladivostok", + "start": 6670967, + "end": 6671709 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Yakutsk", + "start": 6671709, + "end": 6672450 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Yangon", + "start": 6672450, + "end": 6672637 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Yekaterinburg", + "start": 6672637, + "end": 6673397 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Asia/Yerevan", + "start": 6673397, + "end": 6674105 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Azores", + "start": 6674105, + "end": 6675506 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Bermuda", + "start": 6675506, + "end": 6676530 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Canary", + "start": 6676530, + "end": 6677008 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Cape_Verde", + "start": 6677008, + "end": 6677183 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Faeroe", + "start": 6677183, + "end": 6677624 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Faroe", + "start": 6677624, + "end": 6678065 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Jan_Mayen", + "start": 6678065, + "end": 6678770 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Madeira", + "start": 6678770, + "end": 6680142 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Reykjavik", + "start": 6680142, + "end": 6680272 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/South_Georgia", + "start": 6680272, + "end": 6680404 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/St_Helena", + "start": 6680404, + "end": 6680534 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Atlantic/Stanley", + "start": 6680534, + "end": 6681323 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/ACT", + "start": 6681323, + "end": 6682227 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Adelaide", + "start": 6682227, + "end": 6683148 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Brisbane", + "start": 6683148, + "end": 6683437 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Broken_Hill", + "start": 6683437, + "end": 6684378 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Canberra", + "start": 6684378, + "end": 6685282 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Currie", + "start": 6685282, + "end": 6686285 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Darwin", + "start": 6686285, + "end": 6686519 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Eucla", + "start": 6686519, + "end": 6686833 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Hobart", + "start": 6686833, + "end": 6687836 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/LHI", + "start": 6687836, + "end": 6688528 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Lindeman", + "start": 6688528, + "end": 6688853 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Lord_Howe", + "start": 6688853, + "end": 6689545 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Melbourne", + "start": 6689545, + "end": 6690449 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/NSW", + "start": 6690449, + "end": 6691353 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/North", + "start": 6691353, + "end": 6691587 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Perth", + "start": 6691587, + "end": 6691893 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Queensland", + "start": 6691893, + "end": 6692182 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/South", + "start": 6692182, + "end": 6693103 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Sydney", + "start": 6693103, + "end": 6694007 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Tasmania", + "start": 6694007, + "end": 6695010 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Victoria", + "start": 6695010, + "end": 6695914 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/West", + "start": 6695914, + "end": 6696220 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Australia/Yancowinna", + "start": 6696220, + "end": 6697161 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Brazil/Acre", + "start": 6697161, + "end": 6697579 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Brazil/DeNoronha", + "start": 6697579, + "end": 6698063 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Brazil/East", + "start": 6698063, + "end": 6699015 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Brazil/West", + "start": 6699015, + "end": 6699427 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/CET", + "start": 6699427, + "end": 6700530 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/CST6CDT", + "start": 6700530, + "end": 6702284 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Atlantic", + "start": 6702284, + "end": 6703956 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Central", + "start": 6703956, + "end": 6705250 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Eastern", + "start": 6705250, + "end": 6706967 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Mountain", + "start": 6706967, + "end": 6707937 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Newfoundland", + "start": 6707937, + "end": 6709815 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Pacific", + "start": 6709815, + "end": 6711145 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Saskatchewan", + "start": 6711145, + "end": 6711783 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Canada/Yukon", + "start": 6711783, + "end": 6712812 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Chile/Continental", + "start": 6712812, + "end": 6714166 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Chile/EasterIsland", + "start": 6714166, + "end": 6715340 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Cuba", + "start": 6715340, + "end": 6716457 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/EET", + "start": 6716457, + "end": 6717139 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/EST", + "start": 6717139, + "end": 6717288 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/EST5EDT", + "start": 6717288, + "end": 6719032 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Egypt", + "start": 6719032, + "end": 6720341 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Eire", + "start": 6720341, + "end": 6721837 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT", + "start": 6721837, + "end": 6721948 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+0", + "start": 6721948, + "end": 6722059 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+1", + "start": 6722059, + "end": 6722172 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+10", + "start": 6722172, + "end": 6722286 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+11", + "start": 6722286, + "end": 6722400 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+12", + "start": 6722400, + "end": 6722514 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+2", + "start": 6722514, + "end": 6722627 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+3", + "start": 6722627, + "end": 6722740 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+4", + "start": 6722740, + "end": 6722853 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+5", + "start": 6722853, + "end": 6722966 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+6", + "start": 6722966, + "end": 6723079 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+7", + "start": 6723079, + "end": 6723192 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+8", + "start": 6723192, + "end": 6723305 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT+9", + "start": 6723305, + "end": 6723418 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-0", + "start": 6723418, + "end": 6723529 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-1", + "start": 6723529, + "end": 6723643 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-10", + "start": 6723643, + "end": 6723758 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-11", + "start": 6723758, + "end": 6723873 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-12", + "start": 6723873, + "end": 6723988 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-13", + "start": 6723988, + "end": 6724103 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-14", + "start": 6724103, + "end": 6724218 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-2", + "start": 6724218, + "end": 6724332 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-3", + "start": 6724332, + "end": 6724446 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-4", + "start": 6724446, + "end": 6724560 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-5", + "start": 6724560, + "end": 6724674 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-6", + "start": 6724674, + "end": 6724788 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-7", + "start": 6724788, + "end": 6724902 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-8", + "start": 6724902, + "end": 6725016 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT-9", + "start": 6725016, + "end": 6725130 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/GMT0", + "start": 6725130, + "end": 6725241 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/Greenwich", + "start": 6725241, + "end": 6725352 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/UCT", + "start": 6725352, + "end": 6725463 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/UTC", + "start": 6725463, + "end": 6725574 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/Universal", + "start": 6725574, + "end": 6725685 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Etc/Zulu", + "start": 6725685, + "end": 6725796 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Amsterdam", + "start": 6725796, + "end": 6726899 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Andorra", + "start": 6726899, + "end": 6727288 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Astrakhan", + "start": 6727288, + "end": 6728014 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Athens", + "start": 6728014, + "end": 6728696 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Belfast", + "start": 6728696, + "end": 6730295 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Belgrade", + "start": 6730295, + "end": 6730773 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Berlin", + "start": 6730773, + "end": 6731478 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Bratislava", + "start": 6731478, + "end": 6732201 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Brussels", + "start": 6732201, + "end": 6733304 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Bucharest", + "start": 6733304, + "end": 6733965 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Budapest", + "start": 6733965, + "end": 6734731 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Busingen", + "start": 6734731, + "end": 6735228 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Chisinau", + "start": 6735228, + "end": 6735983 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Copenhagen", + "start": 6735983, + "end": 6736688 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Dublin", + "start": 6736688, + "end": 6738184 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Gibraltar", + "start": 6738184, + "end": 6739404 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Guernsey", + "start": 6739404, + "end": 6741003 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Helsinki", + "start": 6741003, + "end": 6741484 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Isle_of_Man", + "start": 6741484, + "end": 6743083 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Istanbul", + "start": 6743083, + "end": 6744283 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Jersey", + "start": 6744283, + "end": 6745882 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Kaliningrad", + "start": 6745882, + "end": 6746786 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Kiev", + "start": 6746786, + "end": 6747344 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Kirov", + "start": 6747344, + "end": 6748079 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Kyiv", + "start": 6748079, + "end": 6748637 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Lisbon", + "start": 6748637, + "end": 6750100 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Ljubljana", + "start": 6750100, + "end": 6750578 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/London", + "start": 6750578, + "end": 6752177 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Luxembourg", + "start": 6752177, + "end": 6753280 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Madrid", + "start": 6753280, + "end": 6754177 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Malta", + "start": 6754177, + "end": 6755105 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Mariehamn", + "start": 6755105, + "end": 6755586 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Minsk", + "start": 6755586, + "end": 6756394 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Monaco", + "start": 6756394, + "end": 6757499 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Moscow", + "start": 6757499, + "end": 6758407 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Nicosia", + "start": 6758407, + "end": 6759004 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Oslo", + "start": 6759004, + "end": 6759709 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Paris", + "start": 6759709, + "end": 6760814 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Podgorica", + "start": 6760814, + "end": 6761292 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Prague", + "start": 6761292, + "end": 6762015 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Riga", + "start": 6762015, + "end": 6762709 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Rome", + "start": 6762709, + "end": 6763656 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Samara", + "start": 6763656, + "end": 6764388 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/San_Marino", + "start": 6764388, + "end": 6765335 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Sarajevo", + "start": 6765335, + "end": 6765813 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Saratov", + "start": 6765813, + "end": 6766539 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Simferopol", + "start": 6766539, + "end": 6767404 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Skopje", + "start": 6767404, + "end": 6767882 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Sofia", + "start": 6767882, + "end": 6768474 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Stockholm", + "start": 6768474, + "end": 6769179 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Tallinn", + "start": 6769179, + "end": 6769854 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Tirane", + "start": 6769854, + "end": 6770458 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Tiraspol", + "start": 6770458, + "end": 6771213 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Ulyanovsk", + "start": 6771213, + "end": 6771973 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Uzhgorod", + "start": 6771973, + "end": 6772531 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Vaduz", + "start": 6772531, + "end": 6773028 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Vatican", + "start": 6773028, + "end": 6773975 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Vienna", + "start": 6773975, + "end": 6774633 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Vilnius", + "start": 6774633, + "end": 6775309 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Volgograd", + "start": 6775309, + "end": 6776062 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Warsaw", + "start": 6776062, + "end": 6776985 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Zagreb", + "start": 6776985, + "end": 6777463 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Zaporozhye", + "start": 6777463, + "end": 6778021 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Europe/Zurich", + "start": 6778021, + "end": 6778518 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Factory", + "start": 6778518, + "end": 6778631 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/GB", + "start": 6778631, + "end": 6780230 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/GB-Eire", + "start": 6780230, + "end": 6781829 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/GMT", + "start": 6781829, + "end": 6781940 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/GMT+0", + "start": 6781940, + "end": 6782051 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/GMT-0", + "start": 6782051, + "end": 6782162 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/GMT0", + "start": 6782162, + "end": 6782273 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Greenwich", + "start": 6782273, + "end": 6782384 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/HST", + "start": 6782384, + "end": 6782605 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Hongkong", + "start": 6782605, + "end": 6783380 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Iceland", + "start": 6783380, + "end": 6783510 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Antananarivo", + "start": 6783510, + "end": 6783701 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Chagos", + "start": 6783701, + "end": 6783853 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Christmas", + "start": 6783853, + "end": 6784005 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Cocos", + "start": 6784005, + "end": 6784192 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Comoro", + "start": 6784192, + "end": 6784383 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Kerguelen", + "start": 6784383, + "end": 6784535 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Mahe", + "start": 6784535, + "end": 6784668 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Maldives", + "start": 6784668, + "end": 6784820 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Mauritius", + "start": 6784820, + "end": 6784999 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Mayotte", + "start": 6784999, + "end": 6785190 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Indian/Reunion", + "start": 6785190, + "end": 6785323 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Iran", + "start": 6785323, + "end": 6786135 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Israel", + "start": 6786135, + "end": 6787209 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Jamaica", + "start": 6787209, + "end": 6787548 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Japan", + "start": 6787548, + "end": 6787761 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Kwajalein", + "start": 6787761, + "end": 6787980 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Libya", + "start": 6787980, + "end": 6788411 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/MET", + "start": 6788411, + "end": 6789514 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/MST", + "start": 6789514, + "end": 6789754 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/MST7MDT", + "start": 6789754, + "end": 6790796 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Mexico/BajaNorte", + "start": 6790796, + "end": 6791875 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Mexico/BajaSur", + "start": 6791875, + "end": 6792565 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Mexico/General", + "start": 6792565, + "end": 6793338 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/NZ", + "start": 6793338, + "end": 6794381 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/NZ-CHAT", + "start": 6794381, + "end": 6795189 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Navajo", + "start": 6795189, + "end": 6796231 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/PRC", + "start": 6796231, + "end": 6796624 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/PST8PDT", + "start": 6796624, + "end": 6797918 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Apia", + "start": 6797918, + "end": 6798325 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Auckland", + "start": 6798325, + "end": 6799368 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Bougainville", + "start": 6799368, + "end": 6799569 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Chatham", + "start": 6799569, + "end": 6800377 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Chuuk", + "start": 6800377, + "end": 6800531 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Easter", + "start": 6800531, + "end": 6801705 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Efate", + "start": 6801705, + "end": 6802047 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Enderbury", + "start": 6802047, + "end": 6802219 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Fakaofo", + "start": 6802219, + "end": 6802372 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Fiji", + "start": 6802372, + "end": 6802768 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Funafuti", + "start": 6802768, + "end": 6802902 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Galapagos", + "start": 6802902, + "end": 6803077 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Gambier", + "start": 6803077, + "end": 6803209 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Guadalcanal", + "start": 6803209, + "end": 6803343 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Guam", + "start": 6803343, + "end": 6803693 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Honolulu", + "start": 6803693, + "end": 6803914 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Johnston", + "start": 6803914, + "end": 6804135 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Kanton", + "start": 6804135, + "end": 6804307 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Kiritimati", + "start": 6804307, + "end": 6804481 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Kosrae", + "start": 6804481, + "end": 6804723 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Kwajalein", + "start": 6804723, + "end": 6804942 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Majuro", + "start": 6804942, + "end": 6805076 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Marquesas", + "start": 6805076, + "end": 6805215 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Midway", + "start": 6805215, + "end": 6805361 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Nauru", + "start": 6805361, + "end": 6805544 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Niue", + "start": 6805544, + "end": 6805698 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Norfolk", + "start": 6805698, + "end": 6805935 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Noumea", + "start": 6805935, + "end": 6806133 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Pago_Pago", + "start": 6806133, + "end": 6806279 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Palau", + "start": 6806279, + "end": 6806427 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Pitcairn", + "start": 6806427, + "end": 6806580 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Pohnpei", + "start": 6806580, + "end": 6806714 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Ponape", + "start": 6806714, + "end": 6806848 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Port_Moresby", + "start": 6806848, + "end": 6807002 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Rarotonga", + "start": 6807002, + "end": 6807408 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Saipan", + "start": 6807408, + "end": 6807758 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Samoa", + "start": 6807758, + "end": 6807904 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Tahiti", + "start": 6807904, + "end": 6808037 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Tarawa", + "start": 6808037, + "end": 6808171 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Tongatapu", + "start": 6808171, + "end": 6808408 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Truk", + "start": 6808408, + "end": 6808562 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Wake", + "start": 6808562, + "end": 6808696 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Wallis", + "start": 6808696, + "end": 6808830 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Pacific/Yap", + "start": 6808830, + "end": 6808984 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Poland", + "start": 6808984, + "end": 6809907 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Portugal", + "start": 6809907, + "end": 6811370 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/ROC", + "start": 6811370, + "end": 6811881 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/ROK", + "start": 6811881, + "end": 6812296 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Singapore", + "start": 6812296, + "end": 6812552 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Turkey", + "start": 6812552, + "end": 6813752 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/UCT", + "start": 6813752, + "end": 6813863 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Alaska", + "start": 6813863, + "end": 6814840 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Aleutian", + "start": 6814840, + "end": 6815809 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Arizona", + "start": 6815809, + "end": 6816049 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Central", + "start": 6816049, + "end": 6817803 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/East-Indiana", + "start": 6817803, + "end": 6818334 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Eastern", + "start": 6818334, + "end": 6820078 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Hawaii", + "start": 6820078, + "end": 6820299 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Indiana-Starke", + "start": 6820299, + "end": 6821315 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Michigan", + "start": 6821315, + "end": 6822214 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Mountain", + "start": 6822214, + "end": 6823256 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Pacific", + "start": 6823256, + "end": 6824550 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/US/Samoa", + "start": 6824550, + "end": 6824696 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/UTC", + "start": 6824696, + "end": 6824807 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Universal", + "start": 6824807, + "end": 6824918 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/W-SU", + "start": 6824918, + "end": 6825826 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/WET", + "start": 6825826, + "end": 6827289 + }, + { + "path": "/tmp/pglite/share/postgresql/timezone/Zulu", + "start": 6827289, + "end": 6827400 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Africa.txt", + "start": 6827400, + "end": 6834373 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/America.txt", + "start": 6834373, + "end": 6845380 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Antarctica.txt", + "start": 6845380, + "end": 6846514 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Asia.txt", + "start": 6846514, + "end": 6854825 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Atlantic.txt", + "start": 6854825, + "end": 6858358 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Australia", + "start": 6858358, + "end": 6859493 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Australia.txt", + "start": 6859493, + "end": 6862877 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Default", + "start": 6862877, + "end": 6890091 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Etc.txt", + "start": 6890091, + "end": 6891341 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Europe.txt", + "start": 6891341, + "end": 6900087 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/India", + "start": 6900087, + "end": 6900680 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Indian.txt", + "start": 6900680, + "end": 6901941 + }, + { + "path": "/tmp/pglite/share/postgresql/timezonesets/Pacific.txt", + "start": 6901941, + "end": 6905709 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/danish.stop", + "start": 6905709, + "end": 6906133 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/dutch.stop", + "start": 6906133, + "end": 6906586 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/english.stop", + "start": 6906586, + "end": 6907208 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/finnish.stop", + "start": 6907208, + "end": 6908787 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/french.stop", + "start": 6908787, + "end": 6909592 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/german.stop", + "start": 6909592, + "end": 6910941 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/hungarian.stop", + "start": 6910941, + "end": 6912168 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/hunspell_sample.affix", + "start": 6912168, + "end": 6912411 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/hunspell_sample_long.affix", + "start": 6912411, + "end": 6913044 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/hunspell_sample_long.dict", + "start": 6913044, + "end": 6913142 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/hunspell_sample_num.affix", + "start": 6913142, + "end": 6913604 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/hunspell_sample_num.dict", + "start": 6913604, + "end": 6913733 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/ispell_sample.affix", + "start": 6913733, + "end": 6914198 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/ispell_sample.dict", + "start": 6914198, + "end": 6914279 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/italian.stop", + "start": 6914279, + "end": 6915933 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/nepali.stop", + "start": 6915933, + "end": 6920194 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/norwegian.stop", + "start": 6920194, + "end": 6921045 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/portuguese.stop", + "start": 6921045, + "end": 6922312 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/russian.stop", + "start": 6922312, + "end": 6923547 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/spanish.stop", + "start": 6923547, + "end": 6925725 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/swedish.stop", + "start": 6925725, + "end": 6926284 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/synonym_sample.syn", + "start": 6926284, + "end": 6926357 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/thesaurus_sample.ths", + "start": 6926357, + "end": 6926830 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/turkish.stop", + "start": 6926830, + "end": 6927090 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/unaccent.rules", + "start": 6927090, + "end": 6937093 + }, + { + "path": "/tmp/pglite/share/postgresql/tsearch_data/xsyn_sample.rules", + "start": 6937093, + "end": 6937232 + } +] diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..93e1038d --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +msrv = "1.92.0" +avoid-breaking-exported-api = true diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..b3d589d5 --- /dev/null +++ b/deny.toml @@ -0,0 +1,31 @@ +[advisories] +yanked = "warn" +ignore = [] + +[licenses] +allow = [ + "0BSD", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "MIT", + "MPL-2.0", + "PostgreSQL", + "Unicode-3.0", + "Unlicense", + "Zlib", +] +confidence-threshold = 0.8 + +[bans] +# Wasmtime 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" +wildcards = "allow" +highlight = "all" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" diff --git a/docs/ALIGNMENT_DIFF.md b/docs/ALIGNMENT_DIFF.md new file mode 100644 index 00000000..c9c3bc8f --- /dev/null +++ b/docs/ALIGNMENT_DIFF.md @@ -0,0 +1,226 @@ +### PGlite Rust vs TypeScript reference – file-by-file differences + +Reference TS sources: https://github.com/electric-sql/pglite/tree/main/packages/pglite/src + +This document lists what is extra/missing on either side. Code references below cite exact lines from our repo (Rust) and the cloned TS reference. + +#### src/pglite/base.rs ↔ packages/pglite/src (init/provisioning) + +- Rust extra: embedded runtime archive unpack (include_bytes, tar.xz) to host FS. +```91:105:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +info!("unpacking embedded runtime"); +let mut decoder = XzDecoder::new(*ARCHIVE_BYTES); +let mut ar = Archive::new(&mut decoder); +let unpack_target = paths + .pgroot + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| paths.pgroot.clone()); +ar.unpack(&unpack_target).with_context(|| { + format!( + "unpack embedded pglite-wasi.tar.xz into {}", + unpack_target.display() + ) +})?; +``` +- TS counterpart: engine load via factory with Emscripten opts (no host tar unpack). +```370:372:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +// Load the database engine +this.mod = await PostgresModFactory(emscriptenOpts) +``` + +- Rust: concrete host paths model `pgroot/tmp/pglite/base`. +```34:39:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +let pgroot = base.join("tmp"); +let pgdata = pgroot.join("pglite").join("base"); +``` +- TS: virtual/adapter FS chosen at runtime. +```190:195:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +const { dataDir, fsType } = parseDataDir(options.dataDir) +this.fs = await loadFs(dataDir, fsType) +``` + +- Cluster detection parity, different surface: + - Rust checks host `PG_VERSION`. +```52:58:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +fn marker_cluster(&self) -> PathBuf { self.pgdata.join("PG_VERSION") } +pub fn is_cluster_initialized(&self) -> bool { self.marker_cluster().exists() } +``` + - TS checks Emscripten FS path. +```388:392:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +if (this.mod.FS.analyzePath(PGDATA + '/PG_VERSION').exists) { /* ... */ } +``` + +Missing in Rust vs TS: TS can load `loadDataDir` tar before init (we currently don’t expose a tar import API at this layer). +```376:385:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +if (options.loadDataDir) { + if (this.mod.FS.analyzePath(PGDATA + '/PG_VERSION').exists) { + throw new Error('Database already exists, cannot load from tarball') + } + await loadTar(this.mod.FS, options.loadDataDir, PGDATA) +} +``` + +#### src/pglite/postgres_mod.rs ↔ packages/pglite/src/pglite.ts + postgresMod.ts + +- Rust extra: wasmtime/WASI process setup with env and argv values. +```379:386:/Users/sid/dev/pglite-oxide/src/pglite/postgres_mod.rs +builder + .env("PREFIX", WASM_PREFIX) + .env("PGDATA", PGDATA_DIR) + .env("PGUSER", "postgres") + .env("PGDATABASE", "template1") + .env("MODE", "REACT") + .env("REPL", "N"); +``` +- TS counterpart: passes the same values via `arguments` to Emscripten. +```200:209:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +const args = [ + `PGDATA=${PGDATA}`, + `PREFIX=${WASM_PREFIX}`, + `PGUSER=${options.username ?? 'postgres'}`, + `PGDATABASE=${options.database ?? 'template1'}`, + 'MODE=REACT', + 'REPL=N', + ...(this.debug ? ['-d', this.debug.toString()] : []), +] +``` + +- Rust extra: preopen host dirs into WASI (`pgroot`→`/tmp`, `pgdata`→`/tmp/pglite/base`, optional `/dev`). TS mounts within virtual FS. +```394:407:/Users/sid/dev/pglite-oxide/src/pglite/postgres_mod.rs +builder.preopened_dir(mount_dir, DirPerms::all(), FilePerms::all(), "/tmp"); +builder.preopened_dir(pgdata_dir, DirPerms::all(), FilePerms::all(), "/tmp/pglite/base"); +``` + +- Export usage parity: + - Rust typed calls vs TS direct exports. +```138:156:/Users/sid/dev/pglite-oxide/src/pglite/postgres_mod.rs +let rc = self.exports.pgl_initdb.call(&mut self.store, ())?; +``` +```397:401:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +const idb = this.mod._pgl_initdb() +``` + +- Transport path parity with differences in fallback: + - Rust implements CMA only; file transport is stubbed. +```244:251:/Users/sid/dev/pglite-oxide/src/pglite/postgres_mod.rs +match self.transport { + TransportMode::Cma { .. } => self.exec_cma(...), + TransportMode::File => bail!("file transport is not supported yet"), +} +``` + - TS supports both CMA and file via “socketfiles”. +```585:606:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +case 'cma': { mod._interactive_write(message.length); mod.HEAPU8.set(message, 1) } +case 'file': { + const pg_lck = '/tmp/pglite/base/.s.PGSQL.5432.lck.in' + const pg_in = '/tmp/pglite/base/.s.PGSQL.5432.in' + mod._interactive_write(0) + mod.FS.writeFile(pg_lck, message) + mod.FS.rename(pg_lck, pg_in) +} +``` + +- Rust extra: seed `/dev/urandom` file in host FS. +```434:448:/Users/sid/dev/pglite-oxide/src/pglite/postgres_mod.rs +let urandom = dev_path.join("urandom"); +if urandom.exists() { return Ok(()); } +let mut buf = [0u8; 128]; +getrandom::fill(&mut buf)?; +std::fs::write(&urandom, buf)?; +``` +- TS counterpart: not present; TS registers `/dev/blob` for COPY. +```259:319:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +// Register /dev/blob device ... mod.FS.registerDevice(...); mod.FS.mkdev('/dev/blob', devId) +``` + +#### src/pglite/client.rs ↔ packages/pglite/src/pglite.ts (client surface) + +- Parity: query/exec/transaction/describe, protocol steps, error wrapping. +```221:291:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +// parse -> describe(S) -> bind -> describe(P) -> execute -> sync; error wrapping +``` +```221:301:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/base.ts +// parse -> describe(S) -> bind -> describe(P) -> execute -> sync; error wrapping +``` + +- Rust extra: `sync_to_fs` best-effort syncs host directories; TS actually syncs its configured virtual/persistent FS after ops. +```487:491:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +pub fn sync_to_fs(&mut self) -> Result<()> { /* best-effort host fsync */ } +``` +```754:776:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +await this.fs!.syncToFs(this.#relaxedDurability) +``` + +- COPY blob handling parity with different surface: + - Rust writes/reads `/dev/blob` via host FS path (`pgroot/dev/blob`). +```723:743:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn dev_blob_path(&self) -> PathBuf { self.pg.paths().pgroot.join("dev/blob") } +``` + - TS implements a virtual `/dev/blob` device within Emscripten FS. +```259:319:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +mod.FS.registerDevice(devId, devOpt); mod.FS.mkdev('/dev/blob', devId) +``` + +#### src/pglite/parse.rs ↔ packages/pglite/src/parse.ts + +- Parity: build Results from backend messages; same rowMode semantics; affectedRows logic. +```11:66:/Users/sid/dev/pglite-oxide/src/pglite/parse.rs +pub fn parse_results(...) +``` +```15:87:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/parse.ts +export function parseResults(...) +``` + +#### src/pglite/interface.rs ↔ packages/pglite/src/interface.ts + +- Parity: QueryOptions, ExecProtocolOptions, Results/Describe types. +```32:41:/Users/sid/dev/pglite-oxide/src/pglite/interface.rs +pub struct QueryOptions { row_mode, parsers, serializers, blob, param_types, on_notice, data_transfer_container } +``` +```23:37:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/interface.ts +export interface QueryOptions { rowMode, parsers, serializers, blob, onNotice, paramTypes } +``` + +- Difference: Rust strong types and Arc callbacks; TS uses structural types. + +#### src/pglite/errors.rs ↔ packages/pglite/src/errors.ts + +- Parity: enrich DatabaseError with query/params/options. +```9:31:/Users/sid/dev/pglite-oxide/src/pglite/errors.rs +pub struct PgliteError { source, query, params, query_options } +``` +```10:21:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/errors.ts +export function makePGliteError({ e, query, params, options }) { /* attach */ } +``` + +#### src/pglite/types.rs ↔ packages/pglite/src/types.ts + +- Parity: default parsers/serializers, array parser/serializer, OID constants. +```34:36:/Users/sid/dev/pglite-oxide/src/pglite/types.rs +pub static DEFAULT_PARSERS ... DEFAULT_SERIALIZERS ... +``` +```184:188:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/types.ts +export const parsers = defaultHandlers.parsers; export const serializers = defaultHandlers.serializers +``` + +- Differences: + - Rust returns JSON for unknown types; TS returns raw string. + - TS has broader OID coverage; Rust includes a focused subset. + +#### src/pglite/transport.rs ↔ pglite.ts execProtocolRaw + +- Rust: CMA implemented; file transport unimplemented. +```35:53:/Users/sid/dev/pglite-oxide/src/pglite/transport.rs +match self { Transport::Cma { .. } => send_cma(...), Transport::File => bail!(...) } +``` +- TS: supports CMA and file via socketfiles inside FS. +```585:647:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +// cma and file branches +``` + +### Summary of extras/missing + +- Rust extras: embedded tar unpack; WASI preopens; `/dev/urandom` file; host `/dev/blob` path; strong typing and explicit error structs. +- TS extras: virtual FS abstraction with idb/node/memory backends; file transport fallback; extension bundle plumbing and dynamic FS bundle loader; real FS sync after each op. + diff --git a/docs/ALIGNMENT_NOTES.md b/docs/ALIGNMENT_NOTES.md new file mode 100644 index 00000000..7d52cc79 --- /dev/null +++ b/docs/ALIGNMENT_NOTES.md @@ -0,0 +1,35 @@ +## Strict Parity Guidance (Non-Web Runtime) + +The `packages/pglite` TypeScript sources define the runtime contract. The Rust port must replicate observable behaviour exactly—unless a feature is explicitly web-only (IDB, OPFS, workers), the flow, data, and surface must match the reference. + +### Core principles + +1. **Clone the JS contract** + - Public APIs, default options, error semantics, result structures, protocol steps, and type conversion must produce the same outcomes as the TS reference. + - Treat the TS implementation as the spec: every change or extension must be traceable back to matching TS code. + +2. **No additional behaviour** + - Never add CLI flags, env vars, filesystem operations, sockets, or configuration knobs that the TS runtime does not use. + - If the wasm module exposes capabilities unused in TS (e.g., file transport, extra exports), leave them inactive until the reference adopts them. + +3. **Match process bootstrap** + - Pass through exactly the `NAME=value` arguments the TS loader supplies (`PGDATA`, `PREFIX`, `PGUSER`, `PGDATABASE`, `MODE`, `REPL`); do not invent alternate argv/env forms. + - Rely on the same runtime resources (shared memory CMA, wasm `Memory`) and keep WASI scaffolding invisible to consumers. + +4. **Mirror filesystem semantics** + - Use the same layout as the embedded archive (`/tmp/pglite/...`), create only the directories the TS runtime expects, and avoid host-specific migrations or markers. + - Do not expose or depend on host-only paths (e.g., `/dev`, `.s.PGSQL.5432`) beyond what the TS code already implies. + +5. **Entropy and devices** + - Source randomness the same way TS does (via the wasm module’s existing hooks). Do not seed host pseudo devices or add alternate entropy paths unless the reference changes. + +6. **Justify unavoidable differences** + - If platform constraints force divergence, document the reason, limiting scope, and confirm that observable behaviour remains identical. + +### When adding/changing code + +- Inspect the TS module first; port its behaviour verbatim (minus web-only features). +- Confirm that each public change has a counterpart in the TS reference. +- Keep test cases and fixtures aligned with TS expectations (results, errors, types). + +This file is a standing reminder: **mirror the TypeScript runtime everywhere except web-specific layers.** diff --git a/docs/ASSETS.md b/docs/ASSETS.md new file mode 100644 index 00000000..64c4a2b7 --- /dev/null +++ b/docs/ASSETS.md @@ -0,0 +1,33 @@ +# 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 `pglite-wasi.tar.xz`, `pglite.wasi`, + `pglite.data`, `pglite.wasm`, `pglite.js`, `pglite.cjs`, `pglite.html`, + `bin/pg_dump.wasm`, and extension archives. + +Current metadata: + +- PostgreSQL runtime: `17.5` +- Upstream branch family: `electric-sql/postgres-pglite` `REL_17_5-pglite` +- Latest JS package checked: `@electric-sql/pglite@0.4.4` on April 24, 2026 +- Runtime archive SHA-256: `c725235f22a4fd50fed363f4065edb151a716fa769cba66f2383b8b854e6bdb5` +- `pglite.wasi` SHA-256: `a72b96adcd4ce40c51dd7201ee76a90f1b5799f633753b9cbb3c9af7b79f8da5` +- `pglite.data` SHA-256: `791a44e2ad1d48830714fb54e8662a3372618883566a0af7fc9f6b8375ab82d1` +- Filesystem manifest SHA-256: `880c9c058f416aad6ddc33fe0a1c84f6213b40c2b378e32587d25167d2f346f5` + +Update checklist: + +1. Check `electric-sql/pglite-build` `gh-pages` for the latest published runtime + artifacts. +2. Replace `assets/pglite-wasi.tar.xz`; update extracted local `assets/pglite.wasi`, + `assets/pglite.data`, and `assets/pglite_fs_manifest.json` when using the + filesystem-bundle fallback. +3. Update `[package.metadata.pglite-oxide.assets]` in `Cargo.toml`. +4. Run `cargo test --test runtime_smoke -- --nocapture`. +5. Run `cargo package --allow-dirty` and verify the package size. diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 00000000..46e887f5 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,48 @@ +# Release Process + +`pglite-oxide` publishes source crates to crates.io with release-plz. The CLI +binaries in this repository are maintenance helpers, so the release path +deliberately avoids binary artifact tooling such as cargo-dist until there is a +user-facing binary to distribute. + +## One-time setup + +- Ensure the crate owner has crates.io publish rights for `pglite-oxide`. +- `pglite-oxide@0.1.0` is already on crates.io, so 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. +- 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. + +## Release intent + +release-plz uses Conventional Commits as the release changeset. PRs that touch +release-affecting package files must use one of these PR title types: + +- `feat:` for user-facing additions +- `fix:` for behavior fixes +- `perf:` for performance improvements +- `refactor:` for behavior-preserving package changes that still need a release +- `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/**`. + +## Releasing from main + +1. Merge release-worthy work to `main`. +2. Open GitHub Actions, run `Release` from `main`, and choose + `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`. + +release-plz publishes unpublished package versions to crates.io, creates the bare +SemVer tag such as `0.2.0`, and creates the GitHub release from the generated +changelog. Bare SemVer tags intentionally match the existing `0.1.0` tag. diff --git a/docs/RUST_PORT_PLAN.md b/docs/RUST_PORT_PLAN.md new file mode 100644 index 00000000..c481be7b --- /dev/null +++ b/docs/RUST_PORT_PLAN.md @@ -0,0 +1,62 @@ +## PGlite Rust Port Plan (Non-Web Parity) + +The TypeScript reference (`packages/pglite/src`) is the specification. We port every runtime behaviour except browser-specific storage adapters (IDBFS, OPFS) and worker glue. This plan tracks remaining work to reach parity. + +### Completed +- Runtime provisioning / installation: unpack embedded archive into `/tmp/pglite`, ensure `/dev` and `/tmp/pglite/base`, seed PGDATA if missing. Mirrors TS `base.ts`, `pglite.ts` init. +- WASI bootstrapping: env/argv limited to `PREFIX`, `PGDATA`, `PGUSER`, `PGDATABASE`, `MODE`, `REPL`; CMA channel as default transport. +- Array type discovery and (de)serialisation: query `pg_type`, register array parsers/serialisers, replicate JS helpers. +- Query/exec/describe flows: end-to-end parity with TS base class (extended/simple query, transaction helpers, error enrichment). +- Blob handling (`/dev/blob`): accept `QueryOptions.blob` input and surface output blobs in results. +- Notification delivery: `listen`/`unlisten` APIs, channel/global callbacks, dispatch from protocol parser. +- Lifecycle tracking: `is_ready`, `is_closed`, `close`, `Drop`, internal ready/closing flags. +- Extension helpers: `install_extension_archive` / `install_extension_bytes` unpack `.tar.gz` bundles into the runtime FS. +- Host filesystem sync after each operation and fallback file transport channel to mirror TS behaviour. +- `load_fs_bundle` exposes the dynamic FS bundle (mirrors JS loader override). + +### Remaining Features +1. **COPY entry points & helpers** + - TS currently lacks dedicated `copyFrom`/`copyTo` helpers; the wasm runtime already supports `/dev/blob` paths. + - Decide whether to expose Rust convenience functions now or wait for the reference. For strict parity, defer until TS lands them. + +2. **Notification API refinements** + - TS normalises channels via `toPostgresName`; confirm all call sites use the same helper (done). + - Provide ability to list active listeners (optional; TS doesn’t expose this). + - Ensure `listen` invoked inside transactions mirrors TS behaviour (errors bubble up; transaction state toggled). + +3. **Live / vector / pg_ivm extensions** + - TS exposes optional Live Query, vector, and pg_ivm helpers. Investigate scope: + - `packages/pglite/src/live/**` + - `packages/pglite/src/vector/**` + - `packages/pglite/src/pg_ivm/**` + - Determine whether to port now or later (these rely on additional wasm assets / JS host features). + +4. **Template utilities & SQL tagging** + - Basic helpers (`QueryTemplate`, `quote_identifier`, `format_query`) implemented; revisit once richer DSL is needed. + +5. **Filesystem adapters (NodeFS, MemoryFS, etc.)** + - In TS these provide storage backends. For Rust we currently use host filesystem only. Document parity and optional future work: + - NodeFS swap-in => host FS (already default) + - MemoryFS / IDBFS / OPFS (web-only) – out of scope. + +6. **Extensions loading** + - TS `extensionUtils.ts` loads tarballs into the wasm FS. Our installer copies bundled extensions from `assets/extensions`. Check parity for runtime `install_extension_bytes/install_extension_archive`. + +7. **Error types & utilities** + - Port `errors.ts` helpers, `makePGliteError` details (already mirrored in `PgliteError`, but review field coverage). + +8. **Polyfills / workers** + - TS polyfills (indirect eval, blank) and worker entry points are inapplicable; document explicitly as out of scope. + +### Next Steps +1. **Live / vector features** + Skeleton modules exist but return "not supported"; port full behaviour (triggers, workers) when feasible. + +2. **Memory-style backends** + Evaluate whether exposing dedicated in-memory paths beyond temporary directories is necessary. + +3. **Testing** + - Expand beyond smoke test: create integration tests for array handling, blob COPY round-trips, notifications. + - Mirror TS test expectations where practical. + +Update this plan as each item is implemented or explicitly descoped. diff --git a/docs/TAURI.md b/docs/TAURI.md new file mode 100644 index 00000000..00dfe1d4 --- /dev/null +++ b/docs/TAURI.md @@ -0,0 +1,68 @@ +# 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. + +## Direct Embedded API + +Use `Pglite` when your Rust code owns the database calls: + +```rust,no_run +use pglite_oxide::Pglite; +use serde_json::json; +use tauri::State; +use std::sync::Mutex; + +struct Db(Mutex); + +#[tauri::command] +fn add_item(db: State<'_, Db>, value: String) -> Result<(), String> { + let mut db = db.0.lock().map_err(|err| err.to_string())?; + db.query( + "INSERT INTO items(value) VALUES ($1)", + &[json!(value)], + None, + ) + .map_err(|err| err.to_string())?; + Ok(()) +} +``` + +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()?; +``` + +## Existing Postgres Clients + +Use `PgliteServer` when another crate expects a PostgreSQL URL: + +```rust,no_run +use pglite_oxide::PgliteServer; + +let server = PgliteServer::builder() + .path("./.pglite") + .start()?; + +let database_url = server.connection_uri(); +``` + +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. +- Mobile targets need separate validation. The current crate targets desktop + Rust with Wasmtime. diff --git a/docs/reviews/base_vs_base_ts.md b/docs/reviews/base_vs_base_ts.md new file mode 100644 index 00000000..a1824fbf --- /dev/null +++ b/docs/reviews/base_vs_base_ts.md @@ -0,0 +1,82 @@ +### Review: Rust `base.rs` vs TS `base.ts`/bootstrap in `pglite.ts` + +Reference TS sources: https://github.com/electric-sql/pglite/tree/main/packages/pglite/src + +#### Purpose mapping + +- Rust `base.rs` handles runtime provisioning (embedded tar.xz), `PGDATA` directory creation, optional extension tar installs, and exposes install/init helpers. +- TS `base.ts` is not a provisioning file; bootstrap happens in `pglite.ts` (Emscripten opts, FS bundle, initdb/backend). This review maps Rust `base.rs` to the nearest TS bootstrap responsibilities. + +#### Embedded runtime vs JS bootstrap + +- Rust: unpacks embedded `pglite-wasi.tar.xz` into parent of `pgroot` and validates presence of `pglite/bin/pglite.wasi` and `share/postgresql/postgres.bki`. +```114:137:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +info!("unpacking embedded runtime"); +let mut decoder = XzDecoder::new(*ARCHIVE_BYTES); +let mut ar = Archive::new(&mut decoder); +let unpack_target = paths.pgroot.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| paths.pgroot.clone()); +ar.unpack(&unpack_target)?; +``` + +- TS: loads FS bundle and wasm via `PostgresModFactory` and `instantiateWasm`; no host tar unpack. +```230:247:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +let emscriptenOpts: Partial = { WASM_PREFIX, arguments: args, INITIAL_MEMORY: options.initialMemory, noExitRuntime: true, instantiateWasm: (...), getPreloadedPackage: (...) } +``` +```370:372:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +this.mod = await PostgresModFactory(emscriptenOpts) +``` + +#### Paths and cluster detection + +- Rust: real `pgroot/tmp/pglite/base` layout and cluster detection via host `PG_VERSION`. +```51:56:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +let pgroot = base.join("tmp"); +let pgdata = pgroot.join("pglite").join("base"); +``` +```75:82:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +fn marker_cluster(&self) -> PathBuf { self.pgdata.join("PG_VERSION") } +pub fn is_cluster_initialized(&self) -> bool { self.marker_cluster().exists() } +``` + +- TS: checks `PG_VERSION` within its virtual FS via `FS.analyzePath`. +```388:392:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +if (this.mod.FS.analyzePath(PGDATA + '/PG_VERSION').exists) { /* ... */ } +``` + +#### Extension installation + +- Rust: supports installing extension tarballs into `pgroot/pglite` from bytes or file. +```139:157:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +fn install_extension_reader(paths: &PglitePaths, reader: R) -> Result<()> { /* tar.gz unpack */ } +pub fn install_extension_archive(...) +pub fn install_extension_bytes(...) +``` + +- TS: extension bundles are registered via `pg_extensions` and compiled after module load; different mechanism (`extensionUtils.ts` and `loadExtensions`). Not directly mirrored here. + +#### Install helpers + +- Rust: `install_default`, `install_into`, `install_and_init`, `install_with_options` return `InstallOutcome`/`MountInfo` with host mount path. +```227:281:/Users/sid/dev/pglite-oxide/src/pglite/base.rs +pub fn install_default(...) +pub fn install_into(...) +pub fn install_and_init(...) +pub fn install_with_options(...) +``` + +- TS: `PGlite.create({ dataDir, ... })` returns an instance; filesystem mount paths are abstracted away by the FS layer. + +#### Differences (Rust extras / TS extras) + +- Rust extras: + - Embedded tar.xz runtime unpack and validation. + - Real host `PGDATA` directory creation. + - Extension tarball install helpers. + - `MountInfo` exposing host mount path and reuse flag. + +- TS extras: + - FS bundle download/selection and wasm instantiation plumbed via Emscripten options. + - Extension bundle integration pipeline and dynamic compilation. + - DataDir tar load (`loadDataDir`) pre-init. + + diff --git a/docs/reviews/pglite_vs_pglite_ts.md b/docs/reviews/pglite_vs_pglite_ts.md new file mode 100644 index 00000000..d458bbb7 --- /dev/null +++ b/docs/reviews/pglite_vs_pglite_ts.md @@ -0,0 +1,141 @@ +### Review: Rust `client.rs` vs TS `pglite.ts` + +Reference TS sources: https://github.com/electric-sql/pglite/tree/main/packages/pglite/src + +#### Scope match + +- Both implement the public client surface: query, exec, transaction, describe, protocol execution, notifications, and array type discovery. + +#### Initialization and engine wiring + +- Rust constructs `Pglite` with a prepared `PostgresMod` and transport. +```86:112:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +impl Pglite { + /// Create a new Pglite instance backed by the provided runtime paths. + 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 { /* fields */ }; + instance.exec_internal("SET search_path TO public;", None)?; + instance.init_array_types(true)?; + Ok(instance) + } +} +``` + +- TS loads the module via `PostgresModFactory`, sets args/env, then calls `_pgl_initdb()` and `_pgl_backend()`. +```370:447:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +this.mod = await PostgresModFactory(emscriptenOpts) +await this.fs!.initialSyncFs() +// (optional) load data dir tar, check PG_VERSION +const idb = this.mod._pgl_initdb() +// ... interpret flags ... +this.mod._pgl_backend() +await this.syncToFs() +``` + +#### Query (extended protocol) flow + +- Rust: parse → describe(S) → bind → describe(P) → execute → sync; errors wrapped into `PgliteError`. +```127:204:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn query_internal(&mut self, sql: &str, params: &[Value], options: Option<&QueryOptions>) -> Result { + // build ExecProtocolOptions + // parse + // describe(S) and read param OIDs + // bind with serialized params + // describe(P) + // execute + // sync and parse results; wrap DatabaseError -> PgliteError +} +``` + +- TS: same flow wrapped in `BasePGlite`. +```221:301:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/base.ts +// parse → describe(S) → bind → describe(P) → execute → sync; DatabaseError -> makePGliteError +``` + +#### Simple query flow + +- Rust: `exec_internal` sends simple query, syncs, wraps errors. +```254:291:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn exec_internal(&mut self, sql: &str, options: Option<&QueryOptions>) -> Result> { /* ... */ } +``` + +- TS: `#runExec` mirrors the same. +```310:352:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/base.ts +async #runExec(query: string, options?: QueryOptions): Promise> { /* ... */ } +``` + +#### Protocol execution wrappers + +- Rust: `exec_protocol` parses wire data with `ProtocolParser`, handles `throw_on_error`, invokes `on_notice`, and fans out notifications to listeners. +```583:637:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn exec_protocol(&mut self, message: &[u8], options: ExecProtocolOptions) -> Result { /* ... */ } +``` + +- TS: `execProtocol` parses via ProtocolParser with same semantics. +```689:744:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +async execProtocol(message: Uint8Array, { syncToFs, throwOnError, onNotice }: ExecProtocolOptions = {}) { /* ... */ } +``` + +#### Protocol raw and transport selection + +- Rust: `exec_protocol_raw` delegates to `transport.send`, then optionally calls `sync_to_fs()`. +```639:652:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn exec_protocol_raw(&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)?; + if sync_to_fs { self.sync_to_fs()?; } + Ok(data) +} +``` + +- TS: `execProtocolRawSync` selects CMA vs file, drives `_interactive_*`, reads result; `execProtocolRaw` optionally syncs FS. +```578:682:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +execProtocolRawSync(message: Uint8Array, options = {}) { /* cma/file branches, interactive_one/read */ } +async execProtocolRaw(message: Uint8Array, { syncToFs = true, dataTransferContainer }: ExecProtocolOptions = {}) { /* ... */ } +``` + +#### Array types discovery + +- Rust runs a SQL query in `init_array_types`, creates parsers/serializers for discovered arrays. +```654:721:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn init_array_types(&mut self, force: bool) -> Result<()> { /* SELECT oid, typarray ... */ } +``` + +- TS: `BasePGlite._initArrayTypes()` mirrors it. +```116:135:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/base.ts +async _initArrayTypes({ force = false } = {}) { /* SELECT oid, typarray ... */ } +``` + +#### Notifications API + +- Rust: `listen`, `unlisten`, global listeners; invokes callbacks during `exec_protocol`. +```293:367:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +pub fn listen(&mut self, channel: &str, callback: F) -> Result { /* ... */ } +``` + +- TS: `listen`, `unlisten`, `onNotification`, `offNotification` with similar behavior. +```787:873:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +onNotification(callback) { /* ... */ } offNotification(callback) { /* ... */ } +``` + +#### Differences (Rust extras / TS extras) + +- Rust extras: + - `sync_to_fs()` is a best-effort host filesystem sync, not a TS persistent-storage sync. + - Blob I/O writes/reads a host path `pgroot/dev/blob`. +```745:777:/Users/sid/dev/pglite-oxide/src/pglite/client.rs +fn get_written_blob(&mut self) -> Result>> { /* reads pgroot/dev/blob */ } +``` + +- TS extras: + - Real FS sync via filesystem backends after each op. +```754:776:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +await this.fs!.syncToFs(this.#relaxedDurability) +``` + - Virtual `/dev/blob` device registered in FS instead of host path. +```259:319:/Users/sid/dev/pglite-oxide/tmp/pglite-ts/packages/pglite/src/pglite.ts +mod.FS.registerDevice(devId, devOpt); mod.FS.mkdev('/dev/blob', devId) +``` + diff --git a/examples/proxy_showcase.rs b/examples/proxy_showcase.rs deleted file mode 100644 index b92aa759..00000000 --- a/examples/proxy_showcase.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::env; - -use anyhow::{bail, Result}; - -use pglite_oxide::interactive; -use pglite_oxide::prepare_default_mount; -use tokio::runtime::Builder; - -#[derive(Copy, Clone, Eq, PartialEq)] -enum ProxyMode { - Unix, - Tcp, -} - -fn main() -> Result<()> { - let mode = parse_mode()?; - let mount = prepare_default_mount()?; - - println!("pglite mount root: {}", mount.mount().display()); - println!("cluster data dir: {}", mount.paths().pgdata.display()); - println!("postgres username: postgres"); - println!("postgres database: template1"); - println!("Press Ctrl+C to stop the proxy\n"); - - interactive::with_default_runtime(|rt| rt.ensure_handshake())?; - - match mode { - ProxyMode::Unix => { - println!("listening on unix socket: /tmp/.s.PGSQL.5432"); - println!("Example connection string: postgresql://postgres@/template1?host=/tmp"); - println!("Most GUI tools call this the \"socket directory\" -> /tmp\n"); - } - ProxyMode::Tcp => { - println!("listening on tcp: 0.0.0.0:5432"); - println!("Example connection string: postgresql://postgres@127.0.0.1:5432/template1"); - println!("(Use your LAN/WAN address instead of 127.0.0.1 if needed)\n"); - } - } - - let rt = Builder::new_multi_thread().enable_all().build()?; - rt.block_on(async { interactive::start_proxy(mode == ProxyMode::Tcp).await }) -} - -fn parse_mode() -> Result { - let mut mode = ProxyMode::Unix; - - for arg in env::args().skip(1) { - match arg.as_str() { - "--tcp" => mode = ProxyMode::Tcp, - "--uds" => mode = ProxyMode::Unix, - "--help" | "-h" => { - print_usage(); - std::process::exit(0); - } - other => { - bail!("unknown argument: {other}"); - } - } - } - - Ok(mode) -} - -fn print_usage() { - eprintln!("Usage: proxy_showcase [--tcp | --uds]"); - eprintln!(" --tcp Bind 0.0.0.0:5432 so TCP clients can connect"); - eprintln!(" --uds Bind the default Unix socket (/tmp/.s.PGSQL.5432)"); - eprintln!("If no flag is provided, the Unix socket mode is used."); -} diff --git a/examples/runtime_showcase.rs b/examples/runtime_showcase.rs deleted file mode 100644 index d532c91b..00000000 --- a/examples/runtime_showcase.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; - -use pglite_oxide::interactive::{self, exec_interactive, PokeInput}; -use pglite_oxide::prepare_default_mount; - -fn main() -> Result<()> { - let mount = prepare_default_mount()?; - - println!("pglite mount root: {}", mount.mount().display()); - println!("pglite socket path: {}", mount.io_socket().display()); - println!("reused existing install: {}", mount.reused_existing()); - - let module_path = interactive::wasm_import("postgres", None)?; - println!("postgres module located at: {}", module_path.display()); - - match exec_interactive(PokeInput::Str("select 1;")) { - Ok(response) => println!( - "interactive response ({} bytes):\n{}", - response.len(), - interactive::hexc(&response, "<-", Some(4)) - ), - Err(err) => println!("interactive exec failed: {err:#}"), - } - - match interactive::run_pg_dump(&["pg_dump", "--version"], &[]) { - Ok(Some(code)) => println!("pg_dump --version exited with status {code}"), - Ok(None) => println!("pg_dump shim not included in this runtime"), - Err(err) => println!("pg_dump failed: {err:#}"), - } - - Ok(()) -} diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 00000000..e201a866 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,34 @@ +[workspace] +changelog_update = true +dependencies_update = false +features_always_increment_minor = false +git_release_enable = true +git_release_name = "{{ version }}" +git_tag_name = "{{ version }}" +pr_branch_prefix = "release-plz-" +pr_labels = ["release"] +pr_name = "chore(release): {{ version }}" +publish = true +publish_timeout = "30m" +release_always = false +release_commits = '^((feat|fix|perf|refactor|revert)(\([a-z0-9][a-z0-9._/-]*\))?(!)?|[a-z]+(\([a-z0-9][a-z0-9._/-]*\))?!): .+' +repo_url = "https://github.com/f0rr0/pglite-oxide" +semver_check = true + +[changelog] +protect_breaking_commits = true +sort_commits = "oldest" +tag_pattern = '^[0-9]+\.[0-9]+\.[0-9]+.*$' +commit_parsers = [ + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Changed" }, + { message = "^revert", group = "Changed" }, + { message = "^.*!", group = "Breaking" }, + { message = "^docs", skip = true }, + { message = "^test", skip = true }, + { message = "^ci", skip = true }, + { message = "^chore", skip = true }, + { message = "^style", skip = true }, +] diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..34952324 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.92" +components = ["rustfmt", "clippy"] +profile = "minimal" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..15df9a7c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2024" +max_width = 100 +use_small_heuristics = "Default" diff --git a/src/bin/pglite_dump.rs b/src/bin/pglite_dump.rs new file mode 100644 index 00000000..a76614a8 --- /dev/null +++ b/src/bin/pglite_dump.rs @@ -0,0 +1,136 @@ +use std::fs::{self, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, bail}; +use serde::Deserialize; +use tar::Archive; +use xz2::read::XzDecoder; + +#[derive(Debug, Deserialize)] +struct ManifestEntry { + path: String, + start: usize, + end: usize, +} + +fn read_manifest() -> Result> { + let manifest_str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/pglite_fs_manifest.json" + )); + let entries: Vec = + serde_json::from_str(manifest_str).context("failed to parse pglite_fs_manifest.json")?; + Ok(entries) +} + +fn read_bundle() -> Result> { + let bundle_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite.data"); + let bytes = fs::read(&bundle_path) + .with_context(|| format!("failed to read {}", bundle_path.display()))?; + Ok(bytes) +} + +fn runtime_tar_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite-wasi.tar.xz") +} + +fn map_dest(root: &Path, manifest_path: &str) -> Result { + if let Some(rest) = manifest_path + .strip_prefix('/') + .and_then(|p| p.strip_prefix("tmp/")) + { + Ok(root.join(rest)) + } else if let Some(rest) = manifest_path.strip_prefix('/').map(|s| s.to_string()) { + Ok(root.join(rest)) + } else { + bail!("unsupported manifest path: {}", manifest_path) + } +} + +fn write_entry(root: &Path, bundle: &[u8], entry: &ManifestEntry) -> Result<()> { + let dest = map_dest(root, &entry.path)?; + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + + let start = entry.start; + let end = entry.end; + if start > end || end > bundle.len() { + bail!( + "manifest entry {} has invalid bounds {}..{} (bundle len {})", + entry.path, + start, + end, + bundle.len() + ); + } + + if start == end { + // empty file + fs::File::create(&dest).with_context(|| format!("create file {}", dest.display()))?; + } else { + let mut file = + fs::File::create(&dest).with_context(|| format!("create file {}", dest.display()))?; + file.write_all(&bundle[start..end]) + .with_context(|| format!("write file {}", dest.display()))?; + } + Ok(()) +} + +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 = XzDecoder::new(file); + 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 dest = match path.strip_prefix("tmp") { + Ok(rest) => dest_root.join(rest), + Err(_) => dest_root.join(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 {}", dest.display()))?; + } + + Ok(()) +} + +fn run(dest_root: &Path) -> Result<()> { + if !PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("assets/pglite.data") + .exists() + { + return unpack_tar_archive(dest_root); + } + + let manifest = read_manifest()?; + let bundle = read_bundle()?; + + for entry in manifest.iter() { + write_entry(dest_root, &bundle, entry) + .with_context(|| format!("extract {}", entry.path))?; + } + Ok(()) +} + +fn main() -> Result<()> { + let mut args = std::env::args().skip(1); + let dest = args.next().unwrap_or_else(|| "./pglite-fs".to_string()); + let dest_path = PathBuf::from(dest); + run(&dest_path) +} diff --git a/src/bin/pglite_manifest_sync.rs b/src/bin/pglite_manifest_sync.rs new file mode 100644 index 00000000..0fe03af1 --- /dev/null +++ b/src/bin/pglite_manifest_sync.rs @@ -0,0 +1,55 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::{Context, Result, anyhow}; +use regex::Regex; +use serde::Serialize; + +#[derive(Debug, Serialize)] +struct ManifestEntryOut<'a> { + path: &'a str, + start: usize, + end: usize, +} + +fn main() -> Result<()> { + let js_path = std::env::args() + .nth(1) + .unwrap_or_else(|| "./pglite.js".to_string()); + let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite_fs_manifest.json"); + + let js = fs::read_to_string(&js_path).with_context(|| format!("read {}", js_path))?; + + // Extract the JSON array passed to loadPackage({ files: [...] }) + let re = Regex::new(r#"loadPackage\(\{\s*\"files\":\s*(\[[^\]]*\])\s*,\s*\"remote_package_size\"\s*:\s*\d+\s*\}\)"#) + .expect("invalid regex"); + let caps = re + .captures(&js) + .ok_or_else(|| anyhow!("failed to locate files array in {}", js_path))?; + let files_json = caps.get(1).unwrap().as_str(); + + // Parse the array of { filename, start, end } + #[derive(serde::Deserialize)] + struct FileRec { + filename: String, + start: usize, + end: usize, + } + let files: Vec = serde_json::from_str(files_json).context("parse files array json")?; + + // Convert to our manifest format + let out: Vec = files + .iter() + .map(|f| ManifestEntryOut { + path: &f.filename, + start: f.start, + end: f.end, + }) + .collect(); + + let pretty = serde_json::to_string_pretty(&out).context("serialize manifest json")?; + fs::write(&out_path, pretty + "\n").with_context(|| format!("write {}", out_path.display()))?; + + println!("updated {} from {}", out_path.display(), js_path); + Ok(()) +} diff --git a/src/bin/pglite_proxy.rs b/src/bin/pglite_proxy.rs new file mode 100644 index 00000000..9c91252b --- /dev/null +++ b/src/bin/pglite_proxy.rs @@ -0,0 +1,81 @@ +use anyhow::{Result, bail}; +use pglite_oxide::PgliteProxy; +use std::env; +use std::path::PathBuf; + +#[derive(Debug)] +enum Bind { + Tcp(String), + #[cfg(unix)] + Unix(PathBuf), +} + +#[derive(Debug)] +struct Args { + root: PathBuf, + bind: Bind, +} + +fn main() -> Result<()> { + let args = parse_args()?; + let proxy = PgliteProxy::new(args.root); + + match args.bind { + Bind::Tcp(addr) => { + eprintln!("listening on tcp: {addr}"); + proxy.serve_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) + } + } +} + +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 args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--root" => { + let value = args + .next() + .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?; + root = PathBuf::from(value); + } + "--tcp" => { + let value = args.next().unwrap_or_else(|| "127.0.0.1:5432".to_string()); + bind = Bind::Tcp(value); + } + #[cfg(unix)] + "--uds" => { + let value = args + .next() + .unwrap_or_else(|| "/tmp/.s.PGSQL.5432".to_string()); + bind = Bind::Unix(PathBuf::from(value)); + } + "--help" | "-h" => { + print_usage(); + std::process::exit(0); + } + other => bail!("unknown argument: {other}"), + } + } + + Ok(Args { root, bind }) +} + +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"); + #[cfg(unix)] + eprintln!(" --uds PATH Listen on Unix socket. Default: /tmp/.s.PGSQL.5432"); +} diff --git a/src/interactive.rs b/src/interactive.rs deleted file mode 100644 index 9e874f4b..00000000 --- a/src/interactive.rs +++ /dev/null @@ -1,1387 +0,0 @@ -use std::collections::HashMap; -use std::ffi::OsString; -use std::fmt::Write as _; -use std::fs; -use std::io::ErrorKind; -use std::path::{Path, PathBuf}; -use std::str; -use std::sync::Mutex; -use std::time::Duration; - -use anyhow::{anyhow, bail, ensure, Context, Result}; -use once_cell::sync::OnceCell; -use tokio::io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; -use tokio::net::{TcpListener, UnixListener, UnixStream}; -use tokio::time::{sleep, timeout}; -use tracing::{debug, warn}; - -use crate::PglitePaths; - -use super::{ - create_engine, ensure_cluster, ensure_runtime, locate_runtime_module, prepare_default_mount, - resolve_io_socket, standard_wasi_builder, MountInfo, -}; - -use md5::{Digest, Md5}; -use wasmtime::{Engine, Instance, Linker, Memory, Module, Store, Trap, TypedFunc}; -use wasmtime_wasi::preview1::add_to_linker_sync; -use wasmtime_wasi::I32Exit; -use wasmtime_wasi::WasiP1Ctx; - -static DEFAULT_RUNTIME: OnceCell> = OnceCell::new(); - -/// Produce a hex + ASCII view of a byte buffer. -pub fn hexc(data: &[u8], way: &str, line_limit: Option) -> String { - const BYTES_PER_LINE: usize = 16; - - let len = data.len(); - let total_lines = len.div_ceil(BYTES_PER_LINE); - let mut result = String::new(); - - let mut lower = 0usize; - let mut upper = len; - if let Some(limit) = line_limit { - if limit > 0 && total_lines > limit { - let preserve = BYTES_PER_LINE * (limit / 2); - lower = preserve; - upper = len.saturating_sub(preserve); - if upper <= lower { - lower = 0; - upper = len; - } - } - } - - let total_as_lines = len as f64 / BYTES_PER_LINE as f64; - if lower == 0 && upper == len { - let _ = writeln!( - result, - "{} : {} bytes, {:.2}/{total_lines} lines", - way, len, total_as_lines, - ); - } else { - let _ = writeln!( - result, - "{} : {} bytes, {:.2}/{total_lines} lines, showing prefix/suffix {} bytes", - way, len, total_as_lines, lower, - ); - } - - let mut offset = 0usize; - let mut skipped = false; - while offset < len { - if offset >= lower && offset < upper { - if !skipped { - let _ = writeln!( - result, - "... {:08x} ", - upper, lower, upper, - ); - skipped = true; - } - offset = upper; - continue; - } - - let line = &data[offset..usize::min(offset + BYTES_PER_LINE, len)]; - let mut hex_repr = String::new(); - for byte in line { - if !hex_repr.is_empty() { - hex_repr.push(' '); - } - let _ = write!(hex_repr, "{:02x}", byte); - } - let ascii_repr: String = line - .iter() - .map(|b| { - if (32..=126).contains(b) { - *b as char - } else { - '.' - } - }) - .collect(); - let _ = writeln!(result, "{:08x} {:<47} {}", offset, hex_repr, ascii_repr); - offset += BYTES_PER_LINE; - } - - result -} - -/// Render a byte count using IEC (KiB, MiB, ...) units. -pub fn si_bytes(bytes: u64) -> String { - const KIB: f64 = 1024.0; - const MIB: f64 = KIB * 1024.0; - const GIB: f64 = MIB * 1024.0; - - let value = bytes as f64; - if bytes < 1024 { - return format!("{bytes:3} B"); - } - if value / KIB < 999.0 { - return format!("{:.2} KiB", value / KIB); - } - if value / MIB < 999.0 { - return format!("{:.2} MiB", value / MIB); - } - if value / GIB < 999.0 { - return format!("{:.2} GiB", value / GIB); - } - format!("{value:.2}") -} - -/// Lazily open stdin for asynchronous reads. -pub fn get_reader() -> BufReader { - BufReader::new(io::stdin()) -} - -/// Prompt for a line of input asynchronously. Returns `None` on EOF. -pub async fn ainput(prompt: &str) -> io::Result>> { - let mut stdout = io::stdout(); - stdout.write_all(prompt.as_bytes()).await?; - stdout.flush().await?; - - let mut reader = get_reader(); - let mut buf = Vec::new(); - let read = reader.read_until(b'\n', &mut buf).await?; - if read == 0 { - return Ok(None); - } - Ok(Some(buf)) -} - -/// Resolve the expected socket path for the embedded cluster. -pub fn get_io_base_path(paths: &PglitePaths) -> PathBuf { - resolve_io_socket(paths) -} - -/// Return the host directory that should be mounted into the WASI filesystem. -pub fn get_mount_root(paths: &PglitePaths) -> PathBuf { - paths.mount_root().to_path_buf() -} - -/// Global, lazily prepared interactive runtime akin to the Python sync importer. -pub fn with_default_runtime(f: F) -> Result -where - F: FnOnce(&mut InteractiveRuntime) -> Result, -{ - let runtime_mutex = DEFAULT_RUNTIME - .get_or_try_init(|| InteractiveRuntime::prepare_default().map(Mutex::new))?; - - let mut runtime = runtime_mutex - .lock() - .map_err(|_| anyhow!("interactive runtime lock poisoned"))?; - f(&mut runtime) -} - -/// Locate a WASM module using the default runtime, mirroring the Python helper. -pub fn wasm_import(alias: &str, wasmfile: Option<&Path>) -> Result { - with_default_runtime(|runtime| runtime.module_path(alias, wasmfile)) -} - -/// Resolve the host path to the bundled pg_dump shim inside the default runtime. -pub fn pg_dump_path() -> Result { - with_default_runtime(|runtime| runtime.pg_dump_path()) -} - -/// Execute pg_dump via Wasmtime when available. Returns `Ok(None)` if the -/// pg_dump shim is not present in the runtime. -pub fn run_pg_dump(argv: &[&str], env: &[(&str, &str)]) -> Result> { - with_default_runtime(|runtime| runtime.run_pg_dump(argv, env)) -} - -/// Access the default mount information produced during initialization. -pub fn default_mount() -> Result { - with_default_runtime(|runtime| Ok(runtime.mount_info().clone())) -} - -/// Copy bytes into the interactive input buffer, matching the Python helper's behaviour. -pub fn poke(input: PokeInput<'_>) -> Result<(Vec, usize)> { - with_default_runtime(|runtime| runtime.poke(input)) -} - -/// Write a payload and run a single interactive frame, returning the raw response bytes. -pub fn exec_interactive(input: PokeInput<'_>) -> Result> { - with_default_runtime(|runtime| runtime.exec_interactive(input)) -} - -pub fn is_file_transport_mode() -> Result { - with_default_runtime(|rt| { - let session = rt.interactive_session()?; - Ok(session.is_file_transport()) - }) -} - -pub async fn run_tests_quick() -> Result<()> { - const TESTS: &str = r#" - SHOW client_encoding; - CREATE OR REPLACE FUNCTION test_func() RETURNS TEXT AS $$ BEGIN RETURN 'test'; END; $$ LANGUAGE plpgsql; - CREATE OR REPLACE FUNCTION addition (entier1 integer, entier2 integer) - RETURNS integer LANGUAGE plpgsql IMMUTABLE AS ' - DECLARE resultat integer; - BEGIN resultat := entier1 + entier2; RETURN resultat; END '; - SELECT test_func(); - SELECT now(), current_database(), session_user, current_user; - SELECT addition(40,2); - "#; - - if !is_file_transport_mode()? { - return Ok(()); - } - - for stmt in TESTS.split(";\n\n") { - let sql = stmt.trim(); - if sql.is_empty() { - continue; - } - let sql = format!("{sql};"); - with_default_runtime(|rt| { - rt.poke(PokeInput::Str(&sql))?; - rt.interactive_one() - })?; - sleep(Duration::from_millis(100)).await; - } - - Ok(()) -} - -/// Start a Unix/TCP proxy server that forwards PostgreSQL wire protocol to pglite. -/// This provides the same functionality as the Python implementation. -/// -/// # Arguments -/// * `use_tcp` - If true, bind to TCP 0.0.0.0:5432, otherwise bind to Unix domain socket /tmp/.s.PGSQL.5432 -/// -/// # Example -/// -/// ```no_run -/// use pglite_oxide::interactive; -/// -/// #[tokio::main] -/// async fn main() -> anyhow::Result<()> { -/// // Start Unix domain socket proxy -/// interactive::start_proxy(false).await?; -/// -/// // Or start TCP proxy -/// interactive::start_proxy(true).await?; -/// Ok(()) -/// } -/// ``` -pub async fn start_proxy(use_tcp: bool) -> Result<()> { - let file_mode = with_default_runtime(|rt| { - let session = rt.interactive_session()?; - Ok(session.is_file_transport()) - }) - .context("prepare pglite interactive runtime")?; - - if file_mode { - debug!("Running pre-connection tests (file transport mode)..."); - if let Err(err) = run_tests_quick().await { - warn!("Pre-connection tests failed: {err:#}"); - } - } - - if use_tcp { - let listener = TcpListener::bind(("0.0.0.0", 5432)) - .await - .context("Failed to bind TCP listener on 0.0.0.0:5432")?; - debug!("listening on TCP 0.0.0.0:5432"); - loop { - let (mut sock, addr) = listener - .accept() - .await - .context("Failed to accept TCP connection")?; - debug!("Accepted TCP connection from {}", addr); - tokio::spawn(async move { - if let Err(e) = handle_client(&mut sock).await { - warn!("Error handling TCP client: {}", e); - } - }); - } - } else { - // UDS path MUST be /tmp/.s.PGSQL.5432 to match Python exactly - let uds_path = "/tmp/.s.PGSQL.5432"; - let _ = std::fs::remove_file(uds_path); - let listener = UnixListener::bind(uds_path).context("Failed to bind Unix domain socket")?; - debug!("listening on UDS {}", uds_path); - loop { - let (mut sock, addr) = listener - .accept() - .await - .context("Failed to accept UDS connection")?; - debug!("Accepted UDS connection from {:?}", addr); - tokio::spawn(async move { - if let Err(e) = handle_uds(&mut sock).await { - warn!("Error handling UDS client: {}", e); - } - }); - } - } -} - -async fn handle_uds(sock: &mut UnixStream) -> Result<()> { - handle_client(sock).await -} - -async fn handle_client(sock: &mut S) -> Result<()> -where - S: AsyncReadExt + AsyncWriteExt + Unpin, -{ - let mut buf = vec![0u8; 64 * 1024]; - let poll_interval = Duration::from_millis(16); - - loop { - let pending = with_default_runtime(|rt| rt.drain_wire())?; - if !pending.is_empty() { - for reply in pending { - if !reply.is_empty() { - sock.write_all(&reply) - .await - .context("Failed to write reply to client")?; - } - } - continue; - } - - match timeout(poll_interval, sock.read(&mut buf)).await { - Ok(Ok(0)) => { - debug!("Client disconnected"); - break; - } - Ok(Ok(n)) => { - if n == 0 { - debug!("Client disconnected"); - break; - } - - let replies = with_default_runtime(|rt| rt.forward_wire(&buf[..n])) - .context("Failed to forward client bytes through pglite")?; - for reply in replies { - if !reply.is_empty() { - sock.write_all(&reply) - .await - .context("Failed to write reply to client")?; - } - } - } - Ok(Err(err)) => { - return Err(err).context("Failed to read from client socket"); - } - Err(_) => { - // timeout -> loop to poll for backend replies again - continue; - } - } - } - Ok(()) -} - -pub struct InteractiveRuntime { - mount: MountInfo, - module_cache: HashMap, - interactive: Option, -} - -impl InteractiveRuntime { - pub fn prepare_default() -> Result { - Ok(Self { - mount: prepare_default_mount()?, - module_cache: HashMap::new(), - interactive: None, - }) - } - - pub fn mount_root(&self) -> &Path { - self.mount.mount() - } - - pub fn io_socket(&self) -> &Path { - self.mount.io_socket() - } - - pub fn reused_existing(&self) -> bool { - self.mount.reused_existing() - } - - pub fn paths(&self) -> &PglitePaths { - self.mount.paths() - } - - pub fn mount_info(&self) -> &MountInfo { - &self.mount - } - - pub fn module_path(&mut self, alias: &str, wasmfile: Option<&Path>) -> Result { - if let Some(explicit) = wasmfile { - return resolve_module(self.mount.paths(), Some(explicit)); - } - - if let Some(cached) = self.module_cache.get(alias) { - return Ok(cached.clone()); - } - - let module = resolve_module(self.mount.paths(), None)?; - self.module_cache.insert(alias.to_string(), module.clone()); - Ok(module) - } - - pub fn pg_dump_path(&self) -> Result { - resolve_pg_dump(self.mount.paths()) - } - - pub fn run_pg_dump(&mut self, argv: &[&str], env: &[(&str, &str)]) -> Result> { - let path = match resolve_pg_dump(self.mount.paths()) { - Ok(p) => p, - Err(_) => return Ok(None), - }; - match run_wasi_command(self.mount.paths(), &path, argv, env) { - Ok(code) => Ok(Some(code)), - Err(err) => { - warn!("pg_dump execution failed: {err:#}"); - Err(err) - } - } - } - - fn interactive_session(&mut self) -> Result<&mut InteractiveSession> { - if self.interactive.is_none() { - let session = InteractiveSession::new(self.mount.paths())?; - self.interactive = Some(session); - } - self.interactive - .as_mut() - .ok_or_else(|| anyhow!("interactive session could not be initialized")) - } - - pub fn poke(&mut self, input: PokeInput<'_>) -> Result<(Vec, usize)> { - let session = self.interactive_session()?; - let (buf, len) = prepare_cstring(input); - - session.use_wire(false)?; - session.write(&buf)?; - session.set_cma_length(len as i32)?; - - Ok((buf, len)) - } - - pub fn exec_interactive(&mut self, input: PokeInput<'_>) -> Result> { - let session = self.interactive_session()?; - session.ensure_handshake()?; - let payload = match input { - PokeInput::Str(sql) => build_simple_query(sql), - PokeInput::Bytes(bytes) => bytes.to_vec(), - }; - session.run_wire(&payload) - } - - pub fn forward_wire(&mut self, payload: &[u8]) -> Result>> { - let session = self.interactive_session()?; - session.forward_wire(payload) - } - - pub fn drain_wire(&mut self) -> Result>> { - let session = self.interactive_session()?; - session.drain_wire() - } - - pub fn use_wire(&mut self, enable: bool) -> Result<()> { - let session = self.interactive_session()?; - session.use_wire(enable) - } - - pub fn interactive_one(&mut self) -> Result<()> { - let session = self.interactive_session()?; - session.run_once() - } - - pub fn interactive_read(&mut self, payload_len: usize) -> Result> { - let session = self.interactive_session()?; - session.read_response(payload_len) - } - - pub fn set_cma_length(&mut self, len: i32) -> Result<()> { - let session = self.interactive_session()?; - session.set_cma_length(len) - } - - pub fn clear_error(&mut self) -> Result<()> { - let session = self.interactive_session()?; - session.clear_error() - } - - pub fn pgl_closed(&mut self) -> Result> { - let session = self.interactive_session()?; - session.pgl_closed() - } - - pub fn buffer_addr(&mut self) -> Result { - let session = self.interactive_session()?; - Ok(session.buffer_addr()) - } - - pub fn buffer_size(&mut self) -> Result { - let session = self.interactive_session()?; - Ok(session.buffer_size()) - } - - pub fn ensure_handshake(&mut self) -> Result<()> { - let session = self.interactive_session()?; - session.ensure_handshake() - } - - pub fn write_buffer(&mut self, bytes: &[u8]) -> Result<()> { - let session = self.interactive_session()?; - session.write(bytes) - } -} - -const INTERACTIVE_ARGV: &[&str] = &["/tmp/pglite/bin/postgres", "--single", "postgres"]; - -const STARTUP_PROTOCOL: u32 = 196_608; // Protocol 3.0 -const DEFAULT_USER: &str = "postgres"; -const DEFAULT_DATABASE: &str = "template1"; -const APPLICATION_NAME: &str = "pglite-oxide"; -const CLIENT_ENCODING: &str = "UTF8"; - -struct Message<'a> { - tag: u8, - body: &'a [u8], -} - -enum Transport { - Cma { - pending_wire_len: usize, - }, - File { - sinput: PathBuf, - slock: PathBuf, - cinput: PathBuf, - clock: PathBuf, - }, -} - -struct InteractiveSession { - paths: PglitePaths, - _engine: Engine, - store: Store, - _instance: Instance, - memory: Memory, - interactive_write: TypedFunc, - interactive_one: TypedFunc<(), ()>, - interactive_read: TypedFunc<(), i32>, - use_wire: Option>, - clear_error: Option>, - pgl_closed: Option>, - buffer_addr: usize, - buffer_size: usize, - transport: Transport, - handshake_complete: bool, - password_cache: Option, -} - -impl InteractiveSession { - fn new(paths: &PglitePaths) -> Result { - ensure_runtime(paths)?; - if !paths.pgdata.join("PG_VERSION").exists() { - ensure_cluster(paths)?; - } - - let module_path = resolve_module(paths, None)?; - let engine = create_engine()?; - let module = Module::from_file(&engine, &module_path)?; - - let mut linker: Linker = Linker::new(&engine); - add_to_linker_sync(&mut linker, |cx: &mut WasiP1Ctx| cx)?; - - let mut builder = standard_wasi_builder(paths)?; - for arg in INTERACTIVE_ARGV { - builder.arg(arg); - } - - let wasi = builder.build(); - let mut store = Store::new(&engine, WasiP1Ctx::new(wasi)); - let instance = linker.instantiate(&mut store, &module)?; - - if let Ok(start) = instance.get_typed_func::<(), ()>(&mut store, "_start") { - let _ = start.call(&mut store, ()); - } - - if let Ok(initdb) = instance.get_typed_func::<(), i32>(&mut store, "pgl_initdb") { - let _ = initdb.call(&mut store, ()); - } - - if let Ok(backend) = instance.get_typed_func::<(), ()>(&mut store, "pgl_backend") { - let _ = backend.call(&mut store, ()); - } - - let memory: Memory = instance - .get_memory(&mut store, "memory") - .context("interactive module missing 'memory' export")?; - let interactive_write = instance - .get_typed_func::(&mut store, "interactive_write") - .context("interactive module missing 'interactive_write' export")?; - let interactive_one = instance - .get_typed_func::<(), ()>(&mut store, "interactive_one") - .context("interactive module missing 'interactive_one' export")?; - let interactive_read = instance - .get_typed_func::<(), i32>(&mut store, "interactive_read") - .context("interactive module missing 'interactive_read' export")?; - let get_channel = instance - .get_typed_func::<(), i32>(&mut store, "get_channel") - .context("interactive module missing 'get_channel' export")?; - let channel = get_channel.call(&mut store, ())?; - let get_buffer_addr = instance - .get_typed_func::(&mut store, "get_buffer_addr") - .context("interactive module missing 'get_buffer_addr' export")?; - let addr = get_buffer_addr.call(&mut store, channel)?; - ensure!(addr >= 0, "interactive buffer address is negative: {addr}"); - let get_buffer_size = instance - .get_typed_func::(&mut store, "get_buffer_size") - .context("interactive module missing 'get_buffer_size' export")?; - let size = get_buffer_size.call(&mut store, channel)?; - ensure!(size >= 0, "interactive buffer size is negative: {size}"); - debug!("interactive transport channel={channel} addr={addr} size={size}"); - - let io_socket = resolve_io_socket(paths); - let transport = if channel >= 0 { - Transport::Cma { - pending_wire_len: 0, - } - } else { - let sinput = append_suffix(&io_socket, ".in"); - let slock = append_suffix(&io_socket, ".lock.in"); - let cinput = append_suffix(&io_socket, ".out"); - let clock = append_suffix(&io_socket, ".lock.out"); - Transport::File { - sinput, - slock, - cinput, - clock, - } - }; - - let use_wire = instance - .get_typed_func::(&mut store, "use_wire") - .ok(); - let clear_error = instance - .get_typed_func::<(), ()>(&mut store, "clear_error") - .ok(); - let pgl_closed = instance - .get_typed_func::<(), i32>(&mut store, "pgl_closed") - .ok(); - - Ok(Self { - paths: paths.clone(), - _engine: engine, - store, - _instance: instance, - memory, - interactive_write, - interactive_one, - interactive_read, - use_wire, - clear_error, - pgl_closed, - buffer_addr: addr as usize, - buffer_size: size as usize, - transport, - handshake_complete: false, - password_cache: None, - }) - } - - fn write(&mut self, payload: &[u8]) -> Result<()> { - ensure!( - payload.len() <= self.buffer_size, - "poke payload {} exceeds interactive buffer {}", - payload.len(), - self.buffer_size - ); - ensure!( - payload.len() <= i32::MAX as usize, - "poke payload {} exceeds i32::MAX", - payload.len() - ); - let write_offset = self.buffer_addr; - let end = write_offset - .checked_add(payload.len()) - .context("interactive payload overflow")?; - let buffer_end = self - .buffer_addr - .checked_add(self.buffer_size) - .context("interactive buffer end overflow")?; - ensure!( - end <= buffer_end, - "payload end {end:#x} exceeds buffer bounds" - ); - self.memory - .write(&mut self.store, write_offset, payload) - .context("write poke payload into WASM memory")?; - Ok(()) - } - - fn set_cma_length(&mut self, len: i32) -> Result<()> { - self.interactive_write - .call(&mut self.store, len) - .context("call interactive_write export")?; - Ok(()) - } - - fn run_once(&mut self) -> Result<()> { - self.interactive_one - .call(&mut self.store, ()) - .context("call interactive_one export")?; - Ok(()) - } - - fn clear_wire_pending(&mut self) -> Result<()> { - if matches!(self.transport, Transport::Cma { .. }) { - self.set_cma_length(0)?; - if let Transport::Cma { pending_wire_len } = &mut self.transport { - *pending_wire_len = 0; - } - } - Ok(()) - } - - fn send_wire(&mut self, payload: &[u8]) -> Result<()> { - if matches!(self.transport, Transport::Cma { .. }) { - if payload.is_empty() { - self.clear_wire_pending()?; - } else { - self.write(payload)?; - self.set_cma_length(payload.len() as i32)?; - if let Transport::Cma { - pending_wire_len, .. - } = &mut self.transport - { - *pending_wire_len = payload.len(); - } - } - return Ok(()); - } - - if let Transport::File { sinput, slock, .. } = &self.transport { - if payload.is_empty() { - return Ok(()); - } - if let Some(parent) = sinput.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("ensure directory {}", parent.display()))?; - } - let _ = fs::remove_file(slock); - fs::write(slock, payload).with_context(|| format!("write {}", slock.display()))?; - fs::rename(slock, sinput) - .with_context(|| format!("rename {} -> {}", slock.display(), sinput.display()))?; - return Ok(()); - } - Ok(()) - } - - fn try_recv_wire(&mut self) -> Result>> { - if matches!(self.transport, Transport::Cma { .. }) { - return self.try_recv_wire_cma(); - } - - if let Transport::File { cinput, clock, .. } = &self.transport { - match fs::read(cinput) { - Ok(data) => { - let _ = fs::remove_file(cinput); - let _ = fs::remove_file(clock); - Ok(Some(data)) - } - Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), - Err(err) => Err(err.into()), - } - } else { - Ok(None) - } - } - - fn try_recv_wire_cma(&mut self) -> Result>> { - let pending = if let Transport::Cma { - pending_wire_len, .. - } = &self.transport - { - *pending_wire_len - } else { - return Ok(None); - }; - - let reply_len = self - .interactive_read - .call(&mut self.store, ()) - .context("call interactive_read export")? as usize; - if reply_len == 0 { - return Ok(None); - } - - let base = self - .buffer_addr - .checked_add(pending) - .and_then(|offset| offset.checked_add(1)) - .context("interactive reply offset overflow")?; - let end = base - .checked_add(reply_len) - .context("interactive reply overflow")?; - let buffer_end = self - .buffer_addr - .checked_add(self.buffer_size) - .context("interactive buffer end overflow")?; - ensure!( - end <= buffer_end, - "interactive reply {end:#x} exceeds buffer bounds {buffer_end:#x}" - ); - let data_size = self.memory.data_size(&self.store); - ensure!( - end <= data_size, - "interactive reply {end:#x} exceeds memory size {data_size:#x}" - ); - - let mut buf = vec![0u8; reply_len]; - self.memory - .read(&mut self.store, base, &mut buf) - .context("read interactive reply from WASM memory")?; - self.set_cma_length(0)?; - if let Transport::Cma { - pending_wire_len, .. - } = &mut self.transport - { - *pending_wire_len = 0; - } - Ok(Some(buf)) - } - - fn collect_replies(&mut self, replies: &mut Vec>) -> Result { - let mut produced = false; - while let Some(reply) = self.try_recv_wire()? { - produced = true; - if !reply.is_empty() { - replies.push(reply); - } - } - Ok(produced) - } - - fn drain_wire(&mut self) -> Result>> { - let mut replies = Vec::new(); - const MAX_TICKS: usize = 128; - for _ in 0..MAX_TICKS { - let produced_before = self.collect_replies(&mut replies)?; - if !produced_before { - self.run_once()?; - let produced_after = self.collect_replies(&mut replies)?; - if !produced_after { - break; - } - } else { - self.run_once()?; - if !self.collect_replies(&mut replies)? { - break; - } - } - } - Ok(replies) - } - - fn forward_wire(&mut self, payload: &[u8]) -> Result>> { - if !payload.is_empty() { - self.use_wire(true)?; - self.send_wire(payload)?; - } - - let mut replies = Vec::new(); - const MAX_TICKS: usize = 256; - for _ in 0..MAX_TICKS { - let produced_before = self.collect_replies(&mut replies)?; - self.run_once()?; - let produced_after = self.collect_replies(&mut replies)?; - if !produced_before && !produced_after { - break; - } - } - Ok(replies) - } - - fn read_response(&mut self, payload_len: usize) -> Result> { - match &mut self.transport { - Transport::Cma { .. } => { - let response_len = - self.interactive_read - .call(&mut self.store, ()) - .context("call interactive_read export")? as usize; - - if response_len == 0 { - return Ok(Vec::new()); - } - - let response_offset = self - .buffer_addr - .checked_add(payload_len) - .and_then(|offset| offset.checked_add(1)) - .context("interactive response offset overflow")?; - let end = response_offset - .checked_add(response_len) - .context("interactive response overflow")?; - let buffer_end = self - .buffer_addr - .checked_add(self.buffer_size) - .context("interactive buffer end overflow")?; - ensure!( - end <= buffer_end, - "interactive response {end:#x} exceeds buffer bounds {buffer_end:#x}" - ); - let data_size = self.memory.data_size(&self.store); - ensure!( - end <= data_size, - "interactive response {end:#x} exceeds memory size {data_size:#x}" - ); - - let mut buf = vec![0u8; response_len]; - self.memory - .read(&mut self.store, response_offset, &mut buf) - .context("read interactive response from WASM memory")?; - Ok(buf) - } - Transport::File { cinput, clock, .. } => match fs::read(cinput.as_path()) { - Ok(data) => { - let _ = fs::remove_file(cinput.as_path()); - let _ = fs::remove_file(clock.as_path()); - Ok(data) - } - Err(err) if err.kind() == ErrorKind::NotFound => Ok(Vec::new()), - Err(err) => Err(err.into()), - }, - } - } - - fn run_wire(&mut self, payload: &[u8]) -> Result> { - let mut combined = Vec::new(); - for reply in self.forward_wire(payload)? { - combined.extend(reply); - } - if combined.is_empty() { - bail!("interactive response timeout"); - } - Ok(combined) - } - - fn ensure_handshake(&mut self) -> Result<()> { - if self.handshake_complete { - return Ok(()); - } - - self.clear_wire_pending()?; - let startup = build_startup_message(DEFAULT_USER, DEFAULT_DATABASE); - let mut response = self.run_wire(&startup)?; - - loop { - let next = self.process_handshake_response(&response)?; - if self.handshake_complete { - return Ok(()); - } - if let Some(payload) = next { - response = self.run_wire(&payload)?; - } else { - bail!("interactive handshake did not complete"); - } - } - } - - fn process_handshake_response(&mut self, data: &[u8]) -> Result>> { - let mut next_payload: Option> = None; - for message in parse_messages(data)? { - match message.tag { - b'R' => { - ensure!(message.body.len() >= 4, "authentication response too short"); - let code = u32::from_be_bytes(message.body[0..4].try_into().unwrap()); - match code { - 0 => {} - 3 => { - let password = self.password()?.as_bytes(); - ensure!(next_payload.is_none(), "multiple auth responses"); - next_payload = Some(build_password_message(password)); - } - 5 => { - ensure!( - message.body.len() >= 8, - "AuthenticationMD5Password missing salt" - ); - let salt: [u8; 4] = message.body[4..8].try_into().unwrap(); - let hashed = build_md5_password(self.password()?, DEFAULT_USER, &salt)?; - ensure!(next_payload.is_none(), "multiple auth responses"); - next_payload = Some(build_password_message(hashed.as_bytes())); - } - other => bail!("unsupported authentication method: {other}"), - } - } - b'S' => { - if let Some((key, value)) = parse_parameter_status(message.body) { - debug!("[pglite_oxide] parameter status: {key}={value}"); - } - } - b'K' => { - debug!("[pglite_oxide] backend key data received"); - } - b'Z' => { - self.handshake_complete = true; - } - b'E' => { - let message = parse_error_message(message.body); - bail!("postgres handshake error: {message}"); - } - b'N' => { - let message = parse_error_message(message.body); - debug!("[pglite_oxide] notice: {message}"); - } - _ => {} - } - } - - Ok(next_payload) - } - - fn password(&mut self) -> Result<&str> { - if self.password_cache.is_none() { - let path = self.paths.pgroot.join("pglite").join("password"); - let mut contents = fs::read_to_string(&path) - .with_context(|| format!("read password file {}", path.display()))?; - contents = contents.trim_end_matches(['\n', '\r']).to_string(); - self.password_cache = Some(contents); - } - Ok(self.password_cache.as_ref().unwrap()) - } - - fn use_wire(&mut self, enable: bool) -> Result<()> { - if let Some(func) = &self.use_wire { - func.call(&mut self.store, if enable { 1 } else { 0 }) - .context("call use_wire export")?; - } - Ok(()) - } - - fn clear_error(&mut self) -> Result<()> { - if let Some(func) = &self.clear_error { - func.call(&mut self.store, ()) - .context("call clear_error export")?; - } - Ok(()) - } - - fn pgl_closed(&mut self) -> Result> { - if let Some(func) = &self.pgl_closed { - return Ok(Some( - func.call(&mut self.store, ()) - .context("call pgl_closed export")?, - )); - } - Ok(None) - } - - fn buffer_addr(&self) -> usize { - self.buffer_addr - } - - fn buffer_size(&self) -> usize { - self.buffer_size - } - - fn is_file_transport(&self) -> bool { - matches!(self.transport, Transport::File { .. }) - } -} - -fn build_simple_query(sql: &str) -> Vec { - let mut buf = Vec::with_capacity(sql.len() + 6); - buf.push(b'Q'); - buf.extend_from_slice(&0u32.to_be_bytes()); - buf.extend_from_slice(sql.as_bytes()); - buf.push(0); - let len = (buf.len() - 1) as u32; - buf[1..5].copy_from_slice(&len.to_be_bytes()); - buf -} - -fn build_startup_message(user: &str, database: &str) -> Vec { - let mut buf = Vec::new(); - buf.extend_from_slice(&0u32.to_be_bytes()); - buf.extend_from_slice(&STARTUP_PROTOCOL.to_be_bytes()); - - for (key, value) in [ - ("user", user), - ("database", database), - ("client_encoding", CLIENT_ENCODING), - ("application_name", APPLICATION_NAME), - ] { - buf.extend_from_slice(key.as_bytes()); - buf.push(0); - buf.extend_from_slice(value.as_bytes()); - buf.push(0); - } - - buf.push(0); - let len = buf.len() as u32; - buf[0..4].copy_from_slice(&len.to_be_bytes()); - buf -} - -fn build_password_message(password: &[u8]) -> Vec { - let mut buf = Vec::with_capacity(password.len() + 6); - buf.push(b'p'); - buf.extend_from_slice(&0u32.to_be_bytes()); - buf.extend_from_slice(password); - if !password.ends_with(&[0]) { - buf.push(0); - } - let len = (buf.len() - 1) as u32; - buf[1..5].copy_from_slice(&len.to_be_bytes()); - buf -} - -fn build_md5_password(password: &str, user: &str, salt: &[u8; 4]) -> Result { - let mut inner = Vec::with_capacity(password.len() + user.len()); - inner.extend_from_slice(password.as_bytes()); - inner.extend_from_slice(user.as_bytes()); - let inner_hex = md5_hex(&inner); - - let mut outer = Vec::with_capacity(inner_hex.len() + salt.len()); - outer.extend_from_slice(inner_hex.as_bytes()); - outer.extend_from_slice(salt); - let outer_hex = md5_hex(&outer); - - Ok(format!("md5{}", outer_hex)) -} - -fn md5_hex(bytes: &[u8]) -> String { - let mut hasher = Md5::new(); - hasher.update(bytes); - let digest = hasher.finalize(); - format!("{:032x}", digest) -} - -fn parse_messages(data: &[u8]) -> Result>> { - let mut messages = Vec::new(); - let mut index = 0usize; - while index < data.len() { - let remaining = &data[index..]; - if remaining.len() < 5 { - bail!("incomplete postgres message"); - } - let tag = remaining[0]; - let len = u32::from_be_bytes(remaining[1..5].try_into().unwrap()) as usize; - ensure!(len >= 4, "invalid postgres message length {len}"); - let total = 1 + len; - ensure!( - index + total <= data.len(), - "postgres message overruns buffer" - ); - let body = &data[index + 5..index + total]; - messages.push(Message { tag, body }); - index += total; - } - Ok(messages) -} - -fn parse_parameter_status(body: &[u8]) -> Option<(String, String)> { - let nul = body.iter().position(|&b| b == 0)?; - let key = str::from_utf8(&body[..nul]).ok()?.to_string(); - let rest = &body[nul + 1..]; - let nul2 = rest.iter().position(|&b| b == 0)?; - let value = str::from_utf8(&rest[..nul2]).ok()?.to_string(); - Some((key, value)) -} - -fn parse_info_fields(body: &[u8]) -> Vec<(char, String)> { - let mut fields = Vec::new(); - let mut index = 0usize; - while index < body.len() { - let code = body[index]; - if code == 0 { - break; - } - index += 1; - if let Some(end) = body[index..].iter().position(|&b| b == 0) { - let value = str::from_utf8(&body[index..index + end]) - .unwrap_or_default() - .to_string(); - fields.push((code as char, value)); - index += end + 1; - } else { - break; - } - } - fields -} - -fn parse_error_message(body: &[u8]) -> String { - let fields = parse_info_fields(body); - if let Some((_, message)) = fields.iter().find(|(code, _)| *code == 'M') { - return message.clone(); - } - fields - .iter() - .map(|(code, message)| format!("{code}:{message}")) - .collect::>() - .join(", ") -} - -/// Inputs supported by poke helper. -pub enum PokeInput<'a> { - Str(&'a str), - Bytes(&'a [u8]), -} - -fn append_suffix(path: &Path, suffix: &str) -> PathBuf { - let mut os: OsString = path.as_os_str().to_os_string(); - os.push(suffix); - PathBuf::from(os) -} - -fn prepare_cstring(input: PokeInput<'_>) -> (Vec, usize) { - match input { - PokeInput::Str(s) => { - let mut data = s.as_bytes().to_vec(); - data.push(0); - let len = data.len(); - (data, len) - } - PokeInput::Bytes(bytes) => { - let mut data = bytes.to_vec(); - if !data.ends_with(&[0]) { - data.push(0); - } - let len = data.len(); - (data, len) - } - } -} - -fn resolve_module(paths: &PglitePaths, wasmfile: Option<&Path>) -> Result { - ensure_runtime(paths)?; - - if let Some(explicit) = wasmfile { - let candidate = if explicit.is_absolute() { - explicit.to_path_buf() - } else { - paths.pgroot.join(explicit) - }; - ensure!( - candidate.exists(), - "wasm module {} does not exist", - candidate.display() - ); - return Ok(candidate); - } - - if let Some((module, _bin_dir)) = locate_runtime_module(paths) { - Ok(module) - } else { - Err(anyhow!( - "runtime module not found under {}", - paths.pgroot.display() - )) - } -} - -fn resolve_pg_dump(paths: &PglitePaths) -> Result { - ensure_runtime(paths)?; - let bin_dir = paths.pgroot.join("pglite").join("bin"); - let candidates = [ - "pg_dump", - "pg_dump.wasi", - "pg_dump.wasm", - "pgdump.wasi", - "pgdump.wasm", - ]; - - for name in candidates { - let candidate = bin_dir.join(name); - if candidate.exists() { - return Ok(candidate); - } - } - - anyhow::bail!( - "pg_dump binary not found under {} (looked for {:?})", - bin_dir.display(), - candidates - ) -} - -fn run_wasi_command( - paths: &PglitePaths, - module_path: &Path, - argv: &[&str], - env: &[(&str, &str)], -) -> Result { - ensure_runtime(paths)?; - let engine = create_engine()?; - let module = Module::from_file(&engine, module_path)?; - - let mut linker: Linker = Linker::new(&engine); - add_to_linker_sync(&mut linker, |cx: &mut WasiP1Ctx| cx)?; - - let mut builder = standard_wasi_builder(paths)?; - - for (key, value) in env { - builder.env(key, value); - } - - let mut argv_vec: Vec = if argv.is_empty() { - vec![module_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("pg_dump") - .to_string()] - } else { - argv.iter().map(|s| (*s).to_string()).collect() - }; - - if let Some(first) = argv_vec.first_mut() { - if Path::new(first).is_relative() { - let guest_path = module_path - .strip_prefix(&paths.pgroot) - .map(|rel| Path::new("/tmp").join(rel)) - .unwrap_or_else(|_| PathBuf::from("/tmp/pglite/bin/pg_dump")); - *first = guest_path.to_string_lossy().into_owned(); - } - } - - for arg in &argv_vec { - builder.arg(arg); - } - - let wasi = builder.build(); - let mut store = Store::new(&engine, WasiP1Ctx::new(wasi)); - let instance = linker.instantiate(&mut store, &module)?; - let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?; - - match start.call(&mut store, ()) { - Ok(()) => Ok(0), - Err(err) => { - for cause in err.chain() { - if let Some(exit) = cause.downcast_ref::() { - return Ok(exit.0); - } - if let Some(trap) = cause.downcast_ref::() { - let module_name = module_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or_default(); - if matches!(trap, Trap::UnreachableCodeReached) - && module_name.contains("pg_dump") - { - warn!("pg_dump exited via unreachable trap; treating as success (0)"); - return Ok(0); - } - } - } - let message = err.to_string(); - if let Some(rest) = message.strip_prefix("Exited with i32 exit status ") { - if let Ok(code) = rest.trim().parse::() { - return Ok(code); - } - } - Err(err) - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 02eff2c4..a082b99e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,621 +1,21 @@ -use anyhow::{anyhow, Context, Result}; -use directories::ProjectDirs; -use std::{ - fs, - io::{Cursor, Read}, - path::{Path, PathBuf}, +#![doc = include_str!("../README.md")] +#![forbid(unsafe_code)] + +mod pglite; +mod protocol; + +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, }; -use tar::Archive; -use tracing::{debug, info, warn}; -use xz2::read::XzDecoder; +pub use protocol::messages::{DatabaseError, NoticeMessage}; -use flate2::read::GzDecoder; - -pub mod interactive; - -use cap_std::ambient_authority; -use cap_std::fs::Dir; -use wasmtime::{Engine, Linker, Module, Store}; -use wasmtime_wasi::preview1::add_to_linker_sync; -use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder, WasiP1Ctx}; - -/// Initialize tracing with verbose logging -pub fn init_tracing() { - use tracing_subscriber::EnvFilter; - - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::new("pglite_oxide=trace,info")) - .init(); -} - -const EMBEDDED_TAR_XZ: &[u8] = include_bytes!("../assets/pglite-wasi.tar.xz"); - -#[cfg(unix)] -fn ensure_shim(src: &Path, dst: &Path) -> Result<()> { - use std::os::unix::fs::symlink; - if !dst.exists() { - if let Err(e) = symlink(src, dst) { - let _ = std::fs::copy(src, dst).with_context(|| { - format!( - "copy {} -> {} (fallback after symlink error: {e})", - src.display(), - dst.display() - ) - })?; - } - } - Ok(()) -} - -#[cfg(not(unix))] -fn ensure_shim(src: &Path, dst: &Path) -> Result<()> { - if !dst.exists() { - std::fs::copy(src, dst) - .with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?; - } - Ok(()) -} - -pub(crate) fn seed_urandom_once(dev_host: &Path) -> Result<()> { - fs::create_dir_all(dev_host)?; - let urandom_path = dev_host.join("urandom"); - if !urandom_path.exists() { - let mut buf = [0u8; 128]; - getrandom::getrandom(&mut buf)?; // real entropy - fs::write(&urandom_path, buf)?; // create once - } - Ok(()) -} - -pub(crate) fn create_engine() -> Result { - let mut cfg = wasmtime::Config::new(); - cfg.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); - Engine::new(&cfg) -} - -pub(crate) fn prepare_guest_dirs(paths: &PglitePaths) -> Result<()> { - let dev_host = paths.pgroot.join("dev"); - seed_urandom_once(&dev_host)?; - fs::create_dir_all(&paths.pgdata)?; - Ok(()) -} - -pub(crate) fn standard_wasi_builder(paths: &PglitePaths) -> Result { - prepare_guest_dirs(paths)?; - - let pgroot_dir = Dir::open_ambient_dir(&paths.pgroot, ambient_authority())?; - let pgdata_dir = Dir::open_ambient_dir(&paths.pgdata, ambient_authority())?; - let dev_dir_path = paths.pgroot.join("dev"); - let dev_dir = Dir::open_ambient_dir(&dev_dir_path, ambient_authority())?; - - let mut builder = WasiCtxBuilder::new(); - builder - .inherit_stdin() - .inherit_stdout() - .inherit_stderr() - .preopened_dir(pgroot_dir, DirPerms::all(), FilePerms::all(), "/tmp") - .preopened_dir( - pgdata_dir, - DirPerms::all(), - FilePerms::all(), - "/tmp/pglite/base", - ) - .preopened_dir(dev_dir, DirPerms::all(), FilePerms::all(), "/dev") - .env("ENVIRONMENT", "wasm32_wasi_preview1") - .env("PREFIX", "/tmp/pglite") - .env("PGDATA", "/tmp/pglite/base") - .env("PGSYSCONFDIR", "/tmp/pglite") - .env("PGUSER", "postgres") - .env("PGDATABASE", "template1") - .env("MODE", "REACT") - .env("REPL", "N") - .env("TZ", "UTC") - .env("PGTZ", "UTC") - .env("PATH", "/tmp/pglite/bin"); - Ok(builder) -} - -fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> { - fs::create_dir_all(dst)?; - for entry in fs::read_dir(src)? { - let entry = entry?; - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - if src_path.is_dir() { - copy_dir_all(&src_path, &dst_path)?; - } else { - fs::copy(&src_path, &dst_path)?; - } - } - Ok(()) -} - -fn assets_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets") -} - -fn install_optional_pg_dump(paths: &PglitePaths) -> Result<()> { - let src = assets_dir().join("bin/pg_dump.wasm"); - if !src.exists() { - return Ok(()); - } - - let dest_dir = paths.pgroot.join("pglite/bin"); - fs::create_dir_all(&dest_dir)?; - - let wasm_dest = dest_dir.join("pg_dump.wasm"); - fs::copy(&src, &wasm_dest) - .with_context(|| format!("copy {} -> {}", src.display(), wasm_dest.display()))?; - - let plain_dest = dest_dir.join("pg_dump"); - if !plain_dest.exists() { - fs::copy(&wasm_dest, &plain_dest).ok(); - } - - Ok(()) -} - -fn install_optional_extensions(paths: &PglitePaths) -> Result<()> { - let dir = assets_dir().join("extensions"); - if !dir.exists() { - return Ok(()); - } - - for entry in fs::read_dir(&dir)? { - let path = entry?.path(); - if !path - .file_name() - .and_then(|s| s.to_str()) - .map(|name| name.ends_with(".tar.gz")) - .unwrap_or(false) - { - continue; - } - - let ext_name = path - .file_name() - .and_then(|s| s.to_str()) - .and_then(|name| name.strip_suffix(".tar.gz")) - .unwrap_or(""); - - let control_path = paths - .pgroot - .join("pglite/share/extension") - .join(format!("{}.control", ext_name)); - if control_path.exists() { - continue; - } - - install_extension_archive(paths, &path)?; - } - - Ok(()) -} - -pub fn install_extension_archive(paths: &PglitePaths, archive_path: &Path) -> Result<()> { - let file = fs::File::open(archive_path) - .with_context(|| format!("open extension archive {}", archive_path.display()))?; - install_extension_reader(paths, file) -} - -pub fn install_extension_bytes(paths: &PglitePaths, bytes: &[u8]) -> Result<()> { - install_extension_reader(paths, Cursor::new(bytes)) -} - -fn install_extension_reader(paths: &PglitePaths, reader: R) -> Result<()> { - let gz = GzDecoder::new(reader); - let mut ar = Archive::new(gz); - let target = paths.pgroot.join("pglite"); - fs::create_dir_all(&target)?; - ar.unpack(&target) - .with_context(|| format!("unpack extension into {}", target.display()))?; - Ok(()) -} - -#[derive(Debug, Clone)] -pub struct PglitePaths { - pub pgroot: PathBuf, - pub pgdata: PathBuf, -} - -impl PglitePaths { - pub fn new(app_qual: (&str, &str, &str)) -> Result { - let pd = ProjectDirs::from(app_qual.0, app_qual.1, app_qual.2) - .context("could not resolve app data dir")?; - let app_dir = pd.data_dir().to_path_buf(); - let pgroot = app_dir.join("pglite"); - let pgdata = app_dir.join("db"); - Ok(Self { pgroot, pgdata }) - } - - pub fn with_root(root: impl Into) -> Self { - let pgroot = root.into(); - let pgdata = pgroot.join("pglite").join("base"); - Self { pgroot, pgdata } - } - - pub fn with_paths(pgroot: impl Into, pgdata: impl Into) -> Self { - Self { - pgroot: pgroot.into(), - pgdata: pgdata.into(), - } - } - - /// Detect the legacy/local mount layouts that the Python helper supports. - pub fn detect_existing_mounts() -> Option { - for raw in ["tmp", "/tmp"] { - let base = PathBuf::from(raw); - let pgdata = base.join("pglite").join("base"); - if pgdata.join("PG_VERSION").exists() { - return Some(Self { - pgroot: base, - pgdata, - }); - } - } - None - } - - pub fn mount_root(&self) -> &Path { - &self.pgroot - } - - fn marker_runtime(&self) -> PathBuf { - self.pgroot.join(".runtime_ready") - } - fn marker_cluster(&self) -> PathBuf { - self.pgdata.join("PG_VERSION") - } -} - -fn promote_nested_runtime(paths: &PglitePaths) -> Result<()> { - let nested = paths.pgroot.join("tmp").join("pglite"); - let nested_bin = nested.join("bin"); - if nested_bin.join("pglite.wasi").exists() { - for entry in std::fs::read_dir(&nested).context("read nested pglite dir")? { - let entry = entry?; - let name = entry.file_name(); - let src = entry.path(); - let dst = paths.pgroot.join(name); - let metadata = match std::fs::symlink_metadata(&dst) { - Ok(metadata) => Some(metadata), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, - Err(err) => { - return Err(err).with_context(|| format!("inspect {}", dst.display())); - } - }; - if let Some(metadata) = metadata { - if metadata.file_type().is_dir() { - std::fs::remove_dir_all(&dst) - .with_context(|| format!("remove dir {}", dst.display()))?; - } else { - std::fs::remove_file(&dst) - .with_context(|| format!("remove file {}", dst.display()))?; - } - } - std::fs::rename(&src, &dst) - .with_context(|| format!("promote {} -> {}", src.display(), dst.display()))?; - } - let _ = std::fs::remove_dir_all(paths.pgroot.join("tmp")); - } - Ok(()) -} - -fn ensure_pglite_layout(paths: &PglitePaths) -> Result<()> { - let pglite_dir = paths.pgroot.join("pglite"); - if !pglite_dir.exists() { - fs::create_dir_all(&pglite_dir)?; - } - - for name in ["bin", "share", "lib", "password"] { - let src = paths.pgroot.join(name); - if src.exists() { - let dst = pglite_dir.join(name); - let moved = std::fs::rename(&src, &dst).is_ok(); - if !moved { - if src.is_dir() { - std::fs::create_dir_all(&dst)?; - copy_dir_all(&src, &dst).with_context(|| { - format!("copy dir {} -> {}", src.display(), dst.display()) - })?; - std::fs::remove_dir_all(&src)?; - } else { - std::fs::copy(&src, &dst).with_context(|| { - format!("copy file {} -> {}", src.display(), dst.display()) - })?; - std::fs::remove_file(&src)?; - } - } - } - } - Ok(()) -} - -pub(crate) fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> { - let pglite_dir = paths.pgroot.join("pglite"); - if !pglite_dir.exists() { - return None; - } - let pglite_bin_dir = pglite_dir.join("bin"); - let module = if pglite_bin_dir.join("pglite.wasi").exists() { - pglite_bin_dir.join("pglite.wasi") - } else { - return None; - }; - - let share = pglite_dir.join("share").join("postgresql"); - if !share.exists() || !share.is_dir() { - return None; - } - if !share.join("postgres.bki").exists() { - return None; - } - Some((module, pglite_bin_dir)) -} - -fn finalize_runtime_setup( - paths: &PglitePaths, - module_path: &Path, - pglite_bin_dir: &Path, -) -> Result<()> { - ensure_shim(module_path, &pglite_bin_dir.join("initdb"))?; - ensure_shim(module_path, &pglite_bin_dir.join("postgres"))?; - fs::write(paths.marker_runtime(), b"ok")?; - Ok(()) -} - -pub fn ensure_runtime(paths: &PglitePaths) -> Result<()> { - if let Some((module_path, bin_dir)) = locate_runtime_module(paths) { - install_optional_pg_dump(paths)?; - install_optional_extensions(paths)?; - finalize_runtime_setup(paths, &module_path, &bin_dir)?; - return Ok(()); - } - - if paths.marker_runtime().exists() { - let _ = fs::remove_file(paths.marker_runtime()); - } - - fs::create_dir_all(&paths.pgroot).context("create pgroot dir")?; - promote_nested_runtime(paths)?; - ensure_pglite_layout(paths)?; - install_optional_pg_dump(paths)?; - install_optional_extensions(paths)?; - - if let Some((module_path, bin_dir)) = locate_runtime_module(paths) { - finalize_runtime_setup(paths, &module_path, &bin_dir)?; - return Ok(()); - } - - if let Ok(override_path) = std::env::var("PGLITE_OXIDE_TAR_XZ") { - let file = std::fs::File::open(&override_path) - .with_context(|| format!("open override tar.xz: {}", override_path))?; - let mut decoder = XzDecoder::new(file); - let mut ar = Archive::new(&mut decoder); - ar.unpack(&paths.pgroot) - .with_context(|| format!("unpack override tar.xz from {}", override_path))?; - } else { - let mut decoder = XzDecoder::new(EMBEDDED_TAR_XZ); - let mut ar = Archive::new(&mut decoder); - ar.unpack(&paths.pgroot) - .context("unpack embedded pglite-wasi.tar.xz")?; - } - - promote_nested_runtime(paths)?; - ensure_pglite_layout(paths)?; - install_optional_pg_dump(paths)?; - install_optional_extensions(paths)?; - - let (module_path, bin_dir) = locate_runtime_module(paths).ok_or_else(|| { - anyhow!( - "runtime missing: could not locate module under {} after install", - paths.pgroot.display() - ) - })?; - - finalize_runtime_setup(paths, &module_path, &bin_dir) -} - -#[allow(clippy::const_is_empty)] -pub fn embedded_runtime_present() -> bool { - !EMBEDDED_TAR_XZ.is_empty() -} - -pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> { - if paths.marker_cluster().exists() { - return Ok(()); - } - - ensure_runtime(paths)?; - fs::create_dir_all(&paths.pgdata).context("create pgdata dir")?; - - // Password file expected at /password (relative to PREFIX) - let pw_path = paths.pgroot.join("pglite").join("password"); - if !pw_path.exists() { - fs::write(&pw_path, "localdevpassword\n").context("write password file")?; - } - - let mut cfg = wasmtime::Config::new(); - cfg.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); - let engine = Engine::new(&cfg)?; - - let pglite_bin_dir = paths.pgroot.join("pglite").join("bin"); - let module_path = pglite_bin_dir.join("pglite.wasi"); - let module = Module::from_file(&engine, &module_path) - .with_context(|| format!("load module at {}", module_path.display()))?; - - let mut linker: Linker = Linker::new(&engine); - add_to_linker_sync(&mut linker, |cx: &mut WasiP1Ctx| cx)?; - - // Ensure cluster dir is empty for clean init - if paths.pgdata.exists() { - for entry in std::fs::read_dir(&paths.pgdata)? { - let entry = entry?; - if entry.file_name() != "PG_VERSION" { - let path = entry.path(); - if path.is_dir() { - std::fs::remove_dir_all(&path)?; - } else { - std::fs::remove_file(&path)?; - } - } - } - } - - // Build WASI ctx/instance - let mut b = standard_wasi_builder(paths)?; - - // Boot argv mirrors: `/tmp/pglite/bin/postgres --single postgres` - let wasi = b - .args(&["/tmp/pglite/bin/postgres", "--single", "postgres"]) - .build(); - - let mut store = Store::new(&engine, WasiP1Ctx::new(wasi)); - let instance = linker.instantiate(&mut store, &module)?; - - // 1) Embed setup first - info!("[pglite_oxide] Starting embed setup..."); - if let Ok(start) = instance.get_typed_func::<(), ()>(&mut store, "_start") { - let _ = start.call(&mut store, ()); - info!("[pglite_oxide] Embed setup completed"); - } else { - warn!("[pglite_oxide] No _start export found"); - } - - // 2) Run initdb on the SAME instance; it reads env (PGDATA, PGSHAREDIR, POSTGRES, PREFIX) - debug!("[pglite_oxide] Looking for initdb export..."); - let initdb = instance.get_typed_func::<(), i32>(&mut store, "pgl_initdb")?; - - info!("[pglite_oxide] Calling initdb..."); - let rc = initdb.call(&mut store, ())?; - info!("[pglite_oxide] initdb returned: {}", rc); - // Some pglite builds return non-zero even on success; trust the marker. - if !paths.marker_cluster().exists() { - anyhow::bail!("pgl_initdb rc={rc} but PG_VERSION not created"); - } - - // Best-effort graceful shutdown if exposed - if let Ok(shutdown) = instance.get_typed_func::<(), ()>(&mut store, "pgl_shutdown") { - let _ = shutdown.call(&mut store, ()); - } - - Ok(()) -} - -#[derive(Debug, Clone, Copy)] -pub struct InstallOptions { - pub ensure_cluster: bool, -} - -impl Default for InstallOptions { - fn default() -> Self { - Self { - ensure_cluster: true, - } - } -} - -pub fn install_and_init(app_qual: (&str, &str, &str)) -> Result { - if let Some(existing) = PglitePaths::detect_existing_mounts() { - info!( - "[pglite_oxide] Reusing existing runtime at {}", - existing.pgroot.display() - ); - return install_with_options(existing, InstallOptions::default()); - } - - let paths = PglitePaths::new(app_qual)?; - install_with_options(paths, InstallOptions::default()) -} - -pub fn install_and_init_with_paths(paths: PglitePaths) -> Result { - install_with_options(paths, InstallOptions::default()) -} - -pub fn install_and_init_in(root: impl Into) -> Result { - let paths = PglitePaths::with_root(root); - install_with_options(paths, InstallOptions::default()) -} - -pub fn install_with_options(paths: PglitePaths, options: InstallOptions) -> Result { - ensure_runtime(&paths)?; - if options.ensure_cluster { - ensure_cluster(&paths)?; - } - Ok(paths) -} - -#[derive(Debug, Clone)] -pub struct MountInfo { - mount: PathBuf, - io_socket: PathBuf, - paths: PglitePaths, - reused_existing: bool, -} - -impl MountInfo { - pub fn into_paths(self) -> PglitePaths { - self.paths - } - - pub fn mount(&self) -> &Path { - &self.mount - } - - pub fn io_socket(&self) -> &Path { - &self.io_socket - } - - pub fn paths(&self) -> &PglitePaths { - &self.paths - } - - pub fn reused_existing(&self) -> bool { - self.reused_existing - } -} - -pub fn prepare_default_mount() -> Result { - if let Some(existing) = PglitePaths::detect_existing_mounts() { - let reused_existing = true; - ensure_runtime(&existing)?; - if !existing.marker_cluster().exists() { - ensure_cluster(&existing)?; - } - let io_socket = resolve_io_socket(&existing); - return Ok(MountInfo { - mount: existing.pgroot.clone(), - io_socket, - paths: existing, - reused_existing, - }); - } - - let local_paths = PglitePaths::with_root(PathBuf::from("tmp")); - install_with_options( - local_paths.clone(), - InstallOptions { - ensure_cluster: false, - }, - )?; - if !local_paths.marker_cluster().exists() { - ensure_cluster(&local_paths)?; - } - let io_socket = resolve_io_socket(&local_paths); - Ok(MountInfo { - mount: local_paths.pgroot.clone(), - io_socket, - paths: local_paths, - reused_existing: false, - }) -} - -fn resolve_io_socket(paths: &PglitePaths) -> PathBuf { - let mount = &paths.pgroot; - let io = paths.pgdata.join(".s.PGSQL.5432"); - if mount.is_absolute() { - io - } else { - Path::new(".").join(io) - } -} +#[doc(hidden)] +pub use pglite::{ + DebugLevel, InstallOptions, InstallOutcome, MountInfo, PglitePaths, PgliteProxy, + ensure_cluster, install_and_init, install_and_init_in, install_default, + install_extension_archive, install_extension_bytes, install_into, install_with_options, +}; diff --git a/src/pglite/base.rs b/src/pglite/base.rs new file mode 100644 index 00000000..f9bdd664 --- /dev/null +++ b/src/pglite/base.rs @@ -0,0 +1,543 @@ +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock, OnceLock}; + +use anyhow::{Context, Result, anyhow, bail}; +use directories::ProjectDirs; +use flate2::read::GzDecoder; +use serde::Deserialize; +use tar::Archive; +use tracing::info; +use xz2::read::XzDecoder; + +use super::postgres_mod::PostgresMod; +use tempfile::TempDir; + +#[derive(Debug, Deserialize)] +struct ManifestEntry { + path: String, + start: usize, + end: usize, +} + +static FS_MANIFEST: LazyLock> = LazyLock::new(|| { + serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/pglite_fs_manifest.json" + ))) + .expect("failed to parse pglite_fs_manifest.json") +}); + +static TEMPLATE_CLUSTER: OnceLock, String>> = + OnceLock::new(); + +#[derive(Debug)] +struct TemplateCluster { + root: PathBuf, + _temp_dir: TempDir, +} + +pub fn load_fs_bundle() -> Result> { + if let Ok(path) = std::env::var("PGLITE_OXIDE_FS_BUNDLE") { + return std::fs::read(&path).with_context(|| format!("read bundle from {}", path)); + } + let default_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite.data"); + std::fs::read(&default_path) + .with_context(|| format!("read bundle from {}", default_path.display())) +} + +#[derive(Debug, Clone)] +pub struct PglitePaths { + pub pgroot: PathBuf, + pub pgdata: PathBuf, +} + +impl PglitePaths { + pub fn new(app_qual: (&str, &str, &str)) -> Result { + let pd = ProjectDirs::from(app_qual.0, app_qual.1, app_qual.2) + .context("could not resolve app data dir")?; + let app_dir = pd.data_dir().to_path_buf(); + Ok(Self::with_root(app_dir)) + } + + pub fn with_root(root: impl Into) -> Self { + let base = root.into(); + let pgroot = base.join("tmp"); + let pgdata = pgroot.join("pglite").join("base"); + Self { pgroot, pgdata } + } + + pub fn with_paths(pgroot: impl Into, pgdata: impl Into) -> Self { + Self { + pgroot: pgroot.into(), + pgdata: pgdata.into(), + } + } + + pub fn mount_root(&self) -> &Path { + &self.pgroot + } + + pub fn with_temp_dir() -> Result<(TempDir, Self)> { + let tmp = TempDir::new().context("create temporary directory")?; + let paths = Self::with_root(tmp.path()); + Ok((tmp, paths)) + } + + fn marker_cluster(&self) -> PathBuf { + self.pgdata.join("PG_VERSION") + } + + pub fn is_cluster_initialized(&self) -> bool { + self.marker_cluster().exists() + } +} + +fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> { + let pglite_dir = paths.pgroot.join("pglite"); + if !pglite_dir.exists() { + return None; + } + let pglite_bin_dir = pglite_dir.join("bin"); + let module = pglite_bin_dir.join("pglite.wasi"); + if !module.exists() { + return None; + } + + let share = pglite_dir.join("share").join("postgresql"); + if !share.exists() || !share.join("postgres.bki").exists() { + return None; + } + Some((module, pglite_bin_dir)) +} + +fn ensure_runtime(paths: &PglitePaths) -> Result { + if locate_runtime_module(paths).is_some() { + return Ok(false); + } + + if let Some(parent) = paths.pgroot.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create parent directory {}", parent.display()))?; + } else { + fs::create_dir_all(&paths.pgroot).context("create pgroot dir")?; + } + + if install_runtime_from_tar(paths)? { + locate_runtime_module(paths).ok_or_else(|| { + anyhow!( + "runtime missing: could not locate module under {} after tar install", + paths.pgroot.display() + ) + })?; + return Ok(true); + } + + info!("installing embedded filesystem bundle"); + let bundle = load_fs_bundle()?; + install_fs_bundle(paths, &bundle)?; + install_wasm_binary(paths)?; + + locate_runtime_module(paths).ok_or_else(|| { + anyhow!( + "runtime missing: could not locate module under {} after install", + paths.pgroot.display() + ) + })?; + + Ok(true) +} + +fn install_fs_bundle(paths: &PglitePaths, bundle: &[u8]) -> Result<()> { + for entry in FS_MANIFEST.iter() { + let dest = manifest_entry_dest(paths, &entry.path) + .with_context(|| format!("unsupported manifest path {}", entry.path))?; + + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + + let start = entry.start; + let end = entry.end; + if start > end || end > bundle.len() { + bail!( + "manifest entry {} has invalid bounds {}..{} (bundle len {})", + entry.path, + start, + end, + bundle.len() + ); + } + + if start == end { + fs::File::create(&dest).with_context(|| format!("create file {}", dest.display()))?; + } else { + fs::write(&dest, &bundle[start..end]) + .with_context(|| format!("write {}", dest.display()))?; + } + } + + let password_path = paths.pgroot.join("pglite/password"); + if password_path.exists() { + fs::write(&password_path, b"postgres\n") + .with_context(|| format!("overwrite {}", password_path.display()))?; + } + + Ok(()) +} + +fn runtime_tar_path() -> Option { + if let Ok(path) = std::env::var("PGLITE_OXIDE_RUNTIME_TAR") { + let candidate = PathBuf::from(path); + if candidate.exists() { + return Some(candidate); + } + } + + let tar_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite-wasi.tar.xz"); + if tar_path.exists() { + return Some(tar_path); + } + + None +} + +fn install_runtime_from_tar(paths: &PglitePaths) -> Result { + let Some(tar_path) = runtime_tar_path() else { + return Ok(false); + }; + + info!("installing runtime from tar archive {}", tar_path.display()); + let file = File::open(&tar_path) + .with_context(|| format!("open runtime archive {}", tar_path.display()))?; + + let mut decoder = XzDecoder::new(file); + let mut archive = Archive::new(&mut decoder); + let unpack_target = paths + .pgroot + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| paths.pgroot.clone()); + archive.unpack(&unpack_target).with_context(|| { + format!( + "unpack runtime archive {} into {}", + tar_path.display(), + unpack_target.display() + ) + })?; + + Ok(true) +} + +fn manifest_entry_dest(paths: &PglitePaths, manifest_path: &str) -> Result { + if let Some(rest) = manifest_path.strip_prefix("/tmp/") { + Ok(paths.pgroot.join(rest)) + } else if let Some(rest) = manifest_path.strip_prefix('/') { + Ok(paths.pgroot.join(rest)) + } else { + Err(anyhow!( + "manifest path {} has unknown prefix", + manifest_path + )) + } +} + +fn install_wasm_binary(paths: &PglitePaths) -> Result<()> { + let src = wasm_asset_path(); + if !src.exists() { + bail!("missing wasm asset at {}", src.display()); + } + + let dest = paths.pgroot.join("pglite/bin/pglite.wasi"); + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + + fs::copy(&src, &dest) + .with_context(|| format!("copy {} to {}", src.display(), dest.display()))?; + Ok(()) +} + +fn wasm_asset_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/pglite.wasi") +} + +fn install_extension_reader(paths: &PglitePaths, reader: R) -> Result<()> { + let mut ar = Archive::new(GzDecoder::new(reader)); + let target = paths.pgroot.join("pglite"); + std::fs::create_dir_all(&target) + .with_context(|| format!("create extension target {}", target.display()))?; + ar.unpack(&target) + .with_context(|| format!("unpack extension into {}", target.display()))?; + Ok(()) +} + +pub fn install_extension_archive(paths: &PglitePaths, archive_path: &Path) -> Result<()> { + let file = std::fs::File::open(archive_path) + .with_context(|| format!("open extension archive {}", archive_path.display()))?; + install_extension_reader(paths, file) +} + +pub fn install_extension_bytes(paths: &PglitePaths, bytes: &[u8]) -> Result<()> { + install_extension_reader(paths, std::io::Cursor::new(bytes)) +} + +fn ensure_pgdata(paths: &PglitePaths) -> Result<()> { + if !paths.pgdata.exists() { + fs::create_dir_all(&paths.pgdata).with_context(|| { + format!( + "failed to create initial pgdata directory at {}", + paths.pgdata.display() + ) + })?; + } + Ok(()) +} + +pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> { + if paths.marker_cluster().exists() { + return Ok(()); + } + + ensure_runtime(paths)?; + ensure_pgdata(paths)?; + + let mut pg = PostgresMod::new(paths.clone())?; + pg.ensure_cluster() +} + +#[derive(Debug)] +pub struct InstallOutcome { + pub paths: PglitePaths, + pub unpacked_runtime: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct InstallOptions { + pub ensure_cluster: bool, +} + +impl Default for InstallOptions { + fn default() -> Self { + Self { + ensure_cluster: true, + } + } +} + +#[derive(Debug, Clone)] +pub struct MountInfo { + mount: PathBuf, + paths: PglitePaths, + reused_existing: bool, +} + +impl MountInfo { + pub fn into_paths(self) -> PglitePaths { + self.paths + } + + pub fn mount(&self) -> &Path { + &self.mount + } + + pub fn paths(&self) -> &PglitePaths { + &self.paths + } + + pub fn reused_existing(&self) -> bool { + self.reused_existing + } +} + +pub fn install_default(app_id: (&str, &str, &str)) -> Result { + let paths = PglitePaths::new(app_id)?; + install_into_internal(paths) +} + +pub fn install_into(root: &Path) -> Result { + let paths = PglitePaths::with_root(root); + install_into_internal(paths) +} + +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)) +} + +fn install_into_internal(paths: PglitePaths) -> Result { + let unpacked_runtime = ensure_runtime(&paths)?; + 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)?; + } + Ok(MountInfo { + mount: outcome.paths.pgroot.clone(), + paths: outcome.paths, + reused_existing: !outcome.unpacked_runtime, + }) +} + +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)?; + } + Ok(MountInfo { + mount: outcome.paths.pgroot.clone(), + paths: outcome.paths, + reused_existing: !outcome.unpacked_runtime, + }) +} + +pub fn install_with_options(paths: PglitePaths, options: InstallOptions) -> Result { + let unpacked_runtime = ensure_runtime(&paths)?; + ensure_pgdata(&paths)?; + if options.ensure_cluster && !paths.marker_cluster().exists() { + ensure_cluster(&paths)?; + } + Ok(MountInfo { + mount: paths.pgroot.clone(), + paths, + reused_existing: !unpacked_runtime, + }) +} + +fn template_cluster() -> Result> { + TEMPLATE_CLUSTER + .get_or_init(|| { + build_template_cluster() + .map(Arc::new) + .map_err(|err| format!("{err:#}")) + }) + .clone() + .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)?; + + Ok(TemplateCluster { + root: temp_dir.path().to_path_buf(), + _temp_dir: temp_dir, + }) +} + +fn copy_dir_filtered(src: &Path, dest: &Path) -> Result<()> { + fs::create_dir_all(dest).with_context(|| format!("create directory {}", dest.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()))?; + 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 file_type = entry + .file_type() + .with_context(|| format!("stat {}", src_path.display()))?; + + if file_type.is_dir() { + copy_dir_filtered(&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()) + })?; + } else if file_type.is_symlink() { + copy_symlink(&src_path, &dest_path)?; + } + } + + Ok(()) +} + +fn should_skip_template_entry(file_name: &OsStr) -> bool { + let name = file_name.to_string_lossy(); + name.starts_with(".s.PGSQL.") || name == "postmaster.pid" +} + +#[cfg(unix)] +fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?; + std::os::unix::fs::symlink(&target, dest) + .with_context(|| format!("create symlink {} -> {}", dest.display(), target.display()))?; + Ok(()) +} + +#[cfg(not(unix))] +fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { + let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?; + let target_path = if target.is_absolute() { + target + } else { + src.parent().unwrap_or_else(|| Path::new(".")).join(target) + }; + + if target_path.is_dir() { + copy_dir_filtered(&target_path, dest) + } else { + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + fs::copy(&target_path, dest) + .with_context(|| format!("copy {} to {}", target_path.display(), dest.display()))?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn template_copy_keeps_cluster_files_and_skips_runtime_state() -> Result<()> { + 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")?; + fs::write(pgdata.join("postmaster.pid"), b"stale pid")?; + fs::write(source.path().join(".s.PGSQL.5432"), b"socket")?; + fs::write(source.path().join(".s.PGSQL.5432.lock"), b"lock")?; + + let dest = TempDir::new()?; + copy_dir_filtered(source.path(), dest.path())?; + + assert!(dest.path().join("tmp/pglite/base/PG_VERSION").exists()); + assert!(!dest.path().join("tmp/pglite/base/postmaster.pid").exists()); + assert!(!dest.path().join(".s.PGSQL.5432").exists()); + assert!(!dest.path().join(".s.PGSQL.5432.lock").exists()); + Ok(()) + } +} diff --git a/src/pglite/builder.rs b/src/pglite/builder.rs new file mode 100644 index 00000000..5f830a86 --- /dev/null +++ b/src/pglite/builder.rs @@ -0,0 +1,127 @@ +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::client::Pglite; + +/// Builder for opening persistent or temporary [`Pglite`] databases. +#[derive(Debug, Clone)] +pub struct PgliteBuilder { + target: Option, + template_cache: bool, +} + +#[derive(Debug, Clone)] +enum PgliteTarget { + Path(PathBuf), + AppId { + qualifier: String, + organization: String, + application: String, + }, + Temporary, +} + +impl Default for PgliteBuilder { + fn default() -> Self { + Self { + target: None, + template_cache: true, + } + } +} + +impl PgliteBuilder { + /// Create a builder. Call [`path`](Self::path), [`app_id`](Self::app_id), + /// or [`temporary`](Self::temporary) before [`open`](Self::open). + pub fn new() -> Self { + Self::default() + } + + /// Open a persistent database rooted at `root`. + pub fn path(mut self, root: impl Into) -> Self { + self.target = Some(PgliteTarget::Path(root.into())); + self + } + + /// Open a persistent database under the platform data directory. + pub fn app( + mut self, + qualifier: impl Into, + organization: impl Into, + application: impl Into, + ) -> Self { + self.target = Some(PgliteTarget::AppId { + qualifier: qualifier.into(), + organization: organization.into(), + application: application.into(), + }); + self + } + + /// Open a persistent database under the platform data directory. + pub fn app_id(self, app_id: (&str, &str, &str)) -> Self { + self.app(app_id.0, app_id.1, app_id.2) + } + + /// Open an ephemeral database removed when the instance is dropped. + /// + /// Temporary databases use the process-local template cluster cache by + /// default, avoiding repeated `initdb` work in test suites. + pub fn temporary(mut self) -> Self { + self.target = Some(PgliteTarget::Temporary); + self + } + + /// Control whether temporary databases are cloned from the process-local + /// template cluster cache. + pub fn template_cache(mut self, enabled: bool) -> Self { + self.template_cache = enabled; + self + } + + /// Open an ephemeral database with a fresh `initdb`. + pub fn fresh_temporary(self) -> Self { + self.temporary().template_cache(false) + } + + /// 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) + } + Some(PgliteTarget::AppId { + qualifier, + organization, + application, + }) => { + let outcome = install_default((&qualifier, &organization, &application))?; + Pglite::new(outcome.paths) + } + Some(PgliteTarget::Temporary) => self.open_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()? + } else { + let temp_dir = TempDir::new()?; + let outcome = install_into(temp_dir.path())?; + (temp_dir, outcome) + }; + + let mut instance = Pglite::new(outcome.paths)?; + instance.attach_temp_dir(temp_dir); + Ok(instance) + } +} diff --git a/src/pglite/client.rs b/src/pglite/client.rs new file mode 100644 index 00000000..b29a6547 --- /dev/null +++ b/src/pglite/client.rs @@ -0,0 +1,934 @@ +use anyhow::{Context, Result, anyhow, bail}; +use serde_json::Value; +use std::collections::HashMap; +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; +use crate::pglite::builder::PgliteBuilder; +use crate::pglite::errors::PgliteError; +use crate::pglite::interface::{ + DataTransferContainer, DescribeQueryParam, DescribeQueryResult, DescribeResultField, + ExecProtocolOptions, ExecProtocolResult, ParserMap, QueryOptions, Results, Serializer, + SerializerMap, TypeParser, +}; +use crate::pglite::parse::{parse_describe_statement_results, parse_results}; +use crate::pglite::postgres_mod::PostgresMod; +use crate::pglite::transport::Transport; +use crate::pglite::types::{ + DEFAULT_PARSERS, DEFAULT_SERIALIZERS, TEXT, parse_array_text, serialize_array_value, +}; +use crate::protocol::messages::{BackendMessage, DatabaseError}; +use crate::protocol::parser::Parser as ProtocolParser; +use crate::protocol::serializer::{BindConfig, BindValue, PortalTarget, Serialize}; + +type ChannelCallback = Arc; +type GlobalCallback = Arc; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ListenerHandle { + channel: String, + normalized_channel: String, + id: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GlobalListenerHandle { + id: u64, +} + +impl ListenerHandle { + pub fn channel(&self) -> &str { + &self.channel + } + + pub fn id(&self) -> u64 { + self.id + } +} + +impl GlobalListenerHandle { + pub fn id(&self) -> u64 { + self.id + } +} + +struct ChannelListener { + id: u64, + callback: ChannelCallback, +} + +struct GlobalListener { + id: u64, + callback: GlobalCallback, +} + +/// Primary entry point for interacting with the embedded Postgres runtime. +pub struct Pglite { + pg: PostgresMod, + _temp_dir: Option, + transport: Transport, + parser: ProtocolParser, + serializers: SerializerMap, + parsers: ParserMap, + array_types_initialized: bool, + in_transaction: bool, + ready: bool, + closing: bool, + closed: bool, + blob_input_provided: bool, + notify_listeners: HashMap>, + global_notify_listeners: Vec, + next_listener_id: u64, + next_global_listener_id: u64, +} + +impl Pglite { + /// Create a builder for opening persistent or temporary PGlite databases. + pub fn builder() -> PgliteBuilder { + PgliteBuilder::new() + } + + /// Open a persistent PGlite database rooted at `root`, installing and initializing it if needed. + pub fn open(root: impl AsRef) -> Result { + Self::builder().path(root.as_ref().to_path_buf()).open() + } + + /// Open a persistent PGlite database under the platform data directory for `app_id`. + pub fn open_app(app_id: (&str, &str, &str)) -> Result { + Self::builder().app_id(app_id).open() + } + + /// Create an ephemeral PGlite database whose files are removed when the instance is dropped. + pub fn temporary() -> Result { + Self::builder().temporary().open() + } + + /// 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, + }; + + instance.exec_internal("SET search_path TO public;", None)?; + instance.init_array_types(true)?; + Ok(instance) + } + + /// Execute a SQL query using the extended protocol. + pub fn query( + &mut self, + sql: &str, + params: &[Value], + options: Option<&QueryOptions>, + ) -> Result { + self.check_ready()?; + self.init_array_types(false)?; + + self.query_internal(sql, params, options) + } + + fn query_internal( + &mut self, + sql: &str, + params: &[Value], + options: Option<&QueryOptions>, + ) -> Result { + let default_options = QueryOptions::default(); + let query_opts = options.unwrap_or(&default_options); + + self.handle_blob_input(query_opts.blob.as_ref())?; + + let params_snapshot: Vec = params.to_vec(); + let options_snapshot = options.cloned(); + let mut collected_messages: Vec = Vec::new(); + + let mut exec_opts = ExecProtocolOptions::no_sync(); + exec_opts.on_notice = query_opts.on_notice.clone(); + exec_opts.data_transfer_container = query_opts.data_transfer_container; + + let result: Result<()> = (|| { + let param_types = if query_opts.param_types.is_empty() { + &[] as &[i32] + } else { + &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); + collected_messages.extend(messages); + + let 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())?; + 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) => { + let enriched = PgliteError::new(db_err, sql, params_snapshot, options_snapshot); + return Err(enriched.into()); + } + Err(err) => { + return Err(err.context(format!("failed to execute extended query: {sql}"))); + } + } + } + + self.finish_query(collected_messages, options) + } + + /// Return `true` if the instance is ready for new work. + pub fn is_ready(&self) -> bool { + self.ready && !self.closing && !self.closed + } + + /// Return the host-side runtime and data-directory paths backing this instance. + #[doc(hidden)] + pub fn paths(&self) -> &PglitePaths { + self.pg.paths() + } + + pub(crate) fn attach_temp_dir(&mut self, temp_dir: TempDir) { + self._temp_dir = Some(temp_dir); + } + + /// Return `true` if the instance has already been closed. + pub fn is_closed(&self) -> bool { + self.closed + } + + /// Shut down the embedded Postgres runtime. + pub fn close(&mut self) -> Result<()> { + if self.closed { + return Ok(()); + } + if self.closing { + bail!("Pglite is closing"); + } + + 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); + self.sync_to_fs() + }; + + self.closing = false; + if result.is_ok() { + self.closed = true; + self.ready = false; + self.notify_listeners.clear(); + self.global_notify_listeners.clear(); + } + result + } + + /// 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) + } + + fn exec_internal(&mut self, sql: &str, options: Option<&QueryOptions>) -> Result> { + let options_snapshot = options.cloned(); + let default_options = QueryOptions::default(); + let exec_opts_ref = options.unwrap_or(&default_options); + let mut exec_opts = ExecProtocolOptions::no_sync(); + exec_opts.on_notice = exec_opts_ref.on_notice.clone(); + exec_opts.data_transfer_container = exec_opts_ref.data_transfer_container; + + self.handle_blob_input(exec_opts_ref.blob.as_ref())?; + + 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::() { + Ok(db_err) => { + let enriched = PgliteError::new(db_err, sql, Vec::new(), options_snapshot); + return Err(enriched.into()); + } + Err(err) => { + return Err(err.context(format!("failed to execute simple query: {sql}"))); + } + } + } + + self.finish_exec(collected_messages, options) + } + + /// Register a listener for `LISTEN channel`. Returns a handle that can be used to unlisten. + pub fn listen(&mut self, channel: &str, callback: F) -> Result + where + F: Fn(&str) + Send + Sync + 'static, + { + self.check_ready()?; + self.init_array_types(false)?; + + let normalized = to_postgres_name(channel); + 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)?; + } + + let callback: ChannelCallback = Arc::new(callback); + let entry = self.notify_listeners.entry(normalized.clone()).or_default(); + let id = self.next_listener_id; + self.next_listener_id = self.next_listener_id.wrapping_add(1); + entry.push(ChannelListener { id, callback }); + + Ok(ListenerHandle { + channel: channel.to_string(), + normalized_channel: normalized, + id, + }) + } + + /// Remove a listener corresponding to the provided handle. + pub fn unlisten(&mut self, handle: ListenerHandle) -> Result<()> { + if let Some(listeners) = self.notify_listeners.get_mut(&handle.normalized_channel) { + 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)?; + } + } + Ok(()) + } + + /// Remove all listeners for the specified channel. + pub fn unlisten_channel(&mut self, channel: &str) -> Result<()> { + let normalized = to_postgres_name(channel); + if self.notify_listeners.remove(&normalized).is_some() { + self.exec_internal(&format!("UNLISTEN {}", channel), None)?; + } + Ok(()) + } + + /// Register a global notification callback. + pub fn on_notification(&mut self, callback: F) -> GlobalListenerHandle + where + F: Fn(&str, &str) + Send + Sync + 'static, + { + let id = self.next_global_listener_id; + self.next_global_listener_id = self.next_global_listener_id.wrapping_add(1); + let callback: GlobalCallback = Arc::new(callback); + self.global_notify_listeners + .push(GlobalListener { id, callback }); + GlobalListenerHandle { id } + } + + /// Deregister a previously registered global notification callback. + pub fn off_notification(&mut self, handle: GlobalListenerHandle) { + self.global_notify_listeners + .retain(|listener| listener.id != handle.id); + } + + /// Describe the parameter and result metadata for a SQL query. + pub fn describe_query( + &mut self, + sql: &str, + 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); + + let options_snapshot = options.cloned(); + let mut exec_opts = ExecProtocolOptions::no_sync(); + exec_opts.on_notice = query_opts.on_notice.clone(); + exec_opts.data_transfer_container = query_opts.data_transfer_container; + + let mut describe_messages: Vec = Vec::new(); + + let result: Result<()> = (|| { + let param_types = if query_opts.param_types.is_empty() { + &[] as &[i32] + } else { + &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())?; + 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) => { + let enriched = PgliteError::new(db_err, sql, Vec::new(), options_snapshot); + return Err(enriched.into()); + } + Err(err) => { + return Err(err.context(format!("failed to describe query: {sql}"))); + } + } + } + + let param_type_ids = parse_describe_statement_results(&describe_messages); + let query_params = param_type_ids + .into_iter() + .map(|oid| DescribeQueryParam { + data_type_id: oid, + serializer: self.serializers.get(&oid).cloned(), + }) + .collect(); + + let result_fields = describe_messages + .iter() + .find_map(|msg| match msg { + BackendMessage::RowDescription(desc) => Some( + desc.fields + .iter() + .map(|field| DescribeResultField { + name: field.name.clone(), + data_type_id: field.data_type_id, + parser: self.parsers.get(&field.data_type_id).cloned(), + }) + .collect::>(), + ), + _ => None, + }) + .unwrap_or_default(); + + Ok(DescribeQueryResult { + query_params, + result_fields, + }) + } + + /// Run a closure within an SQL transaction (`BEGIN .. COMMIT/ROLLBACK`). + pub fn transaction(&mut self, mut callback: F) -> Result + where + F: FnMut(&mut Transaction<'_>) -> Result, + { + self.check_ready()?; + self.init_array_types(false)?; + + // Begin transaction + self.run_exec_command("BEGIN")?; + self.in_transaction = true; + + let mut tx = Transaction::new(self); + let callback_result = callback(&mut tx); + + let txn_result = match callback_result { + Ok(value) => { + if !tx.closed { + tx.commit_internal()?; + } + Ok(value) + } + Err(err) => { + if !tx.closed { + tx.rollback_internal()?; + } + Err(err) + } + }; + + self.in_transaction = false; + txn_result + } + + /// Flush runtime writes to the underlying filesystem. Currently a no-op on the host. + 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(()) + } + + fn prepare_bind_values( + &self, + params: &[Value], + data_type_ids: &[i32], + options: &QueryOptions, + ) -> Result> { + if params.is_empty() { + return Ok(Vec::new()); + } + + let mut values = Vec::with_capacity(params.len()); + let overrides = if options.serializers.is_empty() { + None + } else { + Some(&options.serializers) + }; + + for (idx, value) in params.iter().enumerate() { + if value.is_null() { + values.push(BindValue::Null); + continue; + } + + let oid = data_type_ids.get(idx).copied().unwrap_or(TEXT); + let serializer = overrides + .and_then(|map| map.get(&oid)) + .or_else(|| self.serializers.get(&oid)); + + let serialized = match serializer { + Some(func) => func(value).with_context(|| { + format!("failed to serialize parameter {idx} using OID {oid}") + })?, + None => self.default_serialize_value(value), + }; + + values.push(BindValue::Text(serialized)); + } + + Ok(values) + } + + fn default_serialize_value(&self, value: &Value) -> String { + Self::default_serialize_value_static(value) + } + + pub(crate) fn default_serialize_value_static(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Number(num) => num.to_string(), + Value::Bool(flag) => { + if *flag { + "t".to_string() + } else { + "f".to_string() + } + } + _ => value.to_string(), + } + } + + fn finish_query( + &mut self, + messages: Vec, + options: Option<&QueryOptions>, + ) -> Result { + let blob = self.get_written_blob()?; + self.cleanup_blob()?; + if !self.in_transaction { + self.sync_to_fs()?; + } + let parsed = parse_results(&messages, &self.parsers, options, blob); + parsed + .into_iter() + .next() + .ok_or_else(|| anyhow!("query returned no result sets")) + } + + fn finish_exec( + &mut self, + messages: Vec, + options: Option<&QueryOptions>, + ) -> Result> { + let blob = self.get_written_blob()?; + self.cleanup_blob()?; + if !self.in_transaction { + self.sync_to_fs()?; + } + Ok(parse_results(&messages, &self.parsers, options, blob)) + } + + fn exec_protocol( + &mut self, + message: &[u8], + options: ExecProtocolOptions, + ) -> Result { + let ExecProtocolOptions { + sync_to_fs, + throw_on_error, + on_notice, + data_transfer_container, + } = options; + + let data = self.exec_protocol_raw(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(()) + }) { + match err.downcast::() { + Ok(db_err) => { + self.parser = ProtocolParser::new(); + return Err(anyhow!(db_err)); + } + Err(err) => return Err(err), + } + } + + 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) { + for listener in listeners { + (listener.callback)(¬e.payload); + } + } + for listener in &self.global_notify_listeners { + (listener.callback)(¬e.channel, ¬e.payload); + } + } + } + + Ok(ExecProtocolResult { messages }) + } + + fn exec_protocol_raw( + &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)?; + if 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(()); + } + + 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"))?; + + 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); + + if element_oid == 0 || array_oid == 0 { + continue; + } + + let element_parser = self.parsers.get(&element_oid).cloned(); + let element_serializer = self.serializers.get(&element_oid).cloned(); + + 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); + + 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(()) + }; + + if let Err(err) = result { + self.array_types_initialized = prev; + Err(err) + } else { + Ok(()) + } + } + + fn run_exec_command(&mut self, sql: &str) -> Result<()> { + self.exec_internal(sql, None).map(|_| ()) + } + + fn handle_blob_input(&mut self, blob: Option<&Vec>) -> Result<()> { + let path = self.dev_blob_path(); + if let Some(bytes) = blob { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).with_context(|| { + format!("failed to create blob directory {}", parent.display()) + })?; + } + fs::write(&path, bytes) + .with_context(|| format!("write blob input to {}", path.display()))?; + self.blob_input_provided = true; + } else { + self.blob_input_provided = false; + let _ = fs::remove_file(&path); + } + Ok(()) + } + + fn dev_blob_path(&self) -> PathBuf { + self.pg.paths().pgroot.join("dev/blob") + } + + fn cleanup_blob(&mut self) -> Result<()> { + Ok(()) + } + + fn get_written_blob(&mut self) -> Result>> { + let path = self.dev_blob_path(); + + if self.blob_input_provided { + self.blob_input_provided = false; + let _ = fs::remove_file(&path); + return Ok(None); + } + + match fs::read(&path) { + Ok(data) => { + self.blob_input_provided = false; + let _ = fs::remove_file(&path); + if data.is_empty() { + Ok(None) + } else { + Ok(Some(data)) + } + } + Err(err) => { + if err.kind() == io::ErrorKind::NotFound { + self.blob_input_provided = false; + Ok(None) + } else { + Err(err).with_context(|| format!("read blob output from {}", path.display())) + } + } + } + } + + fn check_ready(&self) -> Result<()> { + if self.closing { + bail!("Pglite instance is closing"); + } + if self.closed { + bail!("Pglite instance is closed"); + } + if !self.ready { + bail!("Pglite instance is not ready"); + } + Ok(()) + } +} + +impl Drop for Pglite { + fn drop(&mut self) { + if !self.closed { + let _ = self.close(); + } + } +} + +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() + } +} + +fn value_to_i32(value: Option<&Value>) -> Option { + match value? { + Value::Number(number) => number.as_i64().map(|value| value as i32), + Value::String(string) => string.parse::().ok(), + _ => None, + } +} + +/// Transaction handle used within [`Pglite::transaction`]. +pub struct Transaction<'a> { + client: &'a mut Pglite, + closed: bool, +} + +impl<'a> Transaction<'a> { + fn new(client: &'a mut Pglite) -> Self { + Self { + client, + closed: false, + } + } + + fn commit_internal(&mut self) -> Result<()> { + self.ensure_open()?; + self.client.exec_internal("COMMIT", None)?; + self.closed = true; + Ok(()) + } + + fn rollback_internal(&mut self) -> Result<()> { + self.ensure_open()?; + self.client.exec_internal("ROLLBACK", None)?; + self.closed = true; + Ok(()) + } + + fn ensure_open(&self) -> Result<()> { + if self.closed { + bail!("transaction is already closed"); + } + Ok(()) + } + + pub fn query( + &mut self, + sql: &str, + params: &[Value], + options: Option<&QueryOptions>, + ) -> Result { + self.ensure_open()?; + self.client.query_internal(sql, params, options) + } + + pub fn exec(&mut self, sql: &str, options: Option<&QueryOptions>) -> Result> { + self.ensure_open()?; + self.client.exec_internal(sql, options) + } + + pub fn commit(&mut self) -> Result<()> { + self.commit_internal() + } + + pub fn rollback(&mut self) -> Result<()> { + self.rollback_internal() + } + + pub fn is_closed(&self) -> bool { + self.closed + } + + pub fn closed(&self) -> bool { + self.closed + } +} diff --git a/src/pglite/errors.rs b/src/pglite/errors.rs new file mode 100644 index 00000000..13e61232 --- /dev/null +++ b/src/pglite/errors.rs @@ -0,0 +1,71 @@ +use std::error::Error; +use std::fmt; + +use serde_json::Value; + +use crate::pglite::interface::QueryOptions; +use crate::protocol::messages::DatabaseError; + +/// Rich error type that mirrors the TypeScript `PGliteError` by carrying the +/// original database error along with query context. +pub struct PgliteError { + source: DatabaseError, + query: String, + params: Vec, + query_options: Option, +} + +impl PgliteError { + pub fn new( + source: DatabaseError, + query: impl Into, + params: Vec, + query_options: Option, + ) -> Self { + Self { + source, + query: query.into(), + params, + query_options, + } + } + + pub fn database_error(&self) -> &DatabaseError { + &self.source + } + + pub fn query(&self) -> &str { + &self.query + } + + pub fn params(&self) -> &[Value] { + &self.params + } + + pub fn query_options(&self) -> Option<&QueryOptions> { + self.query_options.as_ref() + } +} + +impl fmt::Display for PgliteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.source) + } +} + +impl fmt::Debug for PgliteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PgliteError") + .field("source", &self.source) + .field("query", &self.query) + .field("params", &self.params) + .field("has_query_options", &self.query_options.is_some()) + .finish() + } +} + +impl Error for PgliteError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&self.source) + } +} diff --git a/src/pglite/interface.rs b/src/pglite/interface.rs new file mode 100644 index 00000000..6842ef56 --- /dev/null +++ b/src/pglite/interface.rs @@ -0,0 +1,110 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use serde_json::Value; + +use crate::protocol::messages::{BackendMessage, NoticeMessage}; + +/// Row output mode matching the TypeScript `RowMode`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RowMode { + Object, + Array, +} + +/// Debug logging level used by the runtime. Matches the TypeScript enum values. +pub type DebugLevel = u8; + +/// Parser function used to convert textual Postgres values into richer Rust values. +/// Mirrors the signature of the TypeScript parser callbacks. +pub type TypeParser = Arc Value + Send + Sync>; +pub type Serializer = Arc anyhow::Result + Send + Sync>; +pub type NoticeCallback = Arc; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataTransferContainer { + Cma, + File, +} + +pub type ParserMap = HashMap; +pub type SerializerMap = HashMap; + +#[derive(Default, Clone)] +pub struct QueryOptions { + pub row_mode: Option, + pub parsers: ParserMap, + pub serializers: SerializerMap, + pub blob: Option>, + pub param_types: Vec, + pub on_notice: Option, + pub data_transfer_container: Option, +} + +#[derive(Debug, Clone)] +pub struct FieldInfo { + pub name: String, + pub data_type_id: i32, +} + +#[derive(Debug, Clone)] +pub struct Results { + pub rows: Vec, + pub fields: Vec, + pub affected_rows: Option, + pub blob: Option>, +} + +#[derive(Clone)] +pub struct ExecProtocolOptions { + pub sync_to_fs: bool, + pub throw_on_error: bool, + pub on_notice: Option, + pub data_transfer_container: Option, +} + +impl ExecProtocolOptions { + pub const fn no_sync() -> Self { + Self { + sync_to_fs: false, + throw_on_error: true, + on_notice: None, + data_transfer_container: None, + } + } +} + +impl Default for ExecProtocolOptions { + fn default() -> Self { + Self { + sync_to_fs: true, + throw_on_error: true, + on_notice: None, + data_transfer_container: None, + } + } +} + +#[derive(Debug, Clone)] +pub struct ExecProtocolResult { + pub messages: Vec, +} + +#[derive(Clone)] +pub struct DescribeQueryParam { + pub data_type_id: i32, + pub serializer: Option, +} + +#[derive(Clone)] +pub struct DescribeResultField { + pub name: String, + pub data_type_id: i32, + pub parser: Option, +} + +#[derive(Clone)] +pub struct DescribeQueryResult { + pub query_params: Vec, + pub result_fields: Vec, +} diff --git a/src/pglite/mod.rs b/src/pglite/mod.rs new file mode 100644 index 00000000..d1893a76 --- /dev/null +++ b/src/pglite/mod.rs @@ -0,0 +1,29 @@ +pub(crate) mod base; +pub(crate) mod builder; +pub(crate) mod client; +pub(crate) mod errors; +pub(crate) mod interface; +pub(crate) mod parse; +pub(crate) mod postgres_mod; +pub(crate) mod proxy; +pub(crate) mod server; +pub(crate) mod templating; +pub(crate) mod transport; +pub(crate) mod types; + +pub use base::{ + InstallOptions, InstallOutcome, MountInfo, PglitePaths, ensure_cluster, install_and_init, + install_and_init_in, install_default, install_extension_archive, install_extension_bytes, + install_into, install_with_options, +}; +pub use builder::PgliteBuilder; +pub use client::{GlobalListenerHandle, ListenerHandle, Pglite, Transaction}; +pub use errors::PgliteError; +pub use interface::{ + DataTransferContainer, DebugLevel, DescribeQueryParam, DescribeQueryResult, + DescribeResultField, FieldInfo, NoticeCallback, ParserMap, QueryOptions, Results, RowMode, + Serializer, SerializerMap, TypeParser, +}; +pub use proxy::PgliteProxy; +pub use server::{PgliteServer, PgliteServerBuilder}; +pub use templating::{QueryTemplate, TemplatedQuery, format_query, quote_identifier}; diff --git a/src/pglite/parse.rs b/src/pglite/parse.rs new file mode 100644 index 00000000..fd77b757 --- /dev/null +++ b/src/pglite/parse.rs @@ -0,0 +1,130 @@ +use std::collections::HashMap; + +use serde_json::Value; + +use crate::pglite::interface::{FieldInfo, ParserMap, QueryOptions, Results, RowMode}; +use crate::pglite::types::ParserLookup; +use crate::protocol::messages::{ + BackendMessage, CommandCompleteMessage, DataRowMessage, RowDescriptionMessage, +}; + +pub fn parse_results( + messages: &[BackendMessage], + default_parsers: &ParserMap, + options: Option<&QueryOptions>, + blob: Option>, +) -> Vec { + let mut result_sets: Vec = Vec::new(); + let mut current_fields: Vec = Vec::new(); + let mut current_rows: Vec = Vec::new(); + let mut affected_rows = 0usize; + + let empty_parsers = HashMap::new(); + let (row_mode, parsers_override) = options + .map(|opts| (opts.row_mode, &opts.parsers)) + .unwrap_or((None, &empty_parsers)); + + let parser_lookup = ParserLookup::new(default_parsers, parsers_override); + + for message in messages { + match message { + BackendMessage::RowDescription(desc) => { + current_fields = map_fields(desc); + } + BackendMessage::DataRow(row) => { + if current_fields.is_empty() { + continue; + } + let row_value = map_row(row, ¤t_fields, &parser_lookup, row_mode); + current_rows.push(row_value); + } + BackendMessage::CommandComplete(cmd) => { + affected_rows += retrieve_row_count(cmd); + result_sets.push(Results { + rows: std::mem::take(&mut current_rows), + fields: current_fields.clone(), + affected_rows: Some(affected_rows), + blob: blob.clone(), + }); + current_fields.clear(); + } + _ => {} + } + } + + if result_sets.is_empty() { + result_sets.push(Results { + rows: Vec::new(), + fields: Vec::new(), + affected_rows: Some(0), + blob, + }) + } + + result_sets +} + +pub fn parse_describe_statement_results(messages: &[BackendMessage]) -> Vec { + messages + .iter() + .find_map(|msg| match msg { + BackendMessage::ParameterDescription(desc) => Some(desc.data_type_ids.clone()), + _ => None, + }) + .unwrap_or_default() +} + +fn map_fields(desc: &RowDescriptionMessage) -> Vec { + desc.fields + .iter() + .map(|field| FieldInfo { + name: field.name.clone(), + data_type_id: field.data_type_id, + }) + .collect() +} + +fn map_row( + row: &DataRowMessage, + fields: &[FieldInfo], + parsers: &ParserLookup, + row_mode: Option, +) -> Value { + match row_mode { + Some(RowMode::Array) => { + let values: Vec = row + .fields + .iter() + .zip(fields.iter()) + .map(|(value, field)| parse_cell(value.as_deref(), field.data_type_id, parsers)) + .collect(); + Value::Array(values) + } + _ => { + let mut map = serde_json::Map::with_capacity(fields.len()); + for (value, field) in row.fields.iter().zip(fields.iter()) { + let parsed = parse_cell(value.as_deref(), field.data_type_id, parsers); + map.insert(field.name.clone(), parsed); + } + Value::Object(map) + } + } +} + +fn parse_cell(value: Option<&str>, type_id: i32, parsers: &ParserLookup) -> Value { + match value { + None => Value::Null, + Some(text) => parsers.apply(text, type_id), + } +} + +fn retrieve_row_count(msg: &CommandCompleteMessage) -> usize { + let parts: Vec<&str> = msg.text.split(' ').collect(); + match parts.first().copied() { + Some("INSERT") => parts.get(2).and_then(|v| v.parse().ok()).unwrap_or(0), + Some("UPDATE") | Some("DELETE") | Some("COPY") | Some("MERGE") => { + parts.get(1).and_then(|v| v.parse().ok()).unwrap_or(0) + } + _ => 0, + } +} diff --git a/src/pglite/postgres_mod.rs b/src/pglite/postgres_mod.rs new file mode 100644 index 00000000..68849a79 --- /dev/null +++ b/src/pglite/postgres_mod.rs @@ -0,0 +1,433 @@ +use anyhow::{Context, Result, anyhow, bail, ensure}; +use getrandom::fill as fill_random; +use std::collections::HashMap; +use std::collections::hash_map::DefaultHasher; +use std::fmt; +use std::fs; +use std::hash::{Hash, Hasher}; +use std::path::Path; +use std::sync::{LazyLock, Mutex}; +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; + +const WASM_PREFIX: &str = "/tmp/pglite"; +const PGDATA_DIR: &str = "/tmp/pglite/base"; + +pub struct PostgresMod { + _engine: Engine, + store: Store, + _instance: Instance, + memory: Memory, + exports: Exports, + paths: PglitePaths, + transport: TransportMode, + wire_enabled: bool, +} + +enum TransportMode { + Cma { + buffer_addr: usize, + buffer_len: usize, + }, + File, +} + +struct State { + wasi: WasiP1Ctx, +} + +static ENGINE: LazyLock = LazyLock::new(build_engine); +static MODULE_CACHE: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ModuleCacheKey { + len: usize, + hash: u64, +} + +fn with_wasmtime_context( + result: std::result::Result, + context: impl fmt::Display, +) -> Result { + result.map_err(|err| anyhow!("{context}: {err}")) +} + +fn build_engine() -> Engine { + let mut config = Config::new(); + + config.cranelift_opt_level(OptLevel::None); + + #[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}"); + } + } + + Engine::new(&config).expect("failed to create Wasmtime engine") +} + +fn module_cache_key(bytes: &[u8]) -> ModuleCacheKey { + let mut hasher = DefaultHasher::new(); + bytes.hash(&mut hasher); + ModuleCacheKey { + len: bytes.len(), + hash: hasher.finish(), + } +} + +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(); + + if let Some(module) = MODULE_CACHE + .lock() + .map_err(|err| anyhow!("module cache lock poisoned: {err}"))? + .get(&key) + .cloned() + { + return Ok((engine, module)); + } + + let module = with_wasmtime_context( + Module::from_binary(&engine, &bytes), + format!("failed to compile {}", module_path.display()), + )?; + MODULE_CACHE + .lock() + .map_err(|err| anyhow!("module cache lock poisoned: {err}"))? + .insert(key, module.clone()); + + Ok((engine, module)) +} + +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, +} + +impl PostgresMod { + pub fn new(paths: PglitePaths) -> Result { + let module_path = paths.pgroot.join("pglite/bin/pglite.wasi"); + + if !module_path.exists() { + return Err(anyhow!( + "pglite.wasi binary not found at {}", + module_path.display() + )); + } + + let (engine, module) = load_module(&module_path)?; + + 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", + )?; + + let wasi = build_wasi_ctx(&paths)?; + let mut store = Store::new(&engine, State { wasi }); + + let instance = with_wasmtime_context( + linker.instantiate(&mut store, &module), + "failed to instantiate pglite module", + )?; + + let memory = instance + .get_memory(&mut store, "memory") + .context("pglite module is missing exported memory")?; + + 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}"); + } + + let exports = Exports::load(&mut store, &instance)?; + + 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", + )?; + let len = with_wasmtime_context( + exports.get_buffer_size.call(&mut store, channel_id), + "call _get_buffer_size", + )?; + 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 + }; + + Ok(Self { + _engine: engine, + store, + _instance: instance, + memory, + exports, + paths, + transport, + wire_enabled: false, + }) + } + + pub fn paths(&self) -> &PglitePaths { + &self.paths + } + + 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)); + } + + if !self.paths.is_cluster_initialized() { + return Err(anyhow!( + "_pgl_initdb returned success but PG_VERSION is missing" + )); + } + + Ok(()) + } + + pub fn buffer_addr(&self) -> Option { + match self.transport { + TransportMode::Cma { buffer_addr, .. } => Some(buffer_addr), + TransportMode::File => None, + } + } + + pub fn buffer_len(&self) -> Option { + match self.transport { + TransportMode::Cma { buffer_len, .. } => Some(buffer_len), + TransportMode::File => None, + } + } + + 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())) + } + + 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())) + } + + 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(()) + } + + pub fn interactive_one(&mut self) -> Result<()> { + self.exports + .interactive_one + .call(&mut self.store, ()) + .map_err(|err| anyhow!("call _interactive_one: {err}"))?; + Ok(()) + } + + pub fn interactive_read(&mut self) -> Result { + self.exports + .interactive_read + .call(&mut self.store, ()) + .map_err(|err| anyhow!("call _interactive_read: {err}")) + } + + 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; + Ok(()) + } + + pub fn backend(&mut self) -> Result<()> { + self.exports + .pgl_backend + .call(&mut self.store, ()) + .map_err(|err| anyhow!("call _pgl_backend: {err}"))?; + 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) + } + + 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"], + )?; + 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, + }) + } +} + +fn build_wasi_ctx(paths: &PglitePaths) -> Result { + ensure_runtime_dirs(paths)?; + + let mut builder = WasiCtxBuilder::new(); + + 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()))?; + + 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()))?; + } + 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(), + ) + .map_err(|err| { + anyhow!( + "failed to preopen {} as /tmp/pglite/base: {err}", + paths.pgdata.display() + ) + })?; + + 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()))?; + + Ok(builder.build_p1()) +} + +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()))?; + } + let urandom = dev_path.join("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()))?; + } + + if !paths.pgdata.exists() { + std::fs::create_dir_all(&paths.pgdata) + .with_context(|| format!("failed to create {}", paths.pgdata.display()))?; + } + + Ok(()) +} diff --git a/src/pglite/proxy.rs b/src/pglite/proxy.rs new file mode 100644 index 00000000..9883c831 --- /dev/null +++ b/src/pglite/proxy.rs @@ -0,0 +1,525 @@ +use anyhow::{Context, Result, anyhow, bail}; +use std::io::{ErrorKind, Read, Write}; +use std::net::{TcpListener, ToSocketAddrs}; +#[cfg(unix)] +use std::os::unix::net::UnixListener; +use std::path::{Path, PathBuf}; +use std::sync::{ + Arc, + atomic::{AtomicBool, 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; + +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; + +/// 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 affected the old Tokio-based example. +#[derive(Debug, Clone)] +pub struct PgliteProxy { + root: Arc, +} + +impl PgliteProxy { + /// Create a proxy that stores the PGlite runtime and cluster under `root`. + pub fn new(root: impl Into) -> Self { + Self { + root: Arc::new(root.into()), + } + } + + /// Return the root directory used for runtime installation and cluster data. + pub fn root(&self) -> &Path { + &self.root + } + + /// Serve a TCP listener forever. Connections are handled one at a time. + pub fn serve_tcp(&self, addr: A) -> Result<()> + where + A: ToSocketAddrs, + { + let listener = TcpListener::bind(addr).context("bind TCP proxy listener")?; + self.serve_tcp_listener(listener) + } + + /// 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)?; + } + Ok(()) + } + + pub(crate) fn serve_tcp_listener_until_ready( + &self, + listener: TcpListener, + 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)); + } + }; + 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"), + } + } + + Ok(()) + } + + /// Accept and handle one TCP connection. Intended for tests and supervised embedding. + pub fn accept_tcp_once(&self, listener: &TcpListener) -> Result<()> { + self.accept_tcp_connections(listener, 1) + } + + /// 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)?; + } + Ok(()) + } + + /// Serve a Unix-domain socket forever. Connections are handled one at a time. + #[cfg(unix)] + pub fn serve_unix(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + if path.exists() { + std::fs::remove_file(path) + .with_context(|| format!("remove stale socket {}", path.display()))?; + } + let listener = UnixListener::bind(path) + .with_context(|| format!("bind Unix proxy socket {}", path.display()))?; + self.serve_unix_listener(listener) + } + + /// 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)?; + } + Ok(()) + } + + #[cfg(unix)] + pub(crate) fn serve_unix_listener_until_ready( + &self, + listener: UnixListener, + 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)); + } + }; + 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"), + } + } + + Ok(()) + } + + /// Accept and handle one Unix-domain socket connection. + #[cfg(unix)] + pub fn accept_unix_once(&self, listener: &UnixListener) -> Result<()> { + self.accept_unix_connections(listener, 1) + } + + /// 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)?; + } + Ok(()) + } + + fn handle_stream(&self, mut stream: S, backend: &mut WireBackend) -> Result<()> + where + S: Read + Write, + { + let mut reader = FrontendMessageReader::default(); + let mut buffer = [0u8; 64 * 1024]; + let mut protocol_batch = Vec::new(); + + loop { + let read = stream.read(&mut buffer).context("read frontend socket")?; + if read == 0 { + flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + break; + } + + 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")?; + } + FrontendMessageKind::CancelRequest => { + flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + close_after_flush = true; + } + FrontendMessageKind::Terminate => { + flush_protocol_batch(&mut protocol_batch, backend, &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")?; + } + FrontendMessageKind::Protocol => { + let flush_after = should_flush_protocol_batch(&message); + protocol_batch.extend_from_slice(&message); + if flush_after { + flush_protocol_batch(&mut protocol_batch, backend, &mut stream)?; + } + } + } + } + stream.flush().context("flush frontend socket")?; + if close_after_flush { + break; + } + } + + backend.rollback_connection_state(); + Ok(()) + } +} + +struct WireBackend { + pg: PostgresMod, + transport: Transport, +} + +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 }) + } + + fn send(&mut self, message: &[u8]) -> Result> { + self.transport.send(&mut self.pg, message, None) + } + + fn rollback_connection_state(&mut self) { + let _ = self.send(&simple_query_message("ROLLBACK")); + } +} + +#[derive(Default)] +struct FrontendMessageReader { + buffer: Vec, +} + +impl FrontendMessageReader { + 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(&self.buffer)? else { + break; + }; + let message = self.buffer.drain(..message_len).collect(); + messages.push(message); + } + + Ok(messages) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FrontendMessageKind { + Protocol, + Startup, + SslOrGssRequest, + CancelRequest, + Terminate, +} + +fn frontend_message_len(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 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)) +} + +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 => FrontendMessageKind::SslOrGssRequest, + CANCEL_REQUEST_CODE => FrontendMessageKind::CancelRequest, + PROTOCOL_3 => FrontendMessageKind::Startup, + other => bail!("unsupported startup/control packet code {other}"), + }); + } + + if message[0] == b'X' { + return Ok(FrontendMessageKind::Terminate); + } + + Ok(FrontendMessageKind::Protocol) +} + +fn should_flush_protocol_batch(message: &[u8]) -> bool { + matches!(message.first(), Some(b'Q' | b'S' | b'H')) +} + +fn flush_protocol_batch( + protocol_batch: &mut Vec, + backend: &mut WireBackend, + stream: &mut S, +) -> Result<()> +where + S: Write, +{ + 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")?; + } + + 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()); +} + +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_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()); +} + +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 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 +} + +#[cfg(test)] +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(()) + } + + #[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(()) + } + + #[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 + ); + 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 + ); + 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")); + } +} diff --git a/src/pglite/server.rs b/src/pglite/server.rs new file mode 100644 index 00000000..c2a6c9bb --- /dev/null +++ b/src/pglite/server.rs @@ -0,0 +1,319 @@ +use std::net::{SocketAddr, TcpListener}; +#[cfg(unix)] +use std::os::unix::net::UnixListener; +use std::path::{Path, PathBuf}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc::{Receiver, sync_channel}, +}; +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::proxy::PgliteProxy; + +/// A supervised local PostgreSQL socket backed by one embedded PGlite runtime. +/// +/// This is the compatibility entry point for code that expects a PostgreSQL URL, +/// such as `tokio-postgres`, SQLx, or tools that speak the wire protocol. The +/// server owns one embedded backend, so downstream pools should use a single +/// connection. +#[derive(Debug)] +pub struct PgliteServer { + root: PathBuf, + _temp_dir: Option, + endpoint: ServerEndpoint, + shutdown: Arc, + handle: Option>>, +} + +#[derive(Debug, Clone)] +enum ServerEndpoint { + Tcp(SocketAddr), + #[cfg(unix)] + Unix(PathBuf), +} + +impl PgliteServer { + /// Build a local PGlite server. The default is a cached temporary database + /// served on `127.0.0.1:0`. + pub fn builder() -> PgliteServerBuilder { + PgliteServerBuilder::new() + } + + /// Start a cached temporary database on a random local TCP port. + pub fn temporary_tcp() -> Result { + Self::builder().temporary().start() + } + + /// Return the root directory used for runtime files and cluster data. + pub fn root(&self) -> &Path { + &self.root + } + + /// Return the bound TCP address, if this server is using TCP. + pub fn tcp_addr(&self) -> Option { + match self.endpoint { + ServerEndpoint::Tcp(addr) => Some(addr), + #[cfg(unix)] + ServerEndpoint::Unix(_) => None, + } + } + + /// Return the Unix-domain socket path, if this server is using UDS. + #[cfg(unix)] + pub fn socket_path(&self) -> Option<&Path> { + match &self.endpoint { + ServerEndpoint::Tcp(_) => None, + ServerEndpoint::Unix(path) => Some(path), + } + } + + /// 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), + #[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", + host.display(), + port + ) + } + } + } + + /// Request shutdown and wait for the listener thread to exit. + /// + /// Close database clients before calling this method. The current proxy owns + /// one blocking backend connection at a time, so an open client can keep the + /// worker thread busy until it disconnects. + pub fn shutdown(mut self) -> Result<()> { + self.stop() + } + + fn stop(&mut self) -> Result<()> { + self.shutdown.store(true, Ordering::SeqCst); + if let Some(handle) = self.handle.take() { + handle + .join() + .map_err(|_| anyhow!("pglite server thread panicked"))??; + } + Ok(()) + } +} + +impl Drop for PgliteServer { + fn drop(&mut self) { + self.shutdown.store(true, Ordering::SeqCst); + } +} + +/// Builder for [`PgliteServer`]. +#[derive(Debug, Clone)] +pub struct PgliteServerBuilder { + root: ServerRoot, + endpoint: ServerEndpointConfig, +} + +#[derive(Debug, Clone)] +enum ServerRoot { + Temporary { template_cache: bool }, + Path(PathBuf), +} + +#[derive(Debug, Clone)] +enum ServerEndpointConfig { + Tcp(SocketAddr), + #[cfg(unix)] + Unix(PathBuf), +} + +impl Default for PgliteServerBuilder { + fn default() -> Self { + Self { + root: ServerRoot::Temporary { + template_cache: true, + }, + endpoint: ServerEndpointConfig::Tcp(SocketAddr::from(([127, 0, 0, 1], 0))), + } + } +} + +impl PgliteServerBuilder { + /// Create a builder. Defaults to a cached temporary database on + /// `127.0.0.1:0`. + pub fn new() -> Self { + Self::default() + } + + /// Serve a persistent database rooted at `root`. + pub fn path(mut self, root: impl Into) -> Self { + self.root = ServerRoot::Path(root.into()); + self + } + + /// Serve a temporary database cloned from the process-local template cache. + pub fn temporary(mut self) -> Self { + self.root = ServerRoot::Temporary { + template_cache: true, + }; + self + } + + /// Serve a temporary database initialized without the template cache. + pub fn fresh_temporary(mut self) -> Self { + self.root = ServerRoot::Temporary { + template_cache: false, + }; + self + } + + /// Bind the server to a TCP address. + pub fn tcp(mut self, addr: SocketAddr) -> Self { + self.endpoint = ServerEndpointConfig::Tcp(addr); + self + } + + /// Bind the server to a Unix-domain socket path. + #[cfg(unix)] + pub fn unix(mut self, path: impl Into) -> Self { + self.endpoint = ServerEndpointConfig::Unix(path.into()); + 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)) + } + } + }; + + let shutdown = Arc::new(AtomicBool::new(false)); + let proxy = PgliteProxy::new(root.clone()); + + let (endpoint, handle) = match self.endpoint { + ServerEndpointConfig::Tcp(addr) => start_tcp(proxy, addr, shutdown.clone())?, + #[cfg(unix)] + ServerEndpointConfig::Unix(path) => start_unix(proxy, path, shutdown.clone())?, + }; + + Ok(PgliteServer { + root, + _temp_dir: temp_dir, + endpoint, + shutdown, + handle: Some(handle), + }) + } +} + +fn start_tcp( + proxy: PgliteProxy, + 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 (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)?; + Ok((ServerEndpoint::Tcp(addr), handle)) +} + +fn tcp_connection_uri(addr: SocketAddr) -> String { + match addr { + SocketAddr::V4(addr) => { + format!( + "postgresql://postgres@{}:{}/template1?sslmode=disable", + addr.ip(), + addr.port() + ) + } + SocketAddr::V6(addr) => { + format!( + "postgresql://postgres@[{}]:{}/template1?sslmode=disable", + addr.ip(), + addr.port() + ) + } + } +} + +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, +{ + thread::Builder::new() + .name(name.to_string()) + .spawn(f) + .with_context(|| format!("spawn {name} worker"))? + .join() + .map_err(|_| anyhow!("{name} worker panicked"))? +} + +#[cfg(unix)] +fn start_unix( + proxy: PgliteProxy, + 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 listener = 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)?; + Ok((endpoint, handle)) +} + +fn wait_until_ready(ready_rx: &Receiver>) -> Result<()> { + ready_rx + .recv() + .context("PGlite server thread exited before reporting readiness")? +} + +#[cfg(unix)] +fn parse_unix_socket_port(path: &Path) -> Option { + let name = path.file_name()?.to_str()?; + name.strip_prefix(".s.PGSQL.")?.parse().ok() +} diff --git a/src/pglite/templating.rs b/src/pglite/templating.rs new file mode 100644 index 00000000..a00d27ce --- /dev/null +++ b/src/pglite/templating.rs @@ -0,0 +1,133 @@ +use anyhow::{Result, anyhow}; +use regex::Regex; +use serde_json::Value; +use std::sync::LazyLock; + +use crate::pglite::client::Pglite; +use crate::pglite::interface::QueryOptions; +use crate::pglite::types::TEXT; + +#[derive(Debug, Clone)] +pub struct TemplatedQuery { + pub query: String, + pub params: Vec, +} + +#[derive(Debug, Default, Clone)] +pub struct QueryTemplate { + sql: String, + params: Vec, +} + +impl QueryTemplate { + pub fn new() -> Self { + Self::default() + } + + pub fn push_sql(&mut self, sql: impl AsRef) { + self.sql.push_str(sql.as_ref()); + } + + pub fn push_raw(&mut self, sql: impl AsRef) { + self.push_sql(sql); + } + + pub fn push_identifier(&mut self, identifier: &str) { + self.sql.push_str("e_identifier(identifier)); + } + + pub fn push_param(&mut self, value: Value) { + let placeholder = format!("${}", self.params.len() + 1); + self.sql.push_str(&placeholder); + self.params.push(value); + } + + pub fn build(self) -> TemplatedQuery { + TemplatedQuery { + query: self.sql, + params: self.params, + } + } +} + +static DOLLAR_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\$(\d+)").expect("invalid regex")); + +pub fn quote_identifier(ident: &str) -> String { + let escaped = ident.replace('"', "\"\""); + format!("\"{}\"", escaped) +} + +pub fn format_query(pg: &mut Pglite, query: &str, params: &[Value]) -> Result { + if params.is_empty() { + return Ok(query.to_string()); + } + + let described = pg.describe_query(query, None)?; + let data_type_ids = described + .query_params + .iter() + .map(|param| param.data_type_id) + .collect::>(); + + let formatted = DOLLAR_RE + .replace_all(query, |caps: ®ex::Captures| format!("%{}L", &caps[1])) + .to_string(); + + let mut sql = String::from("SELECT format($1"); + for idx in 0..params.len() { + sql.push_str(", $"); + sql.push_str(&(idx as i32 + 2).to_string()); + } + sql.push_str(") AS query"); + + let mut arguments: Vec = Vec::with_capacity(params.len() + 1); + arguments.push(Value::String(formatted)); + arguments.extend(params.iter().cloned()); + + let mut param_types = Vec::with_capacity(arguments.len()); + param_types.push(TEXT); + param_types + .extend((0..params.len()).map(|idx| data_type_ids.get(idx).copied().unwrap_or(TEXT))); + let options = QueryOptions { + param_types, + ..QueryOptions::default() + }; + + let results = pg.query(&sql, &arguments, Some(&options))?; + let row = results + .rows + .first() + .ok_or_else(|| anyhow!("format query returned no rows"))?; + if let Value::Object(map) = row + && let Some(Value::String(formatted)) = map.get("query") + { + return Ok(formatted.clone()); + } + + Err(anyhow!("unexpected format query result")) +} + +#[cfg(test)] +mod tests { + use super::{QueryTemplate, quote_identifier}; + use serde_json::json; + + #[test] + fn template_builder_adds_params() { + let mut tpl = QueryTemplate::new(); + tpl.push_sql("SELECT "); + tpl.push_identifier("foo"); + tpl.push_sql(" WHERE id = "); + tpl.push_param(json!(42)); + let built = tpl.build(); + assert_eq!(built.query, "SELECT \"foo\" WHERE id = $1"); + assert_eq!(built.params.len(), 1); + } + + #[test] + fn quote_identifier_escapes_quotes() { + assert_eq!(quote_identifier("Foo"), "\"Foo\""); + assert_eq!(quote_identifier("a\"b"), "\"a\"\"b\""); + } +} diff --git a/src/pglite/transport.rs b/src/pglite/transport.rs new file mode 100644 index 00000000..e504e8e9 --- /dev/null +++ b/src/pglite/transport.rs @@ -0,0 +1,130 @@ +use anyhow::{Context, Result, bail, ensure}; +use std::fs; +use std::thread; +use std::time::{Duration, Instant}; + +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. +pub enum Transport { + Cma { + buffer_addr: usize, + buffer_len: usize, + }, + #[allow(dead_code)] + File, +} + +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 send( + &self, + pg: &mut PostgresMod, + 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"); + } + thread::sleep(Duration::from_millis(2)); + } +} diff --git a/src/pglite/types.rs b/src/pglite/types.rs new file mode 100644 index 00000000..34585c9b --- /dev/null +++ b/src/pglite/types.rs @@ -0,0 +1,412 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use std::sync::LazyLock; + +use anyhow::{Result, anyhow}; +use serde_json::{Value, json}; + +use super::interface::{ParserMap, Serializer, SerializerMap, TypeParser}; + +macro_rules! const_oid { + ($name:ident = $value:expr) => { + pub const $name: i32 = $value; + }; +} + +const_oid!(BOOL = 16); +const_oid!(BYTEA = 17); +const_oid!(CHAR = 18); +const_oid!(INT8 = 20); +const_oid!(INT2 = 21); +const_oid!(INT4 = 23); +const_oid!(TEXT = 25); +const_oid!(OID = 26); +const_oid!(JSON = 114); +const_oid!(FLOAT4 = 700); +const_oid!(FLOAT8 = 701); +const_oid!(DATE = 1082); +const_oid!(TIMESTAMP = 1114); +const_oid!(TIMESTAMPTZ = 1184); +const_oid!(NUMERIC = 1700); +const_oid!(UUID = 2950); +const_oid!(JSONB = 3802); + +pub static DEFAULT_PARSERS: LazyLock = LazyLock::new(build_default_parsers); +pub static DEFAULT_SERIALIZERS: LazyLock = LazyLock::new(build_default_serializers); + +pub struct ParserLookup<'a> { + defaults: &'a ParserMap, + overrides: &'a ParserMap, +} + +impl<'a> ParserLookup<'a> { + pub fn new(defaults: &'a ParserMap, overrides: &'a ParserMap) -> Self { + Self { + defaults, + overrides, + } + } + + pub fn apply(&self, text: &str, type_id: i32) -> Value { + let parser = self + .overrides + .get(&type_id) + .or_else(|| self.defaults.get(&type_id)); + if let Some(parser) = parser { + parser(text, type_id) + } else { + json!(text) + } + } +} + +fn array_delimiter(typarray: i32) -> char { + if typarray == 1020 { ';' } else { ',' } +} + +pub fn serialize_array_value( + value: &Value, + element_serializer: Option, + typarray: i32, +) -> Result { + match value { + Value::Array(items) => { + if items.is_empty() { + return Ok("{}".to_string()); + } + + let delimiter = array_delimiter(typarray); + let mut parts = Vec::with_capacity(items.len()); + for item in items { + match item { + Value::Null => parts.push("null".to_string()), + Value::Array(_) => { + parts.push(serialize_array_value( + item, + element_serializer.clone(), + typarray, + )?); + } + _ => { + let raw = if let Some(serializer) = element_serializer.as_ref() { + serializer(item)? + } else { + value_to_string(item) + }; + let escaped = raw.replace('\\', "\\\\").replace('"', "\\\""); + parts.push(format!("\"{}\"", escaped)); + } + } + } + let joined = parts.join(&delimiter.to_string()); + Ok(format!("{{{}}}", joined)) + } + Value::Null => Ok("null".to_string()), + _ => Ok(value_to_string(value)), + } +} + +fn value_to_string(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => { + if *b { + "t".to_string() + } else { + "f".to_string() + } + } + Value::Null => "null".to_string(), + Value::Array(_) => value.to_string(), + _ => value.to_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, +) -> Value { + let mut state = ArrayParserState::default(); + let result = parse_array_loop( + text, + &mut state, + 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()), + } +} + +fn parse_array_loop( + text: &str, + state: &mut ArrayParserState, + element_parser: Option<&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); + } + } 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; + } + state.last = state.index + 1; + } else { + state.buffer.push(ch); + } + } 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)); + } + } + 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; + } + + 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)); + } + } + + Value::Array(values) +} + +fn apply_element_parser(slice: &str, parser: Option<&TypeParser>, element_type_id: i32) -> Value { + if let Some(p) = parser { + p(slice, element_type_id) + } else if slice.eq_ignore_ascii_case("NULL") { + Value::Null + } else { + Value::String(slice.to_string()) + } +} + +fn build_default_parsers() -> ParserMap { + let mut map: ParserMap = HashMap::new(); + + map.insert( + TEXT, + Arc::new(|value: &str, _| json!(value.to_string())) as TypeParser, + ); + map.insert(CHAR, Arc::new(|value: &str, _| json!(value.to_string()))); + + map.insert(INT2, Arc::new(|value: &str, _| parse_int(value))); + map.insert(INT4, Arc::new(|value: &str, _| parse_int(value))); + map.insert(INT8, Arc::new(|value: &str, _| parse_bigint(value))); + map.insert(OID, Arc::new(|value: &str, _| parse_int(value))); + map.insert(NUMERIC, Arc::new(|value: &str, _| parse_numeric(value))); + + map.insert(FLOAT4, Arc::new(|value: &str, _| parse_float(value))); + map.insert(FLOAT8, Arc::new(|value: &str, _| parse_float(value))); + + map.insert(BOOL, Arc::new(|value: &str, _| json!(value == "t"))); + + map.insert(JSON, Arc::new(|value: &str, _| parse_json(value))); + map.insert(JSONB, Arc::new(|value: &str, _| parse_json(value))); + + map.insert(BYTEA, Arc::new(|value: &str, _| parse_bytea(value))); + + map.insert(UUID, Arc::new(|value: &str, _| json!(value.to_string()))); + + map.insert( + TIMESTAMP, + Arc::new(|value: &str, _| json!(value.to_string())), + ); + map.insert( + TIMESTAMPTZ, + Arc::new(|value: &str, _| json!(value.to_string())), + ); + map.insert(DATE, Arc::new(|value: &str, _| json!(value.to_string()))); + + map +} + +fn build_default_serializers() -> SerializerMap { + let mut map: SerializerMap = HashMap::new(); + + map.insert( + TEXT, + Arc::new(|value: &Value| serialize_string(value)) as Serializer, + ); + map.insert(CHAR, Arc::new(|value: &Value| serialize_string(value))); + + map.insert(INT2, Arc::new(|value: &Value| serialize_number(value))); + map.insert(INT4, Arc::new(|value: &Value| serialize_number(value))); + map.insert(INT8, Arc::new(|value: &Value| serialize_number(value))); + map.insert(OID, Arc::new(|value: &Value| serialize_number(value))); + map.insert(NUMERIC, Arc::new(|value: &Value| serialize_number(value))); + map.insert(FLOAT4, Arc::new(|value: &Value| serialize_number(value))); + map.insert(FLOAT8, Arc::new(|value: &Value| serialize_number(value))); + + map.insert(BOOL, Arc::new(|value: &Value| serialize_bool(value))); + map.insert(JSON, Arc::new(|value: &Value| serialize_json(value))); + map.insert(JSONB, Arc::new(|value: &Value| serialize_json(value))); + map.insert(BYTEA, Arc::new(|value: &Value| serialize_bytea(value))); + map.insert(UUID, Arc::new(|value: &Value| serialize_string(value))); + map.insert(TIMESTAMP, Arc::new(|value: &Value| serialize_string(value))); + map.insert( + TIMESTAMPTZ, + Arc::new(|value: &Value| serialize_string(value)), + ); + map.insert(DATE, Arc::new(|value: &Value| serialize_string(value))); + + map +} + +fn parse_int(value: &str) -> Value { + match value.parse::() { + Ok(int) => json!(int), + Err(_) => json!(value.to_string()), + } +} + +fn parse_bigint(value: &str) -> Value { + match value.parse::() { + Ok(int) => json!(int), + Err(_) => json!(value.to_string()), + } +} + +fn parse_numeric(value: &str) -> Value { + serde_json::Number::from_str(value) + .map(Value::Number) + .unwrap_or_else(|_| json!(value.to_string())) +} + +fn parse_float(value: &str) -> Value { + match value.parse::() { + Ok(float) => json!(float), + Err(_) => json!(value.to_string()), + } +} + +fn parse_json(value: &str) -> Value { + serde_json::from_str(value).unwrap_or_else(|_| json!(value.to_string())) +} + +fn parse_bytea(value: &str) -> Value { + value + .strip_prefix("\\x") + .and_then(|hex| hex::decode(hex).ok()) + .map(Value::from) + .unwrap_or_else(|| json!(value.to_string())) +} + +fn serialize_string(value: &Value) -> Result { + match value { + Value::String(s) => Ok(s.clone()), + other => Ok(other.to_string()), + } +} + +fn serialize_number(value: &Value) -> Result { + match value { + Value::Number(num) => Ok(num.to_string()), + Value::String(s) => Ok(s.clone()), + other => Err(anyhow!("cannot serialize value {other} as number")), + } +} + +fn serialize_bool(value: &Value) -> Result { + match value { + Value::Bool(b) => Ok(if *b { "t" } else { "f" }.to_string()), + Value::Number(num) => Ok(if num.as_i64().unwrap_or(0) != 0 { + "t" + } else { + "f" + } + .to_string()), + Value::String(s) => Ok(match s.as_ref() { + "true" | "t" | "1" => "t".to_string(), + _ => "f".to_string(), + }), + other => Err(anyhow!("cannot serialize value {other} as boolean")), + } +} + +fn serialize_json(value: &Value) -> Result { + if let Some(value) = value.as_str() { + Ok(value.to_string()) + } else { + serde_json::to_string(value).map_err(|err| anyhow!(err)) + } +} + +fn serialize_bytea(value: &Value) -> Result { + match value { + Value::String(s) => Ok(s.clone()), + Value::Array(arr) => { + let bytes: Vec = arr + .iter() + .filter_map(|v| v.as_u64().map(|n| n as u8)) + .collect(); + Ok(format!("\\x{}", hex::encode(bytes))) + } + Value::Null => Ok("\\x".to_string()), + _ => Err(anyhow!("unsupported value for bytea serialization")), + } +} diff --git a/src/protocol/buffer_reader.rs b/src/protocol/buffer_reader.rs new file mode 100644 index 00000000..a3a03da2 --- /dev/null +++ b/src/protocol/buffer_reader.rs @@ -0,0 +1,72 @@ +use anyhow::{Result, ensure}; + +#[derive(Debug, Default)] +pub struct BufferReader<'a> { + buffer: &'a [u8], + offset: usize, +} + +impl<'a> BufferReader<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn set_buffer(&mut self, offset: usize, buffer: &'a [u8]) { + self.offset = offset; + self.buffer = buffer; + } + + fn take_slice(&mut self, len: usize) -> Result<&'a [u8]> { + ensure!( + self.offset + len <= self.buffer.len(), + "buffer underflow (need {len} bytes, have {})", + self.buffer.len().saturating_sub(self.offset) + ); + let slice = &self.buffer[self.offset..self.offset + len]; + self.offset += len; + Ok(slice) + } + + pub fn int16(&mut self) -> Result { + let bytes = self.take_slice(2)?; + Ok(i16::from_be_bytes([bytes[0], bytes[1]])) + } + + pub fn byte(&mut self) -> Result { + let bytes = self.take_slice(1)?; + Ok(bytes[0]) + } + + pub fn int32(&mut self) -> Result { + let bytes = self.take_slice(4)?; + Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } + + pub fn string(&mut self, length: usize) -> Result { + let bytes = self.take_slice(length)?; + let text = std::str::from_utf8(bytes)?; + Ok(text.to_owned()) + } + + pub fn cstring(&mut self) -> Result { + let start = self.offset; + loop { + let next = self + .buffer + .get(self.offset) + .copied() + .ok_or_else(|| anyhow::anyhow!("unterminated cstring"))?; + self.offset += 1; + if next == 0 { + let slice = &self.buffer[start..self.offset - 1]; + let text = std::str::from_utf8(slice)?; + return Ok(text.to_owned()); + } + } + } + + pub fn bytes(&mut self, length: usize) -> Result> { + let bytes = self.take_slice(length)?; + Ok(bytes.to_vec()) + } +} diff --git a/src/protocol/buffer_writer.rs b/src/protocol/buffer_writer.rs new file mode 100644 index 00000000..f291f38c --- /dev/null +++ b/src/protocol/buffer_writer.rs @@ -0,0 +1,96 @@ +use crate::protocol::string_utils::byte_length_utf8; + +const DEFAULT_SIZE: usize = 256; + +#[derive(Clone, Debug)] +pub struct BufferWriter { + buffer: Vec, + size: usize, + offset: usize, +} + +impl Default for BufferWriter { + fn default() -> Self { + Self::new(DEFAULT_SIZE) + } +} + +impl BufferWriter { + pub fn new(size: usize) -> Self { + let buffer = vec![0; size]; + // reserve header space (code + len = 5 bytes) + Self { + buffer, + size, + offset: 5, + } + } + + fn ensure_capacity(&mut self, additional: usize) { + if self.buffer.len() - self.offset < additional { + let old_len = self.buffer.len(); + // Exponential growth ~1.5x as in TS implementation. + let mut new_len = old_len + (old_len >> 1) + additional; + if new_len == old_len { + new_len += additional; + } + self.buffer.resize(new_len, 0); + } + } + + fn write_be_bytes(&mut self, bytes: &[u8]) { + let len = bytes.len(); + self.ensure_capacity(len); + self.buffer[self.offset..self.offset + len].copy_from_slice(bytes); + self.offset += len; + } + + pub fn add_int32(&mut self, value: i32) -> &mut Self { + self.write_be_bytes(&value.to_be_bytes()); + self + } + + pub fn add_int16(&mut self, value: i16) -> &mut Self { + self.write_be_bytes(&value.to_be_bytes()); + self + } + + pub fn add_cstring(&mut self, value: &str) -> &mut Self { + if !value.is_empty() { + self.add_string(value); + } + self.add_bytes(&[0]); + self + } + + pub fn add_string(&mut self, value: &str) -> &mut Self { + let length = byte_length_utf8(value); + self.ensure_capacity(length); + let end = self.offset + length; + self.buffer[self.offset..end].copy_from_slice(value.as_bytes()); + self.offset = end; + self + } + + pub fn add_bytes(&mut self, bytes: &[u8]) -> &mut Self { + self.write_be_bytes(bytes); + self + } + + fn join(&mut self, code: Option) -> Vec { + if let Some(code) = code { + self.buffer[0] = code; + let length = (self.offset - 1) as i32; + self.buffer[1..5].copy_from_slice(&length.to_be_bytes()); + } + let start = if code.is_some() { 0 } else { 5 }; + self.buffer[start..self.offset].to_vec() + } + + pub fn flush(&mut self, code: Option) -> Vec { + let result = self.join(code); + self.buffer = vec![0; self.size]; + self.offset = 5; + result + } +} diff --git a/src/protocol/messages.rs b/src/protocol/messages.rs new file mode 100644 index 00000000..9d62f72f --- /dev/null +++ b/src/protocol/messages.rs @@ -0,0 +1,471 @@ +use std::fmt; + +use anyhow::Result; + +use crate::protocol::types::Mode; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MessageName { + ParseComplete, + BindComplete, + CloseComplete, + NoData, + PortalSuspended, + ReplicationStart, + EmptyQuery, + CopyDone, + CopyData, + RowDescription, + ParameterDescription, + ParameterStatus, + BackendKeyData, + Notification, + ReadyForQuery, + CommandComplete, + DataRow, + CopyInResponse, + CopyOutResponse, + AuthenticationOk, + AuthenticationMD5Password, + AuthenticationCleartextPassword, + AuthenticationSasl, + AuthenticationSaslContinue, + AuthenticationSaslFinal, + Error, + Notice, +} + +impl fmt::Display for MessageName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use MessageName::*; + let name = match self { + ParseComplete => "parseComplete", + BindComplete => "bindComplete", + CloseComplete => "closeComplete", + NoData => "noData", + PortalSuspended => "portalSuspended", + ReplicationStart => "replicationStart", + EmptyQuery => "emptyQuery", + CopyDone => "copyDone", + CopyData => "copyData", + RowDescription => "rowDescription", + ParameterDescription => "parameterDescription", + ParameterStatus => "parameterStatus", + BackendKeyData => "backendKeyData", + Notification => "notification", + ReadyForQuery => "readyForQuery", + CommandComplete => "commandComplete", + DataRow => "dataRow", + CopyInResponse => "copyInResponse", + CopyOutResponse => "copyOutResponse", + AuthenticationOk => "authenticationOk", + AuthenticationMD5Password => "authenticationMD5Password", + AuthenticationCleartextPassword => "authenticationCleartextPassword", + AuthenticationSasl => "authenticationSASL", + AuthenticationSaslContinue => "authenticationSASLContinue", + AuthenticationSaslFinal => "authenticationSASLFinal", + Error => "error", + Notice => "notice", + }; + write!(f, "{name}") + } +} + +#[derive(Debug, Clone)] +pub enum BackendMessage { + ParseComplete { length: usize }, + BindComplete { length: usize }, + CloseComplete { length: usize }, + NoData { length: usize }, + PortalSuspended { length: usize }, + ReplicationStart { length: usize }, + EmptyQuery { length: usize }, + CopyDone { length: usize }, + ReadyForQuery(ReadyForQueryMessage), + CommandComplete(CommandCompleteMessage), + DataRow(DataRowMessage), + RowDescription(RowDescriptionMessage), + ParameterDescription(ParameterDescriptionMessage), + ParameterStatus(ParameterStatusMessage), + BackendKeyData(BackendKeyDataMessage), + Notification(NotificationResponseMessage), + CopyResponse(CopyResponse), + CopyData(CopyDataMessage), + Authentication(AuthenticationMessage), + Error(DatabaseError), + Notice(NoticeMessage), +} + +impl BackendMessage { + pub fn name(&self) -> MessageName { + use BackendMessage::*; + match self { + ParseComplete { .. } => MessageName::ParseComplete, + BindComplete { .. } => MessageName::BindComplete, + CloseComplete { .. } => MessageName::CloseComplete, + NoData { .. } => MessageName::NoData, + PortalSuspended { .. } => MessageName::PortalSuspended, + ReplicationStart { .. } => MessageName::ReplicationStart, + EmptyQuery { .. } => MessageName::EmptyQuery, + CopyDone { .. } => MessageName::CopyDone, + ReadyForQuery(_) => MessageName::ReadyForQuery, + CommandComplete(_) => MessageName::CommandComplete, + DataRow(_) => MessageName::DataRow, + RowDescription(_) => MessageName::RowDescription, + ParameterDescription(_) => MessageName::ParameterDescription, + ParameterStatus(_) => MessageName::ParameterStatus, + BackendKeyData(_) => MessageName::BackendKeyData, + Notification(_) => MessageName::Notification, + CopyResponse(resp) => match resp.name { + MessageName::CopyInResponse => MessageName::CopyInResponse, + MessageName::CopyOutResponse => MessageName::CopyOutResponse, + _ => resp.name, + }, + CopyData(_) => MessageName::CopyData, + Authentication(auth) => auth.name(), + Error(_) => MessageName::Error, + Notice(_) => MessageName::Notice, + } + } + + pub fn length(&self) -> usize { + use BackendMessage::*; + match self { + ParseComplete { length } + | BindComplete { length } + | CloseComplete { length } + | NoData { length } + | PortalSuspended { length } + | ReplicationStart { length } + | EmptyQuery { length } + | CopyDone { length } => *length, + ReadyForQuery(msg) => msg.length, + CommandComplete(msg) => msg.length, + DataRow(msg) => msg.length, + RowDescription(msg) => msg.length, + ParameterDescription(msg) => msg.length, + ParameterStatus(msg) => msg.length, + BackendKeyData(msg) => msg.length, + Notification(msg) => msg.length, + CopyResponse(msg) => msg.length, + CopyData(msg) => msg.length, + Authentication(msg) => msg.length(), + Error(msg) => msg.length, + Notice(msg) => msg.length, + } + } +} + +#[derive(Debug, Clone)] +pub struct ReadyForQueryMessage { + pub length: usize, + pub status: u8, +} + +#[derive(Debug, Clone)] +pub struct CommandCompleteMessage { + pub length: usize, + pub text: String, +} + +#[derive(Debug, Clone)] +pub struct CopyDataMessage { + pub length: usize, + pub chunk: Vec, +} + +#[derive(Debug, Clone)] +pub struct CopyResponse { + pub length: usize, + pub name: MessageName, + pub binary: bool, + pub column_types: Vec, +} + +#[derive(Debug, Clone)] +pub struct Field { + pub name: String, + pub table_id: i32, + pub column_id: i16, + pub data_type_id: i32, + pub data_type_size: i16, + pub data_type_modifier: i32, + pub format: Mode, +} + +#[derive(Debug, Clone)] +pub struct RowDescriptionMessage { + pub length: usize, + pub fields: Vec, +} + +#[derive(Debug, Clone)] +pub struct ParameterDescriptionMessage { + pub length: usize, + pub data_type_ids: Vec, +} + +#[derive(Debug, Clone)] +pub struct ParameterStatusMessage { + pub length: usize, + pub parameter_name: String, + pub parameter_value: String, +} + +#[derive(Debug, Clone)] +pub struct BackendKeyDataMessage { + pub length: usize, + pub process_id: i32, + pub secret_key: i32, +} + +#[derive(Debug, Clone)] +pub struct NotificationResponseMessage { + pub length: usize, + pub process_id: i32, + pub channel: String, + pub payload: String, +} + +#[derive(Debug, Clone)] +pub struct CommandTag(pub String); + +#[derive(Debug, Clone)] +pub struct DataRowMessage { + pub length: usize, + pub fields: Vec>, +} + +pub trait NoticeOrErrorFields { + fn apply_fields(&mut self, fields: &std::collections::HashMap); +} + +#[derive(Debug, Clone)] +pub struct NoticeMessage { + pub length: usize, + pub message: Option, + pub severity: Option, + pub code: Option, + pub detail: Option, + pub hint: Option, + pub position: Option, + pub internal_position: Option, + pub internal_query: Option, + pub r#where: Option, + pub schema: Option, + pub table: Option, + pub column: Option, + pub data_type: Option, + pub constraint: Option, + pub file: Option, + pub line: Option, + pub routine: Option, +} + +impl NoticeMessage { + pub fn new(length: usize, message: Option) -> Self { + Self { + length, + message, + severity: None, + code: None, + detail: None, + hint: None, + position: None, + internal_position: None, + internal_query: None, + r#where: None, + schema: None, + table: None, + column: None, + data_type: None, + constraint: None, + file: None, + line: None, + routine: None, + } + } +} + +impl NoticeOrErrorFields for NoticeMessage { + fn apply_fields(&mut self, fields: &std::collections::HashMap) { + self.severity = fields.get("S").cloned(); + self.code = fields.get("C").cloned(); + self.detail = fields.get("D").cloned(); + self.hint = fields.get("H").cloned(); + self.position = fields.get("P").cloned(); + self.internal_position = fields.get("p").cloned(); + self.internal_query = fields.get("q").cloned(); + self.r#where = fields.get("W").cloned(); + self.schema = fields.get("s").cloned(); + self.table = fields.get("t").cloned(); + self.column = fields.get("c").cloned(); + self.data_type = fields.get("d").cloned(); + self.constraint = fields.get("n").cloned(); + self.file = fields.get("F").cloned(); + self.line = fields.get("L").cloned(); + self.routine = fields.get("R").cloned(); + } +} + +#[derive(Debug, Clone)] +pub struct DatabaseError { + pub length: usize, + pub message: String, + pub severity: Option, + pub code: Option, + pub detail: Option, + pub hint: Option, + pub position: Option, + pub internal_position: Option, + pub internal_query: Option, + pub r#where: Option, + pub schema: Option, + pub table: Option, + pub column: Option, + pub data_type: Option, + pub constraint: Option, + pub file: Option, + pub line: Option, + pub routine: Option, +} + +impl DatabaseError { + pub fn new(length: usize, message: String) -> Self { + Self { + length, + message, + severity: None, + code: None, + detail: None, + hint: None, + position: None, + internal_position: None, + internal_query: None, + r#where: None, + schema: None, + table: None, + column: None, + data_type: None, + constraint: None, + file: None, + line: None, + routine: None, + } + } +} + +impl std::fmt::Display for DatabaseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for DatabaseError {} + +impl NoticeOrErrorFields for DatabaseError { + fn apply_fields(&mut self, fields: &std::collections::HashMap) { + self.severity = fields.get("S").cloned(); + self.code = fields.get("C").cloned(); + self.detail = fields.get("D").cloned(); + self.hint = fields.get("H").cloned(); + self.position = fields.get("P").cloned(); + self.internal_position = fields.get("p").cloned(); + self.internal_query = fields.get("q").cloned(); + self.r#where = fields.get("W").cloned(); + self.schema = fields.get("s").cloned(); + self.table = fields.get("t").cloned(); + self.column = fields.get("c").cloned(); + self.data_type = fields.get("d").cloned(); + self.constraint = fields.get("n").cloned(); + self.file = fields.get("F").cloned(); + self.line = fields.get("L").cloned(); + self.routine = fields.get("R").cloned(); + } +} + +#[derive(Debug, Clone)] +pub struct AuthenticationOk { + pub length: usize, +} + +#[derive(Debug, Clone)] +pub struct AuthenticationCleartextPassword { + pub length: usize, +} + +#[derive(Debug, Clone)] +pub struct AuthenticationMD5Password { + pub length: usize, + pub salt: Vec, +} + +#[derive(Debug, Clone)] +pub struct AuthenticationSasl { + pub length: usize, + pub mechanisms: Vec, +} + +#[derive(Debug, Clone)] +pub struct AuthenticationSaslContinue { + pub length: usize, + pub data: String, +} + +#[derive(Debug, Clone)] +pub struct AuthenticationSaslFinal { + pub length: usize, + pub data: String, +} + +#[derive(Debug, Clone)] +pub enum AuthenticationMessage { + Ok(AuthenticationOk), + Cleartext(AuthenticationCleartextPassword), + Md5(AuthenticationMD5Password), + Sasl(AuthenticationSasl), + SaslContinue(AuthenticationSaslContinue), + SaslFinal(AuthenticationSaslFinal), +} + +impl AuthenticationMessage { + pub fn name(&self) -> MessageName { + use AuthenticationMessage::*; + match self { + Ok(_) => MessageName::AuthenticationOk, + Cleartext(_) => MessageName::AuthenticationCleartextPassword, + Md5(_) => MessageName::AuthenticationMD5Password, + Sasl(_) => MessageName::AuthenticationSasl, + SaslContinue(_) => MessageName::AuthenticationSaslContinue, + SaslFinal(_) => MessageName::AuthenticationSaslFinal, + } + } + + pub fn length(&self) -> usize { + use AuthenticationMessage::*; + match self { + Ok(msg) => msg.length, + Cleartext(msg) => msg.length, + Md5(msg) => msg.length, + Sasl(msg) => msg.length, + SaslContinue(msg) => msg.length, + SaslFinal(msg) => msg.length, + } + } +} + +pub fn collect_fields( + reader: &mut crate::protocol::buffer_reader::BufferReader<'_>, +) -> Result> { + use std::collections::HashMap; + let mut map = HashMap::new(); + loop { + let field_type = reader.string(1)?; + if field_type == "\0" { + break; + } + let value = reader.cstring()?; + map.insert(field_type, value); + } + Ok(map) +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 00000000..d8d5054b --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,25 @@ +#![allow(dead_code)] +// The internal protocol layer keeps full PostgreSQL message shapes even when +// the high-level API only consumes a subset of each message today. + +pub(crate) mod buffer_reader; +pub(crate) mod buffer_writer; +pub(crate) mod messages; +pub(crate) mod parser; +pub(crate) mod serializer; +pub(crate) mod string_utils; +pub(crate) mod types; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +use messages::{AuthenticationMessage, BackendMessage, Field, MessageName}; +#[cfg(test)] +use parser::Parser; +#[cfg(test)] +use serializer::{BindConfig, ExecConfig, PortalTarget, Serialize}; +#[cfg(test)] +use string_utils::byte_length_utf8; +#[cfg(test)] +use types::Mode; diff --git a/src/protocol/parser.rs b/src/protocol/parser.rs new file mode 100644 index 00000000..88e78510 --- /dev/null +++ b/src/protocol/parser.rs @@ -0,0 +1,354 @@ +use anyhow::{Result, anyhow, ensure}; + +use crate::protocol::buffer_reader::BufferReader; +use crate::protocol::messages::{ + AuthenticationCleartextPassword, AuthenticationMD5Password, AuthenticationMessage, + AuthenticationOk, AuthenticationSasl, AuthenticationSaslContinue, AuthenticationSaslFinal, + BackendKeyDataMessage, BackendMessage, CommandCompleteMessage, CopyDataMessage, CopyResponse, + DataRowMessage, DatabaseError, Field, MessageName, NoticeMessage, NoticeOrErrorFields, + NotificationResponseMessage, ParameterDescriptionMessage, ParameterStatusMessage, + ReadyForQueryMessage, RowDescriptionMessage, collect_fields, +}; +use crate::protocol::types::{BufferParameter, Mode, Modes}; + +const HEADER_LEN: usize = 5; + +const CODE_DATA_ROW: u8 = b'D'; +const CODE_PARSE_COMPLETE: u8 = b'1'; +const CODE_BIND_COMPLETE: u8 = b'2'; +const CODE_CLOSE_COMPLETE: u8 = b'3'; +const CODE_COMMAND_COMPLETE: u8 = b'C'; +const CODE_READY_FOR_QUERY: u8 = b'Z'; +const CODE_NO_DATA: u8 = b'n'; +const CODE_NOTIFICATION_RESPONSE: u8 = b'A'; +const CODE_AUTHENTICATION: u8 = b'R'; +const CODE_PARAMETER_STATUS: u8 = b'S'; +const CODE_BACKEND_KEY_DATA: u8 = b'K'; +const CODE_ERROR: u8 = b'E'; +const CODE_NOTICE: u8 = b'N'; +const CODE_ROW_DESCRIPTION: u8 = b'T'; +const CODE_PARAMETER_DESCRIPTION: u8 = b't'; +const CODE_PORTAL_SUSPENDED: u8 = b's'; +const CODE_REPLICATION_START: u8 = b'W'; +const CODE_EMPTY_QUERY: u8 = b'I'; +const CODE_COPY_IN: u8 = b'G'; +const CODE_COPY_OUT: u8 = b'H'; +const CODE_COPY_DONE: u8 = b'c'; +const CODE_COPY_DATA: u8 = b'd'; + +pub type MessageCallback = dyn FnMut(BackendMessage) -> Result<()>; + +#[derive(Debug, Default)] +pub struct Parser { + buffer: Vec, +} + +impl Parser { + pub fn new() -> Self { + Self { buffer: Vec::new() } + } + + pub fn parse(&mut self, input: BufferParameter, mut callback: F) -> Result<()> + where + F: FnMut(BackendMessage) -> Result<()>, + { + self.buffer.extend_from_slice(input); + + let mut cursor = 0usize; + while self.buffer.len().saturating_sub(cursor) >= HEADER_LEN { + let code = self.buffer[cursor]; + let length_bytes = &self.buffer[cursor + 1..cursor + HEADER_LEN]; + let length = u32::from_be_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) as usize; + let total_len = 1 + length; + + if self.buffer.len() - cursor < total_len { + break; // wait for more data + } + + let body = &self.buffer[cursor + HEADER_LEN..cursor + total_len]; // exclude code+length header + let message = self.handle_packet(code, body, length)?; + callback(message)?; + cursor += total_len; + } + + if cursor > 0 { + self.buffer.drain(0..cursor); + } + + Ok(()) + } + + fn handle_packet(&self, code: u8, bytes: &[u8], length: usize) -> Result { + let mut reader = BufferReader::default(); + reader.set_buffer(0, bytes); + + match code { + CODE_BIND_COMPLETE => Ok(BackendMessage::BindComplete { length: 5 }), + CODE_PARSE_COMPLETE => Ok(BackendMessage::ParseComplete { length: 5 }), + CODE_CLOSE_COMPLETE => Ok(BackendMessage::CloseComplete { length: 5 }), + CODE_NO_DATA => Ok(BackendMessage::NoData { length: 5 }), + CODE_PORTAL_SUSPENDED => Ok(BackendMessage::PortalSuspended { length: 5 }), + CODE_COPY_DONE => Ok(BackendMessage::CopyDone { length: 4 }), + CODE_REPLICATION_START => Ok(BackendMessage::ReplicationStart { length: 4 }), + CODE_EMPTY_QUERY => Ok(BackendMessage::EmptyQuery { length: 4 }), + CODE_COMMAND_COMPLETE => self.parse_command_complete(length, &mut reader), + CODE_READY_FOR_QUERY => self.parse_ready_for_query(length, &mut reader), + CODE_DATA_ROW => self.parse_data_row(length, &mut reader), + CODE_NOTIFICATION_RESPONSE => self.parse_notification(length, &mut reader), + CODE_PARAMETER_STATUS => self.parse_parameter_status(length, &mut reader), + CODE_BACKEND_KEY_DATA => self.parse_backend_key_data(length, &mut reader), + CODE_ERROR => self.parse_error_message(length, &mut reader), + CODE_NOTICE => self.parse_notice_message(length, &mut reader), + CODE_ROW_DESCRIPTION => self.parse_row_description(length, &mut reader), + CODE_PARAMETER_DESCRIPTION => self.parse_parameter_description(length, &mut reader), + CODE_COPY_IN => { + self.parse_copy_message(length, &mut reader, MessageName::CopyInResponse) + } + CODE_COPY_OUT => { + self.parse_copy_message(length, &mut reader, MessageName::CopyOutResponse) + } + CODE_COPY_DATA => self.parse_copy_data(length, bytes), + CODE_AUTHENTICATION => self.parse_authentication(length, &mut reader), + _ => Err(anyhow!("received invalid response: {:x}", code)), + } + } + + fn parse_ready_for_query( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let status = reader.string(1)?; + ensure!(status.len() == 1, "invalid readyForQuery status"); + Ok(BackendMessage::ReadyForQuery(ReadyForQueryMessage { + length, + status: status.as_bytes()[0], + })) + } + + fn parse_command_complete( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let text = reader.cstring()?; + Ok(BackendMessage::CommandComplete(CommandCompleteMessage { + length, + text, + })) + } + + fn parse_copy_data(&self, length: usize, bytes: &[u8]) -> Result { + let data_len = length.saturating_sub(4); + let chunk = bytes[..data_len].to_vec(); + Ok(BackendMessage::CopyData(CopyDataMessage { length, chunk })) + } + + fn parse_copy_message( + &self, + length: usize, + reader: &mut BufferReader<'_>, + name: MessageName, + ) -> Result { + let is_binary = reader.byte()? != 0; + let column_count = reader.int16()? as usize; + let mut column_types = Vec::with_capacity(column_count); + for _ in 0..column_count { + column_types.push(reader.int16()?); + } + Ok(BackendMessage::CopyResponse(CopyResponse { + length, + name, + binary: is_binary, + column_types, + })) + } + + fn parse_notification( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let process_id = reader.int32()?; + let channel = reader.cstring()?; + let payload = reader.cstring()?; + Ok(BackendMessage::Notification(NotificationResponseMessage { + length, + process_id, + channel, + payload, + })) + } + + fn parse_row_description( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let field_count = reader.int16()? as usize; + let mut fields = Vec::with_capacity(field_count); + for _ in 0..field_count { + fields.push(self.parse_field(reader)?); + } + Ok(BackendMessage::RowDescription(RowDescriptionMessage { + length, + fields, + })) + } + + fn parse_field(&self, reader: &mut BufferReader<'_>) -> Result { + let name = reader.cstring()?; + let table_id = reader.int32()?; + let column_id = reader.int16()?; + let data_type_id = reader.int32()?; + let data_type_size = reader.int16()?; + let data_type_modifier = reader.int32()?; + let mode = reader.int16()?; + let format = Mode::try_from(mode).unwrap_or(Modes::TEXT); + Ok(Field { + name, + table_id, + column_id, + data_type_id, + data_type_size, + data_type_modifier, + format, + }) + } + + fn parse_parameter_description( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let count = reader.int16()? as usize; + let mut ids = Vec::with_capacity(count); + for _ in 0..count { + ids.push(reader.int32()?); + } + Ok(BackendMessage::ParameterDescription( + ParameterDescriptionMessage { + length, + data_type_ids: ids, + }, + )) + } + + fn parse_data_row( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let field_count = reader.int16()? as usize; + let mut fields = Vec::with_capacity(field_count); + for _ in 0..field_count { + let len = reader.int32()?; + if len == -1 { + fields.push(None); + } else { + let len = len as usize; + let value = reader.string(len)?; + fields.push(Some(value)); + } + } + Ok(BackendMessage::DataRow(DataRowMessage { length, fields })) + } + + fn parse_parameter_status( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let name = reader.cstring()?; + let value = reader.cstring()?; + Ok(BackendMessage::ParameterStatus(ParameterStatusMessage { + length, + parameter_name: name, + parameter_value: value, + })) + } + + fn parse_backend_key_data( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let process_id = reader.int32()?; + let secret_key = reader.int32()?; + Ok(BackendMessage::BackendKeyData(BackendKeyDataMessage { + length, + process_id, + secret_key, + })) + } + + fn parse_authentication( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let code = reader.int32()?; + let message = match code { + 0 => AuthenticationMessage::Ok(AuthenticationOk { length }), + 3 => AuthenticationMessage::Cleartext(AuthenticationCleartextPassword { length }), + 5 => { + let salt = reader.bytes(4)?; + AuthenticationMessage::Md5(AuthenticationMD5Password { length, salt }) + } + 10 => { + let mut mechanisms = Vec::new(); + loop { + let mechanism = reader.cstring()?; + if mechanism.is_empty() { + break; + } + mechanisms.push(mechanism); + } + AuthenticationMessage::Sasl(AuthenticationSasl { length, mechanisms }) + } + 11 => { + let data = reader.string(length.saturating_sub(8))?; + AuthenticationMessage::SaslContinue(AuthenticationSaslContinue { length, data }) + } + 12 => { + let data = reader.string(length.saturating_sub(8))?; + AuthenticationMessage::SaslFinal(AuthenticationSaslFinal { length, data }) + } + other => { + return Err(anyhow!( + "Unknown authentication message type {other} (length={length})" + )); + } + }; + Ok(BackendMessage::Authentication(message)) + } + + fn parse_error_message( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let fields = collect_fields(reader)?; + let message = fields.get("M").cloned().unwrap_or_default(); + let mut error = DatabaseError::new(length, message); + error.apply_fields(&fields); + Ok(BackendMessage::Error(error)) + } + + fn parse_notice_message( + &self, + length: usize, + reader: &mut BufferReader<'_>, + ) -> Result { + let fields = collect_fields(reader)?; + let message = fields.get("M").cloned(); + let mut notice = NoticeMessage::new(length, message); + notice.apply_fields(&fields); + Ok(BackendMessage::Notice(notice)) + } +} diff --git a/src/protocol/serializer.rs b/src/protocol/serializer.rs new file mode 100644 index 00000000..cd2d5146 --- /dev/null +++ b/src/protocol/serializer.rs @@ -0,0 +1,336 @@ +use std::borrow::Cow; + +use tracing::warn; + +use crate::protocol::buffer_writer::BufferWriter; +use crate::protocol::string_utils::byte_length_utf8; + +const CODE_STARTUP: u8 = b'p'; +const CODE_QUERY: u8 = b'Q'; +const CODE_PARSE: u8 = b'P'; +const CODE_BIND: u8 = b'B'; +const CODE_EXECUTE: u8 = b'E'; +const CODE_FLUSH: u8 = b'H'; +const CODE_SYNC: u8 = b'S'; +const CODE_END: u8 = b'X'; +const CODE_CLOSE: u8 = b'C'; +const CODE_DESCRIBE: u8 = b'D'; +const CODE_COPY_DATA: u8 = b'd'; +const CODE_COPY_DONE: u8 = b'c'; +const CODE_COPY_FAIL: u8 = b'f'; + +#[derive(Debug, Clone)] +pub enum BindValue { + Null, + Text(String), + Binary(Vec), +} + +impl From> for BindValue { + fn from(value: Option<&str>) -> Self { + match value { + Some(text) => BindValue::Text(text.to_owned()), + None => BindValue::Null, + } + } +} + +impl From> for BindValue { + fn from(value: Option) -> Self { + match value { + Some(text) => BindValue::Text(text), + None => BindValue::Null, + } + } +} + +impl From<&str> for BindValue { + fn from(value: &str) -> Self { + BindValue::Text(value.to_owned()) + } +} + +impl From for BindValue { + fn from(value: String) -> Self { + BindValue::Text(value) + } +} + +impl From<&[u8]> for BindValue { + fn from(value: &[u8]) -> Self { + BindValue::Binary(value.to_vec()) + } +} + +impl From> for BindValue { + fn from(value: Vec) -> Self { + BindValue::Binary(value) + } +} + +pub type ValueMapper = Box BindValue + Send + Sync>; + +#[derive(Default)] +pub struct BindConfig { + pub portal: Option, + pub statement: Option, + pub binary: bool, + pub values: Vec, + pub value_mapper: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct ExecConfig { + pub portal: Option, + pub rows: Option, +} + +#[derive(Debug, Clone)] +pub struct PortalTarget { + pub target_type: char, + pub name: Option, +} + +impl PortalTarget { + pub fn new(target_type: char, name: Option) -> Self { + Self { target_type, name } + } +} + +pub struct Serialize; + +impl Serialize { + pub fn startup(options: I) -> Vec + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + let mut writer = BufferWriter::default(); + writer.add_int16(3).add_int16(0); + + for (key, value) in options { + writer.add_cstring(key.as_ref()).add_cstring(value.as_ref()); + } + + writer + .add_cstring("client_encoding") + .add_cstring("UTF8") + .add_cstring(""); + + let body = writer.flush(None); + let length = (body.len() + 4) as i32; + let mut result = Vec::with_capacity(body.len() + 4); + result.extend_from_slice(&length.to_be_bytes()); + result.extend_from_slice(&body); + result + } + + pub fn request_ssl() -> Vec { + let mut buffer = [0u8; 8]; + buffer[..4].copy_from_slice(&8_i32.to_be_bytes()); + buffer[4..8].copy_from_slice(&80877103_i32.to_be_bytes()); + buffer.to_vec() + } + + pub fn password(password: &str) -> Vec { + let mut writer = BufferWriter::default(); + writer.add_cstring(password); + writer.flush(Some(CODE_STARTUP)) + } + + pub fn send_sasl_initial_response_message(mechanism: &str, initial_response: &str) -> Vec { + let mut writer = BufferWriter::default(); + writer + .add_cstring(mechanism) + .add_int32(byte_length_utf8(initial_response) as i32) + .add_string(initial_response); + writer.flush(Some(CODE_STARTUP)) + } + + pub fn send_scram_client_final_message(additional_data: &str) -> Vec { + let mut writer = BufferWriter::default(); + writer.add_string(additional_data); + writer.flush(Some(CODE_STARTUP)) + } + + pub fn query(text: &str) -> Vec { + let mut writer = BufferWriter::default(); + writer.add_cstring(text); + writer.flush(Some(CODE_QUERY)) + } + + pub fn parse(name: Option<&str>, text: &str, types: &[i32]) -> Vec { + if let Some(name) = name + && name.len() > 63 + { + warn!( + "Postgres only supports 63 characters for query names. You supplied {len}", + len = name.len() + ); + } + + let mut writer = BufferWriter::default(); + writer + .add_cstring(name.unwrap_or("")) + .add_cstring(text) + .add_int16(types.len() as i16); + + for oid in types { + writer.add_int32(*oid); + } + + writer.flush(Some(CODE_PARSE)) + } + + pub fn bind(config: &BindConfig) -> Vec { + let mut writer = BufferWriter::default(); + let mut param_writer = BufferWriter::default(); + + let portal = config.portal.as_deref().unwrap_or(""); + let statement = config.statement.as_deref().unwrap_or(""); + let values = &config.values; + let len = values.len() as i16; + + writer.add_cstring(portal).add_cstring(statement); + writer.add_int16(len); + + for (idx, value) in values.iter().enumerate() { + let mapped = if let Some(mapper) = &config.value_mapper { + mapper(value, idx) + } else { + value.clone() + }; + + match mapped { + BindValue::Null => { + writer.add_int16(0); + param_writer.add_int32(-1); + } + BindValue::Binary(bytes) => { + writer.add_int16(1); + param_writer.add_int32(bytes.len() as i32); + param_writer.add_bytes(&bytes); + } + BindValue::Text(text) => { + writer.add_int16(0); + param_writer.add_int32(byte_length_utf8(&text) as i32); + param_writer.add_string(&text); + } + } + } + + writer.add_int16(len); + let param_body = param_writer.flush(None); + writer.add_bytes(¶m_body); + writer.add_int16(if config.binary { 1 } else { 0 }); + writer.flush(Some(CODE_BIND)) + } + + pub fn execute(config: Option<&ExecConfig>) -> Vec { + let Some(cfg) = config else { + return vec![CODE_EXECUTE, 0, 0, 0, 9, 0, 0, 0, 0, 0]; + }; + + if cfg.portal.as_ref().is_none_or(|p| p.is_empty()) && cfg.rows.unwrap_or(0) == 0 { + return vec![CODE_EXECUTE, 0, 0, 0, 9, 0, 0, 0, 0, 0]; + } + + let portal = cfg.portal.as_deref().unwrap_or(""); + let rows = cfg.rows.unwrap_or(0); + + let portal_length = byte_length_utf8(portal); + let len = 4 + portal_length + 1 + 4; + let mut buffer = vec![0u8; 1 + len]; + buffer[0] = CODE_EXECUTE; + let len_bytes = (len as i32).to_be_bytes(); + buffer[1..5].copy_from_slice(&len_bytes); + buffer[5..5 + portal_length].copy_from_slice(portal.as_bytes()); + buffer[5 + portal_length] = 0; + let row_bytes = rows.to_be_bytes(); + let end = buffer.len(); + buffer[end - 4..].copy_from_slice(&row_bytes); + buffer + } + + pub fn describe(target: &PortalTarget) -> Vec { + let mut writer = BufferWriter::default(); + if let Some(name) = &target.name { + let mut text = String::with_capacity(1 + name.len()); + text.push(target.target_type); + text.push_str(name); + writer.add_cstring(&text); + } else { + let mut value = String::with_capacity(2); + value.push(target.target_type); + writer.add_cstring(&value); + } + writer.flush(Some(CODE_DESCRIBE)) + } + + pub fn close(target: &PortalTarget) -> Vec { + let mut text = String::with_capacity(target.name.as_ref().map_or(1, |s| 1 + s.len())); + text.push(target.target_type); + if let Some(name) = &target.name { + text.push_str(name); + } + let mut writer = BufferWriter::default(); + writer.add_cstring(&text); + writer.flush(Some(CODE_CLOSE)) + } + + pub fn flush() -> Vec { + code_only_buffer(CODE_FLUSH) + } + + pub fn sync() -> Vec { + code_only_buffer(CODE_SYNC) + } + + pub fn end() -> Vec { + code_only_buffer(CODE_END) + } + + pub fn copy_data(chunk: &[u8]) -> Vec { + let mut writer = BufferWriter::default(); + writer.add_bytes(chunk); + writer.flush(Some(CODE_COPY_DATA)) + } + + pub fn copy_done() -> Vec { + code_only_buffer(CODE_COPY_DONE) + } + + pub fn copy_fail(message: &str) -> Vec { + let mut writer = BufferWriter::default(); + writer.add_cstring(message); + writer.flush(Some(CODE_COPY_FAIL)) + } + + pub fn cancel(process_id: i32, secret_key: i32) -> Vec { + let mut buffer = vec![0u8; 16]; + buffer[..4].copy_from_slice(&16i32.to_be_bytes()); + let code1 = 1234i16.to_be_bytes(); + let code2 = 5678i16.to_be_bytes(); + buffer[4..6].copy_from_slice(&code1); + buffer[6..8].copy_from_slice(&code2); + buffer[8..12].copy_from_slice(&process_id.to_be_bytes()); + buffer[12..].copy_from_slice(&secret_key.to_be_bytes()); + buffer + } +} + +fn code_only_buffer(code: u8) -> Vec { + let mut buf = vec![0u8; 5]; + buf[0] = code; + buf[1..5].copy_from_slice(&(4i32).to_be_bytes()); + buf +} + +pub trait SerializeExt { + fn serialize(&self) -> Vec; +} + +pub trait SerializeBytes { + fn serialize_bytes(&self) -> Cow<'_, [u8]>; +} diff --git a/src/protocol/string_utils.rs b/src/protocol/string_utils.rs new file mode 100644 index 00000000..3d07df2d --- /dev/null +++ b/src/protocol/string_utils.rs @@ -0,0 +1,3 @@ +pub fn byte_length_utf8(input: &str) -> usize { + input.len() +} diff --git a/src/protocol/tests.rs b/src/protocol/tests.rs new file mode 100644 index 00000000..873cf77b --- /dev/null +++ b/src/protocol/tests.rs @@ -0,0 +1,1057 @@ +use super::serializer::BindValue; +use super::{ + AuthenticationMessage, BackendMessage, BindConfig, ExecConfig, Field, MessageName, Mode, + Parser, PortalTarget, Serialize, byte_length_utf8, +}; +use anyhow::Result; + +mod helpers { + #[derive(Debug, Default, Clone)] + pub struct BufferList { + buffers: Vec>, + } + + impl BufferList { + pub fn new() -> Self { + Self { + buffers: Vec::new(), + } + } + + pub fn add_bytes(&mut self, bytes: &[u8]) -> &mut Self { + self.buffers.push(bytes.to_vec()); + self + } + + pub fn add_int16(&mut self, value: i16) -> &mut Self { + self.buffers.push(value.to_be_bytes().to_vec()); + self + } + + pub fn add_int32(&mut self, value: i32) -> &mut Self { + self.buffers.push(value.to_be_bytes().to_vec()); + self + } + + pub fn add_cstring(&mut self, value: &str) -> &mut Self { + let mut bytes = value.as_bytes().to_vec(); + bytes.push(0); + self.buffers.push(bytes); + self + } + + pub fn add_string(&mut self, value: &str) -> &mut Self { + self.buffers.push(value.as_bytes().to_vec()); + self + } + + pub fn add_char(&mut self, ch: char) -> &mut Self { + assert!( + ch.is_ascii(), + "non-ascii char {ch:?} not supported in these tests" + ); + self.buffers.push(vec![ch as u8]); + self + } + + pub fn add_byte(&mut self, byte: u8) -> &mut Self { + self.buffers.push(vec![byte]); + self + } + + pub fn join(&self, append_length: bool, code: Option) -> Vec { + let body_len: usize = self.buffers.iter().map(|b| b.len()).sum(); + let mut result = Vec::with_capacity( + body_len + usize::from(append_length) * 4 + usize::from(code.is_some()), + ); + + if let Some(code) = code { + result.push(code); + } + + if append_length { + let length = (body_len + 4) as i32; + result.extend_from_slice(&length.to_be_bytes()); + } + + for part in &self.buffers { + result.extend_from_slice(part); + } + + result + } + } + + pub fn concat_slices(parts: &[&[u8]]) -> Vec { + let total_len: usize = parts.iter().map(|p| p.len()).sum(); + let mut result = Vec::with_capacity(total_len); + for part in parts { + result.extend_from_slice(part); + } + result + } +} + +mod test_buffers { + use super::{Field, Mode, helpers::BufferList}; + + pub fn ready_for_query() -> Vec { + let mut list = BufferList::new(); + list.add_bytes(b"I"); + list.join(true, Some(b'Z')) + } + + pub fn authentication_ok() -> Vec { + let mut list = BufferList::new(); + list.add_int32(0); + list.join(true, Some(b'R')) + } + + pub fn authentication_cleartext_password() -> Vec { + let mut list = BufferList::new(); + list.add_int32(3); + list.join(true, Some(b'R')) + } + + pub fn authentication_md5_password() -> Vec { + let mut list = BufferList::new(); + list.add_int32(5); + list.add_bytes(&[1, 2, 3, 4]); + list.join(true, Some(b'R')) + } + + pub fn authentication_sasl() -> Vec { + let mut list = BufferList::new(); + list.add_int32(10); + list.add_cstring("SCRAM-SHA-256"); + list.add_cstring(""); + list.join(true, Some(b'R')) + } + + pub fn authentication_sasl_continue() -> Vec { + let mut list = BufferList::new(); + list.add_int32(11); + list.add_string("data"); + list.join(true, Some(b'R')) + } + + pub fn authentication_sasl_final() -> Vec { + let mut list = BufferList::new(); + list.add_int32(12); + list.add_string("data"); + list.join(true, Some(b'R')) + } + + pub fn parameter_status(name: &str, value: &str) -> Vec { + let mut list = BufferList::new(); + list.add_cstring(name); + list.add_cstring(value); + list.join(true, Some(b'S')) + } + + pub fn backend_key_data(process_id: i32, secret_key: i32) -> Vec { + let mut list = BufferList::new(); + list.add_int32(process_id); + list.add_int32(secret_key); + list.join(true, Some(b'K')) + } + + pub fn command_complete(text: &str) -> Vec { + let mut list = BufferList::new(); + list.add_cstring(text); + list.join(true, Some(b'C')) + } + + pub fn row_description(fields: &[Field]) -> Vec { + let mut list = BufferList::new(); + list.add_int16(fields.len() as i16); + for field in fields { + list.add_cstring(&field.name); + list.add_int32(field.table_id); + list.add_int16(field.column_id); + list.add_int32(field.data_type_id); + list.add_int16(field.data_type_size); + list.add_int32(field.data_type_modifier); + list.add_int16(match field.format { + Mode::Text => 0, + Mode::Binary => 1, + }); + } + list.join(true, Some(b'T')) + } + + pub fn parameter_description(ids: &[i32]) -> Vec { + let mut list = BufferList::new(); + list.add_int16(ids.len() as i16); + for id in ids { + list.add_int32(*id); + } + list.join(true, Some(b't')) + } + + pub fn data_row(values: &[Option<&str>]) -> Vec { + let mut list = BufferList::new(); + list.add_int16(values.len() as i16); + for value in values { + match value { + Some(val) => { + let bytes = val.as_bytes(); + list.add_int32(bytes.len() as i32); + list.add_bytes(bytes); + } + None => { + list.add_int32(-1); + } + } + } + list.join(true, Some(b'D')) + } + + pub fn error(fields: &[(&str, &str)]) -> Vec { + error_or_notice(fields).join(true, Some(b'E')) + } + + pub fn notice(fields: &[(&str, &str)]) -> Vec { + error_or_notice(fields).join(true, Some(b'N')) + } + + fn error_or_notice(fields: &[(&str, &str)]) -> BufferList { + let mut list = BufferList::new(); + for (field_type, value) in fields { + let bytes = field_type.as_bytes(); + assert_eq!( + bytes.len(), + 1, + "field type must be a single character, got {field_type}" + ); + list.add_byte(bytes[0]); + list.add_cstring(value); + } + list.add_byte(0); + list + } + + pub fn parse_complete() -> Vec { + BufferList::new().join(true, Some(b'1')) + } + + pub fn bind_complete() -> Vec { + BufferList::new().join(true, Some(b'2')) + } + + pub fn close_complete() -> Vec { + BufferList::new().join(true, Some(b'3')) + } + + pub fn notification(process_id: i32, channel: &str, payload: &str) -> Vec { + let mut list = BufferList::new(); + list.add_int32(process_id); + list.add_cstring(channel); + list.add_cstring(payload); + list.join(true, Some(b'A')) + } + + pub fn empty_query() -> Vec { + BufferList::new().join(true, Some(b'I')) + } + + pub fn portal_suspended() -> Vec { + BufferList::new().join(true, Some(b's')) + } + + pub fn replication_start() -> Vec { + vec![b'W', 0, 0, 0, 4] + } + + pub fn no_data() -> Vec { + vec![b'n', 0, 0, 0, 4] + } + + pub fn copy_in(cols: usize) -> Vec { + let mut list = BufferList::new(); + list.add_byte(0); + list.add_int16(cols as i16); + for idx in 0..cols { + list.add_int16(idx as i16); + } + list.join(true, Some(b'G')) + } + + pub fn copy_out(cols: usize) -> Vec { + let mut list = BufferList::new(); + list.add_byte(0); + list.add_int16(cols as i16); + for idx in 0..cols { + list.add_int16(idx as i16); + } + list.join(true, Some(b'H')) + } + + pub fn copy_data(bytes: &[u8]) -> Vec { + let mut list = BufferList::new(); + list.add_bytes(bytes); + list.join(true, Some(b'd')) + } + + pub fn copy_done() -> Vec { + BufferList::new().join(true, Some(b'c')) + } +} + +use helpers::{BufferList, concat_slices}; +use test_buffers as buffers; + +fn parse_vec_chunks(chunks: Vec>) -> Result> { + let mut parser = Parser::new(); + let mut messages = Vec::new(); + for chunk in chunks { + parser.parse(chunk.as_slice(), |msg| { + messages.push(msg); + Ok(()) + })?; + } + Ok(messages) +} + +fn parse_slices(chunks: &[&[u8]]) -> Result> { + let mut parser = Parser::new(); + let mut messages = Vec::new(); + for chunk in chunks { + parser.parse(chunk, |msg| { + messages.push(msg); + Ok(()) + })?; + } + Ok(messages) +} + +fn parse_single(buffer: Vec) -> Result { + let mut messages = parse_vec_chunks(vec![buffer])?; + Ok(messages.remove(0)) +} + +fn assert_data_row_fields(message: &BackendMessage, expected: &[Option<&str>]) { + match message { + BackendMessage::DataRow(row) => { + assert_eq!(row.fields.len(), expected.len()); + for (actual, expected_value) in row.fields.iter().zip(expected.iter()) { + match (actual, expected_value) { + (Some(actual), Some(expected)) => assert_eq!(actual, expected), + (None, None) => {} + (other_actual, other_expected) => panic!( + "mismatched field value: expected {:?}, got {:?}", + other_expected, other_actual + ), + } + } + } + other => panic!("expected dataRow message, got {:?}", other.name()), + } +} + +#[test] +fn parser_parses_authentication_messages() -> Result<()> { + match parse_single(buffers::authentication_ok())? { + BackendMessage::Authentication(AuthenticationMessage::Ok(msg)) => { + assert_eq!(msg.length, 8); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::authentication_cleartext_password())? { + BackendMessage::Authentication(AuthenticationMessage::Cleartext(msg)) => { + assert_eq!(msg.length, 8); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::authentication_md5_password())? { + BackendMessage::Authentication(AuthenticationMessage::Md5(msg)) => { + assert_eq!(msg.length, 12); + assert_eq!(msg.salt, vec![1, 2, 3, 4]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::authentication_sasl())? { + BackendMessage::Authentication(AuthenticationMessage::Sasl(msg)) => { + assert_eq!(msg.length, buffers::authentication_sasl().len() - 1); + assert_eq!(msg.mechanisms, vec!["SCRAM-SHA-256".to_string()]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::authentication_sasl_continue())? { + BackendMessage::Authentication(AuthenticationMessage::SaslContinue(msg)) => { + assert_eq!( + msg.length, + buffers::authentication_sasl_continue().len() - 1 + ); + assert_eq!(msg.data, "data"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + let mut extended_continue = buffers::authentication_sasl_continue(); + extended_continue.extend_from_slice(&[1, 2, 3, 4]); + match parse_single(extended_continue)? { + BackendMessage::Authentication(AuthenticationMessage::SaslContinue(msg)) => { + assert_eq!(msg.data, "data"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::authentication_sasl_final())? { + BackendMessage::Authentication(AuthenticationMessage::SaslFinal(msg)) => { + assert_eq!(msg.length, buffers::authentication_sasl_final().len() - 1); + assert_eq!(msg.data, "data"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + let mut extended_final = buffers::authentication_sasl_final(); + extended_final.extend_from_slice(&[1, 2, 4, 5]); + match parse_single(extended_final)? { + BackendMessage::Authentication(AuthenticationMessage::SaslFinal(msg)) => { + assert_eq!(msg.data, "data"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_parses_status_and_notification_messages() -> Result<()> { + match parse_single(buffers::parameter_status("client_encoding", "UTF8"))? { + BackendMessage::ParameterStatus(msg) => { + assert_eq!(msg.length, 25); + assert_eq!(msg.parameter_name, "client_encoding"); + assert_eq!(msg.parameter_value, "UTF8"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::backend_key_data(1, 2))? { + BackendMessage::BackendKeyData(msg) => { + assert_eq!(msg.length, 12); + assert_eq!(msg.process_id, 1); + assert_eq!(msg.secret_key, 2); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::ready_for_query())? { + BackendMessage::ReadyForQuery(msg) => { + assert_eq!(msg.length, 5); + assert_eq!(msg.status, b'I'); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::command_complete("SELECT 3"))? { + BackendMessage::CommandComplete(msg) => { + assert_eq!(msg.length, 13); + assert_eq!(msg.text, "SELECT 3"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::notification(4, "hi", "boom"))? { + BackendMessage::Notification(msg) => { + assert_eq!(msg.length, buffers::notification(4, "hi", "boom").len() - 1); + assert_eq!(msg.process_id, 4); + assert_eq!(msg.channel, "hi"); + assert_eq!(msg.payload, "boom"); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_parses_simple_backend_messages() -> Result<()> { + let cases: Vec<(Vec, MessageName, usize)> = vec![ + (buffers::parse_complete(), MessageName::ParseComplete, 5), + (buffers::bind_complete(), MessageName::BindComplete, 5), + (buffers::close_complete(), MessageName::CloseComplete, 5), + (buffers::portal_suspended(), MessageName::PortalSuspended, 5), + ( + buffers::replication_start(), + MessageName::ReplicationStart, + 4, + ), + (buffers::empty_query(), MessageName::EmptyQuery, 4), + (buffers::copy_done(), MessageName::CopyDone, 4), + (buffers::no_data(), MessageName::NoData, 5), + ]; + + for (buffer, expected_name, expected_length) in cases { + let message = parse_single(buffer)?; + assert_eq!(message.name(), expected_name); + assert_eq!(message.length(), expected_length); + } + + Ok(()) +} + +#[test] +fn parser_parses_row_description_messages() -> Result<()> { + let mut row1 = Field { + name: "id".into(), + table_id: 1, + column_id: 2, + data_type_id: 3, + data_type_size: 4, + data_type_modifier: 5, + format: Mode::Text, + }; + let row1_initial = row1.clone(); + let one_row_buffer = buffers::row_description(std::slice::from_ref(&row1_initial)); + + row1.name = "bang".into(); + let row2 = Field { + name: "whoah".into(), + table_id: 10, + column_id: 11, + data_type_id: 12, + data_type_size: 13, + data_type_modifier: 14, + format: Mode::Text, + }; + let two_row_buffer = buffers::row_description(&[row1.clone(), row2.clone()]); + + let mut empty_list = BufferList::new(); + empty_list.add_int16(0); + let empty_row_buffer = empty_list.join(true, Some(b'T')); + + match parse_single(empty_row_buffer)? { + BackendMessage::RowDescription(msg) => { + assert_eq!(msg.length, 6); + assert!(msg.fields.is_empty()); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(one_row_buffer)? { + BackendMessage::RowDescription(msg) => { + assert_eq!(msg.length, 27); + assert_eq!(msg.fields.len(), 1); + let field = &msg.fields[0]; + assert_eq!(field.name, row1_initial.name); + assert_eq!(field.table_id, row1_initial.table_id); + assert_eq!(field.column_id, row1_initial.column_id); + assert_eq!(field.data_type_id, row1_initial.data_type_id); + assert_eq!(field.data_type_size, row1_initial.data_type_size); + assert_eq!(field.data_type_modifier, row1_initial.data_type_modifier); + assert_eq!(field.format, row1_initial.format); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(two_row_buffer)? { + BackendMessage::RowDescription(msg) => { + assert_eq!(msg.length, 53); + assert_eq!(msg.fields.len(), 2); + let field1 = &msg.fields[0]; + assert_eq!(field1.name, row1.name); + let field2 = &msg.fields[1]; + assert_eq!(field2.name, row2.name); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_parses_parameter_description_messages() -> Result<()> { + match parse_single(buffers::parameter_description(&[]))? { + BackendMessage::ParameterDescription(msg) => { + assert_eq!(msg.length, 6); + assert!(msg.data_type_ids.is_empty()); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::parameter_description(&[1111]))? { + BackendMessage::ParameterDescription(msg) => { + assert_eq!(msg.length, 10); + assert_eq!(msg.data_type_ids, vec![1111]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::parameter_description(&[2222, 3333]))? { + BackendMessage::ParameterDescription(msg) => { + assert_eq!(msg.length, 14); + assert_eq!(msg.data_type_ids, vec![2222, 3333]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_parses_data_row_messages() -> Result<()> { + let buffer_empty = buffers::data_row(&[]); + match parse_single(buffer_empty.clone())? { + BackendMessage::DataRow(msg) => { + assert_eq!(msg.length, buffer_empty.len() - 1); + assert!(msg.fields.is_empty()); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + let buffer_one = buffers::data_row(&[Some("test")]); + match parse_single(buffer_one.clone())? { + BackendMessage::DataRow(msg) => { + assert_eq!(msg.length, buffer_one.len() - 1); + assert_eq!(msg.fields, vec![Some("test".into())]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_parses_notice_and_error_messages() -> Result<()> { + match parse_single(buffers::notice(&[("C", "code")]))? { + BackendMessage::Notice(msg) => { + assert_eq!(msg.length, buffers::notice(&[("C", "code")]).len() - 1); + assert_eq!(msg.code.as_deref(), Some("code")); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + let error_buffer = buffers::error(&[]); + match parse_single(error_buffer.clone())? { + BackendMessage::Error(msg) => { + assert_eq!(msg.length, error_buffer.len() - 1); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + let detailed_error = buffers::error(&[ + ("S", "ERROR"), + ("C", "code"), + ("M", "message"), + ("D", "details"), + ("H", "hint"), + ("P", "100"), + ("p", "101"), + ("q", "query"), + ("W", "where"), + ("F", "file"), + ("L", "line"), + ("R", "routine"), + ("Z", "ignored"), + ]); + match parse_single(detailed_error.clone())? { + BackendMessage::Error(msg) => { + assert_eq!(msg.length, detailed_error.len() - 1); + assert_eq!(msg.severity.as_deref(), Some("ERROR")); + assert_eq!(msg.code.as_deref(), Some("code")); + assert_eq!(msg.message, "message"); + assert_eq!(msg.detail.as_deref(), Some("details")); + assert_eq!(msg.hint.as_deref(), Some("hint")); + assert_eq!(msg.position.as_deref(), Some("100")); + assert_eq!(msg.internal_position.as_deref(), Some("101")); + assert_eq!(msg.internal_query.as_deref(), Some("query")); + assert_eq!(msg.r#where.as_deref(), Some("where")); + assert_eq!(msg.file.as_deref(), Some("file")); + assert_eq!(msg.line.as_deref(), Some("line")); + assert_eq!(msg.routine.as_deref(), Some("routine")); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_parses_copy_messages() -> Result<()> { + match parse_single(buffers::copy_in(0))? { + BackendMessage::CopyResponse(msg) => { + assert_eq!(msg.length, 7); + assert!(!msg.binary); + assert!(msg.column_types.is_empty()); + assert_eq!(msg.name, MessageName::CopyInResponse); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::copy_in(2))? { + BackendMessage::CopyResponse(msg) => { + assert_eq!(msg.length, 11); + assert_eq!(msg.column_types, vec![0, 1]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::copy_out(0))? { + BackendMessage::CopyResponse(msg) => { + assert_eq!(msg.length, 7); + assert_eq!(msg.name, MessageName::CopyOutResponse); + assert!(msg.column_types.is_empty()); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::copy_out(3))? { + BackendMessage::CopyResponse(msg) => { + assert_eq!(msg.length, 13); + assert_eq!(msg.column_types, vec![0, 1, 2]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::copy_done())? { + BackendMessage::CopyDone { length } => { + assert_eq!(length, 4); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + match parse_single(buffers::copy_data(&[5, 6, 7]))? { + BackendMessage::CopyData(msg) => { + assert_eq!(msg.length, 7); + assert_eq!(msg.chunk, vec![5, 6, 7]); + } + other => panic!("unexpected message: {:?}", other.name()), + } + + Ok(()) +} + +#[test] +fn parser_handles_split_single_message() -> Result<()> { + let full = buffers::data_row(&[None, Some("bang"), Some("zug zug"), None, Some("!")]); + + let messages = parse_vec_chunks(vec![full.clone()])?; + assert_eq!(messages.len(), 1); + assert_data_row_fields( + &messages[0], + &[None, Some("bang"), Some("zug zug"), None, Some("!")], + ); + + let splits = [ + 6, + 2, + full.len().saturating_sub(2), + full.len().saturating_sub(1), + full.len().saturating_sub(5), + ]; + + for split in splits { + if split == 0 || split >= full.len() { + continue; + } + let first_len = full.len() - split; + let first = full[..first_len].to_vec(); + let second = full[first_len..].to_vec(); + let messages = parse_vec_chunks(vec![first, second])?; + assert_eq!(messages.len(), 1); + assert_data_row_fields( + &messages[0], + &[None, Some("bang"), Some("zug zug"), None, Some("!")], + ); + } + + Ok(()) +} + +#[test] +fn parser_handles_split_multiple_messages() -> Result<()> { + let data_row = buffers::data_row(&[Some("!")]); + let ready_for_query = buffers::ready_for_query(); + let mut combined = data_row.clone(); + combined.extend_from_slice(&ready_for_query); + + let verify_messages = |messages: &[BackendMessage]| { + assert_eq!(messages.len(), 2); + assert_data_row_fields(&messages[0], &[Some("!")]); + match &messages[1] { + BackendMessage::ReadyForQuery(msg) => assert_eq!(msg.status, b'I'), + other => panic!("unexpected message: {:?}", other.name()), + } + }; + + verify_messages(&parse_vec_chunks(vec![combined.clone()])?); + + let splits = [ + 11, + combined.len().saturating_sub(1), + combined.len().saturating_sub(4), + combined.len().saturating_sub(6), + 8, + 1, + ]; + + for split in splits { + if split == 0 || split >= combined.len() { + continue; + } + let first_len = combined.len() - split; + let first = combined[..first_len].to_vec(); + let second = combined[first_len..].to_vec(); + let messages = parse_vec_chunks(vec![first, second])?; + verify_messages(&messages); + } + + Ok(()) +} + +#[test] +fn parser_respects_buffer_views() -> Result<()> { + let message = buffers::data_row(&[Some("bang")]); + let wrapper = concat_slices(&[&[1, 2, 3, 4], message.as_slice(), &[5, 6, 7, 8]]); + let slice = &wrapper[4..4 + message.len()]; + let messages = parse_slices(&[slice])?; + assert_eq!(messages.len(), 1); + assert_data_row_fields(&messages[0], &[Some("bang")]); + Ok(()) +} + +#[test] +fn serializer_builds_messages() -> Result<()> { + let startup = Serialize::startup([("user", "brian"), ("database", "bang")]); + let mut expected = BufferList::new(); + expected + .add_int16(3) + .add_int16(0) + .add_cstring("user") + .add_cstring("brian") + .add_cstring("database") + .add_cstring("bang") + .add_cstring("client_encoding") + .add_cstring("UTF8") + .add_cstring(""); + assert_eq!(startup, expected.join(true, None)); + + let password = Serialize::password("!"); + let mut expected = BufferList::new(); + expected.add_cstring("!"); + assert_eq!(password, expected.join(true, Some(b'p'))); + + let request_ssl = Serialize::request_ssl(); + let mut expected = BufferList::new(); + expected.add_int32(80877103); + assert_eq!(request_ssl, expected.join(true, None)); + + let sasl_initial = Serialize::send_sasl_initial_response_message("mech", "data"); + let mut expected = BufferList::new(); + expected.add_cstring("mech").add_int32(4).add_string("data"); + assert_eq!(sasl_initial, expected.join(true, Some(b'p'))); + + let sasl_final = Serialize::send_scram_client_final_message("data"); + let mut expected = BufferList::new(); + expected.add_string("data"); + assert_eq!(sasl_final, expected.join(true, Some(b'p'))); + + let query = Serialize::query("select * from boom"); + let mut expected = BufferList::new(); + expected.add_cstring("select * from boom"); + assert_eq!(query, expected.join(true, Some(b'Q'))); + + let parse = Serialize::parse(None, "!", &[]); + let mut expected = BufferList::new(); + expected.add_cstring("").add_cstring("!").add_int16(0); + assert_eq!(parse, expected.join(true, Some(b'P'))); + + let parse_named = Serialize::parse(Some("boom"), "select * from boom", &[]); + let mut expected = BufferList::new(); + expected + .add_cstring("boom") + .add_cstring("select * from boom") + .add_int16(0); + assert_eq!(parse_named, expected.join(true, Some(b'P'))); + + let parse_types = Serialize::parse( + Some("force"), + "select * from bang where name = $1", + &[1, 2, 3, 4], + ); + let mut expected = BufferList::new(); + expected + .add_cstring("force") + .add_cstring("select * from bang where name = $1") + .add_int16(4) + .add_int32(1) + .add_int32(2) + .add_int32(3) + .add_int32(4); + assert_eq!(parse_types, expected.join(true, Some(b'P'))); + + let bind_config = BindConfig { + portal: Some("bang".into()), + statement: Some("woo".into()), + values: vec![ + BindValue::from("1"), + BindValue::from("hi"), + BindValue::Null, + BindValue::from("zing"), + ], + ..BindConfig::default() + }; + let bind = Serialize::bind(&bind_config); + let mut expected = BufferList::new(); + expected + .add_cstring("bang") + .add_cstring("woo") + .add_int16(4) + .add_int16(0) + .add_int16(0) + .add_int16(0) + .add_int16(0) + .add_int16(4) + .add_int32(1) + .add_bytes(b"1") + .add_int32(2) + .add_bytes(b"hi") + .add_int32(-1) + .add_int32(4) + .add_bytes(b"zing") + .add_int16(0); + assert_eq!(bind, expected.join(true, Some(b'B'))); + + let bind_config = BindConfig { + portal: Some("bang".into()), + statement: Some("woo".into()), + values: vec![ + BindValue::from("1"), + BindValue::from("hi"), + BindValue::Null, + BindValue::from(vec![b'z', b'i', b'n', b'g']), + ], + ..BindConfig::default() + }; + let bind = Serialize::bind(&bind_config); + let mut expected = BufferList::new(); + expected + .add_cstring("bang") + .add_cstring("woo") + .add_int16(4) + .add_int16(0) + .add_int16(0) + .add_int16(0) + .add_int16(1) + .add_int16(4) + .add_int32(1) + .add_bytes(b"1") + .add_int32(2) + .add_bytes(b"hi") + .add_int32(-1) + .add_int32(4) + .add_bytes(b"zing") + .add_int16(0); + assert_eq!(bind, expected.join(true, Some(b'B'))); + + let bind_config = BindConfig { + portal: Some("bang".into()), + statement: Some("woo".into()), + values: vec![ + BindValue::from("1"), + BindValue::from("hi"), + BindValue::Null, + BindValue::from("zing"), + ], + value_mapper: Some(Box::new(|_, _| BindValue::Null)), + ..BindConfig::default() + }; + let bind = Serialize::bind(&bind_config); + let mut expected = BufferList::new(); + expected + .add_cstring("bang") + .add_cstring("woo") + .add_int16(4) + .add_int16(0) + .add_int16(0) + .add_int16(0) + .add_int16(0) + .add_int16(4) + .add_int32(-1) + .add_int32(-1) + .add_int32(-1) + .add_int32(-1) + .add_int16(0); + assert_eq!(bind, expected.join(true, Some(b'B'))); + + let default_execute = Serialize::execute(None); + assert_eq!(default_execute, vec![b'E', 0, 0, 0, 9, 0, 0, 0, 0, 0]); + + let exec_config = ExecConfig { + portal: Some("my favorite portal".into()), + rows: Some(100), + }; + let execute = Serialize::execute(Some(&exec_config)); + let mut expected = BufferList::new(); + expected.add_cstring("my favorite portal").add_int32(100); + assert_eq!(execute, expected.join(true, Some(b'E'))); + + assert_eq!(Serialize::flush(), vec![b'H', 0, 0, 0, 4]); + assert_eq!(Serialize::sync(), vec![b'S', 0, 0, 0, 4]); + assert_eq!(Serialize::end(), vec![b'X', 0, 0, 0, 4]); + + let describe_statement = Serialize::describe(&PortalTarget::new('S', Some("bang".into()))); + let mut expected = BufferList::new(); + expected.add_char('S').add_cstring("bang"); + assert_eq!(describe_statement, expected.join(true, Some(b'D'))); + + let describe_portal = Serialize::describe(&PortalTarget::new('P', None)); + let mut expected = BufferList::new(); + expected.add_char('P').add_cstring(""); + assert_eq!(describe_portal, expected.join(true, Some(b'D'))); + + let close_statement = Serialize::close(&PortalTarget::new('S', Some("bang".into()))); + let mut expected = BufferList::new(); + expected.add_char('S').add_cstring("bang"); + assert_eq!(close_statement, expected.join(true, Some(b'C'))); + + let close_portal = Serialize::close(&PortalTarget::new('P', None)); + let mut expected = BufferList::new(); + expected.add_char('P').add_cstring(""); + assert_eq!(close_portal, expected.join(true, Some(b'C'))); + + let copy_data = Serialize::copy_data(&[1, 2, 3]); + let mut expected = BufferList::new(); + expected.add_bytes(&[1, 2, 3]); + assert_eq!(copy_data, expected.join(true, Some(b'd'))); + + let copy_fail = Serialize::copy_fail("err!"); + let mut expected = BufferList::new(); + expected.add_cstring("err!"); + assert_eq!(copy_fail, expected.join(true, Some(b'f'))); + + let copy_done = Serialize::copy_done(); + assert_eq!(copy_done, vec![b'c', 0, 0, 0, 4]); + + let cancel = Serialize::cancel(3, 4); + let mut expected = BufferList::new(); + expected + .add_int16(1234) + .add_int16(5678) + .add_int32(3) + .add_int32(4); + assert_eq!(cancel, expected.join(true, None)); + + Ok(()) +} + +#[test] +fn string_utils_byte_length_utf8_matches_typescript_expectations() { + assert_eq!(byte_length_utf8(""), 0); + assert_eq!(byte_length_utf8("hello"), 5); + assert_eq!(byte_length_utf8("©"), 2); + assert_eq!(byte_length_utf8("你好"), 6); + assert_eq!(byte_length_utf8("𝄞"), 4); + assert_eq!(byte_length_utf8("hello 你好 𝄞"), 17); + assert_eq!(byte_length_utf8("😀"), 4); + assert_eq!( + byte_length_utf8("The quick brown 🦊 jumps over 13 lazy 🐶! 你好世界"), + 58 + ); +} diff --git a/src/protocol/types.rs b/src/protocol/types.rs new file mode 100644 index 00000000..04400f86 --- /dev/null +++ b/src/protocol/types.rs @@ -0,0 +1,35 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Mode { + Text = 0, + Binary = 1, +} + +impl Mode { + pub fn as_i16(self) -> i16 { + match self { + Mode::Text => 0, + Mode::Binary => 1, + } + } +} + +impl TryFrom for Mode { + type Error = &'static str; + + fn try_from(value: i16) -> Result { + match value { + 0 => Ok(Mode::Text), + 1 => Ok(Mode::Binary), + _ => Err("invalid mode"), + } + } +} + +pub struct Modes; + +impl Modes { + pub const TEXT: Mode = Mode::Text; + pub const BINARY: Mode = Mode::Binary; +} + +pub type BufferParameter<'a> = &'a [u8]; diff --git a/tests/client_compat.rs b/tests/client_compat.rs new file mode 100644 index 00000000..a26f2a84 --- /dev/null +++ b/tests/client_compat.rs @@ -0,0 +1,76 @@ +use anyhow::{Context, Result}; +use pglite_oxide::PgliteServer; +use sqlx::{Connection, Row}; +use tokio::time::{Duration, timeout}; +use tokio_postgres::NoTls; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tokio_postgres_extended_query_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 row = client + .query_one("SELECT $1::int4 + 1 AS answer", &[&41_i32]) + .await + .context("run tokio-postgres parameter query")?; + assert_eq!(row.get::<_, i32>("answer"), 42); + + client + .batch_execute( + "CREATE TABLE items(value TEXT); + INSERT INTO items(value) VALUES ('alpha');", + ) + .await?; + let row = client + .query_one("SELECT value FROM items WHERE value = $1", &[&"alpha"]) + .await + .context("run tokio-postgres table query")?; + assert_eq!(row.get::<_, &str>(0), "alpha"); + + drop(client); + wait_for_tokio_postgres(connection_task).await?; + server.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sqlx_query_works() -> Result<()> { + let server = PgliteServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) + .await + .context("connect with SQLx")?; + + let row = sqlx::query("SELECT $1::int4 + 1 AS answer") + .bind(41_i32) + .fetch_one(&mut conn) + .await + .context("run SQLx parameter query")?; + assert_eq!(row.try_get::("answer")?, 42); + + sqlx::query("CREATE TABLE items(value TEXT)") + .execute(&mut conn) + .await?; + sqlx::query("INSERT INTO items(value) VALUES ($1)") + .bind("alpha") + .execute(&mut conn) + .await?; + let row = sqlx::query("SELECT value FROM items WHERE value = $1") + .bind("alpha") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("value")?, "alpha"); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + +async fn wait_for_tokio_postgres( + connection_task: tokio::task::JoinHandle>, +) -> Result<()> { + timeout(Duration::from_secs(5), connection_task).await???; + Ok(()) +} diff --git a/tests/pglite_smoke.rs b/tests/pglite_smoke.rs deleted file mode 100644 index db320e89..00000000 --- a/tests/pglite_smoke.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::fs; -use std::path::{Component, Path}; - -use anyhow::Result; -use serial_test::serial; -use tempfile::TempDir; - -use pglite_oxide::interactive::{ - exec_interactive, pg_dump_path, poke, run_pg_dump, wasm_import, InteractiveRuntime, PokeInput, -}; -use pglite_oxide::{ - ensure_cluster, ensure_runtime, install_and_init, install_and_init_in, install_with_options, - prepare_default_mount, InstallOptions, PglitePaths, -}; - -fn temp_paths() -> (TempDir, PglitePaths) { - let td = tempfile::tempdir().expect("tmpdir"); - let base = td.path().to_path_buf(); - let paths = PglitePaths { - pgroot: base.join("pglite"), - pgdata: base.join("db"), - }; - (td, paths) -} - -fn collect_message_tags(buf: &[u8]) -> Result> { - let mut tags = Vec::new(); - let mut index = 0usize; - while index < buf.len() { - if buf.len() - index < 5 { - anyhow::bail!("incomplete message"); - } - let tag = buf[index]; - let len = u32::from_be_bytes(buf[index + 1..index + 5].try_into().unwrap()) as usize; - if len < 4 { - anyhow::bail!("invalid message length"); - } - let total = 1 + len; - if index + total > buf.len() { - anyhow::bail!("message overruns buffer"); - } - tags.push(tag); - index += total; - } - Ok(tags) -} - -#[test] -#[serial] -fn unpack_runtime_once() -> Result<()> { - let (_td, paths) = temp_paths(); - - ensure_runtime(&paths)?; - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - assert!(paths.pgroot.join("pglite").join("share").exists()); - assert!(paths.pgroot.join("pglite").join("lib").exists()); - Ok(()) -} - -#[test] -#[serial] -fn unpack_runtime_is_idempotent() -> Result<()> { - let (_td, paths) = temp_paths(); - ensure_runtime(&paths)?; - ensure_runtime(&paths)?; - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - Ok(()) -} - -#[test] -#[serial] -fn init_cluster_creates_pgdata() -> Result<()> { - let (_td, paths) = temp_paths(); - ensure_runtime(&paths)?; - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - ensure_cluster(&paths)?; - assert!(paths.pgdata.join("PG_VERSION").exists()); - assert!(paths.pgdata.join("global").join("pg_control").exists()); - Ok(()) -} - -#[test] -#[serial] -fn init_cluster_is_idempotent() -> Result<()> { - let (_td, paths) = temp_paths(); - ensure_runtime(&paths)?; - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - ensure_cluster(&paths)?; - ensure_cluster(&paths)?; - assert!(paths.pgdata.join("PG_VERSION").exists()); - Ok(()) -} - -#[test] -#[serial] -fn end_to_end_install_and_init() -> Result<()> { - let (_td, mut paths) = temp_paths(); - paths = { - ensure_runtime(&paths)?; - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - ensure_cluster(&paths)?; - paths - }; - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - assert!(paths.pgdata.join("PG_VERSION").exists()); - Ok(()) -} - -#[test] -#[serial] -fn install_and_init_in_respects_root() -> Result<()> { - let td = tempfile::tempdir()?; - let root = td.path().join("custom_root"); - let paths = install_and_init_in(&root)?; - - assert_eq!(paths.pgroot, root); - assert_eq!(paths.pgdata, root.join("pglite").join("base")); - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - assert!(paths.pgdata.join("PG_VERSION").exists()); - Ok(()) -} - -#[test] -#[serial] -fn install_and_init_detects_existing_tmp_mount() -> Result<()> { - if Path::new("tmp").exists() { - fs::remove_dir_all("tmp")?; - } - - let initial = install_and_init_in("tmp")?; - assert_eq!(initial.pgroot, Path::new("tmp")); - assert!(initial.pgdata.join("PG_VERSION").exists()); - - let reused = install_and_init(("com", "example", "reuse_tmp"))?; - assert_eq!(reused.pgroot, Path::new("tmp")); - assert!(reused.pgdata.join("PG_VERSION").exists()); - - fs::remove_dir_all("tmp")?; - Ok(()) -} - -#[test] -#[serial] -fn install_with_options_runtime_only() -> Result<()> { - let (_td, paths) = temp_paths(); - let paths = install_with_options( - paths, - InstallOptions { - ensure_cluster: false, - }, - )?; - - assert!(paths - .pgroot - .join("pglite") - .join("bin") - .join("pglite.wasi") - .exists()); - assert!(!paths.pgdata.join("PG_VERSION").exists()); - Ok(()) -} - -#[test] -#[serial] -fn prepare_default_mount_prefers_tmp() -> Result<()> { - if Path::new("tmp").exists() { - fs::remove_dir_all("tmp")?; - } - - let mount = prepare_default_mount()?; - assert_eq!(mount.mount(), Path::new("tmp")); - assert!(matches!( - mount.io_socket().components().next(), - Some(Component::CurDir) - )); - assert!(mount.paths().pgdata.join("PG_VERSION").exists()); - assert!(!mount.reused_existing()); - - let reused = prepare_default_mount()?; - assert!(reused.reused_existing()); - - fs::remove_dir_all("tmp")?; - Ok(()) -} - -#[test] -#[serial] -fn interactive_helpers_align_with_runtime() -> Result<()> { - if Path::new("tmp").exists() { - fs::remove_dir_all("tmp")?; - } - - let mut runtime = InteractiveRuntime::prepare_default()?; - let wasm_path = runtime.module_path("postgres", None)?; - assert!(wasm_path.exists()); - - if let Ok(dump_path) = runtime.pg_dump_path() { - assert!(dump_path.exists()); - } - - // Ensure the default global cache also resolves modules. - let default_path = wasm_import("postgres", None)?; - assert!(default_path.exists()); - - if let Ok(default_dump) = pg_dump_path() { - assert!(default_dump.exists()); - } - - match run_pg_dump(&["pg_dump", "--version"], &[]) { - Ok(Some(code)) => assert_eq!(code, 0), - Ok(None) => (), - Err(err) => { - eprintln!("pg_dump --version failed: {err:#}"); - } - } - - let (bytes, len) = poke(PokeInput::Str("select 1"))?; - assert_eq!(bytes[len - 1], 0); - assert_eq!(len, bytes.len()); - - let response = exec_interactive(PokeInput::Str("select 1;"))?; - let tags = collect_message_tags(&response)?; - assert!(tags.contains(&b'Z')); - - fs::remove_dir_all("tmp")?; - Ok(()) -} diff --git a/tests/proxy_smoke.rs b/tests/proxy_smoke.rs new file mode 100644 index 00000000..ebf8118c --- /dev/null +++ b/tests/proxy_smoke.rs @@ -0,0 +1,179 @@ +use anyhow::{Result, anyhow, bail, ensure}; +use pglite_oxide::PgliteProxy; +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::thread; +use std::time::Duration; + +const SSL_REQUEST_CODE: i32 = 80_877_103; +const PROTOCOL_3: i32 = 196_608; + +#[test] +fn tcp_proxy_handles_psql_style_connections() -> Result<()> { + let temp_dir = tempfile::TempDir::new()?; + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let addr = listener.local_addr()?; + let root = temp_dir.path().to_path_buf(); + + let handle = thread::spawn(move || PgliteProxy::new(root).accept_tcp_connections(&listener, 2)); + + let first = query_proxy(addr, false, "SELECT 1 AS one")?; + assert_eq!(first, vec!["1"]); + + let second = query_proxy(addr, true, "SELECT 2 AS two")?; + assert_eq!(second, vec!["2"]); + + handle + .join() + .map_err(|_| anyhow!("proxy thread panicked"))??; + Ok(()) +} + +fn query_proxy(addr: SocketAddr, request_ssl: bool, sql: &str) -> Result> { + let mut stream = TcpStream::connect(addr)?; + stream.set_read_timeout(Some(Duration::from_secs(30)))?; + stream.set_write_timeout(Some(Duration::from_secs(30)))?; + + if request_ssl { + stream.write_all(&ssl_request())?; + let mut response = [0u8; 1]; + stream.read_exact(&mut response)?; + ensure!(response[0] == b'N', "expected SSL refusal"); + } + + stream.write_all(&startup_message())?; + read_until_ready(&mut stream)?; + + stream.write_all(&simple_query_message(sql))?; + let values = read_query_values(&mut stream)?; + + stream.write_all(&terminate_message())?; + Ok(values) +} + +fn read_until_ready(stream: &mut TcpStream) -> Result<()> { + loop { + let (tag, body) = read_backend_message(stream)?; + match tag { + b'R' => { + ensure!(body.len() >= 4, "authentication message too short"); + let code = i32::from_be_bytes(body[0..4].try_into().unwrap()); + ensure!(code == 0, "unexpected authentication code {code}"); + } + b'E' => bail!("startup error: {}", error_message(&body)), + b'Z' => return Ok(()), + _ => {} + } + } +} + +fn read_query_values(stream: &mut TcpStream) -> Result> { + let mut values = Vec::new(); + loop { + let (tag, body) = read_backend_message(stream)?; + match tag { + b'D' => values.extend(data_row_values(&body)?), + b'E' => bail!("query error: {}", error_message(&body)), + b'Z' => return Ok(values), + _ => {} + } + } +} + +fn read_backend_message(stream: &mut TcpStream) -> Result<(u8, Vec)> { + let mut header = [0u8; 5]; + stream.read_exact(&mut header)?; + let len = i32::from_be_bytes(header[1..5].try_into().unwrap()); + ensure!(len >= 4, "invalid backend message length {len}"); + let mut body = vec![0u8; len as usize - 4]; + stream.read_exact(&mut body)?; + Ok((header[0], body)) +} + +fn data_row_values(body: &[u8]) -> Result> { + ensure!(body.len() >= 2, "data row too short"); + let count = i16::from_be_bytes(body[0..2].try_into().unwrap()) as usize; + let mut offset = 2usize; + let mut values = Vec::with_capacity(count); + + for _ in 0..count { + ensure!(offset + 4 <= body.len(), "data row field length missing"); + let len = i32::from_be_bytes(body[offset..offset + 4].try_into().unwrap()); + offset += 4; + if len < 0 { + values.push(String::new()); + continue; + } + let len = len as usize; + ensure!( + offset + len <= body.len(), + "data row field overruns message" + ); + values.push(std::str::from_utf8(&body[offset..offset + len])?.to_string()); + offset += len; + } + + Ok(values) +} + +fn error_message(body: &[u8]) -> String { + let mut offset = 0usize; + while offset < body.len() { + let code = body[offset]; + if code == 0 { + break; + } + offset += 1; + let Some(end) = body[offset..].iter().position(|byte| *byte == 0) else { + break; + }; + if code == b'M' { + return String::from_utf8_lossy(&body[offset..offset + end]).to_string(); + } + offset += end + 1; + } + String::from_utf8_lossy(body).to_string() +} + +fn ssl_request() -> Vec { + let mut message = Vec::new(); + message.extend_from_slice(&8_i32.to_be_bytes()); + message.extend_from_slice(&SSL_REQUEST_CODE.to_be_bytes()); + message +} + +fn startup_message() -> Vec { + let mut message = Vec::new(); + message.extend_from_slice(&0_i32.to_be_bytes()); + message.extend_from_slice(&PROTOCOL_3.to_be_bytes()); + for (key, value) in [ + ("user", "postgres"), + ("database", "template1"), + ("application_name", "pglite-oxide-test"), + ] { + message.extend_from_slice(key.as_bytes()); + message.push(0); + message.extend_from_slice(value.as_bytes()); + message.push(0); + } + message.push(0); + let len = message.len() as i32; + message[0..4].copy_from_slice(&len.to_be_bytes()); + message +} + +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 terminate_message() -> Vec { + let mut message = Vec::new(); + message.push(b'X'); + message.extend_from_slice(&4_i32.to_be_bytes()); + message +} diff --git a/tests/runtime_smoke.rs b/tests/runtime_smoke.rs new file mode 100644 index 00000000..6799d9a0 --- /dev/null +++ b/tests/runtime_smoke.rs @@ -0,0 +1,195 @@ +use pglite_oxide::{ + Pglite, PgliteError, QueryOptions, QueryTemplate, RowMode, format_query, quote_identifier, +}; +use serde_json::{Value, json}; +use std::sync::{Arc, Mutex}; + +fn first_row(result: &pglite_oxide::Results) -> anyhow::Result<&serde_json::Map> { + result + .rows + .first() + .and_then(Value::as_object) + .ok_or_else(|| anyhow::anyhow!("expected first row object")) +} + +#[test] +fn runtime_smoke() -> anyhow::Result<()> { + let mut pg = Pglite::builder().temporary().open()?; + assert!(pg.paths().pgdata.join("PG_VERSION").exists()); + + let version = pg.query( + "SELECT current_setting('server_version_num')::int AS version_num", + &[], + None, + )?; + let version_num = first_row(&version)? + .get("version_num") + .and_then(Value::as_i64) + .expect("version_num"); + assert!( + version_num >= 170_000, + "expected PostgreSQL 17+, got {version_num}" + ); + + pg.exec("CREATE TABLE items(value TEXT)", None)?; + + // COPY FROM '/dev/blob' + let mut options = QueryOptions::default(); + let rows = b"alpha\nbeta\n"; + options.blob = Some(rows.to_vec()); + pg.exec("COPY items(value) FROM '/dev/blob'", Some(&options))?; + + // COPY TO '/dev/blob' and verify blob contents + let results = pg.exec("COPY items TO '/dev/blob'", None)?; + let blob = results + .last() + .and_then(|res| res.blob.as_ref()) + .expect("expected blob data from COPY TO"); + assert_eq!(std::str::from_utf8(blob)?.trim_end(), "alpha\nbeta"); + + // Listen for notifications + let events = Arc::new(Mutex::new(Vec::new())); + let events_clone = Arc::clone(&events); + let handle = pg.listen("test_channel", move |payload| { + events_clone + .lock() + .expect("lock poisoning") + .push(payload.to_string()); + })?; + + pg.exec("SELECT pg_notify('test_channel', 'hello world')", None)?; + + let recorded = events.lock().unwrap(); + assert_eq!(recorded.len(), 1); + assert_eq!(recorded[0], "hello world"); + drop(recorded); + + pg.unlisten(handle)?; + + let formatted = format_query(&mut pg, "SELECT $1::int", &[json!(42)])?; + assert_eq!(formatted, "SELECT '42'::int"); + + let mut tpl = QueryTemplate::new(); + tpl.push_sql("SELECT "); + tpl.push_identifier("items"); + tpl.push_sql(" WHERE value = "); + tpl.push_param(json!("alpha")); + let templated = tpl.build(); + assert_eq!(templated.query, "SELECT \"items\" WHERE value = $1"); + assert_eq!(templated.params[0], json!("alpha")); + + assert_eq!(quote_identifier("Test"), "\"Test\""); + + let typed_sql = "SELECT \ + ($1::int + 1) AS next_int, \ + $2::bool AS flag, \ + $3::jsonb AS doc, \ + $4::text[] AS labels, \ + $5::bytea AS bytes"; + let typed = pg.query( + typed_sql, + &[ + json!(41), + json!(true), + json!({"name": "pglite", "ok": true}), + json!(["alpha", "beta,gamma"]), + json!([0, 1, 2, 255]), + ], + None, + )?; + let typed_row = first_row(&typed)?; + assert_eq!(typed_row.get("next_int"), Some(&json!(42))); + assert_eq!(typed_row.get("flag"), Some(&json!(true))); + assert_eq!( + typed_row.get("doc").and_then(|value| value.get("name")), + Some(&json!("pglite")) + ); + assert_eq!( + typed_row.get("labels"), + Some(&json!(["alpha", "beta,gamma"])) + ); + assert_eq!(typed_row.get("bytes"), Some(&json!([0, 1, 2, 255]))); + + let array_options = QueryOptions { + row_mode: Some(RowMode::Array), + ..QueryOptions::default() + }; + let array_result = pg.query( + "SELECT 1::int AS one, 'two'::text AS two", + &[], + Some(&array_options), + )?; + assert_eq!(array_result.rows.first(), Some(&json!([1, "two"]))); + + pg.exec("CREATE TABLE tx_items(value TEXT)", None)?; + pg.transaction(|tx| { + tx.query( + "INSERT INTO tx_items(value) VALUES ($1) RETURNING value", + &[json!("committed")], + None, + )?; + Ok(()) + })?; + let rollback: anyhow::Result<()> = pg.transaction(|tx| { + tx.exec("INSERT INTO tx_items(value) VALUES ('rolled back')", None)?; + Err(anyhow::anyhow!("force rollback")) + }); + assert!(rollback.is_err()); + 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 + .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}" + ); + } + + pg.close()?; + assert!(pg.is_closed()); + + let mut restarted = Pglite::temporary()?; + let restarted_result = restarted.query("SELECT 42::int AS answer", &[], None)?; + assert_eq!( + first_row(&restarted_result)?.get("answer"), + Some(&json!(42)) + ); + restarted.close()?; + + let persistent_dir = tempfile::TempDir::new()?; + { + let mut persisted = Pglite::builder().path(persistent_dir.path()).open()?; + persisted.exec("CREATE TABLE persisted(value TEXT)", None)?; + persisted.query( + "INSERT INTO persisted(value) VALUES ($1)", + &[json!("kept")], + None, + )?; + persisted.close()?; + } + { + let mut reopened = Pglite::open(persistent_dir.path())?; + let persisted_result = reopened.query("SELECT value FROM persisted", &[], None)?; + assert_eq!( + first_row(&persisted_result)?.get("value"), + Some(&json!("kept")) + ); + reopened.close()?; + } + + Ok(()) +}