From 307171d16264948239b664366436083cc943cced Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Fri, 18 Sep 2026 23:04:43 +0000 Subject: [PATCH 1/3] chore(pg): remove native pg binding, compile real package from source Removes crates/perry-ext-pg (sqlx::postgres + tokio bridge) and the duplicate pre-#466 in-tree pg implementation in crates/perry-stdlib/src/pg/ (bundled-pg feature), plus every registry entry that pointed at them. import ... from "pg" now falls through to real-source compilation instead of the native binding. wip, base = PR #10674 (fix/10437-cjs-conditional-require) since pg does not run without that fix. --- Cargo.lock | 9 - Cargo.toml | 2 - crates/perry-api-manifest/src/entries.rs | 1 - .../perry-api-manifest/src/entries/part_1.rs | 7 - .../perry-api-manifest/src/entries/part_3.rs | 2 - crates/perry-ext-pg/Cargo.toml | 20 - crates/perry-ext-pg/src/lib.rs | 750 ------------------ crates/perry-stdlib/Cargo.toml | 8 +- crates/perry-stdlib/src/lib.rs | 24 +- crates/perry-stdlib/src/pg/connection.rs | 415 ---------- crates/perry-stdlib/src/pg/mod.rs | 13 - crates/perry-stdlib/src/pg/pool.rs | 199 ----- crates/perry-stdlib/src/pg/result.rs | 69 -- crates/perry-stdlib/src/pg/types.rs | 233 ------ .../commands/compile/optimized_libs/driver.rs | 1 - .../compile/optimized_libs/freshness.rs | 1 - crates/perry/src/commands/stdlib_features.rs | 4 - crates/perry/well_known_bindings.toml | 12 - workspace-architecture.json | 5 - 19 files changed, 11 insertions(+), 1764 deletions(-) delete mode 100644 crates/perry-ext-pg/Cargo.toml delete mode 100644 crates/perry-ext-pg/src/lib.rs delete mode 100644 crates/perry-stdlib/src/pg/connection.rs delete mode 100644 crates/perry-stdlib/src/pg/mod.rs delete mode 100644 crates/perry-stdlib/src/pg/pool.rs delete mode 100644 crates/perry-stdlib/src/pg/result.rs delete mode 100644 crates/perry-stdlib/src/pg/types.rs diff --git a/Cargo.lock b/Cargo.lock index 0894a6f6d7..11e1e7bfc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6151,15 +6151,6 @@ dependencies = [ "printpdf", ] -[[package]] -name = "perry-ext-pg" -version = "0.5.1596" -dependencies = [ - "perry-ffi", - "sqlx", - "tokio", -] - [[package]] name = "perry-ext-qs" version = "0.5.1596" diff --git a/Cargo.toml b/Cargo.toml index 32c668115e..22dc841d26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ members = [ "crates/perry-ext-nodemailer", "crates/perry-ext-cron", "crates/perry-ext-ioredis", - "crates/perry-ext-pg", "crates/perry-ext-mysql2", "crates/perry-ext-fetch", "crates/perry-ext-undici", @@ -504,7 +503,6 @@ perry-ext-ethers = { path = "crates/perry-ext-ethers" } 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" } diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 0e1415b629..505f8447a9 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -31,7 +31,6 @@ 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 "uuid", // RFC-4122 UUID generation "qs", // nested query-string parser/stringifier (Stripe dependency) "bcrypt", // bcrypt 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 aa51556739..28aa777ecb 100644 --- a/crates/perry-api-manifest/src/entries/part_1.rs +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -122,13 +122,6 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[ method("mysql2/promise", "beginTransaction", true, None), method("mysql2/promise", "commit", true, None), method("mysql2/promise", "rollback", true, None), - method_sig("pg", "connect", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig("pg", "Pool", false, None, &[p_any("p0")], TypeSpec::Any), - method("pg", "connect", true, Some("Client")), - method("pg", "query", true, Some("Pool")), - method("pg", "end", true, Some("Pool")), - method("pg", "query", true, None), - method("pg", "end", true, None), method_sig( "ioredis", "createClient", diff --git a/crates/perry-api-manifest/src/entries/part_3.rs b/crates/perry-api-manifest/src/entries/part_3.rs index e7df769587..7207a3a659 100644 --- a/crates/perry-api-manifest/src/entries/part_3.rs +++ b/crates/perry-api-manifest/src/entries/part_3.rs @@ -536,8 +536,6 @@ pub(crate) const API_MANIFEST_PART_3: &[ApiEntry] = &[ class("ioredis", "Redis"), class("mysql2/promise", "Pool"), class("mysql2", "Pool"), - class("pg", "Pool"), - class("pg", "Client"), class("url", "URL"), class("url", "URLSearchParams"), class("url", "URLPattern"), diff --git a/crates/perry-ext-pg/Cargo.toml b/crates/perry-ext-pg/Cargo.toml deleted file mode 100644 index 2f250c14ee..0000000000 --- a/crates/perry-ext-pg/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "perry-ext-pg" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Native bindings for npm `pg` (PostgreSQL client) — uses only `perry-ffi`. Async via `sqlx::postgres` bridged through spawn_blocking + JsPromise + tokio::Handle::current().block_on. Both pre-connect handles and connection pool surfaces." - -[lints] -workspace = true - -[lib] -crate-type = ["staticlib", "rlib"] - -[dependencies] -perry-ffi.workspace = true -sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "postgres", "chrono"] } -tokio = { workspace = true } - -[dev-dependencies] -perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-pg/src/lib.rs b/crates/perry-ext-pg/src/lib.rs deleted file mode 100644 index 686efa4ee5..0000000000 --- a/crates/perry-ext-pg/src/lib.rs +++ /dev/null @@ -1,750 +0,0 @@ -//! Native bindings for the npm `pg` PostgreSQL client — uses only -//! perry-ffi. Async via `sqlx::postgres` bridged through -//! `spawn_blocking + JsPromise + tokio::Handle::current().block_on`. -//! -//! Mirrors perry-stdlib's existing surface: `Client` (pre-connect -//! / connected handle states with `.connect()` deferring the TCP -//! handshake), `Pool` (lazy `connect_lazy`-style + eager -//! `pg.createPool`), parameterized `query()` with `Null`/`String`/ -//! `Number`/`Int`/`Bool` param types, result objects with -//! `rows`/`fields`/`rowCount`/`command` keys, row objects keyed by -//! column name. BigInt param support deferred — perry-ffi's BigInt -//! surface is in place (v0.5.556) but the JS-side array iteration -//! shape needs an extra adapter; followup once any wrapper actually -//! demands it. - -use perry_ffi::{ - alloc_string, build_object_shape, get_handle_mut, js_array_alloc, js_array_get, js_array_push, - js_object_alloc_with_shape, js_object_get_field, js_object_set_field, register_handle, - spawn_blocking, take_handle, ArrayHeader, Handle, JsPromise, JsValue, ObjectHeader, Promise, - StringHeader, -}; -use sqlx::postgres::{PgColumn, PgConnection, PgPool, PgPoolOptions, PgRow}; -use sqlx::{Column, Connection, Row, TypeInfo}; - -/// Connection config — same field shape as perry-stdlib's PgConfig. -#[derive(Debug, Clone)] -pub struct PgConfig { - pub host: String, - pub port: u16, - pub user: String, - pub password: String, - pub database: Option, -} - -impl Default for PgConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 5432, - user: "postgres".to_string(), - password: String::new(), - database: None, - } - } -} - -impl PgConfig { - pub fn to_url(&self) -> String { - let db = self - .database - .as_ref() - .map(|d| format!("/{}", d)) - .unwrap_or_default(); - format!( - "postgres://{}:{}@{}:{}{}", - self.user, self.password, self.host, self.port, db - ) - } -} - -unsafe fn jsvalue_to_string(value: JsValue) -> Option { - if value.is_string() { - let ptr = value.as_string_ptr(); - if !ptr.is_null() { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - return std::str::from_utf8(bytes).ok().map(String::from); - } - } - None -} - -/// Object layout matches perry-stdlib's positional convention: -/// field 0: host (string) -/// field 1: port (number) -/// field 2: user (string) -/// field 3: password (string) -/// field 4: database (string, optional) -unsafe fn parse_pg_config(config: JsValue) -> PgConfig { - let mut result = PgConfig::default(); - let obj_ptr = config.as_pointer::(); - if obj_ptr.is_null() { - return result; - } - - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 0)) { - result.host = s; - } - let port_val = js_object_get_field(obj_ptr, 1); - if port_val.is_number() { - result.port = port_val.to_number() as u16; - } - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 2)) { - result.user = s; - } - if let Some(s) = jsvalue_to_string(js_object_get_field(obj_ptr, 3)) { - result.password = s; - } - let db_val = js_object_get_field(obj_ptr, 4); - if !db_val.is_undefined() && !db_val.is_null() { - if let Some(s) = jsvalue_to_string(db_val) { - result.database = Some(s); - } - } - result -} - -/// Convert a single column value to a JsValue, mapping common -/// PostgreSQL OIDs to JS scalars. Unknown types fall back to a -/// string read. -fn column_value_to_jsvalue(row: &PgRow, index: usize) -> JsValue { - let col = &row.columns()[index]; - let type_name = col.type_info().name(); - match type_name { - "INT4" | "INT2" => row - .try_get::(index) - .map(JsValue::from_int32) - .unwrap_or(JsValue::NULL), - "INT8" => row - .try_get::(index) - .map(|n| JsValue::from_number(n as f64)) - .unwrap_or(JsValue::NULL), - "FLOAT4" | "FLOAT8" | "NUMERIC" => row - .try_get::(index) - .map(JsValue::from_number) - .unwrap_or(JsValue::NULL), - "VARCHAR" | "CHAR" | "TEXT" | "BPCHAR" | "NAME" => row - .try_get::(index) - .map(|s| JsValue::from_string_ptr(alloc_string(&s).as_raw())) - .unwrap_or(JsValue::NULL), - "BOOL" => row - .try_get::(index) - .map(JsValue::from_bool) - .unwrap_or(JsValue::NULL), - _ => row - .try_get::(index) - .map(|s| JsValue::from_string_ptr(alloc_string(&s).as_raw())) - .unwrap_or(JsValue::NULL), - } -} - -/// Build a row object keyed by column names. Replaces perry-stdlib's -/// `js_object_alloc(0, n)` no-shape pattern with a perry-ffi -/// shape-aware allocation — same observable behavior since user code -/// accesses `row.id` through dynamic property lookup either way. -fn row_to_js_object(row: &PgRow) -> *mut ObjectHeader { - let cols: Vec<&str> = row.columns().iter().map(|c| c.name()).collect(); - let (packed, shape_id) = build_object_shape(&cols); - let obj = unsafe { - js_object_alloc_with_shape( - shape_id, - cols.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ) - }; - for i in 0..cols.len() { - let val = column_value_to_jsvalue(row, i); - unsafe { js_object_set_field(obj, i as u32, val) }; - } - obj -} - -/// Build a `FieldDef`-shaped object matching node-pg's `result.fields[i]` -/// (#4917): `dataTypeID` is the numeric type OID, `tableID`/`columnID` come -/// from the RowDescription (0 for expression columns, like Node). -/// `dataTypeSize`/`dataTypeModifier` are not exposed by sqlx 0.8 and report -/// the "unknown/variable" sentinel -1. Twin of -/// `perry_stdlib::pg::types::column_to_field_def` — keep in sync. -fn column_to_field_def(col: &PgColumn) -> *mut ObjectHeader { - let (packed, shape_id) = build_object_shape(&[ - "name", - "tableID", - "columnID", - "dataTypeID", - "dataTypeSize", - "dataTypeModifier", - "format", - ]); - let obj = - unsafe { js_object_alloc_with_shape(shape_id, 7, packed.as_ptr(), packed.len() as u32) }; - let name_str = alloc_string(col.name()); - let table_id = col.relation_id().map(|oid| oid.0 as f64).unwrap_or(0.0); - let column_id = col - .relation_attribute_no() - .map(|attno| attno as f64) - .unwrap_or(0.0); - let data_type_id = col.type_info().oid().map(|oid| oid.0 as f64).unwrap_or(0.0); - let format_str = alloc_string("text"); - unsafe { - js_object_set_field(obj, 0, JsValue::from_string_ptr(name_str.as_raw())); - js_object_set_field(obj, 1, JsValue::from_number(table_id)); - js_object_set_field(obj, 2, JsValue::from_number(column_id)); - js_object_set_field(obj, 3, JsValue::from_number(data_type_id)); - js_object_set_field(obj, 4, JsValue::from_number(-1.0)); - js_object_set_field(obj, 5, JsValue::from_number(-1.0)); - js_object_set_field(obj, 6, JsValue::from_string_ptr(format_str.as_raw())); - } - obj -} - -/// Wrap a query outcome in pg's `{ rows, fields, rowCount, command }` -/// result object. -fn rows_to_pg_result(rows: Vec, columns: &[PgColumn], command: &str) -> JsValue { - let (packed, shape_id) = build_object_shape(&["rows", "fields", "rowCount", "command"]); - let result_obj = - unsafe { js_object_alloc_with_shape(shape_id, 4, packed.as_ptr(), packed.len() as u32) }; - - // rows array - let mut rows_arr = unsafe { js_array_alloc(rows.len() as u32) }; - for row in &rows { - let row_obj = row_to_js_object(row); - rows_arr = unsafe { js_array_push(rows_arr, JsValue::from_object_ptr(row_obj)) }; - } - unsafe { js_object_set_field(result_obj, 0, JsValue::from_object_ptr(rows_arr)) }; - - // fields array - let mut fields_arr = unsafe { js_array_alloc(columns.len() as u32) }; - for col in columns { - let field_obj = column_to_field_def(col); - fields_arr = unsafe { js_array_push(fields_arr, JsValue::from_object_ptr(field_obj)) }; - } - unsafe { js_object_set_field(result_obj, 1, JsValue::from_object_ptr(fields_arr)) }; - - unsafe { - js_object_set_field(result_obj, 2, JsValue::from_number(rows.len() as f64)); - let cmd_str = alloc_string(command); - js_object_set_field(result_obj, 3, JsValue::from_string_ptr(cmd_str.as_raw())); - } - JsValue::from_object_ptr(result_obj) -} - -fn empty_pg_result(command: &str, row_count: u64) -> JsValue { - let value = rows_to_pg_result(Vec::new(), &[], command); - let obj: *mut ObjectHeader = value.as_pointer(); - if !obj.is_null() { - unsafe { - js_object_set_field(obj, 2, JsValue::from_number(row_count as f64)); - } - } - value -} - -#[derive(Clone, Debug)] -enum ParamValue { - Null, - String(String), - Number(f64), - Int(i64), - Bool(bool), -} - -unsafe fn extract_params_from_jsvalue(params: JsValue) -> Vec { - let arr_ptr = params.as_pointer::(); - if arr_ptr.is_null() { - return Vec::new(); - } - // Pull the array length out of the header — the layout matches - // perry-runtime's `ArrayHeader { length: u32, capacity: u32 }`. - let length = (*arr_ptr).length; - - let mut result = Vec::with_capacity(length as usize); - for i in 0..length { - let element = js_array_get(arr_ptr, i); - let param = if element.is_null() || element.is_undefined() { - ParamValue::Null - } else if element.is_string() { - jsvalue_to_string(element) - .map(ParamValue::String) - .unwrap_or(ParamValue::Null) - } else if element.is_int32() { - ParamValue::Int(element.to_int32() as i64) - } else if element.is_bool() { - ParamValue::Bool(element.to_bool()) - } else if element.is_number() { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - } else { - ParamValue::Null - }; - result.push(param); - } - result -} - -fn is_row_returning_query(sql: &str) -> bool { - let trimmed = sql.trim_start(); - let upper = trimmed.get(..10).unwrap_or(trimmed).to_uppercase(); - upper.starts_with("SELECT") - || upper.starts_with("SHOW") - || upper.starts_with("DESC") - || upper.starts_with("EXPLAIN") - || upper.starts_with("WITH") -} - -unsafe fn read_sql(sql_ptr: *const u8) -> String { - if sql_ptr.is_null() { - return String::new(); - } - let header = sql_ptr as *const StringHeader; - let len = (*header).byte_len as usize; - let data = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - std::str::from_utf8(bytes).unwrap_or("").to_string() -} - -// ── Connection (Client) ─────────────────────────────────────────── - -/// Wraps a `PgConnection` so it can sit in the handle registry. -/// Pre-connect: `pending_config = Some, connection = None`. -/// Connected: `pending_config = None, connection = Some`. -pub struct PgConnectionHandle { - pub connection: Option, - pub pending_config: Option, -} - -impl PgConnectionHandle { - pub fn new(conn: PgConnection) -> Self { - Self { - connection: Some(conn), - pending_config: None, - } - } - pub fn pending(config: PgConfig) -> Self { - Self { - connection: None, - pending_config: Some(config), - } - } -} - -/// `new Client(config)` — sync constructor, no TCP touch. -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue (passed as f64 at the FFI -/// boundary). -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_new(config_f: f64) -> Handle { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgConnectionHandle::pending(pg_config)) -} - -/// `client.connect()` — opens the TCP connection using the config -/// stored at `js_pg_client_new` time. No-op success if already -/// connected. -#[no_mangle] -pub extern "C" fn js_pg_client_connect(client_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - // Snapshot the pending config before entering spawn_blocking — - // can't hold a `&mut` across the boundary. - let pending = - get_handle_mut::(client_handle).and_then(|h| h.pending_config.take()); - - let Some(pg_config) = pending else { - promise.resolve_undefined(); - return raw; - }; - - spawn_blocking(move || { - let result = tokio::runtime::Handle::current() - .block_on(async move { PgConnection::connect(&pg_config.to_url()).await }); - match result { - Ok(conn) => { - if let Some(h) = get_handle_mut::(client_handle) { - h.connection = Some(conn); - } - promise.resolve_undefined(); - } - Err(e) => promise.reject_string(&format!("Failed to connect: {}", e)), - } - }); - raw -} - -/// Combined `pg.connect(config)` — sync `new` + async connect; older -/// API kept for back-compat with perry-stdlib callers. -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_pg_connect(config_f: f64) -> *mut Promise { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let result = tokio::runtime::Handle::current() - .block_on(async move { PgConnection::connect(&pg_config.to_url()).await }); - match result { - Ok(conn) => { - let handle = register_handle(PgConnectionHandle::new(conn)); - promise.resolve(JsValue::from_number(handle as f64)); - } - Err(e) => promise.reject_string(&format!("Failed to connect: {}", e)), - } - }); - raw -} - -/// `client.end()` — close the connection. -#[no_mangle] -pub extern "C" fn js_pg_client_end(client_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - if let Some(mut wrapper) = take_handle::(client_handle) { - if let Some(conn) = wrapper.connection.take() { - let result = tokio::runtime::Handle::current().block_on(conn.close()); - match result { - Ok(()) => promise.resolve_undefined(), - Err(e) => promise.reject_string(&format!("Failed to close connection: {}", e)), - } - } else { - promise.reject_string("Connection already closed"); - } - } else { - promise.reject_string("Invalid client handle"); - } - }); - raw -} - -/// `client.query(sql)` — no params. -/// -/// # Safety -/// `sql_ptr` must be null or a Perry-runtime `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query( - client_handle: Handle, - sql_ptr: *const u8, -) -> *mut Promise { - let sql = read_sql(sql_ptr); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let outcome = tokio::runtime::Handle::current().block_on(async move { - let wrapper = get_handle_mut::(client_handle) - .ok_or_else(|| "Invalid client handle".to_string())?; - let conn = wrapper - .connection - .as_mut() - .ok_or_else(|| "Connection already closed".to_string())?; - sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(conn) - .await - .map_err(|e| format!("Query failed: {}", e)) - }); - match outcome { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - let result = rows_to_pg_result(rows, &columns, &command); - promise.resolve(result); - } - Err(e) => promise.reject_string(&e), - } - }); - raw -} - -/// `client.query(sql, params)` — parameterized. -/// -/// # Safety -/// `sql_ptr` must be null or a Perry-runtime `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query_params( - client_handle: Handle, - sql_ptr: *const u8, - params_f: f64, -) -> *mut Promise { - let sql = read_sql(sql_ptr); - let params = JsValue::from_bits(params_f.to_bits()); - let param_values = extract_params_from_jsvalue(params); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - let is_select = is_row_returning_query(&sql); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let outcome = tokio::runtime::Handle::current().block_on(async move { - let wrapper = get_handle_mut::(client_handle) - .ok_or_else(|| "Invalid client handle".to_string())?; - let conn = wrapper - .connection - .as_mut() - .ok_or_else(|| "Connection already closed".to_string())?; - let mut query = sqlx::query(sqlx::AssertSqlSafe(sql.clone())); - for p in ¶m_values { - query = match p { - ParamValue::Null => query.bind(Option::::None), - ParamValue::String(s) => query.bind(s.clone()), - ParamValue::Number(n) => query.bind(*n), - ParamValue::Int(i) => query.bind(*i), - ParamValue::Bool(b) => query.bind(*b), - }; - } - if is_select { - let rows = query - .fetch_all(conn) - .await - .map_err(|e| format!("Query failed: {}", e))?; - Ok::<_, String>(QueryOutcome::Rows(rows)) - } else { - let exec_result = query - .execute(conn) - .await - .map_err(|e| format!("Query failed: {}", e))?; - Ok(QueryOutcome::RowsAffected(exec_result.rows_affected())) - } - }); - match outcome { - Ok(QueryOutcome::Rows(rows)) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - promise.resolve(rows_to_pg_result(rows, &columns, &command)); - } - Ok(QueryOutcome::RowsAffected(n)) => { - promise.resolve(empty_pg_result(&command, n)); - } - Err(e) => promise.reject_string(&e), - } - }); - raw -} - -enum QueryOutcome { - Rows(Vec), - RowsAffected(u64), -} - -// ── Pool ────────────────────────────────────────────────────────── - -pub struct PgPoolHandle { - pub pool: Option, - pub pending_url: Option, -} - -impl PgPoolHandle { - pub fn new(pool: PgPool) -> Self { - Self { - pool: Some(pool), - pending_url: None, - } - } - pub fn pending(url: String) -> Self { - Self { - pool: None, - pending_url: Some(url), - } - } - - pub async fn ensure_pool(&mut self) -> Result<&PgPool, String> { - if self.pool.is_none() { - let url = self - .pending_url - .take() - .ok_or_else(|| "Pool config missing".to_string())?; - let pool = PgPoolOptions::new() - .max_connections(10) - .connect(&url) - .await - .map_err(|e| format!("Failed to create pool: {}", e))?; - self.pool = Some(pool); - } - Ok(self.pool.as_ref().unwrap()) - } -} - -/// `new Pool(config)` — sync constructor; sqlx's pool is built lazily -/// on first query (sqlx 0.8's `connect_lazy` panics outside a Tokio -/// runtime, so we can't even pre-arm it here). -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_new(config_f: f64) -> Handle { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgPoolHandle::pending(pg_config.to_url())) -} - -/// `pg.createPool(config)` — async eager pool factory (back-compat -/// with perry-stdlib's older entry). -/// -/// # Safety -/// `config_f` is a NaN-boxed JsValue. -#[no_mangle] -pub unsafe extern "C" fn js_pg_create_pool(config_f: f64) -> *mut Promise { - let config = JsValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - let promise = JsPromise::new(); - let raw = promise.as_raw(); - - spawn_blocking(move || { - let url = pg_config.to_url(); - let result = tokio::runtime::Handle::current() - .block_on(async move { PgPoolOptions::new().max_connections(10).connect(&url).await }); - match result { - Ok(pool) => { - let handle = register_handle(PgPoolHandle::new(pool)); - promise.resolve(JsValue::from_number(handle as f64)); - } - Err(e) => promise.reject_string(&format!("Failed to create pool: {}", e)), - } - }); - raw -} - -/// `pool.query(sql)` — runs against the lazy-built sqlx pool. -/// -/// # Safety -/// `sql_ptr` must be null or a Perry-runtime `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_query(pool_handle: Handle, sql_ptr: *const u8) -> *mut Promise { - let sql = read_sql(sql_ptr); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - let outcome = tokio::runtime::Handle::current().block_on(async move { - let wrapper = get_handle_mut::(pool_handle) - .ok_or_else(|| "Invalid pool handle".to_string())?; - let pool = wrapper.ensure_pool().await?; - sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(pool) - .await - .map_err(|e| format!("Query failed: {}", e)) - }); - match outcome { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - promise.resolve(rows_to_pg_result(rows, &columns, &command)); - } - Err(e) => promise.reject_string(&e), - } - }); - raw -} - -/// `pool.end()` — close all connections in the pool. -#[no_mangle] -pub extern "C" fn js_pg_pool_end(pool_handle: Handle) -> *mut Promise { - let promise = JsPromise::new(); - let raw = promise.as_raw(); - spawn_blocking(move || { - if let Some(mut wrapper) = take_handle::(pool_handle) { - tokio::runtime::Handle::current().block_on(async move { - if let Some(pool) = wrapper.pool.take() { - pool.close().await; - } - }); - promise.resolve_undefined(); - } else { - promise.reject_string("Invalid pool handle"); - } - }); - raw -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pg_config_defaults() { - let cfg = PgConfig::default(); - assert_eq!(cfg.host, "localhost"); - assert_eq!(cfg.port, 5432); - assert_eq!(cfg.user, "postgres"); - assert!(cfg.database.is_none()); - } - - #[test] - fn to_url_omits_db_when_absent() { - let cfg = PgConfig::default(); - let url = cfg.to_url(); - assert_eq!(url, "postgres://postgres:@localhost:5432"); - } - - #[test] - fn to_url_with_db() { - let mut cfg = PgConfig::default(); - cfg.database = Some("mydb".to_string()); - cfg.user = "u".to_string(); - cfg.password = "p".to_string(); - cfg.host = "db.example.com".to_string(); - cfg.port = 5433; - assert_eq!(cfg.to_url(), "postgres://u:p@db.example.com:5433/mydb"); - } - - #[test] - fn is_row_returning_query_classifier() { - assert!(is_row_returning_query("SELECT * FROM x")); - assert!(is_row_returning_query(" select 1")); - assert!(is_row_returning_query("WITH cte AS ...")); - assert!(!is_row_returning_query("INSERT INTO x VALUES (1)")); - assert!(!is_row_returning_query("UPDATE x SET y = 1")); - } - - #[test] - fn client_new_returns_handle() { - let cfg_obj = unsafe { - let (packed, shape_id) = - build_object_shape(&["host", "port", "user", "password", "database"]); - let obj = js_object_alloc_with_shape(shape_id, 5, packed.as_ptr(), packed.len() as u32); - let host_str = alloc_string("localhost"); - js_object_set_field(obj, 0, JsValue::from_string_ptr(host_str.as_raw())); - js_object_set_field(obj, 1, JsValue::from_number(5432.0)); - JsValue::from_object_ptr(obj) - }; - let h = unsafe { js_pg_client_new(f64::from_bits(cfg_obj.bits())) }; - assert!(h > 0); - } -} diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 2881d64681..4428b2cffc 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -201,11 +201,7 @@ external-tls-server = [ ] # Databases -database = ["database-postgres", "database-mysql", "database-sqlite", "database-redis", "database-mongodb"] -# `database-postgres` umbrella retained for backwards-compat; -# v0.5.566's well-known flip toggles `bundled-pg` instead. -database-postgres = ["bundled-pg"] -bundled-pg = ["dep:sqlx", "async-runtime"] +database = ["database-mysql", "database-sqlite", "database-redis", "database-mongodb"] # `database-mysql` umbrella retained for backwards-compat; # v0.5.567's well-known flip toggles `bundled-mysql2` instead. database-mysql = ["bundled-mysql2"] @@ -369,7 +365,7 @@ rustls-native-certs = { version = "0.8", optional = true } rustls-pemfile = { workspace = true, optional = true } # Database -sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql", "postgres", "chrono"], optional = true } +sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql", "chrono"], optional = true } redis = { version = "1.2", features = ["tokio-comp", "connection-manager"], optional = true } mongodb = { version = "3.7", default-features = false, features = ["bson-3", "compat-3-3-0", "rustls-tls", "dns-resolver"], optional = true } bson = { version = "3.1", optional = true, features = ["serde"] } diff --git a/crates/perry-stdlib/src/lib.rs b/crates/perry-stdlib/src/lib.rs index 1c9346d525..c4deecddd2 100644 --- a/crates/perry-stdlib/src/lib.rs +++ b/crates/perry-stdlib/src/lib.rs @@ -237,23 +237,17 @@ pub mod tls; pub use tls::*; // === Databases === -// pg lives behind `bundled-pg` (v0.5.566); mysql2 lives behind -// `bundled-mysql2` (v0.5.567). Either feature pulls in sqlx, so -// the modules' `#[cfg(any(...))]` covers both bundled gates plus -// the legacy `database-postgres`/`database-mysql` umbrellas (kept -// for backwards-compat). -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub mod pg; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub use pg::connection::*; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] -pub use pg::pool::*; - -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] +// mysql2 lives behind `bundled-mysql2` (v0.5.567), gated on the +// legacy `database-mysql` umbrella (kept for backwards-compat). +// The parallel `pg` module + `bundled-pg` feature (the pre-#466 +// in-tree native implementation of the `pg` npm package) were +// removed alongside `perry-ext-pg` — Perry now compiles the real +// `pg` package from source instead of shipping a bundled reimplementation. +#[cfg(feature = "bundled-mysql2")] pub mod mysql2; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] +#[cfg(feature = "bundled-mysql2")] pub use mysql2::connection::*; -#[cfg(any(feature = "bundled-pg", feature = "bundled-mysql2"))] +#[cfg(feature = "bundled-mysql2")] pub use mysql2::pool::*; #[cfg(feature = "database-sqlite")] diff --git a/crates/perry-stdlib/src/pg/connection.rs b/crates/perry-stdlib/src/pg/connection.rs deleted file mode 100644 index 176d3d4321..0000000000 --- a/crates/perry-stdlib/src/pg/connection.rs +++ /dev/null @@ -1,415 +0,0 @@ -//! PostgreSQL connection implementation - -use perry_runtime::{ - js_array_get_jsvalue, js_array_length, js_promise_new_cross_thread, JSValue, Promise, -}; -use sqlx::postgres::PgConnection; -use sqlx::{Connection, Row}; - -use super::result::{empty_pg_result, rows_to_pg_result}; -use super::types::{parse_pg_config, PgConfig}; -use crate::common::{register_handle, Handle}; - -/// Wrapper around PgConnection that we can store in the handle registry. -/// -/// The npm-pg API has the user construct the client synchronously -/// (`new Client(config)`) and connect explicitly later (`await -/// client.connect()`). To support that without making `new` itself -/// async, we let the handle live in two states: -/// -/// - **Pre-connect**: `pending_config = Some(...)`, `connection = None`. -/// Created by `js_pg_client_new`. Holds the parsed config until -/// `client.connect()` opens the actual TCP connection. -/// - **Connected**: `pending_config = None`, `connection = Some(...)`. -/// The state every existing query/end path expected before the split; -/// created in-place by `js_pg_connect` (the older single-step API -/// that combines new + connect, kept for back-compat). -pub struct PgConnectionHandle { - pub connection: Option, - pub pending_config: Option, -} - -impl PgConnectionHandle { - pub fn new(conn: PgConnection) -> Self { - Self { - connection: Some(conn), - pending_config: None, - } - } - - /// Pre-connect state: holds config until `.connect()` is called. - pub fn pending(config: PgConfig) -> Self { - Self { - connection: None, - pending_config: Some(config), - } - } - - pub fn take(&mut self) -> Option { - self.connection.take() - } -} - -/// `new Client(config)` — synchronous constructor that parses the config -/// and registers a handle WITHOUT opening a connection. The user must -/// call `await client.connect()` (or any query, which will fail with a -/// helpful error until they do) to actually open the TCP socket. -/// -/// Mirrors npm pg's `new Client(config)` semantics — the Client object -/// exists immediately; the connection happens later. -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_new(config_f: f64) -> Handle { - let config = JSValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgConnectionHandle::pending(pg_config)) -} - -/// `client.connect()` — opens the TCP connection using the config that -/// `js_pg_client_new` previously stored on the handle. Returns a -/// Promise that resolves once the connection is up. -/// -/// If the handle was already connected (or if it was created via the -/// older combined `js_pg_connect`), this is a no-op success. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_connect(client_handle: Handle) -> *mut Promise { - use crate::common::get_handle_mut; - - let promise = js_promise_new_cross_thread(); - - // Snapshot the pending config out of the handle BEFORE entering the - // async block — `get_handle_mut` returns a `&mut` that we can't keep - // alive across an await point. - let pending = if let Some(h) = get_handle_mut::(client_handle) { - h.pending_config.take() - } else { - None - }; - - // Already connected (or back-compat handle from js_pg_connect) — resolve immediately. - let Some(pg_config) = pending else { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Ok(JSValue::undefined().bits()) - }); - return promise; - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let url = pg_config.to_url(); - match PgConnection::connect(&url).await { - Ok(conn) => { - if let Some(h) = get_handle_mut::(client_handle) { - h.connection = Some(conn); - } - Ok(JSValue::undefined().bits()) - } - Err(e) => Err(format!("Failed to connect: {}", e)), - } - }); - - promise -} - -/// pg.connect(config) -> Promise -/// -/// Creates a new PostgreSQL connection with the given configuration. -/// Returns a Promise that resolves to a client handle. -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_connect(config_f: f64) -> *mut Promise { - // Take f64 at the FFI boundary to avoid SysV AMD64 ABI mismatch - // (see js_mysql2_create_pool for details). - let config = JSValue::from_bits(config_f.to_bits()); - let promise = js_promise_new_cross_thread(); - - // Parse the config - let pg_config = parse_pg_config(config); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let url = pg_config.to_url(); - - match PgConnection::connect(&url).await { - Ok(conn) => { - let handle = register_handle(PgConnectionHandle::new(conn)); - // Return the handle as bits - Ok(handle as u64) - } - Err(e) => Err(format!("Failed to connect: {}", e)), - } - }); - - promise -} - -/// client.end() -> Promise -/// -/// Closes the PostgreSQL connection. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_end(client_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::take_handle; - - if let Some(mut wrapper) = take_handle::(client_handle) { - if let Some(conn) = wrapper.take() { - match conn.close().await { - Ok(()) => Ok(JSValue::undefined().bits()), - Err(e) => Err(format!("Failed to close connection: {}", e)), - } - } else { - Err("Connection already closed".to_string()) - } - } else { - Err("Invalid client handle".to_string()) - } - }); - - promise -} - -/// client.query(sql) -> Promise -/// -/// Executes a query and returns the results. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query( - client_handle: Handle, - sql_ptr: *const u8, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - // Extract the SQL string - let sql = if sql_ptr.is_null() { - String::new() - } else { - let header = sql_ptr as *const perry_runtime::StringHeader; - let len = (*header).byte_len as usize; - let data_ptr = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(bytes).to_string() - }; - - // Determine command type from SQL - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::get_handle_mut; - - if let Some(wrapper) = get_handle_mut::(client_handle) { - if let Some(conn) = wrapper.connection.as_mut() { - match sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(conn) - .await - { - Ok(rows) => { - // Get column info from first row (if any) - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - - let result = rows_to_pg_result(rows, &columns, &command); - Ok(result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } else { - Err("Connection already closed".to_string()) - } - } else { - Err("Invalid client handle".to_string()) - } - }); - - promise -} - -/// Enum to hold different parameter value types for pg -#[derive(Clone, Debug)] -enum ParamValue { - Null, - String(String), - Number(f64), - Int(i64), - Bool(bool), -} - -/// Extract parameter values from a JSValue array -unsafe fn extract_params_from_jsvalue(params: JSValue) -> Vec { - let mut result = Vec::new(); - - let bits = params.bits(); - - let arr_ptr: *const perry_runtime::ArrayHeader = if params.is_pointer() { - params.as_pointer() as *const perry_runtime::ArrayHeader - } else if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF { - let upper = bits >> 48; - if upper == 0 || (upper > 0 && upper < 0x7FF0) { - bits as *const perry_runtime::ArrayHeader - } else { - return result; - } - } else { - return result; - }; - - if arr_ptr.is_null() { - return result; - } - - let length = js_array_length(arr_ptr); - - for i in 0..length { - let element_bits = js_array_get_jsvalue(arr_ptr, i); - let element = JSValue::from_bits(element_bits); - - let param = if element.is_null() || element.is_undefined() { - ParamValue::Null - } else if element.is_string() { - let str_ptr = element.as_string_ptr(); - if !str_ptr.is_null() { - let len = (*str_ptr).byte_len as usize; - let data_ptr = - (str_ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - ParamValue::String(String::from_utf8_lossy(bytes).to_string()) - } else { - ParamValue::Null - } - } else if element.is_bigint() { - let bigint_ptr = element.as_bigint_ptr(); - if !bigint_ptr.is_null() { - let str_ptr = perry_runtime::bigint::js_bigint_to_string(bigint_ptr); - if !str_ptr.is_null() { - let len = (*str_ptr).byte_len as usize; - let data_ptr = (str_ptr as *const u8) - .add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - ParamValue::String(String::from_utf8_lossy(bytes).to_string()) - } else { - ParamValue::String("0".to_string()) - } - } else { - ParamValue::String("0".to_string()) - } - } else if element.is_int32() { - ParamValue::Int(element.as_int32() as i64) - } else if element.is_bool() { - ParamValue::Bool(element.as_bool()) - } else if element.is_number() { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - } else { - let n = element.to_number(); - if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { - ParamValue::Int(n as i64) - } else { - ParamValue::Number(n) - } - }; - - result.push(param); - } - - result -} - -fn is_row_returning_query(sql: &str) -> bool { - let trimmed = sql.trim_start(); - let upper = trimmed.get(..10).unwrap_or(trimmed).to_uppercase(); - upper.starts_with("SELECT") - || upper.starts_with("SHOW") - || upper.starts_with("DESC") - || upper.starts_with("EXPLAIN") - || upper.starts_with("WITH") -} - -/// client.query(sql, params) -> Promise -/// -/// Executes a parameterized query. -#[no_mangle] -pub unsafe extern "C" fn js_pg_client_query_params( - client_handle: Handle, - sql_ptr: *const u8, - params: JSValue, -) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - let sql = if sql_ptr.is_null() { - String::new() - } else { - let header = sql_ptr as *const perry_runtime::StringHeader; - let len = (*header).byte_len as usize; - let data_ptr = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(bytes).to_string() - }; - - let param_values = extract_params_from_jsvalue(params); - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - let is_select = is_row_returning_query(&sql); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::get_handle_mut; - - if let Some(wrapper) = get_handle_mut::(client_handle) { - if let Some(conn) = wrapper.connection.as_mut() { - let mut query = sqlx::query(sqlx::AssertSqlSafe(sql.clone())); - for param in ¶m_values { - query = match param { - ParamValue::Null => query.bind(Option::::None), - ParamValue::String(s) => query.bind(s.clone()), - ParamValue::Number(n) => query.bind(*n), - ParamValue::Int(i) => query.bind(*i), - ParamValue::Bool(b) => query.bind(*b), - }; - } - if is_select { - match query.fetch_all(conn).await { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - let result = rows_to_pg_result(rows, &columns, &command); - Ok(result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } else { - match query.execute(conn).await { - Ok(result) => { - let pg_result = empty_pg_result(&command, result.rows_affected()); - Ok(pg_result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } - } else { - Err("Connection already closed".to_string()) - } - } else { - Err("Invalid client handle".to_string()) - } - }); - - promise -} diff --git a/crates/perry-stdlib/src/pg/mod.rs b/crates/perry-stdlib/src/pg/mod.rs deleted file mode 100644 index 54a14c10ff..0000000000 --- a/crates/perry-stdlib/src/pg/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! pg compatible native implementation -//! -//! Provides a drop-in replacement for the pg npm package using sqlx. - -pub mod connection; -pub mod pool; -pub mod result; -pub mod types; - -pub use connection::*; -pub use pool::*; -pub use result::*; -pub use types::*; diff --git a/crates/perry-stdlib/src/pg/pool.rs b/crates/perry-stdlib/src/pg/pool.rs deleted file mode 100644 index 080bf413f6..0000000000 --- a/crates/perry-stdlib/src/pg/pool.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! PostgreSQL connection pool implementation - -use perry_runtime::{js_promise_new_cross_thread, JSValue, Promise}; -use sqlx::postgres::{PgPool, PgPoolOptions}; -use sqlx::Row; - -use super::result::rows_to_pg_result; -use super::types::parse_pg_config; -use crate::common::{register_handle, Handle}; - -/// Wrapper around PgPool that we can store in the handle registry. -/// -/// Lives in two states like PgConnectionHandle: pre-pool (`pending_url` -/// holds the connection URL, `pool` is None) and pool-built (`pool` is -/// Some). `new Pool(config)` creates the pre-pool form synchronously -/// without touching the Tokio runtime — sqlx's `connect_lazy` ALSO -/// touches Tokio internals and panics outside a runtime context, so we -/// can't even use it; the actual sqlx pool is built on first query. -/// The older combined `js_pg_create_pool` factory still returns a fully -/// built pool inside its async block. -pub struct PgPoolHandle { - pub pool: Option, - pub pending_url: Option, -} - -impl PgPoolHandle { - pub fn new(pool: PgPool) -> Self { - Self { - pool: Some(pool), - pending_url: None, - } - } - - pub fn pending(url: String) -> Self { - Self { - pool: None, - pending_url: Some(url), - } - } - - /// Lazy-build the sqlx pool on first use. Only callable from within a - /// Tokio runtime context (every spawn_for_promise body). Safe to call - /// repeatedly — only the first call actually builds the pool. - pub async fn ensure_pool(&mut self) -> Result<&PgPool, String> { - if self.pool.is_none() { - let url = self - .pending_url - .take() - .ok_or_else(|| "Pool config missing".to_string())?; - let pool = PgPoolOptions::new() - .max_connections(10) - .connect(&url) - .await - .map_err(|e| format!("Failed to create pool: {}", e))?; - self.pool = Some(pool); - } - Ok(self.pool.as_ref().unwrap()) - } -} - -/// `new Pool(config)` — synchronous constructor matching npm pg's API. -/// -/// Returns a Handle directly (no Promise wrapper). The actual sqlx pool -/// can't be built here because sqlx 0.8's `PgPoolOptions::connect_lazy` -/// touches Tokio runtime internals and panics outside a runtime context, -/// and the synchronous `new` path doesn't have one. Instead we store -/// just the connection URL; `pool.query()` lazy-builds the pool on -/// first use (its spawn_for_promise body runs inside a Tokio runtime). -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_new(config_f: f64) -> Handle { - let config = JSValue::from_bits(config_f.to_bits()); - let pg_config = parse_pg_config(config); - register_handle(PgPoolHandle::pending(pg_config.to_url())) -} - -/// new Pool(config) -> Promise -/// -/// Creates a new PostgreSQL connection pool with the given configuration. -/// -/// # Safety -/// The config parameter must be a valid JSValue representing a config object. -#[no_mangle] -pub unsafe extern "C" fn js_pg_create_pool(config_f: f64) -> *mut Promise { - // Take f64 at the FFI boundary to avoid SysV AMD64 ABI mismatch - // (see js_mysql2_create_pool for details). - let config = JSValue::from_bits(config_f.to_bits()); - let promise = js_promise_new_cross_thread(); - - // Parse the config - let pg_config = parse_pg_config(config); - - // Extract max connections if provided (default to 10) - let max_conns = 10u32; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let url = pg_config.to_url(); - - match PgPoolOptions::new() - .max_connections(max_conns) - .connect(&url) - .await - { - Ok(pool) => { - let handle = register_handle(PgPoolHandle::new(pool)); - Ok(handle as u64) - } - Err(e) => Err(format!("Failed to create pool: {}", e)), - } - }); - - promise -} - -/// pool.query(sql) -> Promise -/// -/// Executes a query on the pool. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_query(pool_handle: Handle, sql_ptr: *const u8) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - // Extract the SQL string - let sql = if sql_ptr.is_null() { - String::new() - } else { - let header = sql_ptr as *const perry_runtime::StringHeader; - let len = (*header).byte_len as usize; - let data_ptr = sql_ptr.add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(bytes).to_string() - }; - - // Determine command type from SQL - let command = sql - .split_whitespace() - .next() - .unwrap_or("SELECT") - .to_uppercase(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::get_handle_mut; - - if let Some(wrapper) = get_handle_mut::(pool_handle) { - // Lazy-build the sqlx pool on first query if `new Pool(config)` - // produced a pre-pool handle. Already-built pools (from the - // older `js_pg_create_pool` factory) skip the build cheaply. - let pool = wrapper.ensure_pool().await?; - match sqlx::query(sqlx::AssertSqlSafe(sql.clone())) - .fetch_all(pool) - .await - { - Ok(rows) => { - let columns: Vec<_> = if !rows.is_empty() { - rows[0].columns().to_vec() - } else { - Vec::new() - }; - - let result = rows_to_pg_result(rows, &columns, &command); - Ok(result.bits()) - } - Err(e) => Err(format!("Query failed: {}", e)), - } - } else { - Err("Invalid pool handle".to_string()) - } - }); - - promise -} - -/// pool.end() -> Promise -/// -/// Closes all connections in the pool. -#[no_mangle] -pub unsafe extern "C" fn js_pg_pool_end(pool_handle: Handle) -> *mut Promise { - let promise = js_promise_new_cross_thread(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - use crate::common::take_handle; - - if let Some(mut wrapper) = take_handle::(pool_handle) { - if let Some(pool) = wrapper.pool.take() { - pool.close().await; - Ok(JSValue::undefined().bits()) - } else { - // Pre-pool handle (`new Pool` ctor never had a query) — close - // is a no-op since no connections were ever opened. - Ok(JSValue::undefined().bits()) - } - } else { - Err("Invalid pool handle".to_string()) - } - }); - - promise -} diff --git a/crates/perry-stdlib/src/pg/result.rs b/crates/perry-stdlib/src/pg/result.rs deleted file mode 100644 index abf590203e..0000000000 --- a/crates/perry-stdlib/src/pg/result.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Query result handling for pg - -use perry_runtime::{js_array_alloc, js_array_set, js_object_alloc, js_object_set_field, JSValue}; -use sqlx::postgres::{PgColumn, PgRow}; - -use super::types::{column_to_field_def, row_to_js_object}; - -/// Convert query results to the pg format: { rows, fields, rowCount, command } -/// -/// Returns a JSValue representing a Result object where: -/// - rows: Array of row objects -/// - fields: Array of field metadata objects -/// - rowCount: Number of rows affected/returned -/// - command: SQL command type (SELECT, INSERT, etc.) -pub fn rows_to_pg_result(rows: Vec, columns: &[PgColumn], command: &str) -> JSValue { - // Create the Result object with 4 fields - let result_obj = js_object_alloc(0, 4); - - // Create rows array (field 0) - let rows_array = js_array_alloc(rows.len() as u32); - for (i, row) in rows.iter().enumerate() { - let row_obj = row_to_js_object(row); - js_array_set( - rows_array, - i as u32, - JSValue::object_ptr(row_obj as *mut u8), - ); - } - js_object_set_field(result_obj, 0, JSValue::array_ptr(rows_array)); - - // Create fields array (field 1) - let fields_array = js_array_alloc(columns.len() as u32); - for (i, col) in columns.iter().enumerate() { - let field_obj = column_to_field_def(col); - js_array_set( - fields_array, - i as u32, - JSValue::object_ptr(field_obj as *mut u8), - ); - } - js_object_set_field(result_obj, 1, JSValue::array_ptr(fields_array)); - - // Set rowCount (field 2) - js_object_set_field(result_obj, 2, JSValue::number(rows.len() as f64)); - - // Set command (field 3) - let cmd_ptr = perry_runtime::js_string_from_bytes(command.as_ptr(), command.len() as u32); - js_object_set_field(result_obj, 3, JSValue::string_ptr(cmd_ptr)); - - JSValue::object_ptr(result_obj as *mut u8) -} - -/// Create an empty result for queries that don't return rows -pub fn empty_pg_result(command: &str, row_count: u64) -> JSValue { - let result_obj = js_object_alloc(0, 4); - - let empty_rows = js_array_alloc(0); - js_object_set_field(result_obj, 0, JSValue::array_ptr(empty_rows)); - - let empty_fields = js_array_alloc(0); - js_object_set_field(result_obj, 1, JSValue::array_ptr(empty_fields)); - - js_object_set_field(result_obj, 2, JSValue::number(row_count as f64)); - - let cmd_ptr = perry_runtime::js_string_from_bytes(command.as_ptr(), command.len() as u32); - js_object_set_field(result_obj, 3, JSValue::string_ptr(cmd_ptr)); - - JSValue::object_ptr(result_obj as *mut u8) -} diff --git a/crates/perry-stdlib/src/pg/types.rs b/crates/perry-stdlib/src/pg/types.rs deleted file mode 100644 index c86b75b9df..0000000000 --- a/crates/perry-stdlib/src/pg/types.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! Type conversions between PostgreSQL types and JSValue - -use perry_runtime::{ - js_array_alloc, js_array_push, js_object_alloc, js_object_get_field, js_object_set_field, - js_object_set_keys, js_string_from_bytes, JSValue, ObjectHeader, StringHeader, -}; -use sqlx::postgres::PgRow; -use sqlx::{Column, Row, TypeInfo}; - -/// PostgreSQL connection configuration -#[derive(Debug, Clone)] -pub struct PgConfig { - pub host: String, - pub port: u16, - pub user: String, - pub password: String, - pub database: Option, -} - -impl Default for PgConfig { - fn default() -> Self { - Self { - host: "localhost".to_string(), - port: 5432, - user: "postgres".to_string(), - password: String::new(), - database: None, - } - } -} - -impl PgConfig { - /// Build a connection URL from the config - pub fn to_url(&self) -> String { - let db_part = self - .database - .as_ref() - .map(|d| format!("/{}", d)) - .unwrap_or_default(); - format!( - "postgres://{}:{}@{}:{}{}", - self.user, self.password, self.host, self.port, db_part - ) - } -} - -/// Extract a Rust String from a JSValue that contains a string pointer -unsafe fn jsvalue_to_string(value: JSValue) -> Option { - if value.is_pointer() { - let ptr = value.as_pointer() as *const StringHeader; - if !ptr.is_null() { - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - return Some(String::from_utf8_lossy(bytes).to_string()); - } - } - None -} - -/// Convert a JSValue config object to PgConfig -/// -/// Expected object layout (based on property order in object literal): -/// - field 0: host (string) -/// - field 1: port (number) -/// - field 2: user (string) -/// - field 3: password (string) -/// - field 4: database (string, optional) -/// -/// # Safety -/// The config must be a valid JSValue representing an object -pub unsafe fn parse_pg_config(config: JSValue) -> PgConfig { - let mut result = PgConfig::default(); - - // Check if config is a valid object pointer - if !config.is_pointer() { - return result; - } - - let obj_ptr = config.as_pointer() as *const ObjectHeader; - if obj_ptr.is_null() { - return result; - } - - // Extract host (field 0) - let host_val = js_object_get_field(obj_ptr, 0); - if let Some(host) = jsvalue_to_string(host_val) { - result.host = host; - } - - // Extract port (field 1) - let port_val = js_object_get_field(obj_ptr, 1); - if port_val.is_number() { - result.port = port_val.to_number() as u16; - } - - // Extract user (field 2) - let user_val = js_object_get_field(obj_ptr, 2); - if let Some(user) = jsvalue_to_string(user_val) { - result.user = user; - } - - // Extract password (field 3) - let password_val = js_object_get_field(obj_ptr, 3); - if let Some(password) = jsvalue_to_string(password_val) { - result.password = password; - } - - // Extract database (field 4, optional) - let database_val = js_object_get_field(obj_ptr, 4); - if !database_val.is_undefined() && !database_val.is_null() { - if let Some(database) = jsvalue_to_string(database_val) { - result.database = Some(database); - } - } - - result -} - -/// Convert a PostgreSQL row to a JS object -/// -/// Returns a pointer to the allocated object -pub fn row_to_js_object(row: &PgRow) -> *mut ObjectHeader { - let columns = row.columns(); - // Class ID 0 for anonymous object, field count = number of columns - let obj = js_object_alloc(0, columns.len() as u32); - - for (i, _col) in columns.iter().enumerate() { - let value = column_value_to_jsvalue(row, i); - js_object_set_field(obj, i as u32, value); - } - - obj -} - -/// Convert a column value to JSValue -fn column_value_to_jsvalue(row: &PgRow, index: usize) -> JSValue { - let columns = row.columns(); - let col = &columns[index]; - let type_name = col.type_info().name(); - - // Try to get the value based on the column type - match type_name { - "INT4" | "INT2" => { - if let Ok(val) = row.try_get::(index) { - JSValue::int32(val) - } else { - JSValue::null() - } - } - "INT8" => { - if let Ok(val) = row.try_get::(index) { - JSValue::number(val as f64) - } else { - JSValue::null() - } - } - "FLOAT4" | "FLOAT8" | "NUMERIC" => { - if let Ok(val) = row.try_get::(index) { - JSValue::number(val) - } else { - JSValue::null() - } - } - "VARCHAR" | "CHAR" | "TEXT" | "BPCHAR" | "NAME" => { - if let Ok(val) = row.try_get::(index) { - let str_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - "BOOL" => { - if let Ok(val) = row.try_get::(index) { - JSValue::bool(val) - } else { - JSValue::null() - } - } - _ => { - // Try as string fallback - if let Ok(val) = row.try_get::(index) { - let str_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - JSValue::string_ptr(str_ptr) - } else { - JSValue::null() - } - } - } -} - -/// Create a FieldDef object for a column, shaped like node-pg's -/// `result.fields[i]` (#4917): `dataTypeID` is the numeric type OID (what -/// `pg-types`-style custom parsers key on), `tableID`/`columnID` come from -/// the RowDescription via sqlx's `relation_id()`/`relation_attribute_no()` -/// (0 for expression columns, like Node). `dataTypeSize`/`dataTypeModifier` -/// are not exposed by sqlx 0.8 and report the "unknown/variable" sentinel -1. -pub fn column_to_field_def(col: &sqlx::postgres::PgColumn) -> *mut ObjectHeader { - let obj = js_object_alloc(0, 7); - let mut keys_array = js_array_alloc(7); - let mut set = |obj: *mut ObjectHeader, idx: u32, key: &str, value: JSValue| { - js_object_set_field(obj, idx, value); - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - keys_array = js_array_push(keys_array, JSValue::string_ptr(key_ptr)); - }; - - let name = col.name(); - let name_ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - set(obj, 0, "name", JSValue::string_ptr(name_ptr)); - - let table_id = col.relation_id().map(|oid| oid.0 as f64).unwrap_or(0.0); - set(obj, 1, "tableID", JSValue::number(table_id)); - - let column_id = col - .relation_attribute_no() - .map(|attno| attno as f64) - .unwrap_or(0.0); - set(obj, 2, "columnID", JSValue::number(column_id)); - - // `oid()` is None only for custom types sqlx has not resolved against - // the catalog; report 0 (the `InvalidOid` sentinel) in that case. - let data_type_id = col.type_info().oid().map(|oid| oid.0 as f64).unwrap_or(0.0); - set(obj, 3, "dataTypeID", JSValue::number(data_type_id)); - - set(obj, 4, "dataTypeSize", JSValue::number(-1.0)); - set(obj, 5, "dataTypeModifier", JSValue::number(-1.0)); - - let format_ptr = js_string_from_bytes("text".as_ptr(), 4); - set(obj, 6, "format", JSValue::string_ptr(format_ptr)); - - js_object_set_keys(obj, keys_array); - obj -} diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index 6f4d32d765..ab9c97cc06 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -358,7 +358,6 @@ pub(crate) fn build_optimized_libs( | "bundled-argon2" | "bundled-nodemailer" | "bundled-ioredis" - | "bundled-pg" | "bundled-mysql2" | "bundled-mongodb" | "bundled-ws" diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs index 8aab6c74c7..9f91fecd82 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -759,7 +759,6 @@ pub(crate) fn binding_needs_shared_tokio(module: &str) -> bool { | "fastify" // Database drivers (mongodb, sqlx, redis) | "mongodb" - | "pg" | "mysql2" | "mysql2/promise" | "ioredis" diff --git a/crates/perry/src/commands/stdlib_features.rs b/crates/perry/src/commands/stdlib_features.rs index a7f4b0561b..b23118a5c7 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -72,10 +72,6 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { // `database-mysql` umbrella retained for backwards-compat; // per-binding gate is `bundled-mysql2` (v0.5.567). "mysql2" | "mysql2/promise" => &["bundled-mysql2"], - // `database-postgres` umbrella retained for backwards-compat; - // per-binding gate is `bundled-pg` (v0.5.566) so the - // well-known flip can route to perry-ext-pg. - "pg" => &["bundled-pg"], "better-sqlite3" => &["database-sqlite"], // node:sqlite (#3183/#3184) shares the rusqlite-backed // `database-sqlite` feature with better-sqlite3 — DatabaseSync / diff --git a/crates/perry/well_known_bindings.toml b/crates/perry/well_known_bindings.toml index 59e13d4598..10ca3ab384 100644 --- a/crates/perry/well_known_bindings.toml +++ b/crates/perry/well_known_bindings.toml @@ -443,18 +443,6 @@ repo = "https://github.com/redis/node-redis" ref = "d400bc9c1e7b17013c53015f098274a25aa70640" ported-at = "6.1.0" date = "2026-07-30" -[bindings.pg] -crate = "perry-ext-pg" -lib = "perry_ext_pg" -tracking = "#466" - -[bindings.pg.upstream] -version = "8.22.0" -sha256 = "2f8b273b9b93b8712251cbe2b0f05378f4da2187eab5348c3c6c34d02622a55e" -repo = "https://github.com/brianc/node-postgres" -ref = "b617619f9fb6fbd231731823e2732a2927ded4be" -ported-at = "8.22.0" -date = "2026-07-30" [bindings.mysql2] crate = "perry-ext-mysql2" lib = "perry_ext_mysql2" diff --git a/workspace-architecture.json b/workspace-architecture.json index 623d2d2711..4c9515d777 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -290,11 +290,6 @@ "decision": "externalize", "migration": "external-package" }, - "perry-ext-pg": { - "category": "binding", - "decision": "externalize", - "migration": "compile-source" - }, "perry-ext-qs": { "category": "binding", "decision": "externalize", From 5f5f35df10b65becfc7a69e2db8bc2da38f65f47 Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Fri, 18 Sep 2026 23:37:26 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore(pg):=20finish=20removal=20=E2=80=94?= =?UTF-8?q?=20codegen=20dispatch=20table,=20docs,=20ratchet=20baselines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - crates/perry-codegen/src/lower_call/native_table/databases.rs: drop the 7 pg NativeModSig rows (js_pg_* runtime symbols that no longer exist). Caught by perry-codegen's every_dispatch_entry_has_manifest_counterpart test, which fails on drift between this table and API_MANIFEST. - docs/api/perry.d.ts, docs/src/api/reference.md: regenerated via --print-api-manifest (drops the pg module section). - docs/src/native-libraries/governance.md: regenerated via binding_governance.py --table (drops the perry-ext-pg row). - docs/src/native-libraries/overview.md: pg no longer routes to an in-tree native wrapper; updated the well-known-binding description. - workspace-architecture.json: refreshed the recorded baseline (workspace_members 83->82, externalize 33->32) that workspace_architecture.py --check compares against. - scripts/string_payload_access_baseline.txt, scripts/unrooted_local_shape_baseline.json: refreshed ratchet baselines now that perry-ext-pg/perry-stdlib/src/pg no longer contribute findings. --- .../src/lower_call/native_table/databases.rs | 81 ------------------- docs/api/perry.d.ts | 13 +-- docs/src/api/reference.md | 20 +---- docs/src/native-libraries/governance.md | 1 - docs/src/native-libraries/overview.md | 8 +- scripts/string_payload_access_baseline.txt | 3 +- scripts/unrooted_local_shape_baseline.json | 9 +-- workspace-architecture.json | 4 +- 8 files changed, 13 insertions(+), 126 deletions(-) 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..50dd024282 100644 --- a/crates/perry-codegen/src/lower_call/native_table/databases.rs +++ b/crates/perry-codegen/src/lower_call/native_table/databases.rs @@ -275,87 +275,6 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_GCPTR, }, - // ========== PostgreSQL (pg) ========== - // `new Client(config)` and `new Pool(config)` are dispatched by - // `lower_builtin_new` (sync constructors that produce real handles). - // The factory-style entries below stay wired for `pg.connect(config)` / - // `pg.Pool(config)` patterns that some npm code uses. - NativeModSig { - module: "pg", - has_receiver: false, - method: "connect", - class_filter: None, - runtime: "js_pg_connect", - args: &[NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: false, - method: "Pool", - class_filter: None, - runtime: "js_pg_create_pool", - args: &[NA_F64], - ret: NR_GCPTR, - }, - // `client.connect()` — async, opens the TCP connection on a handle that - // `new Client(config)` previously created in the pre-connect state. - // No-op if the handle was already connected (e.g. came from the - // older `pg.connect(config)` factory). Class-filtered to Client so - // `pool.connect()` (which has different semantics — checkout a pooled - // connection — not yet implemented) doesn't accidentally land here. - NativeModSig { - module: "pg", - has_receiver: true, - method: "connect", - class_filter: Some("Client"), - runtime: "js_pg_client_connect", - args: &[], - ret: NR_GCPTR, - }, - // Pool-specific query/end — different runtime fns from the Client paths. - // Pre-existing dispatch was unfiltered and routed both Pool and Client - // through the Client query/end fns (latent bug: pool.query() against a - // Pool handle would fail because js_pg_client_query expects a Connection - // handle). Class-filtered Pool rows take precedence over the unfiltered - // Client/default rows below thanks to native_module_lookup's two-pass - // search (exact class_filter match first, then None fallback). - NativeModSig { - module: "pg", - has_receiver: true, - method: "query", - class_filter: Some("Pool"), - runtime: "js_pg_pool_query", - args: &[NA_STR, NA_PTR], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: true, - method: "end", - class_filter: Some("Pool"), - runtime: "js_pg_pool_end", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: true, - method: "query", - class_filter: None, - runtime: "js_pg_client_query", - args: &[NA_STR, NA_PTR], - ret: NR_GCPTR, - }, - NativeModSig { - module: "pg", - has_receiver: true, - method: "end", - class_filter: None, - runtime: "js_pg_client_end", - args: &[], - ret: NR_GCPTR, - }, // ========== ioredis ========== // NB: every row was previously emitting `js_redis_*` symbols which don't // exist in perry-stdlib (the actual fns are `js_ioredis_*`). The bug was diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 87dc9693bb..40544ea1a4 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: 2093 entries across 136 modules +// Coverage: 2089 entries across 135 modules type PerryI8 = number & { readonly __perryI8?: never }; type PerryI16 = number & { readonly __perryI16?: never }; @@ -3450,17 +3450,6 @@ declare module "perry/yoga" { export function unsetMeasureFunc(...args: any[]): any; } -declare module "pg" { - /** stdlib */ - export class Client { [key: string]: any; } - /** stdlib */ - export class Pool { [key: string]: any; } - /** stdlib */ - export function Pool(p0: any): any; - /** stdlib */ - export function connect(p0: any): any; -} - declare module "process" { /** stdlib */ export const _eval: any; diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index afcbe5c54a..e7da0dcb40 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: 3035 entries across 138 modules. +Total: 3026 entries across 137 modules. ## Modules @@ -105,7 +105,6 @@ Total: 3035 entries across 138 modules. - [`perry/widget`](#perrywidget) - [`perry/workloads`](#perryworkloads) - [`perry/yoga`](#perryyoga) -- [`pg`](#pg) - [`process`](#process) - [`punycode`](#punycode) - [`qs`](#qs) @@ -3064,23 +3063,6 @@ Total: 3035 entries across 138 modules. - `setNumber` — module - `unsetMeasureFunc` — module -## `pg` - -### Classes - -- `Client` -- `Pool` - -### Methods - -- `Pool` — module -- `connect` — module -- `connect` — instance *(class: `Client`)* -- `end` — instance *(class: `Pool`)* -- `end` — instance -- `query` — instance *(class: `Pool`)* -- `query` — instance - ## `process` ### Methods diff --git a/docs/src/native-libraries/governance.md b/docs/src/native-libraries/governance.md index 1591f27649..a61ec26e71 100644 --- a/docs/src/native-libraries/governance.md +++ b/docs/src/native-libraries/governance.md @@ -113,7 +113,6 @@ from `well_known_bindings.toml`. Regenerate this table with | `perry-ext-nodemailer` | `nodemailer` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-parcel-watcher` | `@parcel/watcher`
`@parcel/watcher-darwin-arm64`
`@parcel/watcher-darwin-x64`
`@parcel/watcher-linux-arm64-glibc`
`@parcel/watcher-linux-arm64-musl`
`@parcel/watcher-linux-x64-glibc`
`@parcel/watcher-linux-x64-musl`
`@parcel/watcher-win32-arm64`
`@parcel/watcher-win32-x64` | External integration | Move to an external native package | Bundled; migration pending | | `perry-ext-pdf` | `@perryts/pdf` | External integration | Move to an external native package | Bundled; migration pending | -| `perry-ext-pg` | `pg` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-qs` | `qs` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-ratelimit` | `rate-limiter-flexible` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-sharp` | `sharp` | External integration | Move to an external native package | Bundled; migration pending | diff --git a/docs/src/native-libraries/overview.md b/docs/src/native-libraries/overview.md index 8fe98f6f55..fd5a108405 100644 --- a/docs/src/native-libraries/overview.md +++ b/docs/src/native-libraries/overview.md @@ -250,7 +250,7 @@ drivers is the migration target: | Path | Install | Resolver layer | What it is | |---|---|---|---| -| **Well-known native binding** | nothing (bundled) | (c) | Compatibility path: `import 'mysql2'` / `import 'pg'` / `import 'mongodb'` route to in-tree Rust wrappers. They remain available until the source-package migration gates pass. | +| **Well-known native binding** | nothing (bundled) | (c) | Compatibility path: `import 'mysql2'` / `import 'mongodb'` route to in-tree Rust wrappers. They remain available until the source-package migration gates pass. `pg` has already migrated off this path — see the row below. | | **`@perryts/{postgres,mysql,mongodb,redis}`** | `bun add @perryts/postgres` | (a) | Pure-TypeScript wire-protocol drivers — no Rust, no native dep. Use Perry's [`compilePackages`](../packages/porting.md) to compile the TS to native via LLVM. Also run unmodified on Node.js / Bun. Independent semver. | | **External native binding** | `bun add @perryts/tursodb` | (a) | Third-party Rust crate using `perry-ffi`, manifest at `package.json::perry.nativeLibrary`. Today: `@perryts/tursodb`, `@perryts/iroh`. | @@ -263,9 +263,11 @@ shim, just don't import `mysql2`. **When to pick which:** -- **Well-known native (`mysql2` / `pg` / `mongodb`)** — current zero-install +- **Well-known native (`mysql2` / `mongodb`)** — current zero-install compatibility path; its feature set tracks Perry's release cadence and it is - scheduled to yield to compiled package source. + scheduled to yield to compiled package source. `pg` no longer has a native + binding: a plain `import ... from "pg"` compiles the real npm `pg` package + from source instead. - **`@perryts/postgres` / `@perryts/mysql` / `@perryts/mongodb` / `@perryts/redis`** — you want to read / fork / patch the driver in plain TypeScript; you want the same code running on Node.js or Bun for fallback; diff --git a/scripts/string_payload_access_baseline.txt b/scripts/string_payload_access_baseline.txt index 52cc4699a8..466aa27e95 100644 --- a/scripts/string_payload_access_baseline.txt +++ b/scripts/string_payload_access_baseline.txt @@ -9,11 +9,10 @@ inline-offset | perry-ext-http | 1 inline-offset | perry-ext-mysql2 | 1 inline-offset | perry-ext-net | 1 inline-offset | perry-ext-nodemailer | 1 -inline-offset | perry-ext-pg | 2 inline-offset | perry-ext-zlib | 3 inline-offset | perry-ffi | 3 inline-offset | perry-runtime | 350 -inline-offset | perry-stdlib | 40 +inline-offset | perry-stdlib | 34 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 40f260534c..3b48387057 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -25,7 +25,6 @@ "crates/perry-ext-net/src/classes.rs": 2, "crates/perry-ext-net/src/lifecycle.rs": 1, "crates/perry-ext-node-forge/src/lib.rs": 20, - "crates/perry-ext-pg/src/lib.rs": 7, "crates/perry-ext-ratelimit/src/lib.rs": 4, "crates/perry-ext-streams/src/lib.rs": 2, "crates/perry-ext-uuid/src/lib.rs": 2, @@ -51,11 +50,9 @@ "crates/perry-stdlib/src/mysql2/result.rs": 39, "crates/perry-stdlib/src/mysql2/types.rs": 16, "crates/perry-stdlib/src/nodemailer.rs": 3, - "crates/perry-stdlib/src/pg/result.rs": 14, - "crates/perry-stdlib/src/pg/types.rs": 14, "crates/perry-stdlib/src/querystring.rs": 2, "crates/perry-stdlib/src/ratelimit.rs": 4, - "crates/perry-stdlib/src/readline/mod.rs": 5, + "crates/perry-stdlib/src/readline/mod.rs": 4, "crates/perry-stdlib/src/sqlite/backup.rs": 7, "crates/perry-stdlib/src/sqlite/better.rs": 18, "crates/perry-stdlib/src/sqlite/bind.rs": 4, @@ -70,7 +67,7 @@ "crates/perry-stdlib/src/streams/transform.rs": 8, "crates/perry-stdlib/src/streams/writable.rs": 2, "crates/perry-stdlib/src/string_decoder.rs": 4, - "crates/perry-stdlib/src/tls.rs": 4, + "crates/perry-stdlib/src/tls.rs": 3, "crates/perry-stdlib/src/webcrypto/aes.rs": 2, "crates/perry-stdlib/src/webcrypto/encapsulation.rs": 8, "crates/perry-stdlib/src/webcrypto/jwk.rs": 2, @@ -83,5 +80,5 @@ "crates/perry-stdlib/src/zlib.rs": 2 }, "schema_version": 2, - "total": 561 + "total": 524 } diff --git a/workspace-architecture.json b/workspace-architecture.json index 4c9515d777..c0164b5b07 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -25,7 +25,7 @@ ] }, "baseline": { - "workspace_members": 83, + "workspace_members": 82, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -68,7 +68,7 @@ "perry-updater" ], "decision_counts": { - "externalize": 33, + "externalize": 32, "keep": 45, "merge": 1, "remove": 1, From 77e5e1e1468ba6ab023d26a476cba54040eaaf34 Mon Sep 17 00:00:00 2001 From: Perry Bot Date: Fri, 18 Sep 2026 23:38:43 +0000 Subject: [PATCH 3/3] changelog: #10677 --- changelog.d/10677-remove-pg-native-binding.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 changelog.d/10677-remove-pg-native-binding.md diff --git a/changelog.d/10677-remove-pg-native-binding.md b/changelog.d/10677-remove-pg-native-binding.md new file mode 100644 index 0000000000..c8f4cf8129 --- /dev/null +++ b/changelog.d/10677-remove-pg-native-binding.md @@ -0,0 +1,31 @@ +Removed the native `pg` binding: `crates/perry-ext-pg` (sqlx::postgres + +tokio bridge over `perry-ffi`) and the duplicate pre-#466 in-tree +implementation in `crates/perry-stdlib/src/pg/` (the `bundled-pg` feature), +kept alive since before the migration to a separate ext crate. Both defined +the same `extern "C"` symbols (`js_pg_client_new`, `js_pg_client_query`, …); +whichever won the link order silently shadowed the other. `import ... from +"pg"` no longer resolves as a native module at all — it compiles the real +npm `pg` package from source, same as any other TypeScript/JavaScript +dependency, with `pg` and its 13 transitive deps (`pg-connection-string`, +`pg-pool`, `pg-protocol`, `pg-types`, `pgpass`, `pg-int8`, +`postgres-{array,date,interval,bytea}`, `pg-cloudflare`, `split2`, `xtend`) +picked up automatically by Perry's compile-package wildcard when a project +has no `perry.compilePackages` entry, or explicit listing otherwise. + +Removed the `[bindings.pg]` entry (`well_known_bindings.toml`), the `"pg"` +`NATIVE_MODULES` entry and manifest rows (`perry-api-manifest`), the pg +`NativeModSig` dispatch-table rows (`perry-codegen`'s +`lower_call/native_table/databases.rs`), the `stdlib_features.rs` / +`optimized_libs` feature-gate arms, the `bundled-pg`/`database-postgres` +Cargo features and the now-unreachable `sqlx` `"postgres"` feature on +`perry-stdlib`'s dependency (verified nothing else in the workspace +requests it), and the `perry-ext-pg` entry in `workspace-architecture.json`. +Regenerated `docs/api/perry.d.ts`, `docs/src/api/reference.md`, and +`docs/src/native-libraries/governance.md`'s generated table; updated +`docs/src/native-libraries/overview.md`'s well-known-binding description. + +Verified end to end without forcing `compilePackages`: a from-scratch +`node_modules` with a plain `"pg": "^8"` dependency and no +`perry.compilePackages` key compiles, links (24.7 MB binary), and reaches a +genuine `net.connect()` — `Connection refused` against a port with nothing +listening. No live Postgres was available to test a real query round-trip.