From 57883548ceea86c7531ef0867f2ab193e5f54cd1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 3 Jul 2026 14:49:37 +0000 Subject: [PATCH] Accept memoryview parameters in the Rust value mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synapse binds some binary values (e.g. slices of event data) as a `memoryview` rather than `bytes`/`bytearray`. The shim's `from_py` handled the latter two but not `memoryview`, so those parameters raised `TypeError: unsupported parameter type for postgres: memoryview` — e.g. persisting an event over federation (seen via `test_third_party_rules.test_on_new_event`). Accept a `memoryview` as a BYTEA parameter, copying its bytes out with `tobytes()` (the buffer protocol isn't available under the limited ABI the crate builds against). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d --- rust/src/database/postgres/value.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/rust/src/database/postgres/value.rs b/rust/src/database/postgres/value.rs index e03f20c319..178e2dbec6 100644 --- a/rust/src/database/postgres/value.rs +++ b/rust/src/database/postgres/value.rs @@ -47,7 +47,9 @@ use postgres_protocol::types::{ int4_to_sql, int8_from_sql, int8_to_sql, text_to_sql, ArrayDimension, }; use pyo3::exceptions::{PyTypeError, PyValueError}; -use pyo3::types::{PyBool, PyByteArray, PyBytes, PyFloat, PyInt, PyList, PyString, PyTuple}; +use pyo3::types::{ + PyBool, PyByteArray, PyBytes, PyFloat, PyInt, PyList, PyMemoryView, PyString, PyTuple, +}; use pyo3::{prelude::*, BoundObject}; use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, Kind, ToSql, Type, WrongType}; @@ -107,6 +109,14 @@ impl PgValue { if let Ok(b) = obj.cast::() { return Ok(PgValue::Bytea(b.to_vec().into())); } + // A `memoryview` (most often over binary event data) is also bound as + // BYTEA. The buffer protocol isn't available under the limited ABI, so + // copy its bytes out via `tobytes()`. + if let Ok(mv) = obj.cast::() { + let raw = mv.call_method0("tobytes")?; + let bytes = raw.cast::()?; + return Ok(PgValue::Bytea(bytes.as_bytes().into())); + } // A list is bound as a Postgres array (for `= ANY($1)` / `!= ALL($1)`). // Each element is classified recursively. if let Ok(list) = obj.cast::() { @@ -623,6 +633,13 @@ mod tests { PgValue::Bytea(b) => assert_eq!(&*b, b"\x00\xff"), other => panic!("expected Bytea, got {other:?}"), } + + // A `memoryview` over bytes binds as BYTEA via the buffer protocol. + let memoryview = py.eval(c"memoryview(b'\\x00\\xff')", None, None).unwrap(); + match PgValue::from_py(&memoryview).unwrap() { + PgValue::Bytea(b) => assert_eq!(&*b, b"\x00\xff"), + other => panic!("expected Bytea, got {other:?}"), + } }); }