sfackler / rust-postgres

Native PostgreSQL driver for the Rust programming language
Apache License 2.0
3.43k stars 436 forks source link

question: ToSql implementation to insert any custom type into a composite type #1055

Open stevenliebregt opened 1 year ago

stevenliebregt commented 1 year ago

I'm trying to implement a type that has a data field of type serde_json::Value which should be an object, that can be inserted into any composite type so long as the name and fields match.

pub struct CompositeType {
    composite_name: String,
    data: serde_json::Value
}

So if for example I have a composite type

CREATE TYPE my_type AS (a integer, b text);

An instance of CompositeType like the following should be allowed to be inserted there:

CompositeType {
    composite_name: "my_type",
    data: {
        a: Number(123),
        b: String("hello")
    }
}

Now I took the generated ToSql implementation of the derive macro that normally should work for a struct with named fields and tried to adapt it, but it keeps giving the error: db error: ERROR: insufficient data left in message, and I'm not sure what the problem is.

The ToSql implementation is as follows:

impl ToSql for CompositeType {
    fn to_sql(&self, ty: &Type, buf: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> where Self: Sized {
        // Make sure our data is an object
        let data = match &self.data {
            serde_json::Value::Object(data) => data,
            _ => return Err(format!("(@impl ToSql for CompositeType) Expected data to be an object, got: {:?}", self.data).into())
        };

        // Make sure it is a composite type we're inserting into
        let fields = match ty.kind() {
            &Kind::Composite(ref fields) => {
                // Make sure the name matches
                if ty.name() != self.composite_name {
                    return Err(format!("(@impl ToSql for CompositeType) Type name mismatch, expected: {}, got: {}", ty.name(), self.composite_name).into());
                }

                fields
            },
            kind => return Err(format!("(@impl ToSql for CompositeType) Unsupported type: {:?}, can only insert into composite types", kind).into())
        };

// This is copied from the derive macro
        buf.extend_from_slice(&(fields.len() as i32).to_be_bytes());
        for field in fields {
            buf.extend_from_slice(&field.type_().oid().to_be_bytes());
            let base = buf.len();
            buf.extend_from_slice(&[0; 4]);

// Only this line is different, no more matching field names, just get the data
            let r = postgres_types::ToSql::to_sql(&self.data.get(field.name()), field.type_(), buf);

            let count = match r? {
                postgres_types::IsNull::Yes => -1,
                postgres_types::IsNull::No => {
                    let len = buf.len() - base - 4;
                    if len > i32::max_value() as usize { return std::result::Result::Err(std::convert::Into::into("value too large to transmit")); }
                    len as i32
                }
            };
            buf[base..base + 4].copy_from_slice(&count.to_be_bytes());
        }
        std::result::Result::Ok(postgres_types::IsNull::No)
    }

    fn accepts(ty: &Type) -> bool where Self: Sized {
        true
    }

    to_sql_checked!();
}
stevenliebregt commented 1 year ago

I think I may have found the issue, a serde_json::Value containing a number encodes as b"5", while a i32 encodes as b"\0\0\0\x05". So I guess I'll have to do a nice match statement to cast data before encoding it