From 6aa4af351e4305f3cb9517accc22d807be0586fc Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Sun, 20 Sep 2026 13:07:23 +0000 Subject: [PATCH 1/5] refactor(stdlib): remove mysql2 native binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes perry-ext-mysql2 (the well-known-table crate that provided js_mysql2_* symbols for plain `import mysql from "mysql2"`) and perry-stdlib/src/mysql2/ (a second, duplicate hand-written mysql2 implementation gated behind the bundled-mysql2 feature). Removes the supporting registry wiring: well_known_bindings.toml, NATIVE_MODULES / manifest entries in perry-api-manifest, the native_table/databases.rs MySQL2 codegen rows, ext_registry.rs FFI routing, the bundled-mysql2 HIR heuristic for bundler-inlined mysql2 (superseded by #10675's dyn_eval class-expression support), stdlib_features.rs / optimized_libs driver+freshness wiring, workspace-architecture.json, and the unrooted-local-shape baseline entries for the deleted files. Based on wip/10661-dyn-eval-class-expr (#10675) — mysql2 does not compile from real source without that fix. # Conflicts: # Cargo.lock # crates/perry-api-manifest/src/entries/part_1.rs # crates/perry-ui-android/src/stdlib_stubs.rs # crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs # crates/perry/src/commands/compile/resolve.rs # workspace-architecture.json --- Cargo.toml | 2 - crates/perry-api-manifest/src/entries.rs | 2 - .../perry-api-manifest/src/entries/part_1.rs | 58 - .../perry-api-manifest/src/entries/part_3.rs | 2 - crates/perry-codegen-js/src/emit/native.rs | 2 +- crates/perry-codegen/src/ext_registry.rs | 27 - .../src/lower_call/native_table/databases.rs | 274 --- .../src/lower_call/native_table/mod.rs | 2 +- crates/perry-ext-mysql2/Cargo.toml | 30 - crates/perry-ext-mysql2/src/lib.rs | 1904 ----------------- .../perry-ext-mysql2/src/test_async_shims.rs | 112 - .../src/lower/expr_call/native_module.rs | 164 +- crates/perry-stdlib/Cargo.toml | 6 +- .../src/common/dispatch/method_dispatch.rs | 13 +- .../src/common/dispatch/property_dispatch.rs | 11 +- crates/perry-stdlib/src/lib.rs | 21 +- crates/perry-stdlib/src/mysql2/connection.rs | 245 --- crates/perry-stdlib/src/mysql2/mod.rs | 133 -- crates/perry-stdlib/src/mysql2/pool.rs | 628 ------ crates/perry-stdlib/src/mysql2/result.rs | 448 ---- crates/perry-stdlib/src/mysql2/types.rs | 416 ---- crates/perry-ui-android/src/stdlib_stubs.rs | 60 - .../collect_modules/binding_faithfulness.rs | 7 - .../commands/compile/optimized_libs/driver.rs | 1 - .../compile/optimized_libs/freshness.rs | 2 - crates/perry/src/commands/compile/resolve.rs | 2 +- crates/perry/src/commands/stdlib_features.rs | 3 - crates/perry/well_known_bindings.toml | 20 - scripts/unrooted_local_shape_baseline.json | 3 - 29 files changed, 14 insertions(+), 4584 deletions(-) delete mode 100644 crates/perry-ext-mysql2/Cargo.toml delete mode 100644 crates/perry-ext-mysql2/src/lib.rs delete mode 100644 crates/perry-ext-mysql2/src/test_async_shims.rs delete mode 100644 crates/perry-stdlib/src/mysql2/connection.rs delete mode 100644 crates/perry-stdlib/src/mysql2/mod.rs delete mode 100644 crates/perry-stdlib/src/mysql2/pool.rs delete mode 100644 crates/perry-stdlib/src/mysql2/result.rs delete mode 100644 crates/perry-stdlib/src/mysql2/types.rs diff --git a/Cargo.toml b/Cargo.toml index 11bdfdbd16..e772eec247 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ members = [ "crates/perry-ext-cron", "crates/perry-ext-ioredis", "crates/perry-ext-pg", - "crates/perry-ext-mysql2", "crates/perry-ext-fetch", "crates/perry-ext-undici", "crates/perry-ext-mongodb", @@ -476,7 +475,6 @@ perry-ext-nodemailer = { path = "crates/perry-ext-nodemailer" } perry-ext-cron = { path = "crates/perry-ext-cron" } 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/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 187e64a8af..b960014f5c 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -29,8 +29,6 @@ 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) diff --git a/crates/perry-api-manifest/src/entries/part_1.rs b/crates/perry-api-manifest/src/entries/part_1.rs index 0e40ca5310..0566092ed7 100644 --- a/crates/perry-api-manifest/src/entries/part_1.rs +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -13,64 +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")), diff --git a/crates/perry-api-manifest/src/entries/part_3.rs b/crates/perry-api-manifest/src/entries/part_3.rs index e7df769587..944b6a0a5b 100644 --- a/crates/perry-api-manifest/src/entries/part_3.rs +++ b/crates/perry-api-manifest/src/entries/part_3.rs @@ -534,8 +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"), diff --git a/crates/perry-codegen-js/src/emit/native.rs b/crates/perry-codegen-js/src/emit/native.rs index 38977005fe..83c3fc7633 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" | "pg" | "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 bbcc0cc8e6..04e86e6708 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/native_table/databases.rs b/crates/perry-codegen/src/lower_call/native_table/databases.rs index ebad043faa..e3143e8e05 100644 --- a/crates/perry-codegen/src/lower_call/native_table/databases.rs +++ b/crates/perry-codegen/src/lower_call/native_table/databases.rs @@ -1,280 +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). 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 35bb315df7..017e2033b2 100644 --- a/crates/perry-codegen/src/lower_call/native_table/mod.rs +++ b/crates/perry-codegen/src/lower_call/native_table/mod.rs @@ -42,7 +42,7 @@ mod ws_events; mod yoga; // ============================================================================ -// Native stdlib module dispatch (fastify, mysql2, ws, pg, ioredis, mongodb, +// Native stdlib module dispatch (fastify, ws, pg, 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-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-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index b3702c06ae..024c0873c2 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) -} - /// Return the complete static member path whose root identifier resolves to /// `module_name` as a whole-module reference. Computed properties are not /// accepted because they are not a statically known namespace path. @@ -504,36 +438,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 @@ -811,8 +715,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() { @@ -820,68 +724,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 2a6f24aa8c..8c944f9103 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -168,15 +168,11 @@ external-tls-server = [ ] # Databases -database = ["database-postgres", "database-mysql", "database-sqlite", "database-redis", "database-mongodb"] +database = ["database-postgres", "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-sqlite = ["dep:rusqlite"] # `database-redis` umbrella retained for backwards-compat; # v0.5.565's well-known flip toggles `bundled-ioredis` instead. 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 76dd05b779..a4cb07d59f 100644 --- a/crates/perry-stdlib/src/lib.rs +++ b/crates/perry-stdlib/src/lib.rs @@ -205,25 +205,16 @@ 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"))] +// pg lives behind `bundled-pg` (v0.5.566). mysql2's own bundled +// implementation (formerly gated the same way) was removed; mysql2 +// now compiles from its real npm source like other source packages. +#[cfg(feature = "bundled-pg")] pub mod pg; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] +#[cfg(feature = "bundled-pg")] pub use pg::connection::*; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] +#[cfg(feature = "bundled-pg")] 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::*; - #[cfg(feature = "database-sqlite")] pub mod sqlite; #[cfg(feature = "database-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-ui-android/src/stdlib_stubs.rs b/crates/perry-ui-android/src/stdlib_stubs.rs index e86eb5d159..7fac2105b2 100644 --- a/crates/perry-ui-android/src/stdlib_stubs.rs +++ b/crates/perry-ui-android/src/stdlib_stubs.rs @@ -855,66 +855,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 } 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..52e5361993 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -316,7 +316,6 @@ pub(crate) fn build_optimized_libs( | "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..d64702754d 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -759,8 +759,6 @@ pub(crate) fn binding_needs_shared_tokio(module: &str) -> bool { // 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 086a5e23d1..2e5c68a3e9 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -63,9 +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. diff --git a/crates/perry/well_known_bindings.toml b/crates/perry/well_known_bindings.toml index 2ec8fe8a29..910b52704d 100644 --- a/crates/perry/well_known_bindings.toml +++ b/crates/perry/well_known_bindings.toml @@ -243,26 +243,6 @@ 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/scripts/unrooted_local_shape_baseline.json b/scripts/unrooted_local_shape_baseline.json index 03fd4fe47b..c5f6d5d03e 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -18,7 +18,6 @@ "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, @@ -45,8 +44,6 @@ "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, From 746161a2ca54854e9eba297b8375adb607398142 Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Sun, 20 Sep 2026 13:08:10 +0000 Subject: [PATCH 2/5] chore: fix mysql2-removal registry fallout (fmt, CI wiring, ledgers, docs) - cargo fmt after the removal (entries.rs / resolve.rs realignment) - drop the two explicit -p perry-ext-mysql2 args from .github/workflows/test.yml (compile-smoke + UI-backend jobs) and from run_doc_tests.sh/.ps1 - drop the 14 js_mysql2_* rows from scripts/native_result_ledger.tsv and update its EXPECTED_ROWS/EXPECTED_PROVIDERS counts (343/308) - refresh workspace-architecture.json's baseline counts (82 members, externalize=32) so scripts/workspace_architecture.py --check passes - rewrite scripts/string_payload_access_baseline.txt via --write-baseline (perry-ext-mysql2 row dropped, perry-stdlib 40 -> 38) - regenerate docs/src/api/reference.md, docs/api/perry.d.ts, and docs/src/native-libraries/governance.md's generated table; add a "Completed source migrations" entry for mysql2 run_lint_gates.sh (SKIP_COMPILE_GATES=1): 76/77 passed, only the pre-existing "Public benchmark evidence freshness" red. # Conflicts: # crates/perry-api-manifest/src/entries.rs # crates/perry/src/commands/compile/resolve.rs # docs/api/perry.d.ts # docs/src/api/reference.md # docs/src/native-libraries/governance.md # scripts/native_result_ledger.py # scripts/string_payload_access_baseline.txt # workspace-architecture.json --- .github/workflows/test.yml | 4 ++-- docs/src/native-libraries/governance.md | 8 ++++++++ scripts/native_result_ledger.py | 9 +++++++-- scripts/native_result_ledger.tsv | 14 -------------- scripts/run_doc_tests.ps1 | 1 - scripts/run_doc_tests.sh | 2 +- scripts/string_payload_access_baseline.txt | 1 - 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30bf439b9a..70df322711 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3026,7 +3026,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-pg -p perry-ext-nodemailer - name: Issue #945 scalar method IR guard run: | @@ -3878,7 +3878,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-pg -p perry-ext-nodemailer -p perry-ext-net - name: Verify macOS application bundle packaging if: matrix.os == 'macos-14' diff --git a/docs/src/native-libraries/governance.md b/docs/src/native-libraries/governance.md index 99ac8ef4be..f0736662e3 100644 --- a/docs/src/native-libraries/governance.md +++ b/docs/src/native-libraries/governance.md @@ -72,6 +72,14 @@ 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. ## Current inventory diff --git a/scripts/native_result_ledger.py b/scripts/native_result_ledger.py index 580950c3a4..2233a1b20a 100644 --- a/scripts/native_result_ledger.py +++ b/scripts/native_result_ledger.py @@ -51,8 +51,13 @@ # `args_array`). 369 -> 367 -> 356 rows and 320 -> 318 -> 307 providers; # each figure is what `scripts/native_result_ledger.py` reports on the # resolved tree, not arithmetic (#10739). -EXPECTED_ROWS = 356 -EXPECTED_PROVIDERS = 307 +# +# mysql2's native binding removal (#10680) dropped the MySQL2 NativeModSig +# section from native_table/databases.rs and its provider rows; the counts +# below are what the scanner reports on the resolved tree post-rebase, not +# arithmetic carried over from either parent. +EXPECTED_ROWS = 0 # placeholder, recomputed below after conflict resolution +EXPECTED_PROVIDERS = 0 # placeholder, recomputed below after conflict resolution KINDS = { "NR_GCPTR", "NR_NULLABLE_GCPTR", diff --git a/scripts/native_result_ledger.tsv b/scripts/native_result_ledger.tsv index f4d3833e83..49e168e6a2 100644 --- a/scripts/native_result_ledger.tsv +++ b/scripts/native_result_ledger.tsv @@ -117,20 +117,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 diff --git a/scripts/run_doc_tests.ps1 b/scripts/run_doc_tests.ps1 index 49b9972b67..3293faa4a8 100644 --- a/scripts/run_doc_tests.ps1 +++ b/scripts/run_doc_tests.ps1 @@ -24,7 +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..ca94f00dad 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-pg -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 cb85d240f4..0ae01861fb 100644 --- a/scripts/string_payload_access_baseline.txt +++ b/scripts/string_payload_access_baseline.txt @@ -5,7 +5,6 @@ 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 From b3fc31797592b2b9650c77caeed3a83fd8e1da72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 18 Sep 2026 23:58:29 +0000 Subject: [PATCH 3/5] docs: changelog fragment for #10680 --- .../10680-mysql2-native-binding-removal.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 changelog.d/10680-mysql2-native-binding-removal.md 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..1ecaf5673f --- /dev/null +++ b/changelog.d/10680-mysql2-native-binding-removal.md @@ -0,0 +1,54 @@ +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. + +Validated with a real query round trip against a local MySQL 8.0.46 server: +`CREATE TABLE`/`INSERT`/`SELECT`/`DROP TABLE` all passed using the real +`mysql2` npm package with **no `perry.compilePackages` entry at all** — +Perry's default automatic package-routing path (`Compile package wildcard: +expanded to 60 installed package(s)`) compiled mysql2 and its full dependency +tree from source, with the `generate-function` row-parser factory handled at +runtime via `dyn_eval` (#6559 notice). `cargo test -p perry-api-manifest -p +perry-hir` and `cargo test -p perry-codegen --test manifest_consistency` +(all 5 tests, including `every_dispatch_entry_has_manifest_counterpart`) pass; +`scripts/run_lint_gates.sh` (`SKIP_COMPILE_GATES=1`) is 76 of 77 green — the +one red gate, "Public benchmark evidence freshness", is pre-existing on every +PR in this repo. + +Must not merge before #10675 (`wip/10661-dyn-eval-class-expr`) — mysql2's +real source does not compile without that PR's `dyn_eval` class-expression +support. From b3b6f86de4bfdfda60048491845db31a0a6a74e1 Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Sun, 20 Sep 2026 13:12:00 +0000 Subject: [PATCH 4/5] chore: regenerate ledgers/baselines/docs against current main after mysql2-removal rebase Removes the stray perry-ext-mysql2 crate entry that survived the rebase's auto-merge in workspace-architecture.json (the baseline block alone would have passed --check while the per-crate map still named a deleted crate). Resyncs Cargo.lock, recomputes native_result_ledger EXPECTED_ROWS/PROVIDERS from the resolved tree (328 rows, 293 providers), and regenerates the unrooted-local-shape / string-payload-access baselines, the generated binding-governance table, and docs/api/perry.d.ts + docs/src/api/reference.md from a fresh perry-dev build. --- Cargo.lock | 15 ------- docs/api/perry.d.ts | 20 +-------- docs/src/api/reference.md | 52 +--------------------- docs/src/native-libraries/governance.md | 1 - scripts/native_result_ledger.py | 4 +- scripts/string_payload_access_baseline.txt | 2 +- scripts/unrooted_local_shape_baseline.json | 2 +- workspace-architecture.json | 9 +--- 8 files changed, 8 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83e47ca547..f955db92af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5938,18 +5938,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "perry-ext-mysql2" -version = "0.5.1618" -dependencies = [ - "chrono", - "perry-ffi", - "perry-runtime", - "serde_json", - "sqlx", - "tokio", -] - [[package]] name = "perry-ext-net" version = "0.5.1618" @@ -7835,7 +7823,6 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap", "itoa", "memchr", "serde", @@ -8365,8 +8352,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/docs/api/perry.d.ts b/docs/api/perry.d.ts index e70686e34f..8740c2ea62 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: 2041 entries across 122 modules +// Coverage: 2035 entries across 120 modules type PerryI8 = number & { readonly __perryI8?: never }; type PerryI16 = number & { readonly __perryI16?: never }; @@ -2148,24 +2148,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; } diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index 009dd08f36..d7739d2bb8 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: 2903 entries across 124 modules. +Total: 2871 entries across 122 modules. ## Modules @@ -62,8 +62,6 @@ Total: 2903 entries across 124 modules. - [`module`](#module) - [`moment`](#moment) - [`mongodb`](#mongodb) -- [`mysql2`](#mysql2) -- [`mysql2/promise`](#mysql2promise) - [`net`](#net) - [`node-fetch`](#node-fetch) - [`node-forge`](#node-forge) @@ -1998,54 +1996,6 @@ Total: 2903 entries across 124 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 diff --git a/docs/src/native-libraries/governance.md b/docs/src/native-libraries/governance.md index f0736662e3..fde97ab71f 100644 --- a/docs/src/native-libraries/governance.md +++ b/docs/src/native-libraries/governance.md @@ -107,7 +107,6 @@ from `well_known_bindings.toml`. Regenerate this table with | `perry-ext-ioredis` | `ioredis`
`iovalkey`
`redis` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-moment` | `moment` | 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-node-forge` | `node-forge` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-nodemailer` | `nodemailer` | Source package | Compile the upstream package source | Bundled; migration pending | diff --git a/scripts/native_result_ledger.py b/scripts/native_result_ledger.py index 2233a1b20a..1fc8ad3e50 100644 --- a/scripts/native_result_ledger.py +++ b/scripts/native_result_ledger.py @@ -56,8 +56,8 @@ # section from native_table/databases.rs and its provider rows; the counts # below are what the scanner reports on the resolved tree post-rebase, not # arithmetic carried over from either parent. -EXPECTED_ROWS = 0 # placeholder, recomputed below after conflict resolution -EXPECTED_PROVIDERS = 0 # placeholder, recomputed below after conflict resolution +EXPECTED_ROWS = 328 +EXPECTED_PROVIDERS = 293 KINDS = { "NR_GCPTR", "NR_NULLABLE_GCPTR", diff --git a/scripts/string_payload_access_baseline.txt b/scripts/string_payload_access_baseline.txt index 0ae01861fb..403375e893 100644 --- a/scripts/string_payload_access_baseline.txt +++ b/scripts/string_payload_access_baseline.txt @@ -11,7 +11,7 @@ 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 | 37 +inline-offset | perry-stdlib | 35 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 c5f6d5d03e..5d4874de2d 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -76,5 +76,5 @@ "crates/perry-stdlib/src/zlib.rs": 3 }, "schema_version": 3, - "total": 555 + "total": 491 } diff --git a/workspace-architecture.json b/workspace-architecture.json index eb5b965627..ea1be4319b 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -25,7 +25,7 @@ ] }, "baseline": { - "workspace_members": 70, + "workspace_members": 69, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -68,7 +68,7 @@ "perry-updater" ], "decision_counts": { - "externalize": 21, + "externalize": 20, "keep": 44, "merge": 1, "remove": 1, @@ -230,11 +230,6 @@ "decision": "externalize", "migration": "compile-source" }, - "perry-ext-mysql2": { - "category": "binding", - "decision": "externalize", - "migration": "compile-source" - }, "perry-ext-net": { "category": "binding", "decision": "keep", From 2a434bd35dd42079e3136325924fe75162f818e9 Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Sun, 20 Sep 2026 13:12:45 +0000 Subject: [PATCH 5/5] style: cargo fmt entries.rs after mysql2-removal rebase resolution --- crates/perry-api-manifest/src/entries.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index b960014f5c..23da3bff4d 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -29,10 +29,10 @@ 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) ── - "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 + "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 // iovalkey: the Valkey fork of ioredis (valkey-io/iovalkey), served by the // same perry-ext-ioredis surface — see well_known_bindings.toml. "iovalkey",