Stable-friendly version of this trait?

I'm working on a filing-based storage system for an API I'm building. Different kinds of records will be stored in different tables, and since I'm using sqlx's query! macros, the table name has to be explicitly spelled out in the query body. It can't come from a variable, a function, or any other expression.

To support this, I've defined a trait that handles constructing an appropriate query for a given record type, but I've hit a snag. The return type of query_as! is Map<'a, Postgres, F, PgArguments>, where F: FnMut(<Postgres as Database>::Row) -> Result<T> + Send and T is just my record type - in this case, mostly serde_json::Value. This means that the actual return type is not nameable - query_as! emits a lambda expression which implements FnMut, and since it's an anonymous type, I can't use it in a trait.

I've been able to work around this by switching to nightly and defining the trait as

    pub trait Create {
        type Mapper: FnMut(<Postgres as Database>::Row) -> Result<Filed<Value>> + Send;

        fn create<'a>(
            submission: Submission<Value>,
        ) -> Map<'a, Postgres, Self::Mapper, PgArguments>;
    }

    impl Create for crate::bootstrap::records::Bootstrap {
        type Mapper = impl FnMut(<Postgres as Database>::Row) -> sqlx::Result<Filed<Value>>;

        fn create<'a>(
            submission: Submission<Value>,
        ) -> Map<'a, Postgres, Self::Mapper, PgArguments> {
            query_as! {
                Filed,
                r#"
                    insert into bootstrap
                        (number, operation, identity, record, revision)
                    values
                        (
                            $1,
                            $2,
                            $3,
                            $4,
                            $5
                        )
                    returning
                        number, revision, record
                "#,
                submission.number, submission.operation, submission.identity, submission.record, submission.revision
            }
        }
    }

However, the type Mapper = impl … syntax is unstable. Is there some way I can restructure this to avoid using it, and to be able to use stable?

For reference:

  • sqlx::query::Map: Map in sqlx::query - Rust

  • The call site:

    pub struct Repository {
        pool: PgPool,
    }
    
    impl Repository { /* … */
        pub async fn current<N, T>(&self, number: N) -> Result<Option<Filed<T>>>
        where
            N: Serialize,
            T: DeserializeOwned + schema::Current,
        {
            let number = serde_json::to_string(&number)?;
    
            let result = T::current(number).fetch_optional(&self.pool).await?;
            let result = result.map(Filed::into_app_type).transpose()?;
    
            Ok(result)
        }
    }
    

Have you tried a Box<dyn FnMut>?

I hadn't - thank you.

However… it's not super clear to me how to do that. The query_as! macro is from a foreign crate, and the actual function literal occurs inside of its expansion. Nothing in the Map docs seems to provide an obvious handle for turning a Map with f as its row mapping function (which is what this is) into a Map with Box::new(f) as its row mapping function, either. Boxing up the whole Map still requires that the mapping function have a known size… and so I feel like I'm right back at square one again.

Any suggestions?

The expansion of query_as! is as follows (warning: long)

        impl Create for crate::bootstrap::records::Bootstrap {
            type Mapper = impl FnMut(<Postgres as Database>::Row) -> Result<Filed<Value>>;
            fn create<'a>(
                submission: Submission<Value>,
            ) -> Map<'a, Postgres, Self::Mapper, PgArguments> {
                {
                    {
                        #[allow(clippy::all)]
                        {
                            use ::sqlx::Arguments as _;
                            let arg0 = &(submission.number);
                            let arg1 = &(submission.operation);
                            let arg2 = &(submission.identity);
                            let arg3 = &(submission.record);
                            let arg4 = &(submission.revision);
                            if false {
                                use ::sqlx::ty_match::{WrapSameExt as _, MatchBorrowExt as _};
                                let expr = ::sqlx::ty_match::dupe_value(arg0);
                                let ty_check =
                                    ::sqlx::ty_match::WrapSame::<&str, _>::new(&expr).wrap_same();
                                let (mut _ty_check, match_borrow) =
                                    ::sqlx::ty_match::MatchBorrow::new(ty_check, &expr);
                                _ty_check = match_borrow.match_borrow();
                                ::core::panicking::panic("explicit panic");
                            }
                            if false {
                                use ::sqlx::ty_match::{WrapSameExt as _, MatchBorrowExt as _};
                                let expr = ::sqlx::ty_match::dupe_value(arg1);
                                let ty_check =
                                    ::sqlx::ty_match::WrapSame::<&str, _>::new(&expr).wrap_same();
                                let (mut _ty_check, match_borrow) =
                                    ::sqlx::ty_match::MatchBorrow::new(ty_check, &expr);
                                _ty_check = match_borrow.match_borrow();
                                ::core::panicking::panic("explicit panic");
                            }
                            if false {
                                use ::sqlx::ty_match::{WrapSameExt as _, MatchBorrowExt as _};
                                let expr = ::sqlx::ty_match::dupe_value(arg2);
                                let ty_check =
                                    ::sqlx::ty_match::WrapSame::<&str, _>::new(&expr).wrap_same();
                                let (mut _ty_check, match_borrow) =
                                    ::sqlx::ty_match::MatchBorrow::new(ty_check, &expr);
                                _ty_check = match_borrow.match_borrow();
                                ::core::panicking::panic("explicit panic");
                            }
                            if false {
                                use ::sqlx::ty_match::{WrapSameExt as _, MatchBorrowExt as _};
                                let expr = ::sqlx::ty_match::dupe_value(arg3);
                                let ty_check =
                                    ::sqlx::ty_match::WrapSame::<serde_json::Value, _>::new(&expr)
                                        .wrap_same();
                                let (mut _ty_check, match_borrow) =
                                    ::sqlx::ty_match::MatchBorrow::new(ty_check, &expr);
                                _ty_check = match_borrow.match_borrow();
                                ::core::panicking::panic("explicit panic");
                            }
                            if false {
                                use ::sqlx::ty_match::{WrapSameExt as _, MatchBorrowExt as _};
                                let expr = ::sqlx::ty_match::dupe_value(arg4);
                                let ty_check =
                                    ::sqlx::ty_match::WrapSame::<i64, _>::new(&expr).wrap_same();
                                let (mut _ty_check, match_borrow) =
                                    ::sqlx::ty_match::MatchBorrow::new(ty_check, &expr);
                                _ty_check = match_borrow.match_borrow();
                                ::core::panicking::panic("explicit panic");
                            }
                            let mut query_args = < sqlx :: postgres :: Postgres as :: sqlx :: database :: HasArguments > :: Arguments :: default () ;
                            query_args.reserve(
                                5usize,
                                0 + ::sqlx::encode::Encode::<sqlx::postgres::Postgres>::size_hint(
                                    arg0,
                                ) + ::sqlx::encode::Encode::<sqlx::postgres::Postgres>::size_hint(
                                    arg1,
                                ) + ::sqlx::encode::Encode::<sqlx::postgres::Postgres>::size_hint(
                                    arg2,
                                ) + ::sqlx::encode::Encode::<sqlx::postgres::Postgres>::size_hint(
                                    arg3,
                                ) + ::sqlx::encode::Encode::<sqlx::postgres::Postgres>::size_hint(
                                    arg4,
                                ),
                            );
                            query_args.add(arg0);
                            query_args.add(arg1);
                            query_args.add(arg2);
                            query_args.add(arg3);
                            query_args.add(arg4);
                            :: sqlx :: query_with :: < sqlx :: postgres :: Postgres , _ > ("\n                    insert into bootstrap\n                        (number, operation, identity, record, revision)\n                    values\n                        (\n                            $1,\n                            $2,\n                            $3,\n                            $4,\n                            $5\n                        )\n                    returning\n                        number, revision, record\n                " , query_args) . try_map (| row : sqlx :: postgres :: PgRow | { use :: sqlx :: Row as _ ; let sqlx_query_as_number = row . try_get_unchecked :: < String , _ > (0usize) ? ; let sqlx_query_as_revision = row . try_get_unchecked :: < i64 , _ > (1usize) ? ; let sqlx_query_as_record = row . try_get_unchecked :: < serde_json :: Value , _ > (2usize) ? ; Ok (Filed { number : sqlx_query_as_number , revision : sqlx_query_as_revision , record : sqlx_query_as_record , }) })
                        }
                    }
                }
            }
        }

and it would appear the relevant FnMut is near the end, starting at try_map (| row …. Of the code that appears there, precisely none of it is verbatim from the source representation, though I can see the relationships involved.

I think a boxed Execute would be the best option. Like so:

pub trait Create {
    fn create(submission: Submission<Value>) -> Box<dyn Execute<'static, Postgres>>;
}

You just have to call it using the Executor API.

2 Likes

Ah, I glossed over the fact that the closure was created within macro code. I see that try_map adds its own closure around the one you give it, so there's no room for you box it. Maybe sqlx could add a dyn_query_as! macro for this boxed flavor?

Box<dyn Execute> sounds great too, if that's functional.

That's so promising, and it's clearly the "right" answer. I completely missed that trait and I appreciate you for pointing it out!

Unfortunately:

178 |         fn create(submission: Submission<Value>) -> Box<dyn Execute<'static, Postgres>> {
    |                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `sqlx::Execute` cannot be made into an object
    |
    = note: the trait cannot be made into an object because it requires `Self: Sized`
    = note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>

That's probably a design issue on the sqlx side, and I'll bring this up on their issue tracker as it's pretty clear the Execute trait is meant to be used this way.

On the upside, this did help me tidy up the trait definition a bit. It still requires nightly, but:

    pub trait Create {
        type Mapper: Execute<'static, Postgres>;

        fn create(submission: Submission<Value>) -> Self::Mapper;
    }

    impl Create for crate::bootstrap::records::Bootstrap {
        type Mapper = Map<
            'static,
            Postgres,
            impl FnMut(<Postgres as Database>::Row) -> Result<Filed<Value>> + Send,
            PgArguments,
        >;

        fn create(submission: Submission<Value>) -> Self::Mapper {
            query_as! {
                Filed,
                r#"
                    insert into bootstrap
                        (number, operation, identity, record, revision)
                    values
                        (
                            $1,
                            $2,
                            $3,
                            $4,
                            $5
                        )
                    returning
                        number, revision, record
                "#,
                submission.number, submission.operation, submission.identity, submission.record, submission.revision
            }
        }
    }
2 Likes

One final followup - in another thread, someone pointed out that closures with no context can be coerced to fn, and fn types have names. Furthermore, as above, the sqlx query_as! code generates lambdas that close over nothing, so they fit this constraint.

That insight let me go back to stable, while using my original structure:

type StaticMap<'a> =
    Map<'a, Postgres, fn(<Postgres as Database>::Row) -> Result<Filed<Value>>, PgArguments>;

pub trait Create {
    fn create<'a>(submission: Submission<Value>) -> StaticMap<'a>;
}
1 Like