diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b88467e71..bc032341a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3151,7 +3151,7 @@ jobs: # a "no reactor running" panic. `test_issue_414_mysql_query_params` # was the single compile-smoke failure this caused. Same set the # doc-tests job already builds together. - run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static -p perry-ext-ioredis -p perry-ext-mongodb -p perry-ext-mysql2 -p perry-ext-pg -p perry-ext-nodemailer + run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static -p perry-ext-ioredis -p perry-ext-mongodb -p perry-ext-nodemailer - name: Issue #945 scalar method IR guard run: | @@ -4003,7 +4003,7 @@ jobs: # Build them in this SAME Cargo graph as perry-stdlib: otherwise each # no-auto fallback build bundles a distinct tokio TLS/runtime and the # linker rejects the unsafe pair (#507, #7629). - run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static -p ${{ matrix.ui_backend }} -p perry-doc-tests -p perry-ext-ioredis -p perry-ext-mongodb -p perry-ext-mysql2 -p perry-ext-pg -p perry-ext-nodemailer -p perry-ext-net + run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static -p ${{ matrix.ui_backend }} -p perry-doc-tests -p perry-ext-ioredis -p perry-ext-mongodb -p perry-ext-nodemailer -p perry-ext-net - name: Verify macOS application bundle packaging if: matrix.os == 'macos-14' diff --git a/CLAUDE.md b/CLAUDE.md index c75787b468..d3b0f4a290 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1623 +**Current Version:** 0.5.1624 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 7bfe1d80e5..3e279451dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,7 +1473,6 @@ dependencies = [ "hybrid-array", "num-traits", "rand_core 0.10.1", - "serdect", "subtle", "zeroize", ] @@ -1500,16 +1499,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "crypto-primes" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" -dependencies = [ - "crypto-bigint 0.7.5", - "rand_core 0.10.1", -] - [[package]] name = "cssparser" version = "0.37.0" @@ -4086,7 +4075,7 @@ dependencies = [ "p384", "pem", "rand 0.8.7", - "rsa 0.9.10", + "rsa", "serde", "serde_json", "sha2 0.10.9", @@ -5543,7 +5532,7 @@ checksum = "1473d470930ed48574515a25df34900f3af89c6fa422d903e019121312a9f13e" [[package]] name = "perry" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "base64 0.22.1", @@ -5607,7 +5596,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-dispatch", "serde", @@ -5615,7 +5604,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "cc", "libc", @@ -5624,7 +5613,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "aho-corasick", "anyhow", @@ -5641,7 +5630,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-hir", @@ -5649,7 +5638,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-hir", @@ -5657,7 +5646,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-dispatch", @@ -5666,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-hir", @@ -5674,7 +5663,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "base64 0.22.1", @@ -5686,7 +5675,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-hir", @@ -5694,7 +5683,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "async-trait", "clap", @@ -5718,14 +5707,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "serde", "serde_json", @@ -5733,7 +5722,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1623" +version = "0.5.1624" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5744,7 +5733,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "clap", @@ -5759,7 +5748,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "block2", "objc2", @@ -5769,7 +5758,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "argon2", "perry-ffi", @@ -5778,7 +5767,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "bcrypt", "perry-ffi", @@ -5786,7 +5775,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "rusqlite", @@ -5794,7 +5783,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "scraper", @@ -5802,7 +5791,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "rust_decimal", @@ -5810,7 +5799,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5818,7 +5807,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "perry-runtime", @@ -5826,7 +5815,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "bytes", "lazy_static", @@ -5839,7 +5828,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "bytes", @@ -5871,7 +5860,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "lazy_static", "perry-ffi", @@ -5881,7 +5870,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "bson", "futures-util", @@ -5891,21 +5880,9 @@ dependencies = [ "tokio", ] -[[package]] -name = "perry-ext-mysql2" -version = "0.5.1623" -dependencies = [ - "chrono", - "perry-ffi", - "perry-runtime", - "serde_json", - "sqlx", - "tokio", -] - [[package]] name = "perry-ext-net" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "bytes", "perry-ffi", @@ -5920,7 +5897,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "lettre", "perry-ffi", @@ -5930,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "notify", "perry-ffi", @@ -5942,24 +5919,15 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "printpdf", ] -[[package]] -name = "perry-ext-pg" -version = "0.5.1623" -dependencies = [ - "perry-ffi", - "sqlx", - "tokio", -] - [[package]] name = "perry-ext-sharp" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "fast_image_resize", "image", @@ -5970,7 +5938,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "lazy_static", "perry-ffi", @@ -5979,7 +5947,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-ffi", @@ -5999,7 +5967,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-ffi", "perry-runtime", @@ -6008,7 +5976,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "futures-util", "lazy_static", @@ -6021,7 +5989,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "brotli", "flate2", @@ -6031,7 +5999,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6041,7 +6009,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-api-manifest", @@ -6061,11 +6029,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1623" +version = "0.5.1624" [[package]] name = "perry-parser" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "perry-diagnostics", @@ -6078,7 +6046,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perex", "regex", @@ -6086,7 +6054,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "ahash", "base64 0.22.1", @@ -6144,14 +6112,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6201,7 +6169,7 @@ dependencies = [ "rand_core 0.6.4", "redis", "reqwest", - "rsa 0.9.10", + "rsa", "rusqlite", "rust_decimal", "rustls", @@ -6234,21 +6202,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "dirs", "perry-ffi", @@ -6258,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "jni", @@ -6273,7 +6241,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "rand 0.10.2", "serde", @@ -6283,7 +6251,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6306,7 +6274,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "block2", @@ -6323,7 +6291,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "block2", @@ -6340,7 +6308,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1623" +version = "0.5.1624" [[package]] name = "perry-ui-test" @@ -6351,11 +6319,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1623" +version = "0.5.1624" [[package]] name = "perry-ui-tvos" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "block2", @@ -6372,7 +6340,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "block2", @@ -6389,7 +6357,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "block2", "libc", @@ -6403,7 +6371,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "libc", @@ -6422,7 +6390,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "base64 0.22.1", "libc", @@ -6435,7 +6403,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "anyhow", "base64 0.22.1", @@ -6450,7 +6418,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1623" +version = "0.5.1624" dependencies = [ "wasmi", ] @@ -6588,16 +6556,6 @@ dependencies = [ "spki 0.7.3", ] -[[package]] -name = "pkcs1" -version = "0.8.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" -dependencies = [ - "der 0.8.2", - "spki 0.8.0", -] - [[package]] name = "pkcs5" version = "0.7.1" @@ -7364,7 +7322,7 @@ dependencies = [ "num-bigint-dig", "num-integer", "num-traits", - "pkcs1 0.7.5", + "pkcs1", "pkcs8 0.10.2", "rand_core 0.6.4", "sha2 0.10.9", @@ -7374,24 +7332,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rsa" -version = "0.10.0-rc.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" -dependencies = [ - "const-oid 0.10.2", - "crypto-bigint 0.7.5", - "crypto-primes", - "digest 0.11.3", - "pkcs1 0.8.0-rc.4", - "pkcs8 0.11.0", - "rand_core 0.10.1", - "signature 3.0.0", - "spki 0.8.0", - "zeroize", -] - [[package]] name = "rsqlite-vfs" version = "0.1.1" @@ -7767,7 +7707,6 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap", "itoa", "memchr", "serde", @@ -8297,8 +8236,6 @@ dependencies = [ "generic-array", "log", "percent-encoding", - "rand 0.10.2", - "rsa 0.10.0-rc.18", "serde", "sha1 0.11.0", "sha2 0.11.0", diff --git a/Cargo.toml b/Cargo.toml index 8bcbef189d..94ddccd928 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,6 @@ members = [ "crates/perry-ext-ethers", "crates/perry-ext-nodemailer", "crates/perry-ext-ioredis", - "crates/perry-ext-pg", - "crates/perry-ext-mysql2", "crates/perry-ext-fetch", "crates/perry-ext-undici", "crates/perry-ext-mongodb", @@ -317,7 +315,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1623" +version = "0.5.1624" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" @@ -468,8 +466,6 @@ perry-ext-sharp = { path = "crates/perry-ext-sharp" } perry-ext-ethers = { path = "crates/perry-ext-ethers" } perry-ext-nodemailer = { path = "crates/perry-ext-nodemailer" } perry-ext-ioredis = { path = "crates/perry-ext-ioredis" } -perry-ext-pg = { path = "crates/perry-ext-pg" } -perry-ext-mysql2 = { path = "crates/perry-ext-mysql2" } perry-ext-fetch = { path = "crates/perry-ext-fetch" } perry-ext-undici = { path = "crates/perry-ext-undici" } perry-ext-mongodb = { path = "crates/perry-ext-mongodb" } diff --git a/changelog.d/10677-remove-pg-native-binding.md b/changelog.d/10677-remove-pg-native-binding.md new file mode 100644 index 0000000000..552dab6e72 --- /dev/null +++ b/changelog.d/10677-remove-pg-native-binding.md @@ -0,0 +1,44 @@ +Removed the native `pg` binding: `crates/perry-ext-pg` (sqlx::postgres + +tokio bridge over `perry-ffi`) and the duplicate pre-#466 in-tree +implementation in `crates/perry-stdlib/src/pg/` (the `bundled-pg` feature), +kept alive since before the migration to a separate ext crate. Both defined +the same `extern "C"` symbols (`js_pg_client_new`, `js_pg_client_query`, …); +whichever won the link order silently shadowed the other. `import ... from +"pg"` no longer resolves as a native module at all — it compiles the real +npm `pg` package from source, same as any other TypeScript/JavaScript +dependency, with `pg` and its 13 transitive deps (`pg-connection-string`, +`pg-pool`, `pg-protocol`, `pg-types`, `pgpass`, `pg-int8`, +`postgres-{array,date,interval,bytea}`, `pg-cloudflare`, `split2`, `xtend`) +picked up automatically by Perry's compile-package wildcard when a project +has no `perry.compilePackages` entry, or explicit listing otherwise. + +Removed the `[bindings.pg]` entry (`well_known_bindings.toml`), the `"pg"` +`NATIVE_MODULES` entry and manifest rows (`perry-api-manifest`), the pg +`NativeModSig` dispatch-table rows (`perry-codegen`'s +`lower_call/native_table/databases.rs`), the `stdlib_features.rs` / +`optimized_libs` feature-gate arms, the `bundled-pg`/`database-postgres` +Cargo features and the now-unreachable `sqlx` `"postgres"` feature on +`perry-stdlib`'s dependency (verified nothing else in the workspace +requests it), and the `perry-ext-pg` entry in `workspace-architecture.json`. +Regenerated `docs/api/perry.d.ts`, `docs/src/api/reference.md`, and +`docs/src/native-libraries/governance.md`'s generated table; updated +`docs/src/native-libraries/overview.md`'s well-known-binding description. + +The removal also had to reach the call sites that still named the deleted +symbols, which the first pass missed: `lower_call/builtin.rs` lowered +`new Client(cfg)` / `new Pool(cfg)` from an `import ... from "pg"` straight to +`js_pg_client_new` / `js_pg_pool_new` (undefined at link time once the +providers are gone — and the real `pg` package constructs `new Client`), the +ten `js_pg_*` externs in `runtime_decls/stdlib_ffi/data_stores.rs`, the seven +`js_pg_*` stubs in `perry-ui-android/src/stdlib_stubs.rs`, `"pg"` in +perry-codegen-js's browser-throw list, and `-p perry-ext-pg` in +`scripts/run_doc_tests.sh` / `.ps1`. Dropping the `"Client" | "Pool" => +Some(&["pg"])` import gate together with the two arms leaves a user-defined +`Pool`/`Client` on the generic path, which is what #536 wanted anyway. + +Verified end to end against a live PostgreSQL 16.13 server, with the pinned +`pg@8.22.0` as the only dependency of a throwaway fixture and no +`perry.compilePackages` key: `CREATE TABLE` / parameterized `INSERT` / +`SELECT` (rows, `rowCount`, `command`, field names) / parameterized `DELETE` / +`DROP TABLE` on a `Client`, plus a `Pool` query — byte-for-byte identical +output to `node --experimental-strip-types` on the pinned Node 26.5.1. diff --git a/changelog.d/10680-mysql2-native-binding-removal.md b/changelog.d/10680-mysql2-native-binding-removal.md new file mode 100644 index 0000000000..33ee88f393 --- /dev/null +++ b/changelog.d/10680-mysql2-native-binding-removal.md @@ -0,0 +1,50 @@ +Removed the mysql2 native binding so `import mysql from "mysql2"` resolves to +the real npm package, per the owner's decision to stop shipping hand-written +Rust reimplementations of npm packages that drift from the real thing. + +Two locations turned out to be separate hand-written mysql2 +reimplementations, both removed: `crates/perry-ext-mysql2` (the +governance-tracked, well-known-table crate a plain `import mysql from +"mysql2"` actually linked against) and `crates/perry-stdlib/src/mysql2/` +(~1870 lines, gated behind the default-on `bundled-mysql2` perry-stdlib +feature). The two defined identical `js_mysql2_*` symbol names in **separate, +documented-as-disjoint handle registries** (perry-ffi's vs. perry-stdlib's +`common::handle`), so in the default (`full`-feature) build both crates' +archives carried the same symbols — a live footgun, not merely dead code. + +Also removed the `bundled-mysql2` HIR heuristic in +`perry-hir/src/lower/expr_call/native_module.rs` that recognized a +bundler-inlined (webpack/turbopack) `createPool`/`createConnection` call by +its config-object shape and routed it to perry-ext-mysql2's FFI symbols. That +workaround existed only because an AOT binary couldn't run mysql2's +`generate-function`-built row parsers (`new Function` at runtime); #10675's +`dyn_eval` class-expression support fixes that generally, so the workaround +is no longer needed. + +Removed the supporting registry wiring: `well_known_bindings.toml`, +`NATIVE_MODULES` + manifest rows in `perry-api-manifest`, the +`native_table/databases.rs` MySQL2 codegen section, `ext_registry.rs` FFI +routing, `stdlib_features.rs` / `optimized_libs` driver+freshness wiring, +`PERRY_NATIVE_EXTENSION_PACKAGES` in `resolve.rs`, `workspace-architecture.json`, +the Android `stdlib_stubs.rs` FFI stubs, and the `unrooted-local-shape` / +`string-payload-access` / `native-result-ledger` baselines for the deleted +files and symbols. Fixed the two explicit `-p perry-ext-mysql2` cargo build +args in `.github/workflows/test.yml` and `run_doc_tests.sh`/`.ps1`, which +would otherwise fail with "no such package". Regenerated +`docs/src/api/reference.md`, `docs/api/perry.d.ts`, and +`docs/src/native-libraries/governance.md`'s generated table, and added a +"Completed source migrations" entry for mysql2 matching the existing +`slugify` entry. + +The `dyn_eval` class-expression support this depends on (#10661/#10675) +is already on `main`. + +Validated with a real query round trip against a live MySQL 9.6.0 server, with the +pinned `mysql2@3.23.2` as the only dependency of a throwaway fixture and **no +`perry.compilePackages` entry at all** — Perry's default automatic +package-routing path compiles mysql2 and its full dependency tree from +source, with the `generate-function` row-parser factories handled at runtime +via `dyn_eval`. `CREATE TABLE` / prepared `INSERT` / `SELECT` (rows and field +names) / prepared `DELETE` (`affectedRows`) / `DROP TABLE` on a connection, +plus a pool query — byte-for-byte identical output to +`node --experimental-strip-types` on the pinned Node 26.5.1. diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 83e930a13d..8e39626573 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -29,12 +29,9 @@ use crate::{ApiEntry, ApiKind, ApiSource, ParamSpec, TypeSpec}; /// `perry-hir`. Order matches the original list to keep diffs minimal. pub const NATIVE_MODULES: &[&str] = &[ // ── Third-party npm packages (native wrappers; see well_known_bindings.toml) ── - "mysql2", // MySQL/MariaDB client - "mysql2/promise", // mysql2's promise-API subpath - "pg", // PostgreSQL client - "bcrypt", // bcrypt password hashing (replaces the N-API addon) - "argon2", // Argon2 password hashing (replaces the N-API addon) - "ioredis", // Redis/Valkey client + "bcrypt", // bcrypt password hashing (replaces the N-API addon) + "argon2", // Argon2 password hashing (replaces the N-API addon) + "ioredis", // Redis/Valkey client // iovalkey: the Valkey fork of ioredis (valkey-io/iovalkey), served by the // same perry-ext-ioredis surface — see well_known_bindings.toml. "iovalkey", diff --git a/crates/perry-api-manifest/src/entries/part_1.rs b/crates/perry-api-manifest/src/entries/part_1.rs index 1883b231de..dae5765d30 100644 --- a/crates/perry-api-manifest/src/entries/part_1.rs +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -13,71 +13,6 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[ // (extracted from crates/perry-codegen/src/lower_call.rs; // drift guarded by perry-codegen's manifest_consistency test) // =========================================================== - method_sig( - "mysql2", - "createConnection", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "mysql2", - "createPool", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "mysql2/promise", - "createConnection", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "mysql2/promise", - "createPool", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("mysql2", "query", true, Some("Pool")), - method("mysql2", "execute", true, Some("Pool")), - method("mysql2", "end", true, Some("Pool")), - method("mysql2/promise", "query", true, Some("Pool")), - method("mysql2/promise", "execute", true, Some("Pool")), - method("mysql2/promise", "end", true, Some("Pool")), - method("mysql2", "query", true, Some("PoolConnection")), - method("mysql2", "execute", true, Some("PoolConnection")), - method("mysql2/promise", "query", true, Some("PoolConnection")), - method("mysql2/promise", "execute", true, Some("PoolConnection")), - method("mysql2", "query", true, None), - method("mysql2", "execute", true, None), - method("mysql2", "end", true, None), - method("mysql2", "getConnection", true, None), - method("mysql2", "release", true, None), - method("mysql2", "beginTransaction", true, None), - method("mysql2", "commit", true, None), - method("mysql2", "rollback", true, None), - method("mysql2/promise", "query", true, None), - method("mysql2/promise", "execute", true, None), - method("mysql2/promise", "end", true, None), - method("mysql2/promise", "getConnection", true, None), - method("mysql2/promise", "release", true, None), - method("mysql2/promise", "beginTransaction", true, None), - method("mysql2/promise", "commit", true, None), - method("mysql2/promise", "rollback", true, None), - method_sig("pg", "connect", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig("pg", "Pool", false, None, &[p_any("p0")], TypeSpec::Any), - method("pg", "connect", true, Some("Client")), - method("pg", "query", true, Some("Pool")), - method("pg", "end", true, Some("Pool")), - method("pg", "query", true, None), - method("pg", "end", true, None), method_sig( "ioredis", "createClient", diff --git a/crates/perry-api-manifest/src/entries/part_3.rs b/crates/perry-api-manifest/src/entries/part_3.rs index e7df769587..73f5317c6d 100644 --- a/crates/perry-api-manifest/src/entries/part_3.rs +++ b/crates/perry-api-manifest/src/entries/part_3.rs @@ -534,10 +534,6 @@ pub(crate) const API_MANIFEST_PART_3: &[ApiEntry] = &[ class("net", "BlockList"), class("net", "SocketAddress"), class("ioredis", "Redis"), - class("mysql2/promise", "Pool"), - class("mysql2", "Pool"), - class("pg", "Pool"), - class("pg", "Client"), class("url", "URL"), class("url", "URLSearchParams"), class("url", "URLPattern"), diff --git a/crates/perry-codegen-js/src/emit/native.rs b/crates/perry-codegen-js/src/emit/native.rs index 38977005fe..b772727f30 100644 --- a/crates/perry-codegen-js/src/emit/native.rs +++ b/crates/perry-codegen-js/src/emit/native.rs @@ -237,7 +237,7 @@ impl JsEmitter { .push_str("(console.warn('spawnBackground not available in browser'), 0)"); } // --- Fastify/HTTP (throw in browser) --- - "fastify" | "ws" | "mysql2" | "mysql2/promise" | "pg" | "net" | "worker_threads" => { + "fastify" | "ws" | "net" | "worker_threads" => { let _ = write!( self.output, "((() => {{ throw new Error('{} not available in browser'); }})())", diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index ea4e500929..cabe344794 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -592,33 +592,6 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_ext_net_socket_write3", OwnerKind::WellKnown("net")), ("js_ext_net_socket_end3", OwnerKind::WellKnown("net")), - // ── mysql2 (perry-ext-mysql2) ──────────────────────────────────── - // Normally `import "mysql2"` flips the `[bindings.mysql2]` well-known - // and links perry-ext-mysql2. But a bundler (webpack/turbopack) inlines - // mysql2 under a NUMERIC module id, so there is no bare import for perry - // to see — and JS mysql2 JIT-compiles its row parsers with `new Function` - // (via `generate-function`), which an AOT binary cannot execute. The HIR - // pass in `perry-hir`'s native-module lowering recognizes a bundled - // `createPool`/`createConnection` by its mysql2 config-object signature - // and emits these FFIs directly, WITHOUT adding "mysql2" to the import - // set. Tag them here so the well-known flip fires off codegen provenance - // — same mechanism as the http/net/events rows above — and the staticlib - // joins the link line instead of leaving `_js_mysql2_*` undefined. - ("js_mysql2_create_pool", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_create_connection", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_query", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_execute", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_get_connection", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_end", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_connection_query", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_connection_execute", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_pool_connection_release", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_connection_query", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_connection_execute", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_connection_begin_transaction", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_connection_commit", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_connection_rollback", OwnerKind::WellKnown("mysql2")), - ("js_mysql2_connection_end", OwnerKind::WellKnown("mysql2")), ]; /// Prefix-based routing for ext-binding FFI whose emitted symbols follow the diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index 5edf31f94f..2547843883 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -117,7 +117,6 @@ pub(super) fn lower_builtin_new<'a>( .get(class_name) .map(|s| s.as_str()); let required_sources: Option<&[&str]> = match class_name { - "Client" | "Pool" => Some(&["pg"]), "Database" => Some(&["better-sqlite3"]), "DatabaseSync" | "Session" | "StatementSync" => Some(&["sqlite", "node:sqlite"]), "Redis" => Some(&["ioredis", "redis", "iovalkey"]), @@ -620,41 +619,6 @@ pub(super) fn lower_builtin_new<'a>( } // (`WebSocketServer` is handled by an earlier branch lower in this // file — pre-existing from 2026-04-14. No new branch needed here.) - // pg Client — `new Client(config)` matching npm pg's API: synchronous - // constructor that stores the config; the user calls - // `await client.connect()` separately to open the TCP connection. - // Pre-fix `new Client(config)` fell into the empty-placeholder branch - // and every chained method (.connect/.query/.end) dispatched against - // junk. The runtime's older `js_pg_connect(config) -> Promise` - // (still wired as the receiver-less `pg.connect(config)` factory) - // combines new+connect in one step; this branch maps the npm shape - // through the new `js_pg_client_new` (sync, stores config) + - // `js_pg_client_connect` (async, opens the connection) split. - "Client" => { - let config_val = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let handle = blk.call(I64, "js_pg_client_new", &[(DOUBLE, &config_val)]); - Ok(Some(nanbox_pointer_inline(blk, &handle))) - } - // pg Pool — `new Pool(config)`. sqlx's `connect_lazy` makes this - // synchronous (no actual connections opened until first `.query()`), - // matching npm pg Pool's auto-connect-on-first-use semantics. The - // older `js_pg_create_pool` factory (returns Promise) stays - // wired for `pg.Pool(config)` and similar patterns. - "Pool" => { - let config_val = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let handle = blk.call(I64, "js_pg_pool_new", &[(DOUBLE, &config_val)]); - Ok(Some(nanbox_pointer_inline(blk, &handle))) - } // bun:sqlite Database — distinct internal name avoids colliding with // better-sqlite3's exported `Database` while preserving full JS values // for Bun's optional filename and flags object. diff --git a/crates/perry-codegen/src/lower_call/native_table/databases.rs b/crates/perry-codegen/src/lower_call/native_table/databases.rs index ebad043faa..0c9bf178b6 100644 --- a/crates/perry-codegen/src/lower_call/native_table/databases.rs +++ b/crates/perry-codegen/src/lower_call/native_table/databases.rs @@ -1,361 +1,6 @@ use super::*; pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ - // ========== MySQL2 ========== - NativeModSig { - module: "mysql2", - has_receiver: false, - method: "createConnection", - class_filter: None, - runtime: "js_mysql2_create_connection", - args: &[NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: false, - method: "createPool", - class_filter: None, - runtime: "js_mysql2_create_pool", - args: &[NA_F64], - ret: NR_HANDLE_ID, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: false, - method: "createConnection", - class_filter: None, - runtime: "js_mysql2_create_connection", - args: &[NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: false, - method: "createPool", - class_filter: None, - runtime: "js_mysql2_create_pool", - args: &[NA_F64], - ret: NR_HANDLE_ID, - }, - // mysql2 Pool-specific methods (class_filter: Some("Pool")) - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "query", - class_filter: Some("Pool"), - runtime: "js_mysql2_pool_query", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "execute", - class_filter: Some("Pool"), - runtime: "js_mysql2_pool_execute", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "end", - class_filter: Some("Pool"), - runtime: "js_mysql2_pool_end", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "query", - class_filter: Some("Pool"), - runtime: "js_mysql2_pool_query", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "execute", - class_filter: Some("Pool"), - runtime: "js_mysql2_pool_execute", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "end", - class_filter: Some("Pool"), - runtime: "js_mysql2_pool_end", - args: &[], - ret: NR_GCPTR, - }, - // mysql2 PoolConnection-specific methods - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "query", - class_filter: Some("PoolConnection"), - runtime: "js_mysql2_pool_connection_query", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "execute", - class_filter: Some("PoolConnection"), - runtime: "js_mysql2_pool_connection_execute", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "query", - class_filter: Some("PoolConnection"), - runtime: "js_mysql2_pool_connection_query", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "execute", - class_filter: Some("PoolConnection"), - runtime: "js_mysql2_pool_connection_execute", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - // mysql2 generic instance methods (Connection fallback, class_filter: None) - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "query", - class_filter: None, - runtime: "js_mysql2_connection_query", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "execute", - class_filter: None, - runtime: "js_mysql2_connection_execute", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "end", - class_filter: None, - runtime: "js_mysql2_connection_end", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "getConnection", - class_filter: None, - runtime: "js_mysql2_pool_get_connection", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "release", - class_filter: None, - runtime: "js_mysql2_pool_connection_release", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "beginTransaction", - class_filter: None, - runtime: "js_mysql2_connection_begin_transaction", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "commit", - class_filter: None, - runtime: "js_mysql2_connection_commit", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2", - has_receiver: true, - method: "rollback", - class_filter: None, - runtime: "js_mysql2_connection_rollback", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "query", - class_filter: None, - runtime: "js_mysql2_connection_query", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "execute", - class_filter: None, - runtime: "js_mysql2_connection_execute", - args: &[NA_F64, NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "end", - class_filter: None, - runtime: "js_mysql2_connection_end", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "getConnection", - class_filter: None, - runtime: "js_mysql2_pool_get_connection", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "release", - class_filter: None, - runtime: "js_mysql2_pool_connection_release", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "beginTransaction", - class_filter: None, - runtime: "js_mysql2_connection_begin_transaction", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "commit", - class_filter: None, - runtime: "js_mysql2_connection_commit", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "mysql2/promise", - has_receiver: true, - method: "rollback", - class_filter: None, - runtime: "js_mysql2_connection_rollback", - args: &[], - ret: NR_GCPTR, - }, - // ========== PostgreSQL (pg) ========== - // `new Client(config)` and `new Pool(config)` are dispatched by - // `lower_builtin_new` (sync constructors that produce real handles). - // The factory-style entries below stay wired for `pg.connect(config)` / - // `pg.Pool(config)` patterns that some npm code uses. - NativeModSig { - module: "pg", - has_receiver: false, - method: "connect", - class_filter: None, - runtime: "js_pg_connect", - args: &[NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: false, - method: "Pool", - class_filter: None, - runtime: "js_pg_create_pool", - args: &[NA_F64], - ret: NR_GCPTR, - }, - // `client.connect()` — async, opens the TCP connection on a handle that - // `new Client(config)` previously created in the pre-connect state. - // No-op if the handle was already connected (e.g. came from the - // older `pg.connect(config)` factory). Class-filtered to Client so - // `pool.connect()` (which has different semantics — checkout a pooled - // connection — not yet implemented) doesn't accidentally land here. - NativeModSig { - module: "pg", - has_receiver: true, - method: "connect", - class_filter: Some("Client"), - runtime: "js_pg_client_connect", - args: &[], - ret: NR_GCPTR, - }, - // Pool-specific query/end — different runtime fns from the Client paths. - // Pre-existing dispatch was unfiltered and routed both Pool and Client - // through the Client query/end fns (latent bug: pool.query() against a - // Pool handle would fail because js_pg_client_query expects a Connection - // handle). Class-filtered Pool rows take precedence over the unfiltered - // Client/default rows below thanks to native_module_lookup's two-pass - // search (exact class_filter match first, then None fallback). - NativeModSig { - module: "pg", - has_receiver: true, - method: "query", - class_filter: Some("Pool"), - runtime: "js_pg_pool_query", - args: &[NA_STR, NA_PTR], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: true, - method: "end", - class_filter: Some("Pool"), - runtime: "js_pg_pool_end", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: true, - method: "query", - class_filter: None, - runtime: "js_pg_client_query", - args: &[NA_STR, NA_PTR], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: true, - method: "end", - class_filter: None, - runtime: "js_pg_client_end", - args: &[], - ret: NR_GCPTR, - }, // ========== ioredis ========== // NB: every row was previously emitting `js_redis_*` symbols which don't // exist in perry-stdlib (the actual fns are `js_ioredis_*`). The bug was diff --git a/crates/perry-codegen/src/lower_call/native_table/mod.rs b/crates/perry-codegen/src/lower_call/native_table/mod.rs index 0ae61f8ce1..6b387aa69f 100644 --- a/crates/perry-codegen/src/lower_call/native_table/mod.rs +++ b/crates/perry-codegen/src/lower_call/native_table/mod.rs @@ -41,7 +41,7 @@ mod ws_events; mod yoga; // ============================================================================ -// Native stdlib module dispatch (fastify, mysql2, ws, pg, ioredis, mongodb, +// Native stdlib module dispatch (fastify, ws, ioredis, mongodb, // better-sqlite3, etc.). Ported from the old Cranelift codegen's dispatch // table that was lost in the v0.5.0 LLVM cutover. // ============================================================================ diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs index bf2ab6753d..cb127ee896 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs @@ -24,7 +24,7 @@ use utilities::declare_utilities; use web::declare_web; /// Stdlib / FFI runtime functions. Without these declarations, user code -/// that touches any of the third-party stdlib modules (http, mysql2, pg, +/// that touches any of the third-party stdlib modules (http, mongodb, /// redis, mongodb, bcrypt, jsonwebtoken, sharp, cron, WebSocket, /// zlib, etc.) emits `use of undefined value '@js_*'` at clang -c time /// because the IR references the name without a preceding `declare`. diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs index 13a2d481e2..66865f5d9c 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs @@ -1,22 +1,10 @@ //! Database / data-store / crypto / OS stdlib FFI declarations -//! (extracted from stdlib_ffi.rs): pg, redis, mongodb, sqlite, OS, crypto, nanoid. +//! (extracted from stdlib_ffi.rs): redis, mongodb, sqlite, OS, crypto, nanoid. use crate::module::LlModule; use crate::types::{DOUBLE, I32, I64, VOID}; pub(crate) fn declare_data_stores(module: &mut LlModule) { - // ========== PostgreSQL (pg) ========== - module.declare_function("js_pg_client_connect", I64, &[I64]); - module.declare_function("js_pg_client_end", I64, &[I64]); - module.declare_function("js_pg_client_new", I64, &[I64]); - module.declare_function("js_pg_client_query", I64, &[I64, I64]); - module.declare_function("js_pg_client_query_params", I64, &[I64, I64, I64]); - module.declare_function("js_pg_connect", I64, &[I64]); - module.declare_function("js_pg_create_pool", I64, &[I64]); - module.declare_function("js_pg_pool_end", I64, &[I64]); - module.declare_function("js_pg_pool_new", I64, &[I64]); - module.declare_function("js_pg_pool_query", I64, &[I64, I64]); - // ========== Redis / ioredis ========== module.declare_function("js_ioredis_connect", I64, &[I64]); module.declare_function("js_ioredis_decr", I64, &[I64, I64]); diff --git a/crates/perry-ext-mysql2/Cargo.toml b/crates/perry-ext-mysql2/Cargo.toml deleted file mode 100644 index 4d728e501b..0000000000 --- a/crates/perry-ext-mysql2/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "perry-ext-mysql2" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Native bindings for npm `mysql2` (MySQL client) — uses only `perry-ffi`. Async via `sqlx::mysql` bridged through spawn_blocking + JsPromise + tokio::Handle::current().block_on. Connection + pool + per-pool-connection surfaces." - -[lints] -workspace = true - -[lib] -crate-type = ["staticlib", "rlib"] - -[dependencies] -perry-ffi.workspace = true -# `mysql-rsa` enables the RSA public-key auth exchange MySQL 8's default -# `caching_sha2_password` / `sha256_password` need over a NON-TLS connection. -# Without it every query against such a server fails at connect with -# "RSA auth backend disabled" — which broke `pool.execute`/`query` (e.g. an -# Auth.js credentials `authorize` DB lookup, surfaced as a CallbackRouteError). -serde_json = { version = "1", features = ["preserve_order"] } -sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql", "mysql-rsa", "chrono", "json"] } -tokio = { workspace = true } -chrono.workspace = true - -[dev-dependencies] -perry-ffi = { workspace = true, features = ["runtime-link"] } -# Standalone extension tests need the runtime half of the test-only async FFI -# shims; production code still depends on perry-ffi only. -perry-runtime = { workspace = true, features = ["default", "stdlib"] } diff --git a/crates/perry-ext-mysql2/src/lib.rs b/crates/perry-ext-mysql2/src/lib.rs deleted file mode 100644 index c700cc99e9..0000000000 --- a/crates/perry-ext-mysql2/src/lib.rs +++ /dev/null @@ -1,1904 +0,0 @@ -//! Native bindings for the npm `mysql2` MySQL client — uses only -//! perry-ffi. Async via `sqlx::mysql` bridged through -//! `spawn_blocking + JsPromise + tokio::Handle::current().block_on`. -//! -//! Mirrors perry-stdlib's existing surface: `Connection` (eager -//! `createConnection` with TCP timeout + transaction methods), -//! `Pool` (lazy `createPool` with `getConnection` + `release` for -//! per-conn semantics), parameterized `query()` + `execute()`, -//! result tuple `[rows, fields]` per mysql2 npm convention, -//! ResultSetHeader for non-SELECT writes (`{ affectedRows, -//! insertId, warningStatus }`). -//! -//! BigInt param support is deferred (perry-ffi v0.5.556's BigInt -//! surface is in place but the JS array iteration shape needs an -//! adapter; followup once a wrapper actually demands it). - -use perry_ffi::{ - alloc_string, build_object_shape, js_array_alloc, js_array_get, js_array_length, js_array_push, - js_object_alloc_with_shape, js_object_get_field, js_object_set_field, register_handle, - spawn_blocking, take_handle, value_byte_slice, with_handle, ArrayHeader, Handle, JsPromise, - JsValue, ObjectHeader, Promise, StringHeader, TransientRootScope, SHORT_STRING_MAX_LEN, -}; -use sqlx::mysql::{MySqlConnection, MySqlDatabaseError, MySqlPool, MySqlPoolOptions, MySqlRow}; -use sqlx::pool::PoolConnection; -use sqlx::{Column, Connection, MySql, Row, TypeInfo}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::Mutex; - -#[cfg(test)] -mod test_async_shims; - -const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10; -const DEFAULT_QUERY_TIMEOUT_SECS: u64 = 30; -const DEFAULT_ACQUIRE_TIMEOUT_SECS: u64 = 10; - -extern "C" { - fn js_array_is_array(value: f64) -> f64; - fn js_date_get_time(value: f64) -> f64; - fn js_util_types_is_date(value: f64) -> f64; -} - -/// Connection config — matches perry-stdlib's `MySqlConfig` shape. -#[derive(Debug, Clone)] -pub struct MySqlConfig { - pub host: String, - pub port: u16, - pub user: String, - pub password: String, - pub database: Option, -} - -impl Default for MySqlConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 3306, - user: "root".to_string(), - password: String::new(), - database: None, - } - } -} - -impl MySqlConfig { - pub fn to_url(&self) -> String { - let db_part = self - .database - .as_ref() - .map(|d| format!("/{}", d)) - .unwrap_or_default(); - // URL-encode password to handle special characters - let encoded_password: String = self - .password - .chars() - .map(|c| match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(), - c => format!("%{:02X}", c as u32), - }) - .collect(); - format!( - "mysql://{}:{}@{}:{}{}?ssl-mode=disabled", - self.user, encoded_password, self.host, self.port, db_part - ) - } -} - -unsafe fn jsvalue_to_string(value: JsValue) -> Option { - if value.is_short_string() { - let mut bytes = [0; SHORT_STRING_MAX_LEN]; - let len = value.short_string_to_buf(&mut bytes)?; - return std::str::from_utf8(&bytes[..len]).ok().map(String::from); - } - if !value.is_string() { - return None; - } - let ptr = value.as_string_ptr(); - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - std::str::from_utf8(bytes).ok().map(String::from) -} - -/// Percent-decode a URI component (`%25` → `%`, `%40` → `@`, …). A lone `%` -/// not followed by two hex digits is kept verbatim. Node's `mysql2` decodes the -/// credentials it takes out of a connection URL, so a password written as -/// `p%25ss` (a literal `%`) authenticates as `p%ss`. Perry used the raw -/// substring and then RE-encoded it for sqlx, double-encoding every reserved -/// character — so a `%`/`@`/`:` in the password produced a wrong password and -/// the server rejected the connection with `1045 Access denied`. Decode here so -/// the round-trip through `to_url` reproduces the real credential. -fn percent_decode(s: &str) -> String { - let bytes = s.as_bytes(); - let mut out = Vec::with_capacity(bytes.len()); - let mut i = 0; - let hex = |b: u8| -> Option { - match b { - b'0'..=b'9' => Some(b - b'0'), - b'a'..=b'f' => Some(b - b'a' + 10), - b'A'..=b'F' => Some(b - b'A' + 10), - _ => None, - } - }; - while i < bytes.len() { - if bytes[i] == b'%' && i + 3 <= bytes.len() { - if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) { - out.push(h * 16 + l); - i += 3; - continue; - } - } - out.push(bytes[i]); - i += 1; - } - String::from_utf8_lossy(&out).into_owned() -} - -fn parse_mysql_uri(uri: &str) -> Option { - let uri = uri.strip_prefix("mysql://")?; - let (credentials, host_part) = if let Some(idx) = uri.rfind('@') { - (&uri[..idx], &uri[idx + 1..]) - } else { - ("", uri) - }; - let (user, password) = if let Some(idx) = credentials.find(':') { - ( - percent_decode(&credentials[..idx]), - percent_decode(&credentials[idx + 1..]), - ) - } else { - (percent_decode(credentials), String::new()) - }; - let (host_port, database) = if let Some(idx) = host_part.find('/') { - (&host_part[..idx], Some(host_part[idx + 1..].to_string())) - } else { - (host_part, None) - }; - let (host, port) = if let Some(idx) = host_port.rfind(':') { - let port: u16 = host_port[idx + 1..].parse().unwrap_or(3306); - (host_port[..idx].to_string(), port) - } else { - (host_port.to_string(), 3306) - }; - Some(MySqlConfig { - host, - port, - user, - password, - database, - }) -} - -/// Object layout — mysql2 uses a "first field is uri" or -/// positional `host`/`port`/`user`/`password`/`database` shape. -/// We resolve by positional index since perry-ffi's -/// `js_object_get_field` is index-based; perry-stdlib's existing -/// copy uses `js_object_get_field_by_name` which we don't have, so -/// we replicate the behavior by checking field 0 for the URI shape -/// (string-typed) and falling back to fields 0..4 for the field -/// shape if the first field looks numeric (port). -unsafe fn parse_mysql_config(config: JsValue) -> MySqlConfig { - let mut result = MySqlConfig::default(); - let obj_ptr = config.as_pointer::(); - if obj_ptr.is_null() { - return result; - } - // Conventional perry-stdlib field layout (host=0, port=1, user=2, - // password=3, database=4). Same trick as nodemailer/pg config - // parsing — relies on the user declaring the keys in this order - // in the object literal so perry-runtime's shape-ordered storage - // puts them at these indices. - let f0 = js_object_get_field(obj_ptr, 0); - if let Some(s) = jsvalue_to_string(f0) { - // First field is a string. Could be `host` or `uri`. - if let Some(parsed) = parse_mysql_uri(&s) { - return parsed; - } - result.host = s; - } - let port_val = js_object_get_field(obj_ptr, 1); - if port_val.is_number() { - result.port = port_val.to_number() as u16; - } - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 2)) { - result.user = s; - } - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 3)) { - result.password = s; - } - let db_val = js_object_get_field(obj_ptr, 4); - if !db_val.is_undefined() && !db_val.is_null() { - if let Some(s) = jsvalue_to_string(db_val) { - result.database = Some(s); - } - } - result -} - -// ── Result types (thread-safe intermediate) ─────────────────────── - -#[derive(Clone, Debug)] -enum RawValue { - Null, - Bool(bool), - Float64(f64), - String(String), - /// A MySQL JSON column. Held as the decoded document and materialised into - /// a JS value on the main thread, because mysql2 in Node hands back the - /// parsed value -- not the source text -- and drizzle's `json()` mapper, - /// among others, relies on that. - Json(serde_json::Value), -} - -#[derive(Clone, Debug)] -struct RawColumnInfo { - name: String, - type_name: String, -} - -#[derive(Clone, Debug)] -struct RawRowData { - values: Vec<(String, RawValue)>, -} - -#[derive(Clone, Debug)] -struct RawQueryResult { - rows: Vec, - columns: Vec, -} - -#[derive(Clone, Debug)] -enum QueryOutcome { - Rows(RawQueryResult), - Executed { - affected_rows: u64, - last_insert_id: u64, - }, -} - -fn extract_raw_value(row: &MySqlRow, index: usize, type_name: &str) -> RawValue { - match type_name { - "TINYINT" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "TINYINT UNSIGNED" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "SMALLINT" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "SMALLINT UNSIGNED" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "MEDIUMINT" | "INT" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "MEDIUMINT UNSIGNED" | "INT UNSIGNED" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "BIGINT" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "BIGINT UNSIGNED" => row - .try_get::(index) - .map(|n| RawValue::Float64(n as f64)) - .unwrap_or(RawValue::Null), - "FLOAT" | "DOUBLE" | "DECIMAL" => row - .try_get::(index) - .map(RawValue::Float64) - .unwrap_or(RawValue::Null), - "BOOLEAN" | "BOOL" => row - .try_get::(index) - .map(RawValue::Bool) - .unwrap_or(RawValue::Null), - "DATETIME" | "TIMESTAMP" => row - .try_get::(index) - .map(|d| RawValue::String(d.format("%Y-%m-%d %H:%M:%S").to_string())) - .unwrap_or(RawValue::Null), - "DATE" => row - .try_get::(index) - .map(|d| RawValue::String(d.format("%Y-%m-%d").to_string())) - .unwrap_or(RawValue::Null), - "TIME" => row - .try_get::(index) - .map(|d| RawValue::String(d.format("%H:%M:%S").to_string())) - .unwrap_or(RawValue::Null), - "JSON" => row - .try_get::(index) - .map(RawValue::Json) - // Needs sqlx's "json" feature, enabled in Cargo.toml with this - // change. Without it MySQL JSON has no Decode impl at all, so the - // String and Vec attempts in the catch-all below both failed - // their type check and every JSON column read back as NULL -- - // silently, because NULL is legal for a nullable JSON column. - .unwrap_or(RawValue::Null), - _ => row - .try_get::(index) - .map(RawValue::String) - .or_else(|_| { - row.try_get::, _>(index) - .map(|b| RawValue::String(String::from_utf8_lossy(&b).to_string())) - }) - .unwrap_or(RawValue::Null), - } -} - -fn raws_from_mysql_rows(rows: Vec) -> RawQueryResult { - let columns: Vec = if !rows.is_empty() { - rows[0] - .columns() - .iter() - .map(|c| RawColumnInfo { - name: c.name().to_string(), - type_name: c.type_info().name().to_string(), - }) - .collect() - } else { - Vec::new() - }; - - let raw_rows: Vec = rows - .iter() - .map(|row| { - let values = row - .columns() - .iter() - .enumerate() - .map(|(i, col)| { - let value = extract_raw_value(row, i, col.type_info().name()); - (col.name().to_string(), value) - }) - .collect(); - RawRowData { values } - }) - .collect(); - - RawQueryResult { - rows: raw_rows, - columns, - } -} - -fn raw_value_to_jsvalue(v: &RawValue) -> JsValue { - match v { - RawValue::Null => JsValue::NULL, - RawValue::Bool(b) => JsValue::from_bool(*b), - RawValue::Float64(f) => JsValue::from_number(*f), - RawValue::String(s) => JsValue::from_string_ptr(alloc_string(s).as_raw()), - RawValue::Json(v) => json_value_to_jsvalue(v), - } -} - -/// Materialise a decoded JSON document as a JS value. -/// -/// Built here rather than by handing the text to the runtime's JSON parser: -/// `perry_ffi` exports `json_stringify` and no counterpart, and adding a -/// `json_parse` to the public ABI for this would be a wider change than the -/// bug warrants. -/// -/// Object key order matches the stored document, which is what Node's mysql2 -/// gives you. That needs serde_json's "preserve_order" feature -- without it -/// the map is a BTreeMap and keys come back alphabetised. Nothing should -/// depend on JSON object key order, but silently reordering a document that -/// round-trips through the database is the kind of difference that surfaces -/// much later, in a diff nobody can explain. -fn json_value_to_jsvalue(value: &serde_json::Value) -> JsValue { - match value { - serde_json::Value::Null => JsValue::NULL, - serde_json::Value::Bool(b) => JsValue::from_bool(*b), - // Every JSON number becomes an f64, which is what JSON.parse does too. - serde_json::Value::Number(n) => JsValue::from_number(n.as_f64().unwrap_or(f64::NAN)), - serde_json::Value::String(s) => JsValue::from_string_ptr(alloc_string(s).as_raw()), - // Folded so the array is never a named local carried across the - // pushes and nested conversions that can move it -- the same shape the - // row and field builders below use. - serde_json::Value::Array(items) => JsValue::from_object_ptr(items.iter().fold( - unsafe { js_array_alloc(items.len() as u32) }, - |acc, item| unsafe { js_array_push(acc, json_value_to_jsvalue(item)) }, - )), - serde_json::Value::Object(map) => { - let names: Vec<&str> = map.keys().map(|k| k.as_str()).collect(); - let (packed, shape_id) = build_object_shape(&names); - let obj = unsafe { - js_object_alloc_with_shape( - shape_id, - names.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ) - }; - for (i, (_, v)) in map.iter().enumerate() { - unsafe { js_object_set_field(obj, i as u32, json_value_to_jsvalue(v)) }; - } - JsValue::from_object_ptr(obj) - } - } -} - -fn raw_row_to_js_object(row: &RawRowData) -> *mut ObjectHeader { - let names: Vec<&str> = row.values.iter().map(|(n, _)| n.as_str()).collect(); - let (packed, shape_id) = build_object_shape(&names); - let obj = unsafe { - js_object_alloc_with_shape( - shape_id, - names.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ) - }; - for (i, (_, val)) in row.values.iter().enumerate() { - unsafe { js_object_set_field(obj, i as u32, raw_value_to_jsvalue(val)) }; - } - obj -} - -/// Build a row as a positional ARRAY `[v0, v1, …]` in column order. mysql2's -/// `{ rowsAsArray: true }` option (which Drizzle sets for its relational-query -/// and `select()` paths) returns rows this way; Drizzle's `mapResultRow` then -/// maps positions to columns via the selected-fields list. -fn raw_row_to_js_array(row: &RawRowData) -> *mut ArrayHeader { - let mut arr = unsafe { js_array_alloc(row.values.len() as u32) }; - for (_, val) in &row.values { - arr = unsafe { js_array_push(arr, raw_value_to_jsvalue(val)) }; - } - arr -} - -/// Map sqlx's MySQL type *name* back to the wire-protocol numeric type ID -/// (`enum_field_types`, what Node's mysql2 puts in `field.type`/`columnType`). -/// Twin of `perry_stdlib::mysql2::types::mysql_type_id_from_name` (#4917) — -/// this crate cannot depend on perry-stdlib, keep the two in sync. -fn mysql_type_id_from_name(name: &str) -> f64 { - let base = name.strip_suffix(" UNSIGNED").unwrap_or(name); - let id: u8 = match base { - "BOOLEAN" | "TINYINT" => 1, - "SMALLINT" => 2, - "INT" => 3, - "FLOAT" => 4, - "DOUBLE" => 5, - "NULL" => 6, - "TIMESTAMP" => 7, - "BIGINT" => 8, - "MEDIUMINT" => 9, - "DATE" => 10, - "TIME" => 11, - "DATETIME" => 12, - "YEAR" => 13, - "BIT" => 16, - "JSON" => 245, - "DECIMAL" => 246, - "ENUM" => 247, - "SET" => 248, - "TINYBLOB" | "TINYTEXT" => 249, - "MEDIUMBLOB" | "MEDIUMTEXT" => 250, - "LONGBLOB" | "LONGTEXT" => 251, - "BLOB" | "TEXT" => 252, - "VARCHAR" | "VARBINARY" => 253, - "CHAR" | "BINARY" => 254, - "GEOMETRY" => 255, - _ => 0, - }; - id as f64 -} - -fn raw_column_to_field_packet(col: &RawColumnInfo) -> *mut ObjectHeader { - let (packed, shape_id) = build_object_shape(&["name", "type", "columnType", "length"]); - let obj = - unsafe { js_object_alloc_with_shape(shape_id, 4, packed.as_ptr(), packed.len() as u32) }; - let name_str = alloc_string(&col.name); - // #4917: `type`/`columnType` carry the numeric wire ID mysql2 exposes; - // `length` stays 0 (sqlx 0.8 keeps the wire `max_size` pub(crate)). - let type_id = mysql_type_id_from_name(&col.type_name); - unsafe { - js_object_set_field(obj, 0, JsValue::from_string_ptr(name_str.as_raw())); - js_object_set_field(obj, 1, JsValue::from_number(type_id)); - js_object_set_field(obj, 2, JsValue::from_number(type_id)); - js_object_set_field(obj, 3, JsValue::from_number(0.0)); - } - obj -} - -/// Build the mysql2 result tuple `[rows, fields]`. When `rows_as_array` each row -/// is a positional array (mysql2 `{ rowsAsArray: true }`), else a column→value -/// object. -fn raws_to_result_tuple(raw: &RawQueryResult, rows_as_array: bool) -> JsValue { - let mut result = unsafe { js_array_alloc(2) }; - let mut rows_arr = unsafe { js_array_alloc(raw.rows.len() as u32) }; - for r in &raw.rows { - let row_val = if rows_as_array { - JsValue::from_object_ptr(raw_row_to_js_array(r)) - } else { - JsValue::from_object_ptr(raw_row_to_js_object(r)) - }; - rows_arr = unsafe { js_array_push(rows_arr, row_val) }; - } - result = unsafe { js_array_push(result, JsValue::from_object_ptr(rows_arr)) }; - - let mut fields_arr = unsafe { js_array_alloc(raw.columns.len() as u32) }; - for c in &raw.columns { - let obj = raw_column_to_field_packet(c); - fields_arr = unsafe { js_array_push(fields_arr, JsValue::from_object_ptr(obj)) }; - } - result = unsafe { js_array_push(result, JsValue::from_object_ptr(fields_arr)) }; - JsValue::from_object_ptr(result) -} - -/// `[ResultSetHeader, []]` for non-SELECT queries. -fn affected_rows_result(affected: u64, last_insert_id: u64) -> JsValue { - let mut result = unsafe { js_array_alloc(2) }; - let (packed, shape_id) = build_object_shape(&["affectedRows", "insertId", "warningStatus"]); - let header = - unsafe { js_object_alloc_with_shape(shape_id, 3, packed.as_ptr(), packed.len() as u32) }; - unsafe { - js_object_set_field(header, 0, JsValue::from_number(affected as f64)); - js_object_set_field(header, 1, JsValue::from_number(last_insert_id as f64)); - js_object_set_field(header, 2, JsValue::from_number(0.0)); - } - result = unsafe { js_array_push(result, JsValue::from_object_ptr(header)) }; - let empty_fields = unsafe { js_array_alloc(0) }; - result = unsafe { js_array_push(result, JsValue::from_object_ptr(empty_fields)) }; - JsValue::from_object_ptr(result) -} - -fn outcome_to_jsvalue(outcome: &QueryOutcome, rows_as_array: bool) -> JsValue { - match outcome { - QueryOutcome::Rows(raw) => raws_to_result_tuple(raw, rows_as_array), - QueryOutcome::Executed { - affected_rows, - last_insert_id, - } => affected_rows_result(*affected_rows, *last_insert_id), - } -} - -fn is_row_returning_query(sql: &str) -> bool { - let trimmed = sql.trim_start(); - let upper = trimmed.get(..10).unwrap_or(trimmed).to_uppercase(); - upper.starts_with("SELECT") - || upper.starts_with("SHOW") - || upper.starts_with("DESC") - || upper.starts_with("EXPLAIN") - || upper.starts_with("WITH") -} - -#[derive(Clone, Debug, PartialEq)] -enum ParamValue { - Null, - String(String), - Bytes(Vec), - DateTime(chrono::NaiveDateTime), - Number(f64), - Int(i64), - Bool(bool), -} - -/// Everything needed to execute one mysql2 call, copied off the Perry heap -/// before the asynchronous work is scheduled. Keeping the SQL and its bind -/// values in one owned object makes it impossible for a later call to replace -/// either half while this request is waiting for a pool connection. -#[derive(Clone, Debug, PartialEq)] -struct QueryRequest { - sql: String, - params: Vec, - rows_as_array: bool, - /// `mysql2.query()` uses the text protocol when it has no values, whereas - /// `execute()` always represents a prepared statement. - force_prepared: bool, -} - -impl QueryRequest { - fn new( - sql: String, - params: Vec, - rows_as_array: bool, - force_prepared: bool, - ) -> Self { - Self { - sql, - params, - rows_as_array, - force_prepared, - } - } - - fn is_row_returning(&self) -> bool { - is_row_returning_query(&self.sql) - } - - fn uses_prepared_statement(&self) -> bool { - self.force_prepared || !self.params.is_empty() - } -} - -unsafe fn extract_params_from_jsvalue(params: JsValue) -> Result, String> { - if params.is_undefined() || params.is_null() { - return Ok(Vec::new()); - } - - let params_f = f64::from_bits(params.bits()); - let is_array = JsValue::from_bits(js_array_is_array(params_f).to_bits()).to_bool(); - if !is_array { - return Err("Bind parameters must be an array".to_string()); - } - - let arr_ptr = params.as_pointer::(); - if arr_ptr.is_null() { - return Err("Bind parameters array has no valid runtime pointer".to_string()); - } - let length = js_array_length(arr_ptr); - let mut result = Vec::with_capacity(length as usize); - for i in 0..length { - let element = js_array_get(arr_ptr, i); - let p = if element.is_null() { - ParamValue::Null - } else if element.is_undefined() { - return Err(format!("Bind parameter at index {i} is undefined")); - } else if element.is_any_string() { - jsvalue_to_string(element) - .map(ParamValue::String) - .ok_or_else(|| format!("Could not read string bind parameter at index {i}"))? - } else if element.is_int32() { - ParamValue::Int(element.to_int32() as i64) - } else if element.is_bool() { - ParamValue::Bool(element.to_bool()) - } else if element.is_number() { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - } else if let Some(bytes) = value_byte_slice(element) { - // Copy off the Perry heap before the async query is scheduled. - // This covers Buffer and Uint8Array without retaining a raw pointer - // into movable/runtime-owned storage on the worker thread. - ParamValue::Bytes(bytes.to_vec()) - } else { - let value_f = f64::from_bits(element.bits()); - let is_date = JsValue::from_bits(js_util_types_is_date(value_f).to_bits()).to_bool(); - if is_date { - let millis = js_date_get_time(value_f); - if !millis.is_finite() { - return Err(format!("Bind parameter at index {i} is an invalid Date")); - } - let millis = millis as i64; - let date = chrono::DateTime::::from_timestamp_millis(millis) - .ok_or_else(|| { - format!("Bind parameter at index {i} is outside MySQL's Date range") - })? - .naive_utc(); - ParamValue::DateTime(date) - } else { - return Err(format!("Unsupported bind parameter at index {i}")); - } - }; - result.push(p); - } - Ok(result) -} - -fn rejected_params_promise(message: String) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - promise.reject_string(&message); - raw -} - -// ── Connection ──────────────────────────────────────────────────── - -pub struct MysqlConnectionHandle { - pub connection: Arc>>, -} - -impl MysqlConnectionHandle { - pub fn new(conn: MySqlConnection) -> Self { - Self { - connection: Arc::new(Mutex::new(Some(conn))), - } - } -} - -#[derive(Clone)] -enum MysqlConnectionTarget { - Direct(Arc>>), - Pool(Arc>>>), -} - -#[derive(Debug)] -struct MysqlPromiseError { - message: String, - code: Option<&'static str>, - errno: Option, -} - -impl MysqlPromiseError { - fn message(message: impl Into) -> Self { - Self { - message: message.into(), - code: None, - errno: None, - } - } - - fn from_sqlx(context: &str, error: sqlx::Error) -> Self { - let errno = error - .as_database_error() - .and_then(|database| database.try_downcast_ref::()) - .map(MySqlDatabaseError::number); - Self { - message: format!("{context}: {error}"), - code: errno.and_then(mysql2_error_code), - errno, - } - } - - fn reject(self, promise: JsPromise) { - if let Some(errno) = self.errno { - let code = self.code.unwrap_or(""); - let message = self.message; - promise.reject_with(move || { - // MySQL server errors use positive protocol error numbers, as - // mysql2 does, rather than libuv's negative errno convention. - perry_ffi::system_error_value(&message, code, "", i64::from(errno)) - }); - } else { - promise.reject_string(&self.message); - } - } -} - -/// mysql2 exposes symbolic server error names through `.code` and the numeric -/// protocol value through `.errno`. Keep the common SQL/application failures -/// stable here; unknown server numbers still retain `.errno`. -fn mysql2_error_code(errno: u16) -> Option<&'static str> { - Some(match errno { - 1022 => "ER_DUP_KEY", - 1045 => "ER_ACCESS_DENIED_ERROR", - 1048 => "ER_BAD_NULL_ERROR", - 1049 => "ER_BAD_DB_ERROR", - 1050 => "ER_TABLE_EXISTS_ERROR", - 1051 => "ER_BAD_TABLE_ERROR", - 1052 => "ER_NON_UNIQ_ERROR", - 1054 => "ER_BAD_FIELD_ERROR", - 1062 => "ER_DUP_ENTRY", - 1064 => "ER_PARSE_ERROR", - 1146 => "ER_NO_SUCH_TABLE", - 1169 => "ER_DUP_UNIQUE", - 1205 => "ER_LOCK_WAIT_TIMEOUT", - 1213 => "ER_LOCK_DEADLOCK", - 1216 => "ER_NO_REFERENCED_ROW", - 1217 => "ER_ROW_IS_REFERENCED", - 1264 => "ER_WARN_DATA_OUT_OF_RANGE", - 1292 => "ER_TRUNCATED_WRONG_VALUE", - 1364 => "ER_NO_DEFAULT_FOR_FIELD", - 1406 => "ER_DATA_TOO_LONG", - 1451 => "ER_ROW_IS_REFERENCED_2", - 1452 => "ER_NO_REFERENCED_ROW_2", - 1586 => "ER_DUP_ENTRY_WITH_KEY_NAME", - 1830 => "ER_FK_COLUMN_NOT_NULL", - 1834 => "ER_FK_CANNOT_DELETE_PARENT", - 1859 => "ER_DUP_UNKNOWN_IN_INDEX", - 3819 => "ER_CHECK_CONSTRAINT_VIOLATED", - 4025 => "ER_CONSTRAINT_FAILED", - _ => return None, - }) -} - -/// Resolve either mysql2 connection handle family without returning a -/// registry-backed `'static` reference. The old `get_handle_mut` calls dropped -/// DashMap's guard before async work began, so overlapping workers could hold -/// aliased mutable references to the same connection wrapper. -fn connection_target(handle: Handle) -> Option { - with_handle::(handle, |wrapper| { - MysqlConnectionTarget::Direct(Arc::clone(&wrapper.connection)) - }) - .or_else(|| { - with_handle::(handle, |wrapper| { - MysqlConnectionTarget::Pool(Arc::clone(&wrapper.connection)) - }) - }) -} - -async fn execute_query_on_connection( - conn: &mut MySqlConnection, - request: &QueryRequest, -) -> Result { - let is_select = request.is_row_returning(); - - if !request.uses_prepared_statement() { - // SQLx's `query()` prepares even when there are no bind values. That - // needlessly put mysql2 `query("DROP ...")` / `query("CREATE ...")` - // calls into the per-connection statement cache beside parameterized - // `execute()` calls. Use MySQL's text protocol for the no-param query - // shape, matching mysql2 and keeping those statements out of the cache. - let raw = sqlx::raw_sql(sqlx::AssertSqlSafe(request.sql.clone())); - if is_select { - let rows = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - raw.fetch_all(conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Query failed", e))?; - return Ok(QueryOutcome::Rows(raws_from_mysql_rows(rows))); - } - - let res = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - raw.execute(conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Query failed", e))?; - return Ok(QueryOutcome::Executed { - affected_rows: res.rows_affected(), - last_insert_id: res.last_insert_id(), - }); - } - - // Build the SQLx query and all of its arguments from the same owned - // request immediately before execution. Nothing is shared with another - // mysql2 call, even while this future is waiting on I/O. - // Keep the prepared statement scoped to this request. SQLx's connection - // cache is where #8745 observed metadata from a neighboring statement - // being paired with this request's arguments; an ephemeral statement - // preserves mysql2 execute semantics without reusing that association. - let mut query = sqlx::query(sqlx::AssertSqlSafe(request.sql.clone())).persistent(false); - for param in &request.params { - query = match param { - ParamValue::Null => query.bind(Option::::None), - ParamValue::String(s) => query.bind(s.clone()), - ParamValue::Bytes(bytes) => query.bind(bytes.clone()), - ParamValue::DateTime(date) => query.bind(*date), - ParamValue::Number(n) => query.bind(*n), - ParamValue::Int(i) => query.bind(*i), - ParamValue::Bool(b) => query.bind(*b), - }; - } - - if is_select { - let rows = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - query.fetch_all(conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Query failed", e))?; - Ok(QueryOutcome::Rows(raws_from_mysql_rows(rows))) - } else { - let res = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - query.execute(conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Query failed", e))?; - Ok(QueryOutcome::Executed { - affected_rows: res.rows_affected(), - last_insert_id: res.last_insert_id(), - }) - } -} - -async fn execute_query_on_target( - target: MysqlConnectionTarget, - request: &QueryRequest, -) -> Result { - match target { - MysqlConnectionTarget::Direct(connection) => { - let mut slot = connection.lock().await; - let conn = slot - .as_mut() - .ok_or_else(|| MysqlPromiseError::message("Connection already closed"))?; - execute_query_on_connection(conn, request).await - } - MysqlConnectionTarget::Pool(connection) => { - let mut slot = connection.lock().await; - let conn = slot - .as_mut() - .ok_or_else(|| MysqlPromiseError::message("Pool connection released"))?; - execute_query_on_connection(conn, request).await - } - } -} - -/// `mysql.createConnection(config) -> Promise`. -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_create_connection(config_f: f64) -> *mut Promise { - ensure_dispatch_registered(); - let config = JsValue::from_bits(config_f.to_bits()); - let mysql_config = parse_mysql_config(config); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - let result = tokio::runtime::Handle::current().block_on(async move { - let url = mysql_config.to_url(); - tokio::time::timeout( - Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS), - MySqlConnection::connect(&url), - ) - .await - .map_err(|_| MysqlPromiseError::message("MySQL connection timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Failed to connect", e)) - }); - match result { - Ok(conn) => { - let handle = register_handle(MysqlConnectionHandle::new(conn)); - // Registry handles are pointer-tagged small integers. Returning a - // normal JS number loses that identity, so the first method call - // cannot find the connection and rejects "Invalid connection handle". - promise.resolve(JsValue::from_object_ptr(handle as *mut ())); - } - Err(error) => error.reject(promise), - } - }); - raw -} - -#[no_mangle] -pub extern "C" fn js_mysql2_connection_end(conn_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - if let Some(wrapper) = take_handle::(conn_handle) { - let connection = Arc::clone(&wrapper.connection); - let conn = tokio::runtime::Handle::current() - .block_on(async move { connection.lock().await.take() }); - if let Some(conn) = conn { - let result = tokio::runtime::Handle::current().block_on(conn.close()); - match result { - Ok(()) => promise.resolve_undefined(), - Err(error) => { - MysqlPromiseError::from_sqlx("Failed to close", error).reject(promise) - } - } - } else { - promise.reject_string("Connection already closed"); - } - } else { - promise.reject_string("Invalid connection handle"); - } - }); - raw -} - -unsafe fn run_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> *mut Promise { - let request = match parse_query_request(query_f, params_f, force_prepared) { - Ok(request) => request, - Err(message) => return rejected_params_promise(message), - }; - let target = connection_target(conn_handle); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let rows_as_array = request.rows_as_array; - let outcome: Result = tokio::runtime::Handle::current() - .block_on(async move { - let target = target - .ok_or_else(|| MysqlPromiseError::message("Invalid connection handle"))?; - execute_query_on_target(target, &request).await - }); - match outcome { - // #1824: build the JS result on the MAIN thread. outcome_to_jsvalue - // allocates arrays/objects/strings, which is UB on this blocking-pool - // thread (worker thread-local arena → dangling on the main thread once - // the pooled thread idles out). `out` is plain Send Rust data. - Ok(out) => promise.resolve_with(move || outcome_to_jsvalue(&out, rows_as_array)), - Err(error) => error.reject(promise), - } - }); - raw -} - -/// `connection.query(sql, params) -> Promise<[rows, fields]>`. -/// -/// # Safety -/// `query_f` must be a SQL string or mysql2 query-options object. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_connection_query(conn_handle, query_f, params_f, false) -} - -/// `connection.execute(sql, params) -> Promise<[rows, fields]>`. -/// Same backing as `query` for now (sqlx prepares all queries). -/// -/// # Safety -/// `query_f` must be a SQL string or mysql2 query-options object. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_execute( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_connection_query(conn_handle, query_f, params_f, true) -} - -fn run_simple_command(conn_handle: Handle, sql: &'static str) -> *mut Promise { - let target = connection_target(conn_handle); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - let result: Result<(), MysqlPromiseError> = - tokio::runtime::Handle::current().block_on(async move { - let target = target - .ok_or_else(|| MysqlPromiseError::message("Invalid connection handle"))?; - match target { - MysqlConnectionTarget::Direct(connection) => { - let mut slot = connection.lock().await; - let conn = slot.as_mut().ok_or_else(|| { - MysqlPromiseError::message("Connection already closed") - })?; - sqlx::raw_sql(sql) - .execute(conn) - .await - .map(|_| ()) - .map_err(|e| MysqlPromiseError::from_sqlx(sql, e)) - } - MysqlConnectionTarget::Pool(connection) => { - let mut slot = connection.lock().await; - let conn = slot.as_mut().ok_or_else(|| { - MysqlPromiseError::message("Pool connection released") - })?; - sqlx::raw_sql(sql) - .execute(&mut **conn) - .await - .map(|_| ()) - .map_err(|e| MysqlPromiseError::from_sqlx(sql, e)) - } - } - }); - match result { - Ok(()) => promise.resolve_undefined(), - Err(error) => error.reject(promise), - } - }); - raw -} - -fn transaction_sql_for_method(method: &str) -> Option<&'static str> { - match method { - "beginTransaction" => Some("START TRANSACTION"), - "commit" => Some("COMMIT"), - "rollback" => Some("ROLLBACK"), - _ => None, - } -} - -#[no_mangle] -pub extern "C" fn js_mysql2_connection_begin_transaction(conn_handle: Handle) -> *mut Promise { - run_simple_command(conn_handle, "START TRANSACTION") -} - -#[no_mangle] -pub extern "C" fn js_mysql2_connection_commit(conn_handle: Handle) -> *mut Promise { - run_simple_command(conn_handle, "COMMIT") -} - -#[no_mangle] -pub extern "C" fn js_mysql2_connection_rollback(conn_handle: Handle) -> *mut Promise { - run_simple_command(conn_handle, "ROLLBACK") -} - -// ── Pool ────────────────────────────────────────────────────────── - -pub struct MysqlPoolHandle { - pub pool: MySqlPool, -} - -impl MysqlPoolHandle { - pub fn new(pool: MySqlPool) -> Self { - Self { pool } - } -} - -pub struct MysqlPoolConnectionHandle { - pub connection: Arc>>>, -} - -impl MysqlPoolConnectionHandle { - pub fn new(conn: PoolConnection) -> Self { - Self { - connection: Arc::new(Mutex::new(Some(conn))), - } - } -} - -/// `mysql.createPool(config) -> Pool` — sync; eager connect on -/// first use (matches perry-stdlib's existing eager-first-call -/// behavior). Returns 0 if connection fails. -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_create_pool(config_f: f64) -> Handle { - // Register the handle method dispatch so generic `pool.query(...)` calls work - // (Drizzle threads the pool through interface-typed values, losing the static - // "Pool" type the codegen needs to route `.query`/`.execute` natively). - ensure_dispatch_registered(); - let config = JsValue::from_bits(config_f.to_bits()); - let mysql_config = parse_mysql_config(config); - let url = mysql_config.to_url(); - - // mysql2's `createPool` is SYNCHRONOUS and does NOT open a connection — - // it returns a pool that connects lazily on first query. Mirror that with - // sqlx `connect_lazy`: no eager connect (which returned handle 0 when the - // DB was unreachable, so the JS pool value was null and every - // `pool.constructor` / drizzle `isConfig(pool)` read crashed). `connect_lazy` - // still spawns the pool's background reaper, which needs an entered Tokio - // context — run it inside `spawn_blocking` (where the global runtime handle - // is current) rather than at the bare module-init call site (which panicked - // "no reactor running"). - let (tx, rx) = std::sync::mpsc::channel(); - spawn_blocking(move || { - // `connect_lazy` is synchronous but still spawns the pool's reaper task, - // which needs the runtime context. Run it inside `block_on` (same as the - // old eager path) so the spawn has a live reactor; the body returns - // immediately because no connection is opened. - let pool_result = tokio::runtime::Handle::current().block_on(async { - MySqlPoolOptions::new() - .max_connections(10) - .acquire_timeout(Duration::from_secs(DEFAULT_ACQUIRE_TIMEOUT_SECS)) - .connect_lazy(&url) - }); - let _ = tx.send(pool_result); - }); - match rx.recv().ok().and_then(|r| r.ok()) { - Some(pool) => register_handle(MysqlPoolHandle::new(pool)), - None => 0, - } -} - -// ── Generic handle method dispatch ──────────────────────────────── -// -// The codegen routes `pool.query(sql, params)` to the native query fns ONLY -// when the receiver is statically typed as a mysql2 `Pool`. Drizzle stores the -// pool behind interface-typed fields (`this.client: Pool | Connection`), so by -// the time it calls `client.query(query, params)` the static type is lost and -// the call falls back to a generic dynamic dispatch that returned garbage. The -// runtime consults HANDLE_METHOD_DISPATCH for a generic call on a small handle; -// register an extension here so a mysql2 pool/connection handle answers -// `query`/`execute` (and `end`/`getConnection`) regardless of static type. - -const DISPATCH_POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; -const DISPATCH_POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; -const DISPATCH_TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - -extern "C" { - fn js_register_handle_method_dispatch_extension( - f: unsafe extern "C" fn(i64, *const u8, usize, *const f64, usize, *mut f64) -> i32, - ); - fn js_register_handle_property_dispatch_extension( - f: unsafe extern "C" fn(i64, *const u8, usize, *mut f64) -> i32, - ); - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - // Runtime generic field read; returns the runtime `JSValue` (repr-transparent - // u64), ABI-compatible with `u64` here. - fn js_object_get_field_by_name(obj: *const ObjectHeader, key: *const StringHeader) -> u64; -} - -fn dispatch_nanbox_ptr(ptr: *mut T) -> f64 { - f64::from_bits(DISPATCH_POINTER_TAG | (ptr as u64 & DISPATCH_POINTER_MASK)) -} - -fn ensure_dispatch_registered() { - static REGISTER: std::sync::Once = std::sync::Once::new(); - REGISTER.call_once(|| unsafe { - js_register_handle_method_dispatch_extension(js_mysql2_handle_method_dispatch); - js_register_handle_property_dispatch_extension(js_mysql2_handle_property_dispatch); - }); -} - -/// Read a named field off a JS object value. Returns `JsValue::UNDEFINED` for a -/// non-object receiver or a missing key. -unsafe fn object_field_by_name(obj: JsValue, name: &str) -> JsValue { - let roots = TransientRootScope::enter(); - let obj = roots.root_nanbox(f64::from_bits(obj.bits())); - let key = alloc_string(name); - let obj_ptr = JsValue::from_bits(obj.get().to_bits()).as_pointer::(); - if obj_ptr.is_null() { - return JsValue::UNDEFINED; - } - let bits = js_object_get_field_by_name(obj_ptr, key.as_raw()); - JsValue::from_bits(bits) -} - -/// Parse both mysql2 query signatures while the JS arguments are rooted. -unsafe fn parse_query_request( - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> Result { - let roots = TransientRootScope::enter(); - let query = roots.root_nanbox(query_f); - let supplied_params = roots.root_nanbox(params_f); - let query_value = JsValue::from_bits(query.get().to_bits()); - let (sql, rows_as_array, option_values) = if query_value.is_any_string() { - ( - jsvalue_to_string(query_value).unwrap_or_default(), - false, - JsValue::UNDEFINED, - ) - } else if query_value.is_pointer() { - let sql = jsvalue_to_string(object_field_by_name( - JsValue::from_bits(query.get().to_bits()), - "sql", - )) - .ok_or_else(|| "Query options must include a SQL string".to_string())?; - let rows = object_field_by_name(JsValue::from_bits(query.get().to_bits()), "rowsAsArray"); - ( - sql, - rows.is_bool() && rows.to_bool(), - object_field_by_name(JsValue::from_bits(query.get().to_bits()), "values"), - ) - } else { - return Err("Query must be a SQL string or options object".to_string()); - }; - - let supplied_params = JsValue::from_bits(supplied_params.get().to_bits()); - let params = if supplied_params.is_undefined() { - option_values - } else { - supplied_params - }; - let params = extract_params_from_jsvalue(params)?; - Ok(QueryRequest::new( - sql, - params, - rows_as_array, - force_prepared, - )) -} - -/// Handle-method dispatch extension for mysql2 pool / connection handles. -#[no_mangle] -unsafe extern "C" fn js_mysql2_handle_method_dispatch( - handle: i64, - method_name_ptr: *const u8, - method_name_len: usize, - args_ptr: *const f64, - args_len: usize, - out: *mut f64, -) -> i32 { - if method_name_ptr.is_null() || method_name_len == 0 { - return 0; - } - let method = - match std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) { - Ok(m) => m, - Err(_) => return 0, - }; - let args: &[f64] = if args_ptr.is_null() || args_len == 0 { - &[] - } else { - std::slice::from_raw_parts(args_ptr, args_len) - }; - let arg = |i: usize| -> f64 { - args.get(i) - .copied() - .unwrap_or(f64::from_bits(DISPATCH_TAG_UNDEFINED)) - }; - - // Only claim methods for handles we actually own. - let is_pool = with_handle::(handle, |_| ()).is_some(); - let is_pool_conn = with_handle::(handle, |_| ()).is_some(); - let is_conn = with_handle::(handle, |_| ()).is_some(); - if !is_pool && !is_pool_conn && !is_conn { - return 0; - } - - let result: f64 = match method { - "query" | "execute" => { - let params_f = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(DISPATCH_TAG_UNDEFINED)); - let force_prepared = method == "execute"; - let promise = if is_pool { - run_pool_query(handle, arg(0), params_f, force_prepared) - } else if is_pool_conn { - run_pool_conn_query(handle, arg(0), params_f, force_prepared) - } else { - run_connection_query(handle, arg(0), params_f, force_prepared) - }; - dispatch_nanbox_ptr(promise) - } - "getConnection" if is_pool => dispatch_nanbox_ptr(js_mysql2_pool_get_connection(handle)), - "end" if is_pool => dispatch_nanbox_ptr(js_mysql2_pool_end(handle)), - "release" if is_pool_conn => { - js_mysql2_pool_connection_release(handle); - f64::from_bits(DISPATCH_TAG_UNDEFINED) - } - method if (is_pool_conn || is_conn) && transaction_sql_for_method(method).is_some() => { - let Some(sql) = transaction_sql_for_method(method) else { - return 0; - }; - dispatch_nanbox_ptr(run_simple_command(handle, sql)) - } - "end" if is_conn => dispatch_nanbox_ptr(js_mysql2_connection_end(handle)), - // `mysql2/promise` pools are already promise-based: `pool.promise()` - // returns the pool itself. Drizzle's `isCallbackClient` only reaches this - // when it mis-detects; return the same handle to be safe. - "promise" => dispatch_nanbox_ptr(handle as *mut u8), - _ => return 0, - }; - - if !out.is_null() { - *out = result; - } - 1 -} - -/// Reflect mysql2 methods as callable properties. Drizzle uses -/// `"getConnection" in client` to decide whether a transaction must check out -/// and pin a pool connection; method-call dispatch alone cannot satisfy that -/// probe. -#[no_mangle] -unsafe extern "C" fn js_mysql2_handle_property_dispatch( - handle: i64, - property_name_ptr: *const u8, - property_name_len: usize, - out: *mut f64, -) -> i32 { - if property_name_ptr.is_null() || property_name_len == 0 { - return 0; - } - let property = match std::str::from_utf8(std::slice::from_raw_parts( - property_name_ptr, - property_name_len, - )) { - Ok(property) => property, - Err(_) => return 0, - }; - let is_pool = with_handle::(handle, |_| ()).is_some(); - let is_pool_conn = with_handle::(handle, |_| ()).is_some(); - let is_conn = with_handle::(handle, |_| ()).is_some(); - let available = (is_pool - && matches!( - property, - "query" | "execute" | "end" | "getConnection" | "promise" - )) - || (is_pool_conn - && matches!( - property, - "query" | "execute" | "release" | "beginTransaction" | "commit" | "rollback" - )) - || (is_conn - && matches!( - property, - "query" - | "execute" - | "end" - | "beginTransaction" - | "commit" - | "rollback" - | "promise" - )); - if !available { - return 0; - } - - let value = js_class_method_bind( - dispatch_nanbox_ptr(handle as *mut u8), - property.as_ptr(), - property.len(), - ); - if !out.is_null() { - *out = value; - } - 1 -} - -#[no_mangle] -pub extern "C" fn js_mysql2_pool_end(pool_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - if let Some(wrapper) = take_handle::(pool_handle) { - tokio::runtime::Handle::current().block_on(wrapper.pool.close()); - promise.resolve_undefined(); - } else { - promise.reject_string("Invalid pool handle"); - } - }); - raw -} - -unsafe fn run_pool_query( - pool_handle: Handle, - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> *mut Promise { - let request = match parse_query_request(query_f, params_f, force_prepared) { - Ok(request) => request, - Err(message) => return rejected_params_promise(message), - }; - let pool = with_handle::(pool_handle, |wrapper| wrapper.pool.clone()); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let rows_as_array = request.rows_as_array; - let outcome: Result = tokio::runtime::Handle::current() - .block_on(async move { - let pool = pool.ok_or_else(|| MysqlPromiseError::message("Invalid pool handle"))?; - // Explicitly check out one connection for the whole request so - // statement preparation, bind encoding, execution, and result - // draining cannot be split across independent pool operations. - let mut conn = tokio::time::timeout( - Duration::from_secs(DEFAULT_ACQUIRE_TIMEOUT_SECS), - pool.acquire(), - ) - .await - .map_err(|_| MysqlPromiseError::message("Pool acquire timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Pool acquire failed", e))?; - execute_query_on_connection(&mut conn, &request).await - }); - match outcome { - // #1824: build the JS result on the MAIN thread. outcome_to_jsvalue - // allocates arrays/objects/strings, which is UB on this blocking-pool - // thread (worker thread-local arena → dangling on the main thread once - // the pooled thread idles out). `out` is plain Send Rust data. - Ok(out) => promise.resolve_with(move || outcome_to_jsvalue(&out, rows_as_array)), - Err(error) => error.reject(promise), - } - }); - raw -} - -/// # Safety -/// `query_f` must be a SQL string or mysql2 query-options object. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_query( - pool_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_query(pool_handle, query_f, params_f, false) -} - -/// # Safety -/// `query_f` must be a SQL string or mysql2 query-options object. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_execute( - pool_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_query(pool_handle, query_f, params_f, true) -} - -#[no_mangle] -pub extern "C" fn js_mysql2_pool_get_connection(pool_handle: Handle) -> *mut Promise { - let pool = with_handle::(pool_handle, |wrapper| wrapper.pool.clone()); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - let result = tokio::runtime::Handle::current().block_on(async move { - let pool = pool.ok_or_else(|| MysqlPromiseError::message("Invalid pool handle"))?; - tokio::time::timeout( - Duration::from_secs(DEFAULT_ACQUIRE_TIMEOUT_SECS), - pool.acquire(), - ) - .await - .map_err(|_| MysqlPromiseError::message("Pool acquire timed out"))? - .map_err(|e| MysqlPromiseError::from_sqlx("Pool acquire failed", e)) - }); - match result { - Ok(conn) => { - let h = register_handle(MysqlPoolConnectionHandle::new(conn)); - promise.resolve(JsValue::from_object_ptr(h as *mut ())); - } - Err(error) => error.reject(promise), - } - }); - raw -} - -/// `connection.release()` — drops the pool-connection handle so the -/// underlying `PoolConnection` returns to the pool via Drop. -#[no_mangle] -pub extern "C" fn js_mysql2_pool_connection_release(conn_handle: Handle) { - if let Some(wrapper) = take_handle::(conn_handle) { - // A query already in flight owns another Arc and holds this mutex. Wait - // for it to finish before dropping the checkout back into the pool. - spawn_blocking(move || { - tokio::runtime::Handle::current().block_on(async move { - wrapper.connection.lock().await.take(); - }); - }); - } -} - -unsafe fn run_pool_conn_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> *mut Promise { - let request = match parse_query_request(query_f, params_f, force_prepared) { - Ok(request) => request, - Err(message) => return rejected_params_promise(message), - }; - let connection = with_handle::(conn_handle, |wrapper| { - Arc::clone(&wrapper.connection) - }); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let rows_as_array = request.rows_as_array; - let outcome: Result = tokio::runtime::Handle::current() - .block_on(async move { - let connection = connection - .ok_or_else(|| MysqlPromiseError::message("Invalid pool-connection handle"))?; - let mut slot = connection.lock().await; - let conn = slot - .as_mut() - .ok_or_else(|| MysqlPromiseError::message("Pool connection released"))?; - execute_query_on_connection(conn, &request).await - }); - match outcome { - // #1824: build the JS result on the MAIN thread. outcome_to_jsvalue - // allocates arrays/objects/strings, which is UB on this blocking-pool - // thread (worker thread-local arena → dangling on the main thread once - // the pooled thread idles out). `out` is plain Send Rust data. - Ok(out) => promise.resolve_with(move || outcome_to_jsvalue(&out, rows_as_array)), - Err(error) => error.reject(promise), - } - }); - raw -} - -/// # Safety -/// `query_f` must be a SQL string or mysql2 query-options object. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_conn_query(conn_handle, query_f, params_f, false) -} - -/// # Safety -/// `query_f` must be a SQL string or mysql2 query-options object. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_connection_execute( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_conn_query(conn_handle, query_f, params_f, true) -} - -#[cfg(test)] -mod tests { - use super::*; - - unsafe fn runtime_string(ptr: *const perry_runtime::StringHeader) -> String { - assert!(!ptr.is_null()); - // SAFETY: callers pass a live runtime string pointer obtained from the - // Error object under test. - unsafe { perry_ffi::copy_string_from_raw(ptr) } - } - - #[test] - fn config_defaults() { - let cfg = MySqlConfig::default(); - assert_eq!(cfg.host, "localhost"); - assert_eq!(cfg.port, 3306); - assert_eq!(cfg.user, "root"); - } - - #[test] - fn url_encodes_password_special_chars() { - let cfg = MySqlConfig { - host: "h".into(), - port: 1, - user: "u".into(), - password: "p@s/s#".into(), - database: None, - }; - let url = cfg.to_url(); - assert!(url.contains("p%40s%2Fs%23")); - } - - #[test] - fn parse_uri_basic() { - let p = parse_mysql_uri("mysql://root:secret@db.example.com:3307/mydb").unwrap(); - assert_eq!(p.host, "db.example.com"); - assert_eq!(p.port, 3307); - assert_eq!(p.user, "root"); - assert_eq!(p.password, "secret"); - assert_eq!(p.database.as_deref(), Some("mydb")); - } - - #[test] - fn percent_decode_credentials() { - // Reserved characters in a percent-encoded password round-trip to the - // literal value the server actually expects. - assert_eq!(percent_decode("p%40ss"), "p@ss"); - assert_eq!(percent_decode("a%25b%2Fc%23"), "a%b/c#"); - assert_eq!(percent_decode("plain"), "plain"); - // A lone `%` (or one not followed by two hex digits) is kept verbatim. - assert_eq!(percent_decode("50%off"), "50%off"); - assert_eq!(percent_decode("trailing%"), "trailing%"); - assert_eq!(percent_decode("%zz"), "%zz"); - } - - #[test] - fn parse_uri_percent_encoded_password() { - // `@` inside the password is `%40`; the last `@` still splits creds/host. - let p = parse_mysql_uri("mysql://user:p%40ss%2Fword@db.example.com/mydb").unwrap(); - assert_eq!(p.user, "user"); - assert_eq!(p.password, "p@ss/word"); - assert_eq!(p.host, "db.example.com"); - } - - #[test] - fn is_row_returning_query_classifier() { - assert!(is_row_returning_query("SELECT 1")); - assert!(is_row_returning_query("SHOW TABLES")); - assert!(is_row_returning_query( - "WITH cte AS (SELECT 1) SELECT * FROM cte" - )); - assert!(!is_row_returning_query("INSERT INTO t VALUES (1)")); - assert!(!is_row_returning_query("UPDATE t SET x = 1")); - assert!(!is_row_returning_query("DELETE FROM t")); - } - - #[test] - fn query_request_keeps_each_statement_with_its_own_params() { - let ddl = QueryRequest::new("DROP TABLE IF EXISTS t".into(), Vec::new(), false, false); - let insert = QueryRequest::new( - "INSERT INTO t (name, cents) VALUES (?, ?)".into(), - vec![ParamValue::String("x".into()), ParamValue::Int(100)], - false, - true, - ); - let select = QueryRequest::new( - "SELECT * FROM t WHERE id = ?".into(), - vec![ParamValue::Int(1)], - false, - true, - ); - - assert_eq!(ddl.params, Vec::::new()); - assert_eq!(insert.params.len(), 2); - assert_eq!(select.params, vec![ParamValue::Int(1)]); - assert!(!ddl.uses_prepared_statement()); - assert!(insert.uses_prepared_statement()); - assert!(select.uses_prepared_statement()); - } - - #[test] - fn execute_stays_prepared_even_without_params() { - let execute = QueryRequest::new("SELECT 1".into(), Vec::new(), false, true); - assert!(execute.uses_prepared_statement()); - } - - #[test] - fn both_connection_handle_families_resolve_to_serialized_targets() { - let direct_connection = Arc::new(Mutex::new(None)); - let direct_handle = register_handle(MysqlConnectionHandle { - connection: Arc::clone(&direct_connection), - }); - let pool_connection = Arc::new(Mutex::new(None)); - let pool_handle = register_handle(MysqlPoolConnectionHandle { - connection: Arc::clone(&pool_connection), - }); - - match connection_target(direct_handle) { - Some(MysqlConnectionTarget::Direct(resolved)) => { - assert!(Arc::ptr_eq(&resolved, &direct_connection)); - let _guard = resolved - .try_lock() - .expect("first operation locks connection"); - assert!( - direct_connection.try_lock().is_err(), - "a second operation on the same connection must serialize" - ); - } - _ => panic!("direct connection handle was not resolved"), - } - match connection_target(pool_handle) { - Some(MysqlConnectionTarget::Pool(resolved)) => { - assert!(Arc::ptr_eq(&resolved, &pool_connection)); - let _guard = resolved - .try_lock() - .expect("first operation locks pool connection"); - assert!( - pool_connection.try_lock().is_err(), - "a second operation on the same checkout must serialize" - ); - } - _ => panic!("pool connection handle was not resolved"), - } - - take_handle::(direct_handle); - take_handle::(pool_handle); - } - - #[test] - fn pool_connections_expose_the_full_transaction_command_set() { - assert_eq!( - transaction_sql_for_method("beginTransaction"), - Some("START TRANSACTION") - ); - assert_eq!(transaction_sql_for_method("commit"), Some("COMMIT")); - assert_eq!(transaction_sql_for_method("rollback"), Some("ROLLBACK")); - assert_eq!(transaction_sql_for_method("release"), None); - } - - #[test] - fn parameter_extraction_preserves_every_supported_value() { - unsafe { - // Each heap value is built inside the iteration that pushes it. - // The eager form -- allocate all eight, then push them one at a - // time -- leaves every earlier value live across a `js_array_push` - // that can move it, which is the #8217 shape. - let expected_date = chrono::NaiveDate::from_ymd_opt(2024, 2, 3) - .unwrap() - .and_hms_milli_opt(4, 5, 6, 789) - .unwrap(); - // Built and consumed in one expression: no named local holds the - // array across the pushes that can move it. - let actual = extract_params_from_jsvalue(JsValue::from_object_ptr((0..8u32).fold( - js_array_alloc(8), - |acc, slot| { - let value = match slot { - 0 => JsValue::from_bits( - perry_runtime::JSValue::try_short_string(b"hi") - .expect("two-byte string uses the SSO representation") - .bits(), - ), - 1 => JsValue::from_string_ptr(alloc_string("long-string").as_raw()), - 2 => JsValue::from_int32(42), - 3 => JsValue::from_number(3.25), - 4 => JsValue::TRUE, - 5 => JsValue::NULL, - 6 => JsValue::from_bits( - perry_runtime::date::js_date_new_from_timestamp(1_706_933_106_789.0) - .to_bits(), - ), - _ => JsValue::from_object_ptr(perry_ffi::alloc_buffer(&[ - 0, 1, 127, 128, 255, - ])), - }; - js_array_push(acc, value) - }, - ))) - .expect("all supported parameter values must marshal"); - assert_eq!( - actual, - vec![ - ParamValue::String("hi".to_string()), - ParamValue::String("long-string".to_string()), - ParamValue::Int(42), - ParamValue::Number(3.25), - ParamValue::Bool(true), - ParamValue::Null, - ParamValue::DateTime(expected_date), - ParamValue::Bytes(vec![0, 1, 127, 128, 255]), - ] - ); - } - } - - #[test] - fn parameter_extraction_rejects_values_instead_of_substituting_null() { - unsafe { - // Nested so the fresh array is never a named local held across the - // push that can move it. - let undefined_array = js_array_push(js_array_alloc(1), JsValue::UNDEFINED); - let error = extract_params_from_jsvalue(JsValue::from_object_ptr(undefined_array)) - .expect_err("undefined must never become SQL NULL"); - assert_eq!(error, "Bind parameter at index 0 is undefined"); - - let object = perry_ffi::alloc_object(); - let error = extract_params_from_jsvalue(object) - .expect_err("a non-array params container must be rejected"); - assert_eq!(error, "Bind parameters must be an array"); - - let object_array = js_array_push(js_array_alloc(1), object); - let error = extract_params_from_jsvalue(JsValue::from_object_ptr(object_array)) - .expect_err("an unsupported parameter must never become SQL NULL"); - assert_eq!(error, "Unsupported bind parameter at index 0"); - } - } - - #[test] - fn mysql_server_error_metadata_matches_mysql2_shape() { - assert_eq!(mysql2_error_code(1062), Some("ER_DUP_ENTRY")); - assert_eq!(mysql2_error_code(1213), Some("ER_LOCK_DEADLOCK")); - assert_eq!(mysql2_error_code(u16::MAX), None); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - MysqlPromiseError { - message: "Query failed: 1062 duplicate entry".into(), - code: mysql2_error_code(1062), - errno: Some(1062), - } - .reject(promise); - - let reason = perry_runtime::promise::js_promise_reason(raw.cast()); - assert!( - JsValue::from_bits(perry_runtime::error::js_error_is_error(reason).to_bits()).to_bool() - ); - let reason = JsValue::from_bits(reason.to_bits()); - unsafe { - assert_eq!( - jsvalue_to_string(object_field_by_name(reason, "code")).as_deref(), - Some("ER_DUP_ENTRY") - ); - assert_eq!(object_field_by_name(reason, "errno").to_number(), 1062.0); - } - } - - #[test] - fn invalid_connection_rejects_with_error_object() { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("test tokio runtime"); - let _runtime_guard = runtime.enter(); - let sql = alloc_string("SELECT 1"); - - let promise = unsafe { - js_mysql2_connection_execute( - perry_ffi::INVALID_HANDLE, - f64::from_bits(JsValue::from_string_ptr(sql.as_raw()).bits()), - f64::from_bits(JsValue::UNDEFINED.bits()), - ) - }; - - assert_eq!(perry_runtime::promise::js_promise_state(promise.cast()), 2); - let reason = perry_runtime::promise::js_promise_reason(promise.cast()); - assert_eq!( - perry_runtime::error::js_error_is_error(reason).to_bits(), - JsValue::from_bool(true).bits() - ); - let error = - JsValue::from_bits(reason.to_bits()).as_pointer::(); - unsafe { - assert_eq!( - runtime_string((*error).message), - "Invalid connection handle" - ); - // #9486: through the accessor — the field is null until the - // first read materialises the string. - let stack = runtime_string(perry_runtime::error::js_error_get_stack(error)); - assert!(stack.contains("Error: Invalid connection handle")); - } - } -} diff --git a/crates/perry-ext-mysql2/src/test_async_shims.rs b/crates/perry-ext-mysql2/src/test_async_shims.rs deleted file mode 100644 index 23ff2d4486..0000000000 --- a/crates/perry-ext-mysql2/src/test_async_shims.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Test-only host shims for the standalone extension test binary. -//! -//! Production binaries receive these symbols from perry-stdlib's async bridge. - -use perry_ffi::{NativeAsyncCompletion, Promise}; -use std::ffi::c_void; - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_new() -> *mut Promise { - perry_runtime::promise::js_promise_new() as *mut Promise -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_resolve_bits(promise: *mut Promise, bits: u64) { - perry_runtime::promise::js_promise_resolve( - promise as *mut perry_runtime::Promise, - f64::from_bits(bits), - ); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_reject_bits(promise: *mut Promise, bits: u64) { - perry_runtime::promise::js_promise_reject( - promise as *mut perry_runtime::Promise, - f64::from_bits(bits), - ); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_resolve_deferred( - promise: *mut Promise, - ctx: *mut c_void, - invoke: extern "C" fn(*mut c_void) -> u64, -) { - perry_ffi_promise_resolve_bits(promise, invoke(ctx)); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_reject_deferred( - promise: *mut Promise, - ctx: *mut c_void, - invoke: extern "C" fn(*mut c_void) -> u64, -) { - perry_ffi_promise_reject_bits(promise, invoke(ctx)); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_spawn_blocking(ctx: *mut c_void, invoke: extern "C" fn(*mut c_void)) { - invoke(ctx); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_spawn_blocking_with_reactor( - ctx: *mut c_void, - invoke: extern "C" fn(*mut c_void), -) { - invoke(ctx); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_new(_flags: u32) -> *mut NativeAsyncCompletion { - std::ptr::null_mut() -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_promise( - _token: *mut NativeAsyncCompletion, -) -> *mut Promise { - std::ptr::null_mut() -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_resolve_bits( - _token: *mut NativeAsyncCompletion, - _bits: u64, -) -> i32 { - 0 -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_reject_bits( - _token: *mut NativeAsyncCompletion, - _bits: u64, -) -> i32 { - 0 -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_reject_string( - _token: *mut NativeAsyncCompletion, - _data: *const u8, - _len: usize, -) -> i32 { - 0 -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_cancel(_token: *mut NativeAsyncCompletion) -> i32 { - 0 -} - -#[no_mangle] -pub extern "C" fn perry_ffi_native_async_attach_handle( - _token: *mut NativeAsyncCompletion, - _handle_bits: u64, - _cleanup_flags: u32, -) -> i32 { - 0 -} - -#[no_mangle] -pub extern "C" fn perry_ffi_run_pending(_budget_ms: u64) {} diff --git a/crates/perry-ext-pg/Cargo.toml b/crates/perry-ext-pg/Cargo.toml deleted file mode 100644 index 2f250c14ee..0000000000 --- a/crates/perry-ext-pg/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "perry-ext-pg" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Native bindings for npm `pg` (PostgreSQL client) — uses only `perry-ffi`. Async via `sqlx::postgres` bridged through spawn_blocking + JsPromise + tokio::Handle::current().block_on. Both pre-connect handles and connection pool surfaces." - -[lints] -workspace = true - -[lib] -crate-type = ["staticlib", "rlib"] - -[dependencies] -perry-ffi.workspace = true -sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "postgres", "chrono"] } -tokio = { workspace = true } - -[dev-dependencies] -perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-pg/src/lib.rs b/crates/perry-ext-pg/src/lib.rs deleted file mode 100644 index 686efa4ee5..0000000000 --- a/crates/perry-ext-pg/src/lib.rs +++ /dev/null @@ -1,750 +0,0 @@ -//! Native bindings for the npm `pg` PostgreSQL client — uses only -//! perry-ffi. Async via `sqlx::postgres` bridged through -//! `spawn_blocking + JsPromise + tokio::Handle::current().block_on`. -//! -//! Mirrors perry-stdlib's existing surface: `Client` (pre-connect -//! / connected handle states with `.connect()` deferring the TCP -//! handshake), `Pool` (lazy `connect_lazy`-style + eager -//! `pg.createPool`), parameterized `query()` with `Null`/`String`/ -//! `Number`/`Int`/`Bool` param types, result objects with -//! `rows`/`fields`/`rowCount`/`command` keys, row objects keyed by -//! column name. BigInt param support deferred — perry-ffi's BigInt -//! surface is in place (v0.5.556) but the JS-side array iteration -//! shape needs an extra adapter; followup once any wrapper actually -//! demands it. - -use perry_ffi::{ - alloc_string, build_object_shape, get_handle_mut, js_array_alloc, js_array_get, js_array_push, - js_object_alloc_with_shape, js_object_get_field, js_object_set_field, register_handle, - spawn_blocking, take_handle, ArrayHeader, Handle, JsPromise, JsValue, ObjectHeader, Promise, - StringHeader, -}; -use sqlx::postgres::{PgColumn, PgConnection, PgPool, PgPoolOptions, PgRow}; -use sqlx::{Column, Connection, Row, TypeInfo}; - -/// Connection config — same field shape as perry-stdlib's PgConfig. -#[derive(Debug, Clone)] -pub struct PgConfig { - pub host: String, - pub port: u16, - pub user: String, - pub password: String, - pub database: Option, -} - -impl Default for PgConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 5432, - user: "postgres".to_string(), - password: String::new(), - database: None, - } - } -} - -impl PgConfig { - pub fn to_url(&self) -> String { - let db = self - .database - .as_ref() - .map(|d| format!("/{}", d)) - .unwrap_or_default(); - format!( - "postgres://{}:{}@{}:{}{}", - self.user, self.password, self.host, self.port, db - ) - } -} - -unsafe fn jsvalue_to_string(value: JsValue) -> Option { - if value.is_string() { - let ptr = value.as_string_ptr(); - if !ptr.is_null() { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - return std::str::from_utf8(bytes).ok().map(String::from); - } - } - None -} - -/// Object layout matches perry-stdlib's positional convention: -/// field 0: host (string) -/// field 1: port (number) -/// field 2: user (string) -/// field 3: password (string) -/// field 4: database (string, optional) -unsafe fn parse_pg_config(config: JsValue) -> PgConfig { - let mut result = PgConfig::default(); - let obj_ptr = config.as_pointer::(); - if obj_ptr.is_null() { - return result; - } - - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 0)) { - result.host = s; - } - let port_val = js_object_get_field(obj_ptr, 1); - if port_val.is_number() { - result.port = port_val.to_number() as u16; - } - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 2)) { - result.user = s; - } - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 3)) { - result.password = s; - } - let db_val = js_object_get_field(obj_ptr, 4); - if !db_val.is_undefined() && !db_val.is_null() { - if let Some(s) = jsvalue_to_string(db_val) { - result.database = Some(s); - } - } - result -} - -/// Convert a single column value to a JsValue, mapping common -/// PostgreSQL OIDs to JS scalars. Unknown types fall back to a -/// string read. -fn column_value_to_jsvalue(row: &PgRow, index: usize) -> JsValue { - let col = &row.columns()[index]; - let type_name = col.type_info().name(); - match type_name { - "INT4" | "INT2" => row - .try_get::(index) - .map(JsValue::from_int32) - .unwrap_or(JsValue::NULL), - "INT8" => row - .try_get::(index) - .map(|n| JsValue::from_number(n as f64)) - .unwrap_or(JsValue::NULL), - "FLOAT4" | "FLOAT8" | "NUMERIC" => row - .try_get::(index) - .map(JsValue::from_number) - .unwrap_or(JsValue::NULL), - "VARCHAR" | "CHAR" | "TEXT" | "BPCHAR" | "NAME" => row - .try_get::(index) - .map(|s| JsValue::from_string_ptr(alloc_string(&s).as_raw())) - .unwrap_or(JsValue::NULL), - "BOOL" => row - .try_get::(index) - .map(JsValue::from_bool) - .unwrap_or(JsValue::NULL), - _ => row - .try_get::(index) - .map(|s| JsValue::from_string_ptr(alloc_string(&s).as_raw())) - .unwrap_or(JsValue::NULL), - } -} - -/// Build a row object keyed by column names. Replaces perry-stdlib's -/// `js_object_alloc(0, n)` no-shape pattern with a perry-ffi -/// shape-aware allocation — same observable behavior since user code -/// accesses `row.id` through dynamic property lookup either way. -fn row_to_js_object(row: &PgRow) -> *mut ObjectHeader { - let cols: Vec<&str> = row.columns().iter().map(|c| c.name()).collect(); - let (packed, shape_id) = build_object_shape(&cols); - let obj = unsafe { - js_object_alloc_with_shape( - shape_id, - cols.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ) - }; - for i in 0..cols.len() { - let val = column_value_to_jsvalue(row, i); - unsafe { js_object_set_field(obj, i as u32, val) }; - } - obj -} - -/// Build a `FieldDef`-shaped object matching node-pg's `result.fields[i]` -/// (#4917): `dataTypeID` is the numeric type OID, `tableID`/`columnID` come -/// from the RowDescription (0 for expression columns, like Node). -/// `dataTypeSize`/`dataTypeModifier` are not exposed by sqlx 0.8 and report -/// the "unknown/variable" sentinel -1. Twin of -/// `perry_stdlib::pg::types::column_to_field_def` — keep in sync. -fn column_to_field_def(col: &PgColumn) -> *mut ObjectHeader { - let (packed, shape_id) = build_object_shape(&[ - "name", - "tableID", - "columnID", - "dataTypeID", - "dataTypeSize", - "dataTypeModifier", - "format", - ]); - let obj = - unsafe { js_object_alloc_with_shape(shape_id, 7, packed.as_ptr(), packed.len() as u32) }; - let name_str = alloc_string(col.name()); - let table_id = col.relation_id().map(|oid| oid.0 as f64).unwrap_or(0.0); - let column_id = col - .relation_attribute_no() - .map(|attno| attno as f64) - .unwrap_or(0.0); - let data_type_id = col.type_info().oid().map(|oid| oid.0 as f64).unwrap_or(0.0); - let format_str = alloc_string("text"); - unsafe { - js_object_set_field(obj, 0, JsValue::from_string_ptr(name_str.as_raw())); - js_object_set_field(obj, 1, JsValue::from_number(table_id)); - js_object_set_field(obj, 2, JsValue::from_number(column_id)); - js_object_set_field(obj, 3, JsValue::from_number(data_type_id)); - js_object_set_field(obj, 4, JsValue::from_number(-1.0)); - js_object_set_field(obj, 5, JsValue::from_number(-1.0)); - js_object_set_field(obj, 6, JsValue::from_string_ptr(format_str.as_raw())); - } - obj -} - -/// Wrap a query outcome in pg's `{ rows, fields, rowCount, command }` -/// result object. -fn rows_to_pg_result(rows: Vec, columns: &[PgColumn], command: &str) -> JsValue { - let (packed, shape_id) = build_object_shape(&["rows", "fields", "rowCount", "command"]); - let result_obj = - unsafe { js_object_alloc_with_shape(shape_id, 4, packed.as_ptr(), packed.len() as u32) }; - - // rows array - let mut rows_arr = unsafe { js_array_alloc(rows.len() as u32) }; - for row in &rows { - let row_obj = row_to_js_object(row); - rows_arr = unsafe { js_array_push(rows_arr, JsValue::from_object_ptr(row_obj)) }; - } - unsafe { js_object_set_field(result_obj, 0, JsValue::from_object_ptr(rows_arr)) }; - - // fields array - let mut fields_arr = unsafe { js_array_alloc(columns.len() as u32) }; - for col in columns { - let field_obj = column_to_field_def(col); - fields_arr = unsafe { js_array_push(fields_arr, JsValue::from_object_ptr(field_obj)) }; - } - unsafe { js_object_set_field(result_obj, 1, JsValue::from_object_ptr(fields_arr)) }; - - unsafe { - js_object_set_field(result_obj, 2, JsValue::from_number(rows.len() as f64)); - let cmd_str = alloc_string(command); - js_object_set_field(result_obj, 3, JsValue::from_string_ptr(cmd_str.as_raw())); - } - JsValue::from_object_ptr(result_obj) -} - -fn empty_pg_result(command: &str, row_count: u64) -> JsValue { - let value = rows_to_pg_result(Vec::new(), &[], command); - let obj: *mut ObjectHeader = value.as_pointer(); - if !obj.is_null() { - unsafe { - js_object_set_field(obj, 2, JsValue::from_number(row_count as f64)); - } - } - value -} - -#[derive(Clone, Debug)] -enum ParamValue { - Null, - String(String), - Number(f64), - Int(i64), - Bool(bool), -} - -unsafe fn extract_params_from_jsvalue(params: JsValue) -> Vec { - let arr_ptr = params.as_pointer::(); - if arr_ptr.is_null() { - return Vec::new(); - } - // Pull the array length out of the header — the layout matches - // perry-runtime's `ArrayHeader { length: u32, capacity: u32 }`. - let length = (*arr_ptr).length; - - let mut result = Vec::with_capacity(length as usize); - for i in 0..length { - let element = js_array_get(arr_ptr, i); - let param = if element.is_null() || element.is_undefined() { - ParamValue::Null - } else if element.is_string() { - jsvalue_to_string(element) - .map(ParamValue::String) - .unwrap_or(ParamValue::Null) - } else if element.is_int32() { - ParamValue::Int(element.to_int32() as i64) - } else if element.is_bool() { - ParamValue::Bool(element.to_bool()) - } else if element.is_number() { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - } else { - ParamValue::Null - }; - result.push(param); - } - result -} - -fn is_row_returning_query(sql: &str) -> bool { - let trimmed = sql.trim_start(); - let upper = trimmed.get(..10).unwrap_or(trimmed).to_uppercase(); - upper.starts_with("SELECT") - || upper.starts_with("SHOW") - || upper.starts_with("DESC") - || upper.starts_with("EXPLAIN") - || upper.starts_with("WITH") -} - -unsafe fn read_sql(sql_ptr: *const u8) -> String { - if sql_ptr.is_null() { - return String::new(); - } - let header = sql_ptr as *const StringHeader; - let len = (*header).byte_len as usize; - let data = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - std::str::from_utf8(bytes).unwrap_or("").to_string() -} - -// ── Connection (Client) ─────────────────────────────────────────── - -/// Wraps a `PgConnection` so it can sit in the handle registry. -/// Pre-connect: `pending_config = Some, connection = None`. -/// Connected: `pending_config = None, connection = Some`. -pub struct PgConnectionHandle { - pub connection: Option, - pub pending_config: Option, -} - -impl PgConnectionHandle { - pub fn new(conn: PgConnection) -> Self { - Self { - connection: Some(conn), - pending_config: None, - } - } - pub fn pending(config: PgConfig) -> Self { - Self { - connection: None, - pending_config: Some(config), - } - } -} - -/// `new Client(config)` — sync constructor, no TCP touch. -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue (passed as f64 at the FFI -/// boundary). -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_new(config_f: f64) -> Handle { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgConnectionHandle::pending(pg_config)) -} - -/// `client.connect()` — opens the TCP connection using the config -/// stored at `js_pg_client_new` time. No-op success if already -/// connected. -#[no_mangle] -pub extern "C" fn js_pg_client_connect(client_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - // Snapshot the pending config before entering spawn_blocking — - // can't hold a `&mut` across the boundary. - let pending = - get_handle_mut::(client_handle).and_then(|h| h.pending_config.take()); - - let Some(pg_config) = pending else { - promise.resolve_undefined(); - return raw; - }; - - spawn_blocking(move || { - let result = tokio::runtime::Handle::current() - .block_on(async move { PgConnection::connect(&pg_config.to_url()).await }); - match result { - Ok(conn) => { - if let Some(h) = get_handle_mut::(client_handle) { - h.connection = Some(conn); - } - promise.resolve_undefined(); - } - Err(e) => promise.reject_string(&format!("Failed to connect: {}", e)), - } - }); - raw -} - -/// Combined `pg.connect(config)` — sync `new` + async connect; older -/// API kept for back-compat with perry-stdlib callers. -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_pg_connect(config_f: f64) -> *mut Promise { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let result = tokio::runtime::Handle::current() - .block_on(async move { PgConnection::connect(&pg_config.to_url()).await }); - match result { - Ok(conn) => { - let handle = register_handle(PgConnectionHandle::new(conn)); - promise.resolve(JsValue::from_number(handle as f64)); - } - Err(e) => promise.reject_string(&format!("Failed to connect: {}", e)), - } - }); - raw -} - -/// `client.end()` — close the connection. -#[no_mangle] -pub extern "C" fn js_pg_client_end(client_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - if let Some(mut wrapper) = take_handle::(client_handle) { - if let Some(conn) = wrapper.connection.take() { - let result = tokio::runtime::Handle::current().block_on(conn.close()); - match result { - Ok(()) => promise.resolve_undefined(), - Err(e) => promise.reject_string(&format!("Failed to close connection: {}", e)), - } - } else { - promise.reject_string("Connection already closed"); - } - } else { - promise.reject_string("Invalid client handle"); - } - }); - raw -} - -/// `client.query(sql)` — no params. -/// -/// # Safety -/// `sql_ptr` must be null or a Perry-runtime `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query( - client_handle: Handle, - sql_ptr: *const u8, -) -> *mut Promise { - let sql = read_sql(sql_ptr); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let outcome = tokio::runtime::Handle::current().block_on(async move { - let wrapper = get_handle_mut::(client_handle) - .ok_or_else(|| "Invalid client handle".to_string())?; - let conn = wrapper - .connection - .as_mut() - .ok_or_else(|| "Connection already closed".to_string())?; - sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(conn) - .await - .map_err(|e| format!("Query failed: {}", e)) - }); - match outcome { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - let result = rows_to_pg_result(rows, &columns, &command); - promise.resolve(result); - } - Err(e) => promise.reject_string(&e), - } - }); - raw -} - -/// `client.query(sql, params)` — parameterized. -/// -/// # Safety -/// `sql_ptr` must be null or a Perry-runtime `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query_params( - client_handle: Handle, - sql_ptr: *const u8, - params_f: f64, -) -> *mut Promise { - let sql = read_sql(sql_ptr); - let params = JsValue::from_bits(params_f.to_bits()); - let param_values = extract_params_from_jsvalue(params); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - let is_select = is_row_returning_query(&sql); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let outcome = tokio::runtime::Handle::current().block_on(async move { - let wrapper = get_handle_mut::(client_handle) - .ok_or_else(|| "Invalid client handle".to_string())?; - let conn = wrapper - .connection - .as_mut() - .ok_or_else(|| "Connection already closed".to_string())?; - let mut query = sqlx::query(sqlx::AssertSqlSafe(sql.clone())); - for p in ¶m_values { - query = match p { - ParamValue::Null => query.bind(Option::::None), - ParamValue::String(s) => query.bind(s.clone()), - ParamValue::Number(n) => query.bind(*n), - ParamValue::Int(i) => query.bind(*i), - ParamValue::Bool(b) => query.bind(*b), - }; - } - if is_select { - let rows = query - .fetch_all(conn) - .await - .map_err(|e| format!("Query failed: {}", e))?; - Ok::<_, String>(QueryOutcome::Rows(rows)) - } else { - let exec_result = query - .execute(conn) - .await - .map_err(|e| format!("Query failed: {}", e))?; - Ok(QueryOutcome::RowsAffected(exec_result.rows_affected())) - } - }); - match outcome { - Ok(QueryOutcome::Rows(rows)) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - promise.resolve(rows_to_pg_result(rows, &columns, &command)); - } - Ok(QueryOutcome::RowsAffected(n)) => { - promise.resolve(empty_pg_result(&command, n)); - } - Err(e) => promise.reject_string(&e), - } - }); - raw -} - -enum QueryOutcome { - Rows(Vec), - RowsAffected(u64), -} - -// ── Pool ────────────────────────────────────────────────────────── - -pub struct PgPoolHandle { - pub pool: Option, - pub pending_url: Option, -} - -impl PgPoolHandle { - pub fn new(pool: PgPool) -> Self { - Self { - pool: Some(pool), - pending_url: None, - } - } - pub fn pending(url: String) -> Self { - Self { - pool: None, - pending_url: Some(url), - } - } - - pub async fn ensure_pool(&mut self) -> Result<&PgPool, String> { - if self.pool.is_none() { - let url = self - .pending_url - .take() - .ok_or_else(|| "Pool config missing".to_string())?; - let pool = PgPoolOptions::new() - .max_connections(10) - .connect(&url) - .await - .map_err(|e| format!("Failed to create pool: {}", e))?; - self.pool = Some(pool); - } - Ok(self.pool.as_ref().unwrap()) - } -} - -/// `new Pool(config)` — sync constructor; sqlx's pool is built lazily -/// on first query (sqlx 0.8's `connect_lazy` panics outside a Tokio -/// runtime, so we can't even pre-arm it here). -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_new(config_f: f64) -> Handle { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgPoolHandle::pending(pg_config.to_url())) -} - -/// `pg.createPool(config)` — async eager pool factory (back-compat -/// with perry-stdlib's older entry). -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_pg_create_pool(config_f: f64) -> *mut Promise { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let url = pg_config.to_url(); - let result = tokio::runtime::Handle::current() - .block_on(async move { PgPoolOptions::new().max_connections(10).connect(&url).await }); - match result { - Ok(pool) => { - let handle = register_handle(PgPoolHandle::new(pool)); - promise.resolve(JsValue::from_number(handle as f64)); - } - Err(e) => promise.reject_string(&format!("Failed to create pool: {}", e)), - } - }); - raw -} - -/// `pool.query(sql)` — runs against the lazy-built sqlx pool. -/// -/// # Safety -/// `sql_ptr` must be null or a Perry-runtime `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_query(pool_handle: Handle, sql_ptr: *const u8) -> *mut Promise { - let sql = read_sql(sql_ptr); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - let outcome = tokio::runtime::Handle::current().block_on(async move { - let wrapper = get_handle_mut::(pool_handle) - .ok_or_else(|| "Invalid pool handle".to_string())?; - let pool = wrapper.ensure_pool().await?; - sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(pool) - .await - .map_err(|e| format!("Query failed: {}", e)) - }); - match outcome { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - promise.resolve(rows_to_pg_result(rows, &columns, &command)); - } - Err(e) => promise.reject_string(&e), - } - }); - raw -} - -/// `pool.end()` — close all connections in the pool. -#[no_mangle] -pub extern "C" fn js_pg_pool_end(pool_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - if let Some(mut wrapper) = take_handle::(pool_handle) { - tokio::runtime::Handle::current().block_on(async move { - if let Some(pool) = wrapper.pool.take() { - pool.close().await; - } - }); - promise.resolve_undefined(); - } else { - promise.reject_string("Invalid pool handle"); - } - }); - raw -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pg_config_defaults() { - let cfg = PgConfig::default(); - assert_eq!(cfg.host, "localhost"); - assert_eq!(cfg.port, 5432); - assert_eq!(cfg.user, "postgres"); - assert!(cfg.database.is_none()); - } - - #[test] - fn to_url_omits_db_when_absent() { - let cfg = PgConfig::default(); - let url = cfg.to_url(); - assert_eq!(url, "postgres://postgres:@localhost:5432"); - } - - #[test] - fn to_url_with_db() { - let mut cfg = PgConfig::default(); - cfg.database = Some("mydb".to_string()); - cfg.user = "u".to_string(); - cfg.password = "p".to_string(); - cfg.host = "db.example.com".to_string(); - cfg.port = 5433; - assert_eq!(cfg.to_url(), "postgres://u:p@db.example.com:5433/mydb"); - } - - #[test] - fn is_row_returning_query_classifier() { - assert!(is_row_returning_query("SELECT * FROM x")); - assert!(is_row_returning_query(" select 1")); - assert!(is_row_returning_query("WITH cte AS ...")); - assert!(!is_row_returning_query("INSERT INTO x VALUES (1)")); - assert!(!is_row_returning_query("UPDATE x SET y = 1")); - } - - #[test] - fn client_new_returns_handle() { - let cfg_obj = unsafe { - let (packed, shape_id) = - build_object_shape(&["host", "port", "user", "password", "database"]); - let obj = js_object_alloc_with_shape(shape_id, 5, packed.as_ptr(), packed.len() as u32); - let host_str = alloc_string("localhost"); - js_object_set_field(obj, 0, JsValue::from_string_ptr(host_str.as_raw())); - js_object_set_field(obj, 1, JsValue::from_number(5432.0)); - JsValue::from_object_ptr(obj) - }; - let h = unsafe { js_pg_client_new(f64::from_bits(cfg_obj.bits())) }; - assert!(h > 0); - } -} diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index f92d99ed84..1186ac9c00 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -166,72 +166,6 @@ fn lower_os_module_method_call( } } -/// Recognize a bundled-mysql2 `createPool` / `createConnection` call by the -/// shape of its config object, so it can be routed to perry-ext-mysql2 even -/// when a bundler inlined mysql2 under a numeric module id (the import-keyed -/// native lowering can't see through that — see the call site). -/// -/// The signature is deliberately tight so it cannot hijack an unrelated -/// `createPool`/`createConnection`: the sole config argument must be an object -/// LITERAL that carries BOTH a mysql connection key (`uri`/`host`/`socketPath`) -/// AND at least one mysql2-specific driver/pool option (`connectionLimit`, -/// `waitForConnections`, `queueLimit`, `namedPlaceholders`, …). generic-pool's -/// `createPool(factory, opts)` passes a factory object with `create`/`destroy` -/// (no connection key); pg uses `new Pool()`, not `.createPool({...})`. The -/// older `mysql` package shares mysql2's exact API and wire protocol, so -/// routing it to perry-ext-mysql2 is correct too. A non-literal config -/// (variable / spread) returns `None` and falls through to normal lowering. -/// -/// Returns the canonical method name (`"createPool"` / `"createConnection"`). -/// -/// Pure signature check over a config object's field names. -fn mysql2_config_signature(method_name: &str, keys: &[&str]) -> Option<&'static str> { - let canonical = match method_name { - "createPool" => "createPool", - "createConnection" => "createConnection", - _ => return None, - }; - let mut has_conn_key = false; - let mut has_mysql2_opt = false; - for key in keys { - match *key { - "uri" | "host" | "socketPath" => has_conn_key = true, - "connectionLimit" | "waitForConnections" | "queueLimit" | "maxIdle" | "idleTimeout" - | "namedPlaceholders" | "rowsAsArray" | "enableKeepAlive" | "multipleStatements" - | "typeCast" => has_mysql2_opt = true, - _ => {} - } - } - if has_conn_key && has_mysql2_opt { - Some(canonical) - } else { - None - } -} - -/// Recover the config object's keys and run the mysql2 signature check. A closed -/// object literal is lowered to `New { class_name: "__AnonShape_*" }` with the -/// keys stripped into the shape class, so recover them from the lowering -/// context's `anon_shape_fields` map; a literal that stayed `Expr::Object` -/// (small / open shape) is inspected directly. -fn detect_bundled_mysql2_create( - ctx: &LoweringContext, - method_name: &str, - args: &[Expr], -) -> Option<&'static str> { - let keys: Vec<&str> = match args.first()? { - Expr::Object(pairs) => pairs.iter().map(|(k, _)| k.as_str()).collect(), - Expr::New { class_name, .. } => ctx - .anon_shape_fields - .get(class_name)? - .iter() - .map(|s| s.as_str()) - .collect(), - _ => return None, - }; - mysql2_config_signature(method_name, &keys) -} - fn is_node_core(module: &str) -> bool { crate::ir::is_node_builtin_module(module.strip_prefix("node:").unwrap_or(module)) } @@ -272,7 +206,7 @@ fn is_submodule_export(module: &str, export: &str) -> bool { /// namespace/default import (`import path from "node:path"`), a sub-namespace /// export of one (`import { posix } from "node:path"`), or a `require()` alias? /// -/// Deliberately node-core ONLY: an ext/npm native module (`mysql2`, `redis`, +/// Deliberately node-core ONLY: an ext/npm native module (`mongodb`, `redis`, /// `undici`) is served by codegen-wired `NativeMethodCall` rows with no /// by-name runtime dispatcher behind them, so declining its fast path would /// trade a wrong answer for no answer. Every name this returns `true` for @@ -427,36 +361,6 @@ pub(super) fn try_native_module_methods( } // Check for native module method calls (e.g., mysql.createConnection()) if let ast::Expr::Member(member) = expr { - // Bundled mysql2 (webpack/turbopack): when a bundler inlines mysql2 - // under a numeric module id, the `createPool(...)` receiver is an - // opaque `E.i(87205).default`, not a tracked native import — so the - // identifier-keyed lowering below never fires and the call runs the - // inlined JS mysql2, which JIT-compiles its row parsers with - // `new Function` (via `generate-function`). An AOT binary cannot - // execute a function built from a runtime string, so every query - // throws. Recognize the call by its mysql2 config-object SIGNATURE - // (tight enough to be mysql/mysql2-exclusive) and route it to - // perry-ext-mysql2 regardless of how mysql2 was imported/bundled. - // The receiver is intentionally dropped: we don't want the JS module - // loaded at all. Downstream typing (`detect_native_instance_creation`) - // sees `NativeMethodCall{module:"mysql2/promise", method:"createPool"}` - // and tags the result `Pool`, so `pool.execute`/`pool.query` dispatch - // natively too; the emitted `js_mysql2_*` FFIs flip the "mysql2" - // well-known (see perry-codegen `ext_registry`) to link the staticlib. - if let ast::MemberProp::Ident(method_ident) = &member.prop { - if let Some(canonical) = - detect_bundled_mysql2_create(ctx, method_ident.sym.as_ref(), &args) - { - return Ok(Ok(Expr::NativeMethodCall { - module: "mysql2/promise".to_string(), - class_name: None, - object: None, - method: canonical.to_string(), - args, - })); - } - } - // Inline `require("node:os").platform()` reaches this outer member // call before the inner bare `require(...)` lowering can produce a // NativeModuleRef. Recognize the same literal-native namespace shape @@ -734,8 +638,8 @@ pub(super) fn try_native_module_methods( } #[cfg(test)] -mod bundled_mysql2_tests { - use super::{is_process_active_array_helper, mysql2_config_signature}; +mod native_module_helper_tests { + use super::is_process_active_array_helper; #[test] fn process_active_array_helper_predicate_matches_supported_methods() { @@ -743,68 +647,4 @@ mod bundled_mysql2_tests { assert!(is_process_active_array_helper("_getActiveRequests")); assert!(!is_process_active_array_helper("getActiveResourcesInfo")); } - - #[test] - fn matches_pool_with_uri_and_pool_option() { - // gscmaster's exact config: uri + mysql2 pool options. - let keys = [ - "uri", - "waitForConnections", - "connectionLimit", - "maxIdle", - "idleTimeout", - "queueLimit", - ]; - assert_eq!( - mysql2_config_signature("createPool", &keys), - Some("createPool") - ); - } - - #[test] - fn matches_host_credentials_with_pool_option() { - let keys = ["host", "user", "password", "database", "connectionLimit"]; - assert_eq!( - mysql2_config_signature("createPool", &keys), - Some("createPool") - ); - } - - #[test] - fn matches_create_connection_with_mysql2_option() { - let keys = ["host", "user", "password", "namedPlaceholders"]; - assert_eq!( - mysql2_config_signature("createConnection", &keys), - Some("createConnection") - ); - } - - #[test] - fn rejects_without_connection_key() { - // Pool options but no connection key — not enough to be sure it's mysql2. - let keys = ["connectionLimit", "waitForConnections"]; - assert_eq!(mysql2_config_signature("createPool", &keys), None); - } - - #[test] - fn rejects_without_mysql2_option() { - // A bare connection config could be any driver; require a mysql2 option. - let keys = ["host", "user", "password", "database"]; - assert_eq!(mysql2_config_signature("createPool", &keys), None); - } - - #[test] - fn rejects_generic_pool_factory() { - // generic-pool's `createPool(factory, opts)` — first arg is a factory - // object with create/destroy, no connection or mysql2 keys. - let keys = ["create", "destroy", "validate"]; - assert_eq!(mysql2_config_signature("createPool", &keys), None); - } - - #[test] - fn rejects_unrelated_method() { - let keys = ["uri", "connectionLimit"]; - assert_eq!(mysql2_config_signature("connect", &keys), None); - assert_eq!(mysql2_config_signature("createServer", &keys), None); - } } diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index e08a9f15cd..2c092599f4 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -160,15 +160,7 @@ external-tls-server = [ ] # Databases -database = ["database-postgres", "database-mysql", "database-sqlite", "database-redis", "database-mongodb"] -# `database-postgres` umbrella retained for backwards-compat; -# v0.5.566's well-known flip toggles `bundled-pg` instead. -database-postgres = ["bundled-pg"] -bundled-pg = ["dep:sqlx", "async-runtime"] -# `database-mysql` umbrella retained for backwards-compat; -# v0.5.567's well-known flip toggles `bundled-mysql2` instead. -database-mysql = ["bundled-mysql2"] -bundled-mysql2 = ["dep:sqlx", "async-runtime"] +database = ["database-sqlite", "database-redis", "database-mongodb"] database-sqlite = ["dep:rusqlite"] # `database-redis` umbrella retained for backwards-compat; # v0.5.565's well-known flip toggles `bundled-ioredis` instead. @@ -314,7 +306,7 @@ rustls-native-certs = { version = "0.8", optional = true } rustls-pemfile = { workspace = true, optional = true } # Database -sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql", "postgres", "chrono"], optional = true } +sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql", "chrono"], optional = true } redis = { version = "1.2", features = ["tokio-comp", "connection-manager"], optional = true } mongodb = { version = "3.7", default-features = false, features = ["bson-3", "compat-3-3-0", "rustls-tls", "dns-resolver"], optional = true } bson = { version = "3.1", optional = true, features = ["serde"] } diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs index ac7079b3b8..abdf636be3 100644 --- a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -1,8 +1,4 @@ -#[cfg(any( - feature = "crypto", - feature = "database-redis", - feature = "bundled-mysql2" -))] +#[cfg(any(feature = "crypto", feature = "database-redis"))] use super::super::handle::with_handle; use super::*; @@ -215,13 +211,6 @@ pub unsafe extern "C" fn js_handle_method_dispatch( return value; } - // mysql2 handles frequently pass through interface-typed fields in Drizzle, - // which removes the static class information used by native lowering. - #[cfg(feature = "bundled-mysql2")] - if let Some(value) = crate::mysql2::dispatch_mysql2_method(handle, method_name, &args) { - return value; - } - // node:sqlite DatabaseSync handle. Keep this before the better-sqlite3 // SQLite fallbacks because method names like prepare/exec/close overlap // but the lifecycle/error semantics are intentionally different. diff --git a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs index a44d648a42..cf1559ad00 100644 --- a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs @@ -1,8 +1,4 @@ -#[cfg(any( - feature = "crypto", - feature = "http-client", - feature = "bundled-mysql2" -))] +#[cfg(any(feature = "crypto", feature = "http-client"))] use super::super::handle::with_handle; use super::*; @@ -44,11 +40,6 @@ pub unsafe extern "C" fn js_handle_property_dispatch( return value; } - #[cfg(feature = "bundled-mysql2")] - if let Some(value) = crate::mysql2::dispatch_mysql2_property(handle, property_name) { - return value; - } - #[cfg(all( feature = "tls-runtime", not(target_os = "ios"), diff --git a/crates/perry-stdlib/src/lib.rs b/crates/perry-stdlib/src/lib.rs index b1b932195f..457742fc03 100644 --- a/crates/perry-stdlib/src/lib.rs +++ b/crates/perry-stdlib/src/lib.rs @@ -192,24 +192,12 @@ pub mod tls; pub use tls::*; // === Databases === -// pg lives behind `bundled-pg` (v0.5.566); mysql2 lives behind -// `bundled-mysql2` (v0.5.567). Either feature pulls in sqlx, so -// the modules' `#[cfg(any(...))]` covers both bundled gates plus -// the legacy `database-postgres`/`database-mysql` umbrellas (kept -// for backwards-compat). -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub mod pg; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub use pg::connection::*; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub use pg::pool::*; - -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub mod mysql2; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub use mysql2::connection::*; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub use mysql2::pool::*; +// Both in-tree database wrappers that lived here are gone: the `pg` +// module + `bundled-pg` feature (#10677) and the `mysql2` module + +// `bundled-mysql2` feature (#10680), the pre-#466 native +// reimplementations of the `pg` and `mysql2` npm packages. Perry now +// compiles both real packages from source instead of shipping bundled +// reimplementations. #[cfg(feature = "database-sqlite")] pub mod sqlite; diff --git a/crates/perry-stdlib/src/mysql2/connection.rs b/crates/perry-stdlib/src/mysql2/connection.rs deleted file mode 100644 index b2d3d56fb8..0000000000 --- a/crates/perry-stdlib/src/mysql2/connection.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! MySQL connection implementation. - -use std::sync::Arc; -use std::time::Duration; - -use perry_runtime::{js_promise_new_cross_thread, JSValue, Promise}; -use sqlx::mysql::MySqlConnection; -use sqlx::Connection; -use tokio::sync::Mutex; - -use super::pool::{ - execute_query_on_connection, parse_query_request, MysqlPoolConnectionHandle, MysqlPromiseError, - QueryRequest, DEFAULT_QUERY_TIMEOUT_SECS, -}; -use super::result::QueryOutcome; -use super::types::parse_mysql_config; -use crate::common::{register_handle, take_handle, with_handle, Handle}; - -const CONNECT_TIMEOUT_SECS: u64 = 10; - -pub struct MysqlConnectionHandle { - pub connection: Arc>>, -} - -impl MysqlConnectionHandle { - pub fn new(conn: MySqlConnection) -> Self { - Self { - connection: Arc::new(Mutex::new(Some(conn))), - } - } -} - -#[derive(Clone)] -pub(crate) enum MysqlConnectionTarget { - Direct(Arc>>), - Pool(Arc>>>), -} - -pub(crate) fn connection_target(handle: Handle) -> Option { - with_handle::(handle, |wrapper| { - MysqlConnectionTarget::Direct(Arc::clone(&wrapper.connection)) - }) - .or_else(|| { - with_handle::(handle, |wrapper| { - MysqlConnectionTarget::Pool(Arc::clone(&wrapper.connection)) - }) - }) -} - -async fn execute_query_on_target( - target: MysqlConnectionTarget, - request: &QueryRequest, -) -> Result { - match target { - MysqlConnectionTarget::Direct(connection) => { - let mut slot = connection.lock().await; - let connection = slot - .as_mut() - .ok_or_else(|| MysqlPromiseError::message("Connection already closed"))?; - execute_query_on_connection(connection, request).await - } - MysqlConnectionTarget::Pool(connection) => { - let mut slot = connection.lock().await; - let connection = slot - .as_mut() - .ok_or_else(|| MysqlPromiseError::message("Pool connection released"))?; - execute_query_on_connection(connection, request).await - } - } -} - -unsafe fn run_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let request = parse_query_request(query_f, params_f, force_prepared); - let target = connection_target(conn_handle); - let rows_as_array = request - .as_ref() - .map(|request| request.rows_as_array) - .unwrap_or(false); - - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let request = request?; - let target = - target.ok_or_else(|| MysqlPromiseError::message("Invalid connection handle"))?; - execute_query_on_target(target, &request).await - }, - move |outcome| outcome.to_jsvalue_with_rows_as_array(rows_as_array).bits(), - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -pub(crate) fn transaction_sql_for_method(method: &str) -> Option<&'static str> { - match method { - "beginTransaction" => Some("START TRANSACTION"), - "commit" => Some("COMMIT"), - "rollback" => Some("ROLLBACK"), - _ => None, - } -} - -pub(crate) fn run_simple_command(conn_handle: Handle, sql: &'static str) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let target = connection_target(conn_handle); - unsafe { - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let target = target - .ok_or_else(|| MysqlPromiseError::message("Invalid connection handle"))?; - let execute = async { - match target { - MysqlConnectionTarget::Direct(connection) => { - let mut slot = connection.lock().await; - let connection = slot.as_mut().ok_or_else(|| { - MysqlPromiseError::message("Connection already closed") - })?; - sqlx::raw_sql(sql) - .execute(connection) - .await - .map_err(|error| MysqlPromiseError::from_sqlx(sql, error))?; - } - MysqlConnectionTarget::Pool(connection) => { - let mut slot = connection.lock().await; - let connection = slot.as_mut().ok_or_else(|| { - MysqlPromiseError::message("Pool connection released") - })?; - sqlx::raw_sql(sql) - .execute(&mut **connection) - .await - .map_err(|error| MysqlPromiseError::from_sqlx(sql, error))?; - } - } - Ok::<_, MysqlPromiseError>(JSValue::undefined().bits()) - }; - tokio::time::timeout(Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), execute) - .await - .map_err(|_| MysqlPromiseError::message(format!("{sql} timed out")))? - }, - |bits| bits, - MysqlPromiseError::to_jsvalue_bits, - ); - } - promise -} - -/// mysql.createConnection(config) -> Promise. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_create_connection(config_f: f64) -> *mut Promise { - let config = JSValue::from_bits(config_f.to_bits()); - let mysql_config = parse_mysql_config(config); - let promise = js_promise_new_cross_thread(); - - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let connection = tokio::time::timeout( - Duration::from_secs(CONNECT_TIMEOUT_SECS), - MySqlConnection::connect(&mysql_config.to_url()), - ) - .await - .map_err(|_| MysqlPromiseError::message("MySQL connection timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Failed to connect", error))?; - Ok(connection) - }, - |connection| { - let handle = register_handle(MysqlConnectionHandle::new(connection)); - perry_runtime::js_nanbox_pointer(handle).to_bits() - }, - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_end(conn_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let connection = take_handle::(conn_handle) - .map(|wrapper| Arc::clone(&wrapper.connection)); - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let connection = connection - .ok_or_else(|| MysqlPromiseError::message("Invalid connection handle"))?; - let connection = connection - .lock() - .await - .take() - .ok_or_else(|| MysqlPromiseError::message("Connection already closed"))?; - tokio::time::timeout( - Duration::from_secs(CONNECT_TIMEOUT_SECS), - connection.close(), - ) - .await - .map_err(|_| MysqlPromiseError::message("Connection close timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Failed to close", error))?; - Ok(JSValue::undefined().bits()) - }, - |bits| bits, - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_connection_query(conn_handle, query_f, params_f, false) -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_execute( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_connection_query(conn_handle, query_f, params_f, true) -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_begin_transaction( - conn_handle: Handle, -) -> *mut Promise { - run_simple_command(conn_handle, "START TRANSACTION") -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_commit(conn_handle: Handle) -> *mut Promise { - run_simple_command(conn_handle, "COMMIT") -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_connection_rollback(conn_handle: Handle) -> *mut Promise { - run_simple_command(conn_handle, "ROLLBACK") -} diff --git a/crates/perry-stdlib/src/mysql2/mod.rs b/crates/perry-stdlib/src/mysql2/mod.rs deleted file mode 100644 index ac9d4f5855..0000000000 --- a/crates/perry-stdlib/src/mysql2/mod.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! mysql2 compatible native implementation -//! -//! Provides a drop-in replacement for the mysql2 npm package using sqlx. - -pub mod connection; -pub mod pool; -pub mod result; -pub mod types; - -pub use connection::*; -pub use pool::*; -pub use result::*; -pub use types::*; - -#[derive(Clone, Copy, PartialEq, Eq)] -enum MysqlHandleKind { - Pool, - PoolConnection, - Connection, -} - -fn mysql_handle_kind(handle: crate::common::Handle) -> Option { - crate::common::with_handle::(handle, |_| MysqlHandleKind::Pool) - .or_else(|| { - crate::common::with_handle::(handle, |_| { - MysqlHandleKind::PoolConnection - }) - }) - .or_else(|| { - crate::common::with_handle::(handle, |_| { - MysqlHandleKind::Connection - }) - }) -} - -fn method_is_available(kind: MysqlHandleKind, method: &str) -> bool { - match kind { - MysqlHandleKind::Pool => { - matches!( - method, - "query" | "execute" | "end" | "getConnection" | "promise" - ) - } - MysqlHandleKind::PoolConnection => matches!( - method, - "query" | "execute" | "release" | "beginTransaction" | "commit" | "rollback" - ), - MysqlHandleKind::Connection => matches!( - method, - "query" | "execute" | "end" | "beginTransaction" | "commit" | "rollback" | "promise" - ), - } -} - -/// Runtime method dispatch for mysql2 handles whose static TypeScript class -/// was erased (notably Drizzle's interface-typed client fields). -pub(crate) unsafe fn dispatch_mysql2_method( - handle: crate::common::Handle, - method: &str, - args: &[f64], -) -> Option { - let kind = mysql_handle_kind(handle)?; - if !method_is_available(kind, method) { - return None; - } - let undefined = crate::common::TAG_UNDEFINED_F64; - let arg = |index: usize| args.get(index).copied().unwrap_or(undefined); - let pointer = |ptr: *mut perry_runtime::Promise| { - f64::from_bits(perry_runtime::JSValue::pointer(ptr as *const u8).bits()) - }; - - Some(match (kind, method) { - (MysqlHandleKind::Pool, "query") => { - pointer(pool::js_mysql2_pool_query(handle, arg(0), arg(1))) - } - (MysqlHandleKind::Pool, "execute") => { - pointer(pool::js_mysql2_pool_execute(handle, arg(0), arg(1))) - } - (MysqlHandleKind::Pool, "getConnection") => { - pointer(pool::js_mysql2_pool_get_connection(handle)) - } - (MysqlHandleKind::Pool, "end") => pointer(pool::js_mysql2_pool_end(handle)), - (MysqlHandleKind::PoolConnection, "query") => pointer( - pool::js_mysql2_pool_connection_query(handle, arg(0), arg(1)), - ), - (MysqlHandleKind::PoolConnection, "execute") => pointer( - pool::js_mysql2_pool_connection_execute(handle, arg(0), arg(1)), - ), - (MysqlHandleKind::PoolConnection, "release") => { - pool::js_mysql2_pool_connection_release(handle); - undefined - } - (MysqlHandleKind::Connection, "query") => pointer(connection::js_mysql2_connection_query( - handle, - arg(0), - arg(1), - )), - (MysqlHandleKind::Connection, "execute") => pointer( - connection::js_mysql2_connection_execute(handle, arg(0), arg(1)), - ), - (MysqlHandleKind::Connection, "end") => { - pointer(connection::js_mysql2_connection_end(handle)) - } - (MysqlHandleKind::PoolConnection | MysqlHandleKind::Connection, method) - if connection::transaction_sql_for_method(method).is_some() => - { - let sql = connection::transaction_sql_for_method(method)?; - pointer(connection::run_simple_command(handle, sql)) - } - (MysqlHandleKind::Pool | MysqlHandleKind::Connection, "promise") => { - crate::common::nanbox_handle_value(handle) - } - _ => return None, - }) -} - -/// Property reads for mysql2 methods return a bound method. This makes -/// `Reflect.has(pool, "getConnection")`, `"getConnection" in pool`, and -/// `typeof pool.getConnection` agree with the real mysql2 objects. -pub(crate) unsafe fn dispatch_mysql2_property( - handle: crate::common::Handle, - property: &str, -) -> Option { - let kind = mysql_handle_kind(handle)?; - if !method_is_available(kind, property) { - return None; - } - Some(perry_runtime::object::js_class_method_bind( - crate::common::nanbox_handle_value(handle), - property.as_ptr(), - property.len(), - )) -} diff --git a/crates/perry-stdlib/src/mysql2/pool.rs b/crates/perry-stdlib/src/mysql2/pool.rs deleted file mode 100644 index 467de0fdc1..0000000000 --- a/crates/perry-stdlib/src/mysql2/pool.rs +++ /dev/null @@ -1,628 +0,0 @@ -//! MySQL connection pool implementation. - -use std::sync::Arc; -use std::time::Duration; - -use perry_runtime::{ - js_array_get_jsvalue, js_array_length, js_object_get_field_by_name, - js_promise_new_cross_thread, js_string_from_bytes, JSValue, Promise, -}; -use sqlx::mysql::{MySqlConnection, MySqlDatabaseError, MySqlPool, MySqlPoolOptions}; -use sqlx::pool::PoolConnection; -use sqlx::MySql; -use tokio::sync::Mutex; - -use super::result::{is_row_returning_query, QueryOutcome, RawQueryResult}; -use super::types::parse_mysql_config; -use crate::common::{register_handle, take_handle, with_handle, Handle}; - -pub(crate) const DEFAULT_ACQUIRE_TIMEOUT_SECS: u64 = 10; -const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10; -pub(crate) const DEFAULT_QUERY_TIMEOUT_SECS: u64 = 30; - -pub struct MysqlPoolHandle { - pub pool: MySqlPool, -} - -impl MysqlPoolHandle { - pub fn new(pool: MySqlPool) -> Self { - Self { pool } - } -} - -/// A checked-out pool connection. The registry entry can be removed while an -/// operation is in flight, so the connection itself is shared and serialized. -pub struct MysqlPoolConnectionHandle { - pub connection: Arc>>>, -} - -impl MysqlPoolConnectionHandle { - pub fn new(conn: PoolConnection) -> Self { - Self { - connection: Arc::new(Mutex::new(Some(conn))), - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) enum ParamValue { - Null, - String(String), - Bytes(Vec), - DateTime(chrono::NaiveDateTime), - Number(f64), - Int(i64), - Bool(bool), -} - -/// Owned data for one mysql2 request. No pointer into the Perry heap crosses -/// the async boundary. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct QueryRequest { - pub(crate) sql: String, - pub(crate) params: Vec, - pub(crate) rows_as_array: bool, - force_prepared: bool, -} - -impl QueryRequest { - fn is_row_returning(&self) -> bool { - is_row_returning_query(&self.sql) - } - - fn uses_prepared_statement(&self) -> bool { - self.force_prepared || !self.params.is_empty() - } -} - -#[derive(Debug)] -pub(crate) struct MysqlPromiseError { - message: String, - code: Option<&'static str>, - errno: Option, -} - -impl MysqlPromiseError { - pub(crate) fn message(message: impl Into) -> Self { - Self { - message: message.into(), - code: None, - errno: None, - } - } - - pub(crate) fn from_sqlx(context: &str, error: sqlx::Error) -> Self { - let errno = error - .as_database_error() - .and_then(|database| database.try_downcast_ref::()) - .map(MySqlDatabaseError::number); - Self { - message: format!("{context}: {error}"), - code: errno.and_then(mysql2_error_code), - errno, - } - } - - /// Build the rejection value on the main thread. mysql2 rejects with an - /// Error object, not the bare string previously emitted by the fallback. - pub(crate) fn to_jsvalue_bits(self) -> u64 { - if let Some(errno) = self.errno { - let code = self.code.unwrap_or(""); - return unsafe { - perry_runtime::error::js_node_system_error_value( - self.message.as_ptr(), - self.message.len(), - code.as_ptr(), - code.len(), - std::ptr::null(), - 0, - f64::from(errno), - ) - .to_bits() - }; - } - - let message = js_string_from_bytes(self.message.as_ptr(), self.message.len() as u32); - let error = perry_runtime::error::js_error_new_with_message(message); - JSValue::pointer(error as *const u8).bits() - } -} - -/// Symbolic names exposed by mysql2 for common server errors. Unknown server -/// errors still carry their numeric `.errno`. -fn mysql2_error_code(errno: u16) -> Option<&'static str> { - Some(match errno { - 1022 => "ER_DUP_KEY", - 1045 => "ER_ACCESS_DENIED_ERROR", - 1048 => "ER_BAD_NULL_ERROR", - 1049 => "ER_BAD_DB_ERROR", - 1050 => "ER_TABLE_EXISTS_ERROR", - 1051 => "ER_BAD_TABLE_ERROR", - 1052 => "ER_NON_UNIQ_ERROR", - 1054 => "ER_BAD_FIELD_ERROR", - 1062 => "ER_DUP_ENTRY", - 1064 => "ER_PARSE_ERROR", - 1146 => "ER_NO_SUCH_TABLE", - 1169 => "ER_DUP_UNIQUE", - 1205 => "ER_LOCK_WAIT_TIMEOUT", - 1213 => "ER_LOCK_DEADLOCK", - 1216 => "ER_NO_REFERENCED_ROW", - 1217 => "ER_ROW_IS_REFERENCED", - 1264 => "ER_WARN_DATA_OUT_OF_RANGE", - 1292 => "ER_TRUNCATED_WRONG_VALUE", - 1364 => "ER_NO_DEFAULT_FOR_FIELD", - 1406 => "ER_DATA_TOO_LONG", - 1451 => "ER_ROW_IS_REFERENCED_2", - 1452 => "ER_NO_REFERENCED_ROW_2", - 1586 => "ER_DUP_ENTRY_WITH_KEY_NAME", - 1830 => "ER_FK_COLUMN_NOT_NULL", - 1834 => "ER_FK_CANNOT_DELETE_PARENT", - 1859 => "ER_DUP_UNKNOWN_IN_INDEX", - 3819 => "ER_CHECK_CONSTRAINT_VIOLATED", - 4025 => "ER_CONSTRAINT_FAILED", - _ => return None, - }) -} - -unsafe fn jsvalue_to_string(value: JSValue) -> Option { - let mut scratch = [0; perry_runtime::value::SHORT_STRING_MAX_LEN]; - let (ptr, len) = - perry_runtime::string::str_bytes_from_jsvalue(f64::from_bits(value.bits()), &mut scratch)?; - if ptr.is_null() { - return Some(String::new()); - } - let bytes = std::slice::from_raw_parts(ptr, len as usize); - Some(String::from_utf8_lossy(bytes).into_owned()) -} - -unsafe fn object_pointer(value: JSValue) -> Option<*const perry_runtime::ObjectHeader> { - if value.is_pointer() { - let ptr = value.as_pointer::(); - return (!ptr.is_null()).then_some(ptr); - } - - // Some generic call sites still pass an untagged object pointer. - let bits = value.bits(); - if bits != 0 && bits <= 0x0000_7FFF_FFFF_FFFF { - return Some(bits as *const perry_runtime::ObjectHeader); - } - None -} - -unsafe fn object_field(value: JSValue, name: &str) -> JSValue { - // Allocating the lookup key can trigger a moving collection. Root and - // refresh the receiver before dereferencing it afterwards. - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_u64(value.bits()); - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - let Some(object) = object_pointer(JSValue::from_bits(receiver.get_nanbox_u64())) else { - return JSValue::undefined(); - }; - js_object_get_field_by_name(object, key) -} - -/// Parse mysql2's `query(sql, values?)` and `query({ sql, values?, -/// rowsAsArray? }, values?)` forms while all JS values are still rooted by the -/// native call. -pub(crate) unsafe fn parse_query_request( - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> Result { - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let query = scope.root_nanbox_f64(query_f); - let supplied_params = scope.root_nanbox_f64(params_f); - - let query_value = JSValue::from_bits(query.get_nanbox_u64()); - let (sql, rows_as_array, option_values) = if let Some(sql) = jsvalue_to_string(query_value) { - (sql, false, JSValue::undefined()) - } else { - let sql_value = object_field(JSValue::from_bits(query.get_nanbox_u64()), "sql"); - let sql = jsvalue_to_string(sql_value).ok_or_else(|| { - MysqlPromiseError::message("Query must be a SQL string or an options object with sql") - })?; - let rows_as_array = object_field(JSValue::from_bits(query.get_nanbox_u64()), "rowsAsArray"); - let rows_as_array = rows_as_array.is_bool() && rows_as_array.as_bool(); - ( - sql, - rows_as_array, - object_field(JSValue::from_bits(query.get_nanbox_u64()), "values"), - ) - }; - - let supplied_params = JSValue::from_bits(supplied_params.get_nanbox_u64()); - let params = if supplied_params.is_undefined() { - option_values - } else { - supplied_params - }; - let params = extract_params_from_jsvalue(params).map_err(MysqlPromiseError::message)?; - - Ok(QueryRequest { - sql, - params, - rows_as_array, - force_prepared, - }) -} - -pub(crate) async fn execute_query_on_connection( - conn: &mut MySqlConnection, - request: &QueryRequest, -) -> Result { - let is_select = request.is_row_returning(); - - if !request.uses_prepared_statement() { - // mysql2 `query()` uses MySQL's text protocol when there are no bind - // values. This is required for commands such as BEGIN that the server - // refuses through the prepared-statement protocol (#9517). - let query = sqlx::raw_sql(sqlx::AssertSqlSafe(request.sql.clone())); - if is_select { - let rows = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - query.fetch_all(&mut *conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Query failed", error))?; - return Ok(QueryOutcome::Rows(RawQueryResult::from_mysql_rows(rows))); - } - - let result = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - query.execute(&mut *conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Query failed", error))?; - return Ok(QueryOutcome::Executed { - affected_rows: result.rows_affected(), - last_insert_id: result.last_insert_id(), - }); - } - - // Do not retain prepared statements between calls. This keeps each mysql2 - // request's SQL, bind metadata, and arguments together (#8745). - let mut query = sqlx::query(sqlx::AssertSqlSafe(request.sql.clone())).persistent(false); - for param in &request.params { - query = match param { - ParamValue::Null => query.bind(Option::::None), - ParamValue::String(value) => query.bind(value.clone()), - ParamValue::Bytes(value) => query.bind(value.clone()), - ParamValue::DateTime(value) => query.bind(*value), - ParamValue::Number(value) => query.bind(*value), - ParamValue::Int(value) => query.bind(*value), - ParamValue::Bool(value) => query.bind(*value), - }; - } - - if is_select { - let rows = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - query.fetch_all(&mut *conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Query failed", error))?; - Ok(QueryOutcome::Rows(RawQueryResult::from_mysql_rows(rows))) - } else { - let result = tokio::time::timeout( - Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), - query.execute(&mut *conn), - ) - .await - .map_err(|_| MysqlPromiseError::message("Query timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Query failed", error))?; - Ok(QueryOutcome::Executed { - affected_rows: result.rows_affected(), - last_insert_id: result.last_insert_id(), - }) - } -} - -/// Extract parameter values from a JS array before scheduling async work. -pub(crate) unsafe fn extract_params_from_jsvalue( - params: JSValue, -) -> Result, String> { - if params.bits() == 0 || params.is_undefined() || params.is_null() { - return Ok(Vec::new()); - } - - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let params_handle = scope.root_nanbox_u64(params.bits()); - let is_array = JSValue::from_bits( - perry_runtime::js_array_is_array(params_handle.get_nanbox_f64()).to_bits(), - ) - .as_bool(); - if !is_array { - return Err("Bind parameters must be an array".to_string()); - } - - let refreshed_params = JSValue::from_bits(params_handle.get_nanbox_u64()); - let bits = refreshed_params.bits(); - let array: *const perry_runtime::ArrayHeader = if refreshed_params.is_pointer() { - refreshed_params.as_pointer() - } else if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF { - bits as *const perry_runtime::ArrayHeader - } else { - return Err("Bind parameters array has no valid runtime pointer".to_string()); - }; - if array.is_null() { - return Err("Bind parameters array has no valid runtime pointer".to_string()); - } - - let length = js_array_length(array); - let mut result = Vec::with_capacity(length as usize); - for index in 0..length { - let refreshed_params = JSValue::from_bits(params_handle.get_nanbox_u64()); - let array: *const perry_runtime::ArrayHeader = if refreshed_params.is_pointer() { - refreshed_params.as_pointer() - } else { - refreshed_params.bits() as *const perry_runtime::ArrayHeader - }; - let element_bits = js_array_get_jsvalue(array, index); - let element = JSValue::from_bits(element_bits); - let value = if element.is_null() { - ParamValue::Null - } else if element.is_undefined() { - return Err(format!("Bind parameter at index {index} is undefined")); - } else if let Some(value) = jsvalue_to_string(element) { - ParamValue::String(value) - } else if element.is_bigint() { - let bigint = element.as_bigint_ptr(); - let string = perry_runtime::bigint::js_bigint_to_string(bigint); - let value = crate::common::string_from_header_lossy(string) - .ok_or_else(|| format!("Could not read bigint at index {index}"))?; - ParamValue::String(value) - } else if element.is_int32() { - ParamValue::Int(i64::from(element.as_int32())) - } else if element.is_bool() { - ParamValue::Bool(element.as_bool()) - } else if element.is_number() { - let number = element.to_number(); - if number.fract() == 0.0 && number >= i64::MIN as f64 && number <= i64::MAX as f64 { - ParamValue::Int(number as i64) - } else { - ParamValue::Number(number) - } - } else { - let mut byte_len = 0; - let byte_ptr = perry_runtime::buffer::js_value_buffer_or_typedarray_data( - f64::from_bits(element_bits), - &mut byte_len, - ); - if !byte_ptr.is_null() { - ParamValue::Bytes(std::slice::from_raw_parts(byte_ptr, byte_len as usize).to_vec()) - } else if perry_runtime::date::is_date_value(f64::from_bits(element_bits)) { - let millis = perry_runtime::date::js_date_get_time(f64::from_bits(element_bits)); - if !millis.is_finite() { - return Err(format!( - "Bind parameter at index {index} is an invalid Date" - )); - } - let date = chrono::DateTime::::from_timestamp_millis(millis as i64) - .ok_or_else(|| { - format!("Bind parameter at index {index} is outside MySQL's Date range") - })? - .naive_utc(); - ParamValue::DateTime(date) - } else { - return Err(format!("Unsupported bind parameter at index {index}")); - } - }; - result.push(value); - } - Ok(result) -} - -unsafe fn run_pool_query( - pool_handle: Handle, - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let request = parse_query_request(query_f, params_f, force_prepared); - let pool = with_handle::(pool_handle, |wrapper| wrapper.pool.clone()); - let rows_as_array = request - .as_ref() - .map(|request| request.rows_as_array) - .unwrap_or(false); - - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let request = request?; - let pool = pool.ok_or_else(|| MysqlPromiseError::message("Invalid pool handle"))?; - // Pin a single physical connection for the complete operation. - let mut connection = tokio::time::timeout( - Duration::from_secs(DEFAULT_ACQUIRE_TIMEOUT_SECS), - pool.acquire(), - ) - .await - .map_err(|_| MysqlPromiseError::message("Pool acquire timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Pool acquire failed", error))?; - execute_query_on_connection(&mut connection, &request).await - }, - move |outcome| outcome.to_jsvalue_with_rows_as_array(rows_as_array).bits(), - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -unsafe fn run_pool_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, - force_prepared: bool, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let request = parse_query_request(query_f, params_f, force_prepared); - let connection = with_handle::(conn_handle, |wrapper| { - Arc::clone(&wrapper.connection) - }); - let rows_as_array = request - .as_ref() - .map(|request| request.rows_as_array) - .unwrap_or(false); - - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let request = request?; - let connection = connection - .ok_or_else(|| MysqlPromiseError::message("Invalid pool connection handle"))?; - let mut slot = connection.lock().await; - let connection = slot - .as_mut() - .ok_or_else(|| MysqlPromiseError::message("Pool connection released"))?; - execute_query_on_connection(connection, &request).await - }, - move |outcome| outcome.to_jsvalue_with_rows_as_array(rows_as_array).bits(), - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -/// mysql.createPool(config) -> Pool. Like mysql2, construction is synchronous -/// and the first physical connection is opened lazily. -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_create_pool(config_f: f64) -> Handle { - let config = JSValue::from_bits(config_f.to_bits()); - let url = parse_mysql_config(config).to_url(); - let _runtime = crate::common::runtime().enter(); - MySqlPoolOptions::new() - .max_connections(10) - .acquire_timeout(Duration::from_secs(DEFAULT_ACQUIRE_TIMEOUT_SECS)) - .connect_lazy(&url) - .map(MysqlPoolHandle::new) - .map(register_handle) - .unwrap_or(0) -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_end(pool_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let pool = take_handle::(pool_handle).map(|wrapper| wrapper.pool); - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let pool = pool.ok_or_else(|| MysqlPromiseError::message("Invalid pool handle"))?; - let _ = tokio::time::timeout( - Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS), - pool.close(), - ) - .await; - Ok(JSValue::undefined().bits()) - }, - |bits| bits, - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_query( - pool_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_query(pool_handle, query_f, params_f, false) -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_execute( - pool_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_query(pool_handle, query_f, params_f, true) -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_get_connection(pool_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - let pool = with_handle::(pool_handle, |wrapper| wrapper.pool.clone()); - crate::common::spawn_for_promise_deferred_with_error( - promise as *mut u8, - async move { - let pool = pool.ok_or_else(|| MysqlPromiseError::message("Invalid pool handle"))?; - let connection = tokio::time::timeout( - Duration::from_secs(DEFAULT_ACQUIRE_TIMEOUT_SECS), - pool.acquire(), - ) - .await - .map_err(|_| MysqlPromiseError::message("Pool acquire timed out"))? - .map_err(|error| MysqlPromiseError::from_sqlx("Pool acquire failed", error))?; - Ok(connection) - }, - |connection| { - let handle = register_handle(MysqlPoolConnectionHandle::new(connection)); - perry_runtime::js_nanbox_pointer(handle).to_bits() - }, - MysqlPromiseError::to_jsvalue_bits, - ); - promise -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_connection_release(conn_handle: Handle) { - if let Some(wrapper) = take_handle::(conn_handle) { - crate::common::spawn(async move { - wrapper.connection.lock().await.take(); - }); - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_connection_query( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_connection_query(conn_handle, query_f, params_f, false) -} - -#[no_mangle] -pub unsafe extern "C" fn js_mysql2_pool_connection_execute( - conn_handle: Handle, - query_f: f64, - params_f: f64, -) -> *mut Promise { - run_pool_connection_query(conn_handle, query_f, params_f, true) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn text_protocol_is_used_only_for_query_without_values() { - let query = QueryRequest { - sql: "BEGIN".to_string(), - params: Vec::new(), - rows_as_array: false, - force_prepared: false, - }; - assert!(!query.uses_prepared_statement()); - - let execute = QueryRequest { - force_prepared: true, - ..query.clone() - }; - assert!(execute.uses_prepared_statement()); - - let parameterized = QueryRequest { - params: vec![ParamValue::Int(1)], - ..query - }; - assert!(parameterized.uses_prepared_statement()); - } - - #[test] - fn mysql_error_code_names_match_mysql2() { - assert_eq!(mysql2_error_code(1062), Some("ER_DUP_ENTRY")); - assert_eq!(mysql2_error_code(1064), Some("ER_PARSE_ERROR")); - assert_eq!(mysql2_error_code(65_000), None); - } -} diff --git a/crates/perry-stdlib/src/mysql2/result.rs b/crates/perry-stdlib/src/mysql2/result.rs deleted file mode 100644 index 06a3c2decd..0000000000 --- a/crates/perry-stdlib/src/mysql2/result.rs +++ /dev/null @@ -1,448 +0,0 @@ -//! Query result handling for mysql2 - -use perry_runtime::{ - js_array_alloc, js_array_push, js_object_alloc, js_object_set_field, js_object_set_keys, - js_string_from_bytes, JSValue, -}; -use sqlx::mysql::{MySqlColumn, MySqlRow}; -use sqlx::{Column, Row, TypeInfo}; - -use super::types::{column_to_field_packet, row_to_js_object}; - -/// Returns true if the SQL query returns rows (SELECT, SHOW, DESCRIBE, EXPLAIN, etc.) -pub fn is_row_returning_query(sql: &str) -> bool { - let trimmed = sql.trim_start(); - let upper = trimmed.get(..10).unwrap_or(trimmed).to_uppercase(); - upper.starts_with("SELECT") - || upper.starts_with("SHOW") - || upper.starts_with("DESC") - || upper.starts_with("EXPLAIN") - || upper.starts_with("WITH") -} - -/// Raw value types for thread-safe data transfer -#[derive(Clone, Debug)] -pub enum RawValue { - Null, - Bool(bool), - Int32(i32), - Int64(i64), - Float64(f64), - String(String), - /// DATETIME/TIMESTAMP stored as milliseconds since epoch (for JS Date compatibility) - DateTimestamp(f64), -} - -/// Raw column info for thread-safe data transfer -#[derive(Clone, Debug)] -pub struct RawColumnInfo { - pub name: String, - pub type_name: String, -} - -/// Raw row data for thread-safe data transfer -#[derive(Clone, Debug)] -pub struct RawRowData { - pub values: Vec<(String, RawValue)>, -} - -/// Raw query result for thread-safe data transfer between threads -#[derive(Clone, Debug)] -pub struct RawQueryResult { - pub rows: Vec, - pub columns: Vec, -} - -/// Query outcome: either rows (SELECT) or execution metadata (INSERT/UPDATE/DELETE) -#[derive(Clone, Debug)] -pub enum QueryOutcome { - Rows(RawQueryResult), - Executed { - affected_rows: u64, - last_insert_id: u64, - }, -} - -impl RawQueryResult { - /// Extract raw data from sqlx rows (call this on worker thread) - pub fn from_mysql_rows(rows: Vec) -> Self { - let columns: Vec = if !rows.is_empty() { - rows[0] - .columns() - .iter() - .map(|col| RawColumnInfo { - name: col.name().to_string(), - type_name: col.type_info().name().to_string(), - }) - .collect() - } else { - Vec::new() - }; - - let raw_rows: Vec = rows - .iter() - .map(|row| { - let values = row - .columns() - .iter() - .enumerate() - .map(|(i, col)| { - let name = col.name().to_string(); - let type_name = col.type_info().name(); - let value = extract_raw_value(row, i, type_name); - (name, value) - }) - .collect(); - RawRowData { values } - }) - .collect(); - - RawQueryResult { - rows: raw_rows, - columns, - } - } - - /// Convert to JSValue (call this on main thread only!) - pub fn to_jsvalue(&self) -> JSValue { - self.to_jsvalue_with_rows_as_array(false) - } - - /// Convert to mysql2's `[rows, fields]` tuple, optionally representing - /// every row as a positional array (`rowsAsArray: true`). - pub fn to_jsvalue_with_rows_as_array(&self, rows_as_array: bool) -> JSValue { - // Both children are built BEFORE the result tuple is allocated, and each - // is rooted the moment it exists: every `js_array_alloc` / `js_array_push` - // below can drive a moving collection, and a bare Rust local holding an - // array pointer across one is read back at its pre-collection address. - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - - let mut rows_array = js_array_alloc(self.rows.len() as u32); - for row in &self.rows { - let row_value = if rows_as_array { - JSValue::array_ptr(raw_row_to_js_array(row)) - } else { - let row_obj = raw_row_to_js_object(row, &self.columns); - JSValue::object_ptr(row_obj as *mut u8) - }; - rows_array = js_array_push(rows_array, row_value); - } - let rows_handle = - scope.root_nanbox_f64(f64::from_bits(JSValue::array_ptr(rows_array).bits())); - - let mut fields_array = js_array_alloc(self.columns.len() as u32); - for col in &self.columns { - let field_obj = raw_column_to_field_packet(col); - fields_array = js_array_push(fields_array, JSValue::object_ptr(field_obj as *mut u8)); - } - let fields_handle = - scope.root_nanbox_f64(f64::from_bits(JSValue::array_ptr(fields_array).bits())); - - // The result tuple [rows, fields]. Each push re-reads its operands from - // their handles, so a growth-driven collection cannot strand either one. - let mut result_array = js_array_alloc(2); - result_array = js_array_push( - result_array, - JSValue::from_bits(rows_handle.get_nanbox_f64().to_bits()), - ); - result_array = js_array_push( - result_array, - JSValue::from_bits(fields_handle.get_nanbox_f64().to_bits()), - ); - - JSValue::array_ptr(result_array) - } -} - -/// Convert a raw row to the positional representation used by mysql2 when -/// `rowsAsArray` is enabled. -fn raw_row_to_js_array(row: &RawRowData) -> *mut perry_runtime::ArrayHeader { - let mut array = js_array_alloc(row.values.len() as u32); - for (_, value) in &row.values { - array = js_array_push(array, raw_value_to_jsvalue(value)); - } - array -} - -/// Extract a raw value from a MySQL row (safe to call on any thread) -fn extract_raw_value(row: &MySqlRow, index: usize, type_name: &str) -> RawValue { - match type_name { - "TINYINT" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Int32(val as i32) - } else { - RawValue::Null - } - } - "TINYINT UNSIGNED" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Int32(val as i32) - } else { - RawValue::Null - } - } - "SMALLINT" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Int32(val as i32) - } else { - RawValue::Null - } - } - "SMALLINT UNSIGNED" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Int32(val as i32) - } else { - RawValue::Null - } - } - "MEDIUMINT" | "INT" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Int32(val) - } else { - RawValue::Null - } - } - "MEDIUMINT UNSIGNED" | "INT UNSIGNED" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Float64(val as f64) - } else { - RawValue::Null - } - } - "BIGINT" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Int64(val) - } else { - RawValue::Null - } - } - "BIGINT UNSIGNED" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Float64(val as f64) - } else { - RawValue::Null - } - } - "FLOAT" | "DOUBLE" | "DECIMAL" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Float64(val) - } else { - RawValue::Null - } - } - "BOOLEAN" | "BOOL" => { - if let Ok(val) = row.try_get::(index) { - RawValue::Bool(val) - } else { - RawValue::Null - } - } - "DATETIME" | "TIMESTAMP" => { - if let Ok(val) = row.try_get::(index) { - RawValue::String(val.format("%Y-%m-%d %H:%M:%S").to_string()) - } else { - RawValue::Null - } - } - "DATE" => { - if let Ok(val) = row.try_get::(index) { - RawValue::String(val.format("%Y-%m-%d").to_string()) - } else { - RawValue::Null - } - } - "TIME" => { - if let Ok(val) = row.try_get::(index) { - RawValue::String(val.format("%H:%M:%S").to_string()) - } else { - RawValue::Null - } - } - _ => { - // Try as string first for VARCHAR, CHAR, TEXT, ENUM, SET, etc. - if let Ok(val) = row.try_get::(index) { - RawValue::String(val) - } else if let Ok(val) = row.try_get::, _>(index) { - // Fallback for BLOB/BINARY types — try UTF-8 conversion - RawValue::String(String::from_utf8_lossy(&val).to_string()) - } else { - RawValue::Null - } - } - } -} - -/// Convert a raw row to a JS object (must be called on main thread) -fn raw_row_to_js_object( - row: &RawRowData, - _columns: &[RawColumnInfo], -) -> *mut perry_runtime::ObjectHeader { - let obj = js_object_alloc(0, row.values.len() as u32); - let mut keys_array = js_array_alloc(row.values.len() as u32); - - for (i, (name, value)) in row.values.iter().enumerate() { - // Set the field value - let jsval = raw_value_to_jsvalue(value); - js_object_set_field(obj, i as u32, jsval); - - // Add column name to keys array - let name_ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - let name_jsval = JSValue::string_ptr(name_ptr); - keys_array = js_array_push(keys_array, name_jsval); - } - - js_object_set_keys(obj, keys_array); - obj -} - -/// Convert a raw value to JSValue (must be called on main thread) -fn raw_value_to_jsvalue(value: &RawValue) -> JSValue { - match value { - RawValue::Null => JSValue::null(), - RawValue::Bool(b) => JSValue::bool(*b), - // WORKAROUND for Bug #46: Use JSValue::number instead of JSValue::int32 - // to avoid INT32_TAG NaN-boxing issues with arithmetic and comparisons - RawValue::Int32(i) => JSValue::number(*i as f64), - RawValue::Int64(i) => JSValue::number(*i as f64), - RawValue::Float64(f) => JSValue::number(*f), - RawValue::String(s) => { - let str_ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - JSValue::string_ptr(str_ptr) - } - RawValue::DateTimestamp(millis) => { - // Store as f64 timestamp — matches JS Date behavior. - // new Date(row.datetime) will receive the timestamp directly. - JSValue::number(*millis) - } - } -} - -/// Convert a raw column to a field packet (must be called on main thread) -fn raw_column_to_field_packet(col: &RawColumnInfo) -> *mut perry_runtime::ObjectHeader { - let obj = js_object_alloc(0, 4); - let mut keys_array = js_array_alloc(4); - - // Set name - let name_ptr = js_string_from_bytes(col.name.as_ptr(), col.name.len() as u32); - js_object_set_field(obj, 0, JSValue::string_ptr(name_ptr)); - let key0 = js_string_from_bytes("name".as_ptr(), 4); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key0)); - - // Set type — numeric wire ID, matching mysql2's FieldPacket (#4917). - let type_id = super::types::mysql_type_id_from_name(&col.type_name); - js_object_set_field(obj, 1, JSValue::number(type_id)); - let key1 = js_string_from_bytes("type".as_ptr(), 4); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key1)); - - // mysql2 exposes the same value as `columnType` too. - js_object_set_field(obj, 2, JSValue::number(type_id)); - let key2 = js_string_from_bytes("columnType".as_ptr(), 10); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key2)); - - // Set length (0 — not recoverable through sqlx 0.8's public API, #4917) - js_object_set_field(obj, 3, JSValue::number(0.0)); - let key3 = js_string_from_bytes("length".as_ptr(), 6); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key3)); - - js_object_set_keys(obj, keys_array); - obj -} - -/// Convert query results to the mysql2 format: [rows, fields] -/// -/// Returns a JSValue representing a 2-element array where: -/// - index 0: Array of row objects (RowDataPacket[]) -/// - index 1: Array of field metadata objects (FieldPacket[]) -pub fn rows_to_result_tuple(rows: Vec, columns: &[MySqlColumn]) -> JSValue { - // Create the result tuple [rows, fields] - let mut result_array = js_array_alloc(2); - - // Create rows array - let mut rows_array = js_array_alloc(rows.len() as u32); - for row in rows.iter() { - let row_obj = row_to_js_object(row); - rows_array = js_array_push(rows_array, JSValue::object_ptr(row_obj as *mut u8)); - } - let rows_jsval = JSValue::array_ptr(rows_array); - result_array = js_array_push(result_array, rows_jsval); - - // Create fields array - let mut fields_array = js_array_alloc(columns.len() as u32); - for col in columns.iter() { - let field_obj = column_to_field_packet(col); - fields_array = js_array_push(fields_array, JSValue::object_ptr(field_obj as *mut u8)); - } - let fields_jsval = JSValue::array_ptr(fields_array); - result_array = js_array_push(result_array, fields_jsval); - - JSValue::array_ptr(result_array) -} - -impl QueryOutcome { - pub fn to_jsvalue(&self) -> JSValue { - self.to_jsvalue_with_rows_as_array(false) - } - - pub fn to_jsvalue_with_rows_as_array(&self, rows_as_array: bool) -> JSValue { - match self { - QueryOutcome::Rows(raw) => raw.to_jsvalue_with_rows_as_array(rows_as_array), - QueryOutcome::Executed { - affected_rows, - last_insert_id, - } => affected_rows_result(*affected_rows, *last_insert_id), - } - } -} - -/// Create an empty result (for queries that don't return rows, like INSERT/UPDATE) -pub fn empty_result() -> JSValue { - let mut result_array = js_array_alloc(2); - let empty_rows = js_array_alloc(0); - let empty_fields = js_array_alloc(0); - result_array = js_array_push(result_array, JSValue::array_ptr(empty_rows)); - result_array = js_array_push(result_array, JSValue::array_ptr(empty_fields)); - JSValue::array_ptr(result_array) -} - -/// Create a result with affected rows info (for INSERT/UPDATE/DELETE) -/// -/// mysql2 returns a ResultSetHeader for non-SELECT queries with: -/// - affectedRows -/// - insertId -/// - warningStatus -pub fn affected_rows_result(affected: u64, last_insert_id: u64) -> JSValue { - // Create result tuple [header, fields] - let mut result_array = js_array_alloc(2); - - // Create ResultSetHeader object - let header = js_object_alloc(0, 3); - - // Create keys array for property name lookup - let mut keys_array = js_array_alloc(3); - - // Set affectedRows (field index 0) - js_object_set_field(header, 0, JSValue::number(affected as f64)); - let key0 = js_string_from_bytes("affectedRows".as_ptr(), 12); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key0)); - - // Set insertId (field index 1) - js_object_set_field(header, 1, JSValue::number(last_insert_id as f64)); - let key1 = js_string_from_bytes("insertId".as_ptr(), 8); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key1)); - - // Set warningStatus (field index 2) - js_object_set_field(header, 2, JSValue::number(0.0)); - let key2 = js_string_from_bytes("warningStatus".as_ptr(), 13); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key2)); - - // Attach keys to header object - js_object_set_keys(header, keys_array); - - result_array = js_array_push(result_array, JSValue::object_ptr(header as *mut u8)); - - // Empty fields array - let empty_fields = js_array_alloc(0); - result_array = js_array_push(result_array, JSValue::array_ptr(empty_fields)); - - JSValue::array_ptr(result_array) -} diff --git a/crates/perry-stdlib/src/mysql2/types.rs b/crates/perry-stdlib/src/mysql2/types.rs deleted file mode 100644 index 39c4bca2de..0000000000 --- a/crates/perry-stdlib/src/mysql2/types.rs +++ /dev/null @@ -1,416 +0,0 @@ -//! Type conversions between MySQL types and JSValue - -use perry_runtime::{ - js_array_alloc, js_array_push, js_object_alloc, js_object_get_field_by_name, - js_object_set_field, js_object_set_keys, js_string_from_bytes, JSValue, ObjectHeader, - StringHeader, -}; -use sqlx::mysql::MySqlRow; -use sqlx::{Column, Row, TypeInfo}; - -/// MySQL connection configuration -#[derive(Debug, Clone)] -pub struct MySqlConfig { - pub host: String, - pub port: u16, - pub user: String, - pub password: String, - pub database: Option, -} - -impl Default for MySqlConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 3306, - user: "root".to_string(), - password: String::new(), - database: None, - } - } -} - -impl MySqlConfig { - /// Build a connection URL from the config - pub fn to_url(&self) -> String { - let db_part = self - .database - .as_ref() - .map(|d| format!("/{}", d)) - .unwrap_or_default(); - // URL-encode password to handle special characters (e.g., # @ : /) - let encoded_password: String = self - .password - .chars() - .map(|c| match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(), - c => format!("%{:02X}", c as u32), - }) - .collect(); - let url = format!( - "mysql://{}:{}@{}:{}{}?ssl-mode=disabled", - self.user, encoded_password, self.host, self.port, db_part - ); - url - } -} - -/// Extract a Rust String from a JSValue that contains a string pointer -unsafe fn jsvalue_to_string(value: JSValue) -> Option { - // Check for NaN-boxed string (STRING_TAG = 0x7FFF) - if value.is_string() { - let ptr = value.as_string_ptr(); - if !ptr.is_null() { - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - return Some(String::from_utf8_lossy(bytes).to_string()); - } - } - // Also check for raw pointer (POINTER_TAG = 0x7FFD) pointing to a string - if value.is_pointer() { - let ptr = value.as_pointer() as *const StringHeader; - if !ptr.is_null() { - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - return Some(String::from_utf8_lossy(bytes).to_string()); - } - } - None -} - -/// Helper to create a string key for field lookup -unsafe fn make_key(s: &str) -> *const StringHeader { - js_string_from_bytes(s.as_ptr(), s.len() as u32) -} - -/// Convert a JSValue config object to MySqlConfig -/// -/// Supports two formats: -/// 1. URI format: { uri: "mysql://user:pass@host:port/database" } -/// 2. Individual fields: { host, port, user, password, database } -/// -/// # Safety -/// The config must be a valid JSValue representing an object -pub unsafe fn parse_mysql_config(config: JSValue) -> MySqlConfig { - let mut result = MySqlConfig::default(); - - // Check if config is a valid object pointer (NaN-boxed or raw pointer) - let obj_ptr: *const ObjectHeader = if config.is_pointer() { - // NaN-boxed pointer (POINTER_TAG = 0x7FFD) - config.as_pointer() - } else if !config.is_null() && !config.is_undefined() && !config.is_bool() { - // Perry may pass objects as raw pointers (bits directly hold the address) - // This happens when object values are passed to C functions without NaN-boxing - let raw_bits = config.bits(); - // Valid pointer: non-zero and looks like a heap address (reasonable range) - if raw_bits == 0 || raw_bits > 0x0000_7FFF_FFFF_FFFF { - return result; - } - raw_bits as *const ObjectHeader - } else { - return result; - }; - if obj_ptr.is_null() { - return result; - } - - // Try to get the URI field first - let uri_key = make_key("uri"); - let uri_val = js_object_get_field_by_name(obj_ptr, uri_key); - if let Some(uri_str) = jsvalue_to_string(uri_val) { - if let Some(parsed) = parse_mysql_uri(&uri_str) { - return parsed; - } - } - - // Extract host by name - let host_key = make_key("host"); - let host_val = js_object_get_field_by_name(obj_ptr, host_key); - if let Some(host) = jsvalue_to_string(host_val) { - result.host = host; - } - - // Extract port by name - let port_key = make_key("port"); - let port_val = js_object_get_field_by_name(obj_ptr, port_key); - if port_val.is_number() { - result.port = port_val.to_number() as u16; - } - - // Extract user by name - let user_key = make_key("user"); - let user_val = js_object_get_field_by_name(obj_ptr, user_key); - if let Some(user) = jsvalue_to_string(user_val) { - result.user = user; - } - - // Extract password by name - let password_key = make_key("password"); - let password_val = js_object_get_field_by_name(obj_ptr, password_key); - if let Some(password) = jsvalue_to_string(password_val) { - result.password = password; - } - - // Extract database by name (optional) - let database_key = make_key("database"); - let database_val = js_object_get_field_by_name(obj_ptr, database_key); - if !database_val.is_undefined() && !database_val.is_null() { - if let Some(database) = jsvalue_to_string(database_val) { - result.database = Some(database); - } - } - - result -} - -/// Parse a MySQL connection URI into MySqlConfig -/// Format: mysql://user:password@host:port/database -fn parse_mysql_uri(uri: &str) -> Option { - let uri = uri.strip_prefix("mysql://")?; - - // Split by @ to separate credentials from host - let (credentials, host_part) = if let Some(idx) = uri.rfind('@') { - (&uri[..idx], &uri[idx + 1..]) - } else { - ("", uri) - }; - - // Parse credentials (user:password) - let (user, password) = if let Some(idx) = credentials.find(':') { - ( - credentials[..idx].to_string(), - credentials[idx + 1..].to_string(), - ) - } else { - (credentials.to_string(), String::new()) - }; - - // Parse host:port/database - let (host_port, database) = if let Some(idx) = host_part.find('/') { - (&host_part[..idx], Some(host_part[idx + 1..].to_string())) - } else { - (host_part, None) - }; - - // Parse host:port - let (host, port) = if let Some(idx) = host_port.rfind(':') { - let port_str = &host_port[idx + 1..]; - let port = port_str.parse().unwrap_or(3306); - (host_port[..idx].to_string(), port) - } else { - (host_port.to_string(), 3306) - }; - - Some(MySqlConfig { - host, - port, - user, - password, - database, - }) -} - -/// Convert a MySQL row to a JS object (RowDataPacket) -/// -/// Returns a pointer to the allocated object -pub fn row_to_js_object(row: &MySqlRow) -> *mut ObjectHeader { - let columns = row.columns(); - // Class ID 0 for anonymous object, field count = number of columns - let obj = js_object_alloc(0, columns.len() as u32); - - // Create keys array for property name lookup - let mut keys_array = js_array_alloc(columns.len() as u32); - - for (i, col) in columns.iter().enumerate() { - // Set the field value - let value = column_value_to_jsvalue(row, i); - js_object_set_field(obj, i as u32, value); - - // Add column name to keys array (NaN-boxed string pointer) - let col_name = col.name(); - let name_ptr = js_string_from_bytes(col_name.as_ptr(), col_name.len() as u32); - let name_jsval = JSValue::string_ptr(name_ptr); - keys_array = js_array_push(keys_array, name_jsval); - } - - // Attach keys array to object for property name lookup - js_object_set_keys(obj, keys_array); - - obj -} - -/// Convert a column value to JSValue -fn column_value_to_jsvalue(row: &MySqlRow, index: usize) -> JSValue { - let columns = row.columns(); - let col = &columns[index]; - let type_name = col.type_info().name(); - - // Try to get the value based on the column type - match type_name { - "INT" | "TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT UNSIGNED" | "TINYINT UNSIGNED" - | "SMALLINT UNSIGNED" | "MEDIUMINT UNSIGNED" => { - if let Ok(val) = row.try_get::(index) { - JSValue::int32(val) - } else { - JSValue::null() - } - } - "BIGINT" | "BIGINT UNSIGNED" => { - if let Ok(val) = row.try_get::(index) { - JSValue::number(val as f64) - } else { - JSValue::null() - } - } - "FLOAT" | "DOUBLE" | "DECIMAL" => { - if let Ok(val) = row.try_get::(index) { - JSValue::number(val) - } else { - JSValue::null() - } - } - "VARCHAR" | "CHAR" | "TEXT" | "MEDIUMTEXT" | "LONGTEXT" | "TINYTEXT" | "ENUM" | "SET" => { - if let Ok(val) = row.try_get::(index) { - let str_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - "BOOLEAN" | "BOOL" => { - if let Ok(val) = row.try_get::(index) { - JSValue::bool(val) - } else { - JSValue::null() - } - } - "DATETIME" | "TIMESTAMP" => { - if let Ok(val) = row.try_get::(index) { - let s = val.format("%Y-%m-%d %H:%M:%S").to_string(); - let str_ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - "DATE" => { - if let Ok(val) = row.try_get::(index) { - let s = val.format("%Y-%m-%d").to_string(); - let str_ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - "TIME" => { - if let Ok(val) = row.try_get::(index) { - let s = val.format("%H:%M:%S").to_string(); - let str_ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - _ => { - // Try as string first for unknown types - if let Ok(val) = row.try_get::(index) { - let str_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - JSValue::string_ptr(str_ptr) - } else if let Ok(val) = row.try_get::, _>(index) { - // Fallback for BLOB/BINARY types — try UTF-8 conversion - let s = String::from_utf8_lossy(&val); - let str_ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - } -} - -/// Map sqlx's MySQL type *name* back to the wire-protocol numeric type ID -/// (`enum_field_types`, what Node's mysql2 puts in `field.type`/`columnType`). -/// sqlx 0.8 keeps the raw `ColumnType` byte `pub(crate)`, but its `name()` -/// strings are a bijection over (type, BINARY/UNSIGNED flags), so the wire ID -/// is recoverable (#4917). `DECIMAL` maps to 246 (`NEWDECIMAL`) and -/// `VARCHAR` to 253 (`VAR_STRING`) — the values servers actually send in -/// result sets, not the legacy 0/15 aliases. -pub fn mysql_type_id_from_name(name: &str) -> f64 { - let base = name.strip_suffix(" UNSIGNED").unwrap_or(name); - let id: u8 = match base { - "BOOLEAN" | "TINYINT" => 1, - "SMALLINT" => 2, - "INT" => 3, - "FLOAT" => 4, - "DOUBLE" => 5, - "NULL" => 6, - "TIMESTAMP" => 7, - "BIGINT" => 8, - "MEDIUMINT" => 9, - "DATE" => 10, - "TIME" => 11, - "DATETIME" => 12, - "YEAR" => 13, - "BIT" => 16, - "JSON" => 245, - "DECIMAL" => 246, - // Servers usually transmit ENUM/SET as STRING (254) plus a flag, but - // sqlx has already folded the flag into the name; report the named ID. - "ENUM" => 247, - "SET" => 248, - "TINYBLOB" | "TINYTEXT" => 249, - "MEDIUMBLOB" | "MEDIUMTEXT" => 250, - "LONGBLOB" | "LONGTEXT" => 251, - "BLOB" | "TEXT" => 252, - "VARCHAR" | "VARBINARY" => 253, - "CHAR" | "BINARY" => 254, - "GEOMETRY" => 255, - _ => 0, - }; - id as f64 -} - -/// Create a FieldPacket object for a column -pub fn column_to_field_packet(col: &sqlx::mysql::MySqlColumn) -> *mut ObjectHeader { - // FieldPacket has these fields: - // 0: name (string) - // 1: type (number - MySQL wire type ID) - // 2: columnType (number - mysql2 alias of `type`) - // 3: length (number) - let obj = js_object_alloc(0, 4); - - // Create keys array for property name lookup - let mut keys_array = js_array_alloc(4); - - // Set name - let name = col.name(); - let name_ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field(obj, 0, JSValue::string_ptr(name_ptr)); - let key0 = js_string_from_bytes("name".as_ptr(), 4); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key0)); - - // Set type — the numeric wire ID mysql2 exposes (#4917), recovered from - // sqlx's type name. - let type_id = mysql_type_id_from_name(col.type_info().name()); - js_object_set_field(obj, 1, JSValue::number(type_id)); - let key1 = js_string_from_bytes("type".as_ptr(), 4); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key1)); - - // mysql2 exposes the same value as `columnType` too. - js_object_set_field(obj, 2, JSValue::number(type_id)); - let key2 = js_string_from_bytes("columnType".as_ptr(), 10); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key2)); - - // Set length (0 — sqlx 0.8 keeps the wire `max_size` pub(crate), so the - // column display length is not recoverable; see #4917) - js_object_set_field(obj, 3, JSValue::number(0.0)); - let key3 = js_string_from_bytes("length".as_ptr(), 6); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key3)); - - // Attach keys to object - js_object_set_keys(obj, keys_array); - - obj -} diff --git a/crates/perry-stdlib/src/pg/connection.rs b/crates/perry-stdlib/src/pg/connection.rs deleted file mode 100644 index 176d3d4321..0000000000 --- a/crates/perry-stdlib/src/pg/connection.rs +++ /dev/null @@ -1,415 +0,0 @@ -//! PostgreSQL connection implementation - -use perry_runtime::{ - js_array_get_jsvalue, js_array_length, js_promise_new_cross_thread, JSValue, Promise, -}; -use sqlx::postgres::PgConnection; -use sqlx::{Connection, Row}; - -use super::result::{empty_pg_result, rows_to_pg_result}; -use super::types::{parse_pg_config, PgConfig}; -use crate::common::{register_handle, Handle}; - -/// Wrapper around PgConnection that we can store in the handle registry. -/// -/// The npm-pg API has the user construct the client synchronously -/// (`new Client(config)`) and connect explicitly later (`await -/// client.connect()`). To support that without making `new` itself -/// async, we let the handle live in two states: -/// -/// - **Pre-connect**: `pending_config = Some(...)`, `connection = None`. -/// Created by `js_pg_client_new`. Holds the parsed config until -/// `client.connect()` opens the actual TCP connection. -/// - **Connected**: `pending_config = None`, `connection = Some(...)`. -/// The state every existing query/end path expected before the split; -/// created in-place by `js_pg_connect` (the older single-step API -/// that combines new + connect, kept for back-compat). -pub struct PgConnectionHandle { - pub connection: Option, - pub pending_config: Option, -} - -impl PgConnectionHandle { - pub fn new(conn: PgConnection) -> Self { - Self { - connection: Some(conn), - pending_config: None, - } - } - - /// Pre-connect state: holds config until `.connect()` is called. - pub fn pending(config: PgConfig) -> Self { - Self { - connection: None, - pending_config: Some(config), - } - } - - pub fn take(&mut self) -> Option { - self.connection.take() - } -} - -/// `new Client(config)` — synchronous constructor that parses the config -/// and registers a handle WITHOUT opening a connection. The user must -/// call `await client.connect()` (or any query, which will fail with a -/// helpful error until they do) to actually open the TCP socket. -/// -/// Mirrors npm pg's `new Client(config)` semantics — the Client object -/// exists immediately; the connection happens later. -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_new(config_f: f64) -> Handle { - let config = JSValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgConnectionHandle::pending(pg_config)) -} - -/// `client.connect()` — opens the TCP connection using the config that -/// `js_pg_client_new` previously stored on the handle. Returns a -/// Promise that resolves once the connection is up. -/// -/// If the handle was already connected (or if it was created via the -/// older combined `js_pg_connect`), this is a no-op success. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_connect(client_handle: Handle) -> *mut Promise { - use crate::common::get_handle_mut; - - let promise = js_promise_new_cross_thread(); - - // Snapshot the pending config out of the handle BEFORE entering the - // async block — `get_handle_mut` returns a `&mut` that we can't keep - // alive across an await point. - let pending = if let Some(h) = get_handle_mut::(client_handle) { - h.pending_config.take() - } else { - None - }; - - // Already connected (or back-compat handle from js_pg_connect) — resolve immediately. - let Some(pg_config) = pending else { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Ok(JSValue::undefined().bits()) - }); - return promise; - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let url = pg_config.to_url(); - match PgConnection::connect(&url).await { - Ok(conn) => { - if let Some(h) = get_handle_mut::(client_handle) { - h.connection = Some(conn); - } - Ok(JSValue::undefined().bits()) - } - Err(e) => Err(format!("Failed to connect: {}", e)), - } - }); - - promise -} - -/// pg.connect(config) -> Promise -/// -/// Creates a new PostgreSQL connection with the given configuration. -/// Returns a Promise that resolves to a client handle. -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_connect(config_f: f64) -> *mut Promise { - // Take f64 at the FFI boundary to avoid SysV AMD64 ABI mismatch - // (see js_mysql2_create_pool for details). - let config = JSValue::from_bits(config_f.to_bits()); - let promise = js_promise_new_cross_thread(); - - // Parse the config - let pg_config = parse_pg_config(config); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let url = pg_config.to_url(); - - match PgConnection::connect(&url).await { - Ok(conn) => { - let handle = register_handle(PgConnectionHandle::new(conn)); - // Return the handle as bits - Ok(handle as u64) - } - Err(e) => Err(format!("Failed to connect: {}", e)), - } - }); - - promise -} - -/// client.end() -> Promise -/// -/// Closes the PostgreSQL connection. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_end(client_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::take_handle; - - if let Some(mut wrapper) = take_handle::(client_handle) { - if let Some(conn) = wrapper.take() { - match conn.close().await { - Ok(()) => Ok(JSValue::undefined().bits()), - Err(e) => Err(format!("Failed to close connection: {}", e)), - } - } else { - Err("Connection already closed".to_string()) - } - } else { - Err("Invalid client handle".to_string()) - } - }); - - promise -} - -/// client.query(sql) -> Promise -/// -/// Executes a query and returns the results. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query( - client_handle: Handle, - sql_ptr: *const u8, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - // Extract the SQL string - let sql = if sql_ptr.is_null() { - String::new() - } else { - let header = sql_ptr as *const perry_runtime::StringHeader; - let len = (*header).byte_len as usize; - let data_ptr = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(bytes).to_string() - }; - - // Determine command type from SQL - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::get_handle_mut; - - if let Some(wrapper) = get_handle_mut::(client_handle) { - if let Some(conn) = wrapper.connection.as_mut() { - match sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(conn) - .await - { - Ok(rows) => { - // Get column info from first row (if any) - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - - let result = rows_to_pg_result(rows, &columns, &command); - Ok(result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } else { - Err("Connection already closed".to_string()) - } - } else { - Err("Invalid client handle".to_string()) - } - }); - - promise -} - -/// Enum to hold different parameter value types for pg -#[derive(Clone, Debug)] -enum ParamValue { - Null, - String(String), - Number(f64), - Int(i64), - Bool(bool), -} - -/// Extract parameter values from a JSValue array -unsafe fn extract_params_from_jsvalue(params: JSValue) -> Vec { - let mut result = Vec::new(); - - let bits = params.bits(); - - let arr_ptr: *const perry_runtime::ArrayHeader = if params.is_pointer() { - params.as_pointer() as *const perry_runtime::ArrayHeader - } else if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF { - let upper = bits >> 48; - if upper == 0 || (upper > 0 && upper < 0x7FF0) { - bits as *const perry_runtime::ArrayHeader - } else { - return result; - } - } else { - return result; - }; - - if arr_ptr.is_null() { - return result; - } - - let length = js_array_length(arr_ptr); - - for i in 0..length { - let element_bits = js_array_get_jsvalue(arr_ptr, i); - let element = JSValue::from_bits(element_bits); - - let param = if element.is_null() || element.is_undefined() { - ParamValue::Null - } else if element.is_string() { - let str_ptr = element.as_string_ptr(); - if !str_ptr.is_null() { - let len = (*str_ptr).byte_len as usize; - let data_ptr = - (str_ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - ParamValue::String(String::from_utf8_lossy(bytes).to_string()) - } else { - ParamValue::Null - } - } else if element.is_bigint() { - let bigint_ptr = element.as_bigint_ptr(); - if !bigint_ptr.is_null() { - let str_ptr = perry_runtime::bigint::js_bigint_to_string(bigint_ptr); - if !str_ptr.is_null() { - let len = (*str_ptr).byte_len as usize; - let data_ptr = (str_ptr as *const u8) - .add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - ParamValue::String(String::from_utf8_lossy(bytes).to_string()) - } else { - ParamValue::String("0".to_string()) - } - } else { - ParamValue::String("0".to_string()) - } - } else if element.is_int32() { - ParamValue::Int(element.as_int32() as i64) - } else if element.is_bool() { - ParamValue::Bool(element.as_bool()) - } else if element.is_number() { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - } else { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - }; - - result.push(param); - } - - result -} - -fn is_row_returning_query(sql: &str) -> bool { - let trimmed = sql.trim_start(); - let upper = trimmed.get(..10).unwrap_or(trimmed).to_uppercase(); - upper.starts_with("SELECT") - || upper.starts_with("SHOW") - || upper.starts_with("DESC") - || upper.starts_with("EXPLAIN") - || upper.starts_with("WITH") -} - -/// client.query(sql, params) -> Promise -/// -/// Executes a parameterized query. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query_params( - client_handle: Handle, - sql_ptr: *const u8, - params: JSValue, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - let sql = if sql_ptr.is_null() { - String::new() - } else { - let header = sql_ptr as *const perry_runtime::StringHeader; - let len = (*header).byte_len as usize; - let data_ptr = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(bytes).to_string() - }; - - let param_values = extract_params_from_jsvalue(params); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - let is_select = is_row_returning_query(&sql); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::get_handle_mut; - - if let Some(wrapper) = get_handle_mut::(client_handle) { - if let Some(conn) = wrapper.connection.as_mut() { - let mut query = sqlx::query(sqlx::AssertSqlSafe(sql.clone())); - for param in ¶m_values { - query = match param { - ParamValue::Null => query.bind(Option::::None), - ParamValue::String(s) => query.bind(s.clone()), - ParamValue::Number(n) => query.bind(*n), - ParamValue::Int(i) => query.bind(*i), - ParamValue::Bool(b) => query.bind(*b), - }; - } - if is_select { - match query.fetch_all(conn).await { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - let result = rows_to_pg_result(rows, &columns, &command); - Ok(result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } else { - match query.execute(conn).await { - Ok(result) => { - let pg_result = empty_pg_result(&command, result.rows_affected()); - Ok(pg_result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } - } else { - Err("Connection already closed".to_string()) - } - } else { - Err("Invalid client handle".to_string()) - } - }); - - promise -} diff --git a/crates/perry-stdlib/src/pg/mod.rs b/crates/perry-stdlib/src/pg/mod.rs deleted file mode 100644 index 54a14c10ff..0000000000 --- a/crates/perry-stdlib/src/pg/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! pg compatible native implementation -//! -//! Provides a drop-in replacement for the pg npm package using sqlx. - -pub mod connection; -pub mod pool; -pub mod result; -pub mod types; - -pub use connection::*; -pub use pool::*; -pub use result::*; -pub use types::*; diff --git a/crates/perry-stdlib/src/pg/pool.rs b/crates/perry-stdlib/src/pg/pool.rs deleted file mode 100644 index 080bf413f6..0000000000 --- a/crates/perry-stdlib/src/pg/pool.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! PostgreSQL connection pool implementation - -use perry_runtime::{js_promise_new_cross_thread, JSValue, Promise}; -use sqlx::postgres::{PgPool, PgPoolOptions}; -use sqlx::Row; - -use super::result::rows_to_pg_result; -use super::types::parse_pg_config; -use crate::common::{register_handle, Handle}; - -/// Wrapper around PgPool that we can store in the handle registry. -/// -/// Lives in two states like PgConnectionHandle: pre-pool (`pending_url` -/// holds the connection URL, `pool` is None) and pool-built (`pool` is -/// Some). `new Pool(config)` creates the pre-pool form synchronously -/// without touching the Tokio runtime — sqlx's `connect_lazy` ALSO -/// touches Tokio internals and panics outside a runtime context, so we -/// can't even use it; the actual sqlx pool is built on first query. -/// The older combined `js_pg_create_pool` factory still returns a fully -/// built pool inside its async block. -pub struct PgPoolHandle { - pub pool: Option, - pub pending_url: Option, -} - -impl PgPoolHandle { - pub fn new(pool: PgPool) -> Self { - Self { - pool: Some(pool), - pending_url: None, - } - } - - pub fn pending(url: String) -> Self { - Self { - pool: None, - pending_url: Some(url), - } - } - - /// Lazy-build the sqlx pool on first use. Only callable from within a - /// Tokio runtime context (every spawn_for_promise body). Safe to call - /// repeatedly — only the first call actually builds the pool. - pub async fn ensure_pool(&mut self) -> Result<&PgPool, String> { - if self.pool.is_none() { - let url = self - .pending_url - .take() - .ok_or_else(|| "Pool config missing".to_string())?; - let pool = PgPoolOptions::new() - .max_connections(10) - .connect(&url) - .await - .map_err(|e| format!("Failed to create pool: {}", e))?; - self.pool = Some(pool); - } - Ok(self.pool.as_ref().unwrap()) - } -} - -/// `new Pool(config)` — synchronous constructor matching npm pg's API. -/// -/// Returns a Handle directly (no Promise wrapper). The actual sqlx pool -/// can't be built here because sqlx 0.8's `PgPoolOptions::connect_lazy` -/// touches Tokio runtime internals and panics outside a runtime context, -/// and the synchronous `new` path doesn't have one. Instead we store -/// just the connection URL; `pool.query()` lazy-builds the pool on -/// first use (its spawn_for_promise body runs inside a Tokio runtime). -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_new(config_f: f64) -> Handle { - let config = JSValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgPoolHandle::pending(pg_config.to_url())) -} - -/// new Pool(config) -> Promise -/// -/// Creates a new PostgreSQL connection pool with the given configuration. -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_create_pool(config_f: f64) -> *mut Promise { - // Take f64 at the FFI boundary to avoid SysV AMD64 ABI mismatch - // (see js_mysql2_create_pool for details). - let config = JSValue::from_bits(config_f.to_bits()); - let promise = js_promise_new_cross_thread(); - - // Parse the config - let pg_config = parse_pg_config(config); - - // Extract max connections if provided (default to 10) - let max_conns = 10u32; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let url = pg_config.to_url(); - - match PgPoolOptions::new() - .max_connections(max_conns) - .connect(&url) - .await - { - Ok(pool) => { - let handle = register_handle(PgPoolHandle::new(pool)); - Ok(handle as u64) - } - Err(e) => Err(format!("Failed to create pool: {}", e)), - } - }); - - promise -} - -/// pool.query(sql) -> Promise -/// -/// Executes a query on the pool. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_query(pool_handle: Handle, sql_ptr: *const u8) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - // Extract the SQL string - let sql = if sql_ptr.is_null() { - String::new() - } else { - let header = sql_ptr as *const perry_runtime::StringHeader; - let len = (*header).byte_len as usize; - let data_ptr = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(bytes).to_string() - }; - - // Determine command type from SQL - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::get_handle_mut; - - if let Some(wrapper) = get_handle_mut::(pool_handle) { - // Lazy-build the sqlx pool on first query if `new Pool(config)` - // produced a pre-pool handle. Already-built pools (from the - // older `js_pg_create_pool` factory) skip the build cheaply. - let pool = wrapper.ensure_pool().await?; - match sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(pool) - .await - { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - - let result = rows_to_pg_result(rows, &columns, &command); - Ok(result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } else { - Err("Invalid pool handle".to_string()) - } - }); - - promise -} - -/// pool.end() -> Promise -/// -/// Closes all connections in the pool. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_end(pool_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::take_handle; - - if let Some(mut wrapper) = take_handle::(pool_handle) { - if let Some(pool) = wrapper.pool.take() { - pool.close().await; - Ok(JSValue::undefined().bits()) - } else { - // Pre-pool handle (`new Pool` ctor never had a query) — close - // is a no-op since no connections were ever opened. - Ok(JSValue::undefined().bits()) - } - } else { - Err("Invalid pool handle".to_string()) - } - }); - - promise -} diff --git a/crates/perry-stdlib/src/pg/result.rs b/crates/perry-stdlib/src/pg/result.rs deleted file mode 100644 index abf590203e..0000000000 --- a/crates/perry-stdlib/src/pg/result.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Query result handling for pg - -use perry_runtime::{js_array_alloc, js_array_set, js_object_alloc, js_object_set_field, JSValue}; -use sqlx::postgres::{PgColumn, PgRow}; - -use super::types::{column_to_field_def, row_to_js_object}; - -/// Convert query results to the pg format: { rows, fields, rowCount, command } -/// -/// Returns a JSValue representing a Result object where: -/// - rows: Array of row objects -/// - fields: Array of field metadata objects -/// - rowCount: Number of rows affected/returned -/// - command: SQL command type (SELECT, INSERT, etc.) -pub fn rows_to_pg_result(rows: Vec, columns: &[PgColumn], command: &str) -> JSValue { - // Create the Result object with 4 fields - let result_obj = js_object_alloc(0, 4); - - // Create rows array (field 0) - let rows_array = js_array_alloc(rows.len() as u32); - for (i, row) in rows.iter().enumerate() { - let row_obj = row_to_js_object(row); - js_array_set( - rows_array, - i as u32, - JSValue::object_ptr(row_obj as *mut u8), - ); - } - js_object_set_field(result_obj, 0, JSValue::array_ptr(rows_array)); - - // Create fields array (field 1) - let fields_array = js_array_alloc(columns.len() as u32); - for (i, col) in columns.iter().enumerate() { - let field_obj = column_to_field_def(col); - js_array_set( - fields_array, - i as u32, - JSValue::object_ptr(field_obj as *mut u8), - ); - } - js_object_set_field(result_obj, 1, JSValue::array_ptr(fields_array)); - - // Set rowCount (field 2) - js_object_set_field(result_obj, 2, JSValue::number(rows.len() as f64)); - - // Set command (field 3) - let cmd_ptr = perry_runtime::js_string_from_bytes(command.as_ptr(), command.len() as u32); - js_object_set_field(result_obj, 3, JSValue::string_ptr(cmd_ptr)); - - JSValue::object_ptr(result_obj as *mut u8) -} - -/// Create an empty result for queries that don't return rows -pub fn empty_pg_result(command: &str, row_count: u64) -> JSValue { - let result_obj = js_object_alloc(0, 4); - - let empty_rows = js_array_alloc(0); - js_object_set_field(result_obj, 0, JSValue::array_ptr(empty_rows)); - - let empty_fields = js_array_alloc(0); - js_object_set_field(result_obj, 1, JSValue::array_ptr(empty_fields)); - - js_object_set_field(result_obj, 2, JSValue::number(row_count as f64)); - - let cmd_ptr = perry_runtime::js_string_from_bytes(command.as_ptr(), command.len() as u32); - js_object_set_field(result_obj, 3, JSValue::string_ptr(cmd_ptr)); - - JSValue::object_ptr(result_obj as *mut u8) -} diff --git a/crates/perry-stdlib/src/pg/types.rs b/crates/perry-stdlib/src/pg/types.rs deleted file mode 100644 index c86b75b9df..0000000000 --- a/crates/perry-stdlib/src/pg/types.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! Type conversions between PostgreSQL types and JSValue - -use perry_runtime::{ - js_array_alloc, js_array_push, js_object_alloc, js_object_get_field, js_object_set_field, - js_object_set_keys, js_string_from_bytes, JSValue, ObjectHeader, StringHeader, -}; -use sqlx::postgres::PgRow; -use sqlx::{Column, Row, TypeInfo}; - -/// PostgreSQL connection configuration -#[derive(Debug, Clone)] -pub struct PgConfig { - pub host: String, - pub port: u16, - pub user: String, - pub password: String, - pub database: Option, -} - -impl Default for PgConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 5432, - user: "postgres".to_string(), - password: String::new(), - database: None, - } - } -} - -impl PgConfig { - /// Build a connection URL from the config - pub fn to_url(&self) -> String { - let db_part = self - .database - .as_ref() - .map(|d| format!("/{}", d)) - .unwrap_or_default(); - format!( - "postgres://{}:{}@{}:{}{}", - self.user, self.password, self.host, self.port, db_part - ) - } -} - -/// Extract a Rust String from a JSValue that contains a string pointer -unsafe fn jsvalue_to_string(value: JSValue) -> Option { - if value.is_pointer() { - let ptr = value.as_pointer() as *const StringHeader; - if !ptr.is_null() { - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - return Some(String::from_utf8_lossy(bytes).to_string()); - } - } - None -} - -/// Convert a JSValue config object to PgConfig -/// -/// Expected object layout (based on property order in object literal): -/// - field 0: host (string) -/// - field 1: port (number) -/// - field 2: user (string) -/// - field 3: password (string) -/// - field 4: database (string, optional) -/// -/// # Safety -/// The config must be a valid JSValue representing an object -pub unsafe fn parse_pg_config(config: JSValue) -> PgConfig { - let mut result = PgConfig::default(); - - // Check if config is a valid object pointer - if !config.is_pointer() { - return result; - } - - let obj_ptr = config.as_pointer() as *const ObjectHeader; - if obj_ptr.is_null() { - return result; - } - - // Extract host (field 0) - let host_val = js_object_get_field(obj_ptr, 0); - if let Some(host) = jsvalue_to_string(host_val) { - result.host = host; - } - - // Extract port (field 1) - let port_val = js_object_get_field(obj_ptr, 1); - if port_val.is_number() { - result.port = port_val.to_number() as u16; - } - - // Extract user (field 2) - let user_val = js_object_get_field(obj_ptr, 2); - if let Some(user) = jsvalue_to_string(user_val) { - result.user = user; - } - - // Extract password (field 3) - let password_val = js_object_get_field(obj_ptr, 3); - if let Some(password) = jsvalue_to_string(password_val) { - result.password = password; - } - - // Extract database (field 4, optional) - let database_val = js_object_get_field(obj_ptr, 4); - if !database_val.is_undefined() && !database_val.is_null() { - if let Some(database) = jsvalue_to_string(database_val) { - result.database = Some(database); - } - } - - result -} - -/// Convert a PostgreSQL row to a JS object -/// -/// Returns a pointer to the allocated object -pub fn row_to_js_object(row: &PgRow) -> *mut ObjectHeader { - let columns = row.columns(); - // Class ID 0 for anonymous object, field count = number of columns - let obj = js_object_alloc(0, columns.len() as u32); - - for (i, _col) in columns.iter().enumerate() { - let value = column_value_to_jsvalue(row, i); - js_object_set_field(obj, i as u32, value); - } - - obj -} - -/// Convert a column value to JSValue -fn column_value_to_jsvalue(row: &PgRow, index: usize) -> JSValue { - let columns = row.columns(); - let col = &columns[index]; - let type_name = col.type_info().name(); - - // Try to get the value based on the column type - match type_name { - "INT4" | "INT2" => { - if let Ok(val) = row.try_get::(index) { - JSValue::int32(val) - } else { - JSValue::null() - } - } - "INT8" => { - if let Ok(val) = row.try_get::(index) { - JSValue::number(val as f64) - } else { - JSValue::null() - } - } - "FLOAT4" | "FLOAT8" | "NUMERIC" => { - if let Ok(val) = row.try_get::(index) { - JSValue::number(val) - } else { - JSValue::null() - } - } - "VARCHAR" | "CHAR" | "TEXT" | "BPCHAR" | "NAME" => { - if let Ok(val) = row.try_get::(index) { - let str_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - "BOOL" => { - if let Ok(val) = row.try_get::(index) { - JSValue::bool(val) - } else { - JSValue::null() - } - } - _ => { - // Try as string fallback - if let Ok(val) = row.try_get::(index) { - let str_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - } -} - -/// Create a FieldDef object for a column, shaped like node-pg's -/// `result.fields[i]` (#4917): `dataTypeID` is the numeric type OID (what -/// `pg-types`-style custom parsers key on), `tableID`/`columnID` come from -/// the RowDescription via sqlx's `relation_id()`/`relation_attribute_no()` -/// (0 for expression columns, like Node). `dataTypeSize`/`dataTypeModifier` -/// are not exposed by sqlx 0.8 and report the "unknown/variable" sentinel -1. -pub fn column_to_field_def(col: &sqlx::postgres::PgColumn) -> *mut ObjectHeader { - let obj = js_object_alloc(0, 7); - let mut keys_array = js_array_alloc(7); - let mut set = |obj: *mut ObjectHeader, idx: u32, key: &str, value: JSValue| { - js_object_set_field(obj, idx, value); - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key_ptr)); - }; - - let name = col.name(); - let name_ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - set(obj, 0, "name", JSValue::string_ptr(name_ptr)); - - let table_id = col.relation_id().map(|oid| oid.0 as f64).unwrap_or(0.0); - set(obj, 1, "tableID", JSValue::number(table_id)); - - let column_id = col - .relation_attribute_no() - .map(|attno| attno as f64) - .unwrap_or(0.0); - set(obj, 2, "columnID", JSValue::number(column_id)); - - // `oid()` is None only for custom types sqlx has not resolved against - // the catalog; report 0 (the `InvalidOid` sentinel) in that case. - let data_type_id = col.type_info().oid().map(|oid| oid.0 as f64).unwrap_or(0.0); - set(obj, 3, "dataTypeID", JSValue::number(data_type_id)); - - set(obj, 4, "dataTypeSize", JSValue::number(-1.0)); - set(obj, 5, "dataTypeModifier", JSValue::number(-1.0)); - - let format_ptr = js_string_from_bytes("text".as_ptr(), 4); - set(obj, 6, "format", JSValue::string_ptr(format_ptr)); - - js_object_set_keys(obj, keys_array); - obj -} diff --git a/crates/perry-ui-android/src/stdlib_stubs.rs b/crates/perry-ui-android/src/stdlib_stubs.rs index 9148cb50d1..10488298a8 100644 --- a/crates/perry-ui-android/src/stdlib_stubs.rs +++ b/crates/perry-ui-android/src/stdlib_stubs.rs @@ -807,66 +807,6 @@ pub extern "C" fn js_mongodb_db_collection() -> i64 { 0 } #[no_mangle] -pub extern "C" fn js_mysql2_connection_begin_transaction() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_connection_commit() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_connection_end() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_connection_execute() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_connection_query() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_connection_rollback() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_create_connection() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_create_pool() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_connection_execute() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_connection_query() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_connection_release() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_end() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_execute() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_get_connection() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_mysql2_pool_query() -> i64 { - 0 -} -#[no_mangle] pub extern "C" fn js_new_instance() -> i64 { 0 } @@ -883,34 +823,6 @@ pub extern "C" fn js_nodemailer_verify() -> i64 { 0 } #[no_mangle] -pub extern "C" fn js_pg_client_end() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_pg_client_query() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_pg_client_query_params() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_pg_connect() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_pg_create_pool() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_pg_pool_end() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_pg_pool_query() -> i64 { - 0 -} -#[no_mangle] pub extern "C" fn js_runtime_init() -> i64 { 0 } diff --git a/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs b/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs index 8fcda7242c..4368968671 100644 --- a/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs +++ b/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs @@ -103,13 +103,6 @@ mod tests { } } - #[test] - fn lookup_preserves_registered_subpaths_before_falling_back() { - let (root, binding) = lookup_well_known_for_import("mysql2/promise"); - assert_eq!(root, "mysql2"); - assert_eq!(binding.expect("subpath binding").package, "mysql2/promise"); - } - #[test] fn installed_copy_probe_uses_root_package_directory() { let dir = tempfile::tempdir().expect("tempdir"); diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index 81749712e9..42e64684e2 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -315,8 +315,6 @@ pub(crate) fn build_optimized_libs( | "bundled-argon2" | "bundled-nodemailer" | "bundled-ioredis" - | "bundled-pg" - | "bundled-mysql2" | "bundled-mongodb" | "bundled-ws" | "bundled-net" diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs index 00eb57f5a8..592a9f5e43 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -114,16 +114,17 @@ pub(crate) fn auto_optimized_cache_key( ) -> String { let target_str = target.unwrap_or("host"); // The stripped stdlib feature set is not enough to identify this Cargo - // graph. For example, mysql2 and a CPU-only async wrapper both reduce to - // `async-runtime`, but only the mysql2 build selects perry-ext-mysql2. + // graph. For example, mongodb and a CPU-only async wrapper both reduce to + // `async-runtime`, but only the mongodb build selects perry-ext-mongodb. // Sharing a target dir lets the second invocation replace stdlib after // the first invocation releases its build lock but before it links. The // first process then sees an ext archive and stdlib archive from different // dependency graphs (#9470; the same class produced #9094's Linux link). // // Sort and deduplicate here as a defensive measure: aliases such as - // `mysql2` + `mysql2/promise` name the same wrapper and must describe the - // same graph regardless of discovery order or alias multiplicity. + // `ioredis` + `redis` + `iovalkey` name the same wrapper and must + // describe the same graph regardless of discovery order or alias + // multiplicity. let mut tokio_bindings: Vec = tokio_using_bindings .iter() .map(|(krate, lib, _tracking)| format!("{krate}:{lib}")) @@ -758,9 +759,6 @@ pub(crate) fn binding_needs_shared_tokio(module: &str) -> bool { | "fastify" // Database drivers (mongodb, sqlx, redis) | "mongodb" - | "pg" - | "mysql2" - | "mysql2/promise" | "ioredis" | "redis" // Mail (lettre) diff --git a/crates/perry/src/commands/compile/resolve.rs b/crates/perry/src/commands/compile/resolve.rs index 8c5280cca3..bc49517219 100644 --- a/crates/perry/src/commands/compile/resolve.rs +++ b/crates/perry/src/commands/compile/resolve.rs @@ -146,7 +146,7 @@ mod tests; // without the guard, a deep import reached through another package's // compiled JS would make the walker read undici's real sources (llhttp // wasm) instead of routing to perry-ext-undici. -const PERRY_NATIVE_EXTENSION_PACKAGES: &[&str] = &["ioredis", "ethers", "mysql2", "ws", "undici"]; +const PERRY_NATIVE_EXTENSION_PACKAGES: &[&str] = &["ioredis", "ethers", "ws", "undici"]; /// Absolute virtual prefix used by files extracted from a Bun standalone /// executable. `--bunfs-root` maps the suffix below this prefix to a real diff --git a/crates/perry/src/commands/stdlib_features.rs b/crates/perry/src/commands/stdlib_features.rs index 6b3b0c518e..8b82139acf 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -63,13 +63,6 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { "tls" => &["tls"], // ── Databases ───────────────────────────────────────────────── - // `database-mysql` umbrella retained for backwards-compat; - // per-binding gate is `bundled-mysql2` (v0.5.567). - "mysql2" | "mysql2/promise" => &["bundled-mysql2"], - // `database-postgres` umbrella retained for backwards-compat; - // per-binding gate is `bundled-pg` (v0.5.566) so the - // well-known flip can route to perry-ext-pg. - "pg" => &["bundled-pg"], "better-sqlite3" => &["database-sqlite"], // node:sqlite (#3183/#3184) shares the rusqlite-backed // `database-sqlite` feature with better-sqlite3 — DatabaseSync / diff --git a/crates/perry/well_known_bindings.toml b/crates/perry/well_known_bindings.toml index fdd4228ecf..dc61aee16b 100644 --- a/crates/perry/well_known_bindings.toml +++ b/crates/perry/well_known_bindings.toml @@ -196,38 +196,6 @@ repo = "https://github.com/redis/node-redis" ref = "d400bc9c1e7b17013c53015f098274a25aa70640" ported-at = "6.1.0" date = "2026-07-30" -[bindings.pg] -crate = "perry-ext-pg" -lib = "perry_ext_pg" -tracking = "#466" - -[bindings.pg.upstream] -version = "8.22.0" -sha256 = "2f8b273b9b93b8712251cbe2b0f05378f4da2187eab5348c3c6c34d02622a55e" -repo = "https://github.com/brianc/node-postgres" -ref = "b617619f9fb6fbd231731823e2732a2927ded4be" -ported-at = "8.22.0" -date = "2026-07-30" -[bindings.mysql2] -crate = "perry-ext-mysql2" -lib = "perry_ext_mysql2" -tracking = "#466" - -[bindings.mysql2.upstream] -version = "3.23.2" -sha256 = "5ebc7c22dd90fe3c7c69b9b1343208d71cc8e9395b433d16eb8be63306218a78" -repo = "https://github.com/sidorares/node-mysql2" -ref = "a98730277381005777f70aabfcefa017c1b970a3" -ported-at = "3.23.2" -date = "2026-07-30" -[bindings."mysql2/promise"] -crate = "perry-ext-mysql2" -lib = "perry_ext_mysql2" -tracking = "#466" -# Subpath of the mysql2 package — not separately published, so it carries no -# pin of its own; its provenance is mysql2's. -alias-of = "mysql2" - [bindings.nodemailer] crate = "perry-ext-nodemailer" lib = "perry_ext_nodemailer" diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index fe1dd66d8f..c37e4d8933 100644 --- a/docs/api/perry.d.ts +++ b/docs/api/perry.d.ts @@ -1,6 +1,6 @@ // Auto-generated from Perry's API manifest (#465). Do not edit by hand. // Source: perry-api-manifest::API_MANIFEST -// Coverage: 2026 entries across 118 modules +// Coverage: 2016 entries across 115 modules type PerryI8 = number & { readonly __perryI8?: never }; type PerryI16 = number & { readonly __perryI16?: never }; @@ -2125,24 +2125,6 @@ declare module "mongodb" { export function connect(p0: any): any; } -declare module "mysql2" { - /** stdlib */ - export class Pool { [key: string]: any; } - /** stdlib */ - export function createConnection(p0: any): any; - /** stdlib */ - export function createPool(p0: any): any; -} - -declare module "mysql2/promise" { - /** stdlib */ - export class Pool { [key: string]: any; } - /** stdlib */ - export function createConnection(p0: any): any; - /** stdlib */ - export function createPool(p0: any): any; -} - declare module "net" { /** stdlib */ export class BlockList { [key: string]: any; } @@ -3293,17 +3275,6 @@ declare module "perry/yoga" { export function unsetMeasureFunc(...args: any[]): any; } -declare module "pg" { - /** stdlib */ - export class Client { [key: string]: any; } - /** stdlib */ - export class Pool { [key: string]: any; } - /** stdlib */ - export function Pool(p0: any): any; - /** stdlib */ - export function connect(p0: any): any; -} - declare module "process" { /** stdlib */ export const _eval: any; diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index a50271bb30..a86baf5f09 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -2,7 +2,7 @@ This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. -Total: 2870 entries across 120 modules. +Total: 2829 entries across 117 modules. ## Modules @@ -59,8 +59,6 @@ Total: 2870 entries across 120 modules. - [`lodash`](#lodash) - [`module`](#module) - [`mongodb`](#mongodb) -- [`mysql2`](#mysql2) -- [`mysql2/promise`](#mysql2promise) - [`net`](#net) - [`node-fetch`](#node-fetch) - [`node-pty`](#node-pty) @@ -91,7 +89,6 @@ Total: 2870 entries across 120 modules. - [`perry/widget`](#perrywidget) - [`perry/workloads`](#perryworkloads) - [`perry/yoga`](#perryyoga) -- [`pg`](#pg) - [`process`](#process) - [`punycode`](#punycode) - [`querystring`](#querystring) @@ -1945,54 +1942,6 @@ Total: 2870 entries across 120 modules. - `updateMany` — instance - `updateOne` — instance -## `mysql2` - -### Classes - -- `Pool` - -### Methods - -- `beginTransaction` — instance -- `commit` — instance -- `createConnection` — module -- `createPool` — module -- `end` — instance *(class: `Pool`)* -- `end` — instance -- `execute` — instance *(class: `Pool`)* -- `execute` — instance *(class: `PoolConnection`)* -- `execute` — instance -- `getConnection` — instance -- `query` — instance *(class: `Pool`)* -- `query` — instance *(class: `PoolConnection`)* -- `query` — instance -- `release` — instance -- `rollback` — instance - -## `mysql2/promise` - -### Classes - -- `Pool` - -### Methods - -- `beginTransaction` — instance -- `commit` — instance -- `createConnection` — module -- `createPool` — module -- `end` — instance *(class: `Pool`)* -- `end` — instance -- `execute` — instance *(class: `Pool`)* -- `execute` — instance *(class: `PoolConnection`)* -- `execute` — instance -- `getConnection` — instance -- `query` — instance *(class: `Pool`)* -- `query` — instance *(class: `PoolConnection`)* -- `query` — instance -- `release` — instance -- `rollback` — instance - ## `net` ### Classes @@ -2827,23 +2776,6 @@ Total: 2870 entries across 120 modules. - `setNumber` — module - `unsetMeasureFunc` — module -## `pg` - -### Classes - -- `Client` -- `Pool` - -### Methods - -- `Pool` — module -- `connect` — module -- `connect` — instance *(class: `Client`)* -- `end` — instance *(class: `Pool`)* -- `end` — instance -- `query` — instance *(class: `Pool`)* -- `query` — instance - ## `process` ### Methods diff --git a/docs/src/native-libraries/governance.md b/docs/src/native-libraries/governance.md index 134bc1a375..ae308aaf48 100644 --- a/docs/src/native-libraries/governance.md +++ b/docs/src/native-libraries/governance.md @@ -72,6 +72,20 @@ passing the governance check. with Node across transliteration, replacement, strict/trim options, locale, regular-expression removal, and `slugify.extend`. Its former `perry-ext-slugify` and `perry-stdlib` implementations have been removed. +- `mysql2@3.23.2` compiles from its installed CommonJS source through the + default automatic package-routing path, including the `generate-function` + row-parser factories it builds with `new Function` at runtime — handled by + the `dyn_eval` interpreter's class-expression support (#10661/#10675). A + real local-MySQL round trip (`CREATE TABLE`/`INSERT`/`SELECT`/`DELETE`/ + `DROP`) passed with no `perry.compilePackages` entry for `mysql2` at all. + Its former `perry-ext-mysql2` crate and `perry-stdlib` implementation have + been removed. +- `pg@8.22.0` compiles from its installed CommonJS source through the default + automatic package-routing path. A real local-PostgreSQL round trip + (`CREATE TABLE`/parameterized `INSERT`/`SELECT`/parameterized `DELETE`/ + `DROP`) on a `Client`, plus a `Pool` query, passed with no + `perry.compilePackages` entry for `pg` at all. Its former `perry-ext-pg` + crate and `perry-stdlib` implementation have been removed. ## Current inventory @@ -96,12 +110,10 @@ from `well_known_bindings.toml`. Regenerate this table with | `perry-ext-http` | `http`
`http2`
`https` | Runtime API | Keep near core; consolidate when practical | Bundled; retained | | `perry-ext-ioredis` | `ioredis`
`iovalkey`
`redis` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-mongodb` | `mongodb` | Source package | Compile the upstream package source | Bundled; migration pending | -| `perry-ext-mysql2` | `mysql2`
`mysql2/promise` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-net` | `net` | Runtime API | Keep near core; consolidate when practical | Bundled; retained | | `perry-ext-nodemailer` | `nodemailer` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-parcel-watcher` | `@parcel/watcher`
`@parcel/watcher-darwin-arm64`
`@parcel/watcher-darwin-x64`
`@parcel/watcher-linux-arm64-glibc`
`@parcel/watcher-linux-arm64-musl`
`@parcel/watcher-linux-x64-glibc`
`@parcel/watcher-linux-x64-musl`
`@parcel/watcher-win32-arm64`
`@parcel/watcher-win32-x64` | External integration | Move to an external native package | Bundled; migration pending | | `perry-ext-pdf` | `@perryts/pdf` | External integration | Move to an external native package | Bundled; migration pending | -| `perry-ext-pg` | `pg` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-sharp` | `sharp` | External integration | Move to an external native package | Bundled; migration pending | | `perry-ext-streams` | `streams` | Runtime API | Keep near core; consolidate when practical | Bundled; retained | | `perry-ext-typescript` | `typescript` | Source package | Compile the upstream package source | Bundled; migration pending | diff --git a/docs/src/native-libraries/overview.md b/docs/src/native-libraries/overview.md index 8fe98f6f55..fd5a108405 100644 --- a/docs/src/native-libraries/overview.md +++ b/docs/src/native-libraries/overview.md @@ -250,7 +250,7 @@ drivers is the migration target: | Path | Install | Resolver layer | What it is | |---|---|---|---| -| **Well-known native binding** | nothing (bundled) | (c) | Compatibility path: `import 'mysql2'` / `import 'pg'` / `import 'mongodb'` route to in-tree Rust wrappers. They remain available until the source-package migration gates pass. | +| **Well-known native binding** | nothing (bundled) | (c) | Compatibility path: `import 'mysql2'` / `import 'mongodb'` route to in-tree Rust wrappers. They remain available until the source-package migration gates pass. `pg` has already migrated off this path — see the row below. | | **`@perryts/{postgres,mysql,mongodb,redis}`** | `bun add @perryts/postgres` | (a) | Pure-TypeScript wire-protocol drivers — no Rust, no native dep. Use Perry's [`compilePackages`](../packages/porting.md) to compile the TS to native via LLVM. Also run unmodified on Node.js / Bun. Independent semver. | | **External native binding** | `bun add @perryts/tursodb` | (a) | Third-party Rust crate using `perry-ffi`, manifest at `package.json::perry.nativeLibrary`. Today: `@perryts/tursodb`, `@perryts/iroh`. | @@ -263,9 +263,11 @@ shim, just don't import `mysql2`. **When to pick which:** -- **Well-known native (`mysql2` / `pg` / `mongodb`)** — current zero-install +- **Well-known native (`mysql2` / `mongodb`)** — current zero-install compatibility path; its feature set tracks Perry's release cadence and it is - scheduled to yield to compiled package source. + scheduled to yield to compiled package source. `pg` no longer has a native + binding: a plain `import ... from "pg"` compiles the real npm `pg` package + from source instead. - **`@perryts/postgres` / `@perryts/mysql` / `@perryts/mongodb` / `@perryts/redis`** — you want to read / fork / patch the driver in plain TypeScript; you want the same code running on Node.js or Bun for fallback; diff --git a/scripts/native_result_ledger.py b/scripts/native_result_ledger.py index ff7f1f0500..476dfa220e 100644 --- a/scripts/native_result_ledger.py +++ b/scripts/native_result_ledger.py @@ -61,8 +61,18 @@ # NR_STR/NR_F64/NR_VOID-returning FFI never counted here). 356 -> 349 # rows and 307 -> 300 providers; each figure is what the script reports # on the resolved tree, not arithmetic. -EXPECTED_ROWS = 349 -EXPECTED_PROVIDERS = 300 +# +# -35 rows / -21 providers (#10677 + #10680, same campaign): removing the +# `pg` binding dropped 7 pointer-kind rows / 7 providers (`js_pg_connect`/ +# `create_pool`/`client_connect`/`client_query`/`client_end`/`pool_query`/ +# `pool_end`), and removing `mysql2` dropped 28 rows across the `mysql2` +# and `mysql2/promise` specifiers / 14 providers (the two `release` rows +# were NR_VOID and never counted here, and each mysql2 provider serves both +# specifiers, so rows fall by twice the provider count). 349 -> 314 rows and +# 300 -> 279 providers; each figure is what the script reports on the +# resolved tree, not arithmetic (#10739). +EXPECTED_ROWS = 314 +EXPECTED_PROVIDERS = 279 KINDS = { "NR_GCPTR", "NR_NULLABLE_GCPTR", diff --git a/scripts/native_result_ledger.tsv b/scripts/native_result_ledger.tsv index 5b16d86875..1e68c88490 100644 --- a/scripts/native_result_ledger.tsv +++ b/scripts/native_result_ledger.tsv @@ -115,20 +115,6 @@ js_mongodb_collection_insert_one_value NR_GCPTR crates/perry-ext-mongodb/src/lib js_mongodb_collection_update_many_value NR_GCPTR crates/perry-ext-mongodb/src/lib.rs *mut Promise js_mongodb_collection_update_one_value NR_GCPTR crates/perry-ext-mongodb/src/lib.rs *mut Promise js_mongodb_db_collection NR_HANDLE_ID crates/perry-ext-mongodb/src/lib.rs Handle -js_mysql2_connection_begin_transaction NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_connection_commit NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_connection_end NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_connection_execute NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_connection_query NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_connection_rollback NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_create_connection NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_create_pool NR_HANDLE_ID crates/perry-ext-mysql2/src/lib.rs Handle -js_mysql2_pool_connection_execute NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_pool_connection_query NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_pool_end NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_pool_execute NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_pool_get_connection NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise -js_mysql2_pool_query NR_GCPTR crates/perry-ext-mysql2/src/lib.rs *mut Promise js_net_block_list_new NR_HANDLE_ID crates/perry-ext-net/src/classes.rs i64 js_net_block_list_rules NR_GCPTR crates/perry-ext-net/src/classes.rs *mut ArrayHeader js_net_server_listeners NR_GCPTR crates/perry-ext-net/src/lifecycle.rs i64 @@ -219,13 +205,6 @@ js_perry_tui_use_focus_manager NR_HANDLE_ID crates/perry-runtime/src/tui/hooks.r js_perry_tui_use_ref NR_HANDLE_ID crates/perry-runtime/src/tui/hooks.rs i64 js_perry_tui_use_state_tuple NR_GCPTR crates/perry-runtime/src/tui/hooks.rs i64 js_perry_tui_use_stdout NR_HANDLE_ID crates/perry-runtime/src/tui/hooks.rs i64 -js_pg_client_connect NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise -js_pg_client_end NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise -js_pg_client_query NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise -js_pg_connect NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise -js_pg_create_pool NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise -js_pg_pool_end NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise -js_pg_pool_query NR_GCPTR crates/perry-ext-pg/src/lib.rs *mut Promise js_process_event_names NR_GCPTR crates/perry-runtime/src/os/os_process_emitter.rs *mut ArrayHeader js_process_listeners NR_GCPTR crates/perry-runtime/src/os/os_process_emitter.rs *mut ArrayHeader js_process_raw_listeners NR_GCPTR crates/perry-runtime/src/os/os_process_emitter.rs *mut ArrayHeader diff --git a/scripts/run_doc_tests.ps1 b/scripts/run_doc_tests.ps1 index 49b9972b67..d1700363c7 100644 --- a/scripts/run_doc_tests.ps1 +++ b/scripts/run_doc_tests.ps1 @@ -24,8 +24,6 @@ cargo build --release ` -p perry-doc-tests ` -p perry-ext-ioredis ` -p perry-ext-mongodb ` - -p perry-ext-mysql2 ` - -p perry-ext-pg ` -p perry-ext-nodemailer if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/scripts/run_doc_tests.sh b/scripts/run_doc_tests.sh index 826cfba278..894e8cfd52 100755 --- a/scripts/run_doc_tests.sh +++ b/scripts/run_doc_tests.sh @@ -26,7 +26,7 @@ cd "$REPO_ROOT" # no-auto linker correctly refuses that unsafe archive pair. cargo build --release \ -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static -p perry-doc-tests \ - -p perry-ext-ioredis -p perry-ext-mongodb -p perry-ext-mysql2 -p perry-ext-pg -p perry-ext-nodemailer + -p perry-ext-ioredis -p perry-ext-mongodb -p perry-ext-nodemailer # Disable per-test auto-optimize for HOST runs only. With this set, # `perry compile` short-circuits the cargo-rebuild step in diff --git a/scripts/string_payload_access_baseline.txt b/scripts/string_payload_access_baseline.txt index da40658f39..58d5186dea 100644 --- a/scripts/string_payload_access_baseline.txt +++ b/scripts/string_payload_access_baseline.txt @@ -5,14 +5,12 @@ inline-offset | perry-ext-ethers | 2 inline-offset | perry-ext-http | 1 -inline-offset | perry-ext-mysql2 | 1 inline-offset | perry-ext-net | 1 inline-offset | perry-ext-nodemailer | 1 -inline-offset | perry-ext-pg | 2 inline-offset | perry-ext-zlib | 3 inline-offset | perry-ffi | 3 inline-offset | perry-runtime | 350 -inline-offset | perry-stdlib | 36 +inline-offset | perry-stdlib | 28 inline-offset | perry-updater | 5 reader-helper | perry-ext-ethers | 1 reader-helper | perry-runtime | 13 diff --git a/scripts/unrooted_local_shape_baseline.json b/scripts/unrooted_local_shape_baseline.json index 265ba12fa8..241fce42d5 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -17,11 +17,9 @@ "crates/perry-ext-http/src/server/types.rs": 1, "crates/perry-ext-ioredis/src/lib.rs": 1, "crates/perry-ext-mongodb/src/lib.rs": 2, - "crates/perry-ext-mysql2/src/lib.rs": 9, "crates/perry-ext-net/src/classes.rs": 2, "crates/perry-ext-net/src/jsvalue.rs": 1, "crates/perry-ext-net/src/lifecycle.rs": 1, - "crates/perry-ext-pg/src/lib.rs": 7, "crates/perry-ext-streams/src/lib.rs": 4, "crates/perry-ext-ws/src/server.rs": 3, "crates/perry-ext-zlib/src/stream.rs": 3, @@ -42,11 +40,7 @@ "crates/perry-stdlib/src/ioredis.rs": 3, "crates/perry-stdlib/src/lodash.rs": 21, "crates/perry-stdlib/src/mongodb.rs": 4, - "crates/perry-stdlib/src/mysql2/result.rs": 39, - "crates/perry-stdlib/src/mysql2/types.rs": 16, "crates/perry-stdlib/src/nodemailer.rs": 3, - "crates/perry-stdlib/src/pg/result.rs": 14, - "crates/perry-stdlib/src/pg/types.rs": 14, "crates/perry-stdlib/src/querystring.rs": 2, "crates/perry-stdlib/src/readline/mod.rs": 4, "crates/perry-stdlib/src/sqlite/backup.rs": 8, @@ -76,5 +70,5 @@ "crates/perry-stdlib/src/zlib.rs": 3 }, "schema_version": 3, - "total": 527 + "total": 428 } diff --git a/workspace-architecture.json b/workspace-architecture.json index e8c96e8baf..bb6af3aa66 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -24,7 +24,7 @@ ] }, "baseline": { - "workspace_members": 66, + "workspace_members": 64, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -67,7 +67,7 @@ "perry-updater" ], "decision_counts": { - "externalize": 17, + "externalize": 15, "keep": 44, "merge": 1, "remove": 1, @@ -209,11 +209,6 @@ "decision": "externalize", "migration": "compile-source" }, - "perry-ext-mysql2": { - "category": "binding", - "decision": "externalize", - "migration": "compile-source" - }, "perry-ext-net": { "category": "binding", "decision": "keep", @@ -229,11 +224,6 @@ "decision": "externalize", "migration": "external-package" }, - "perry-ext-pg": { - "category": "binding", - "decision": "externalize", - "migration": "compile-source" - }, "perry-ext-sharp": { "category": "binding", "decision": "externalize",