Bounds on concrete type

While implementing sqlx::FromRow and using sqlx::Row::try_get I arrived at an implementation based on compiler suggestions that I don't entirely understand.

Specifically my implementation (below) has what I only know how describe as bounds on a concrete type in the where clause:

impl<'r, R: sqlx::Row> FromRow<'r, R> for Entry
where
    &'r str: sqlx::ColumnIndex<R>,
    String: sqlx::decode::Decode<'r, R::Database>,
    String: sqlx::types::Type<R::Database>,
{
    fn from_row(row: &'r R) -> Result<Self, sqlx::Error> {
        let id: String = row.try_get("id")?;
        Ok(Entry { id })
    }
}

I've been calling these bounds on concrete types:

where
    &'r str: sqlx::ColumnIndex<R>,
    String: sqlx::decode::Decode<'r, R::Database>,
    String: sqlx::types::Type<R::Database>,

Is that an accurate description?

While this compiles, I don't understand what these bounds are saying or why they're required. Could anyone enlighten me?

They do exactly the same as if they were on a type variable. Type variables would just stand in for concrete types anyway. So, for example:

where String: sqlx::types::Type<R::Database>

means "this function requires that the type String implements the trait sqlx::types::Type<R::Database>".

Again, for the same reason – the implementation of the from_row function wants to use functionality from e.g. trait sqlx::types::Type<R::Database> implemented on the String type.


I think your confusion arises from thinking that trait bounds can only constrain the left-hand side of the colon. That is not the case. Rust's type and trait system is based on mathematical logic, and it's a pretty universal framework for expressing all sorts of constraints.

While the most frequent use case for a where clause is indeed to constrain a type variable on the left-hand side, it is certainly not the only possibility. Where clauses don't constrain the LHS – they constrain the type variables, wherever they appear. In the implementation above, the relevant type variable is R.

Now, the deserialization of a type from a SQL row requires that the fields of the type know how to deserialize themselves from primitive types provided by a particular database engine. However, database engines don't provide the same set of primitives, and they might (and do) also provide different concrete mechanisms for retrieving values of said primitive types. For example, SQLite only provides null, signed int, float, string, and blob. In contrast, Postgres also provides decimals, datetime, JSON, etc.

Therefore, the deserialization of your custom type depends on two things:

  • The types of its fields, and
  • The underlying raw database type.

This means that, regardless of the types of the fields and the type of the database, the following relation must hold: type T of your field must know how to create itself from a database row of primitive types R.

Of course, if your struct were generic, then at least some its fields would be typed using a type variable. But they aren't – you declared your type as Entry { id: String }, so its id field is of the very concrete type String. This doesn't mean, however, that it automatically knows how to deserialize itself from any database row!

It is, for instance, conceivable that some low-level databases simply don't know what strings are (e.g. because they only deal with blobs). In this case, you won't be able to use your Entry type with such a database – but how would you express this constraint in the signature? Remember that Rust doesn't allow for post-monomorphization errors, i.e. the body of a generic function can't fail to compile once it has been typechecked successfully, so you have to deal with this kind of question even if you want a concrete type out of the DB.

So, in order to be able to get a string from your DB, you have to declare in the function interface that the DB knows how to hand out strings. This actually constrains the R type variable (i.e. the database engine) itself, and not the String concrete type. However the syntax is always Type: Trait, regardless of which side(s) has/have type variables, and which side(s) is/are purely concrete.

5 Likes

Yes. Bounds can be applied both to concrete types and generic parameters.

The bounds aren't particularly readable, since the FromRow trait is intended to be derived with a macro. The first bound asserts that a &str can be used to index into the row. The second bound asserts that the database can yield Strings as values. The third bound asserts that String is a valid SQL type for the database.

What a great response! I especially appreciate the in-depth explanation what these bounds are communicating when it comes to my implementation and what R::Database supports for primitive types and deserialization.

While the most frequent use case for a where clause is indeed to constrain a type variable on the left-hand side, it is certainly not the only possibility. Where clauses don't constrain the LHS – they constrain the type variables , wherever they appear. In the implementation above, the relevant type variable is R.

I had definitely been assuming the LHS was always what was being constrained and completely overlooked what R::Database was telling me.

Feeling enlightened. Thanks for your help!

2 Likes

If it helps, I like to think of where-clauses as "equations", where the : is almost like a = or (implies).

When you word it like that, it makes logical sense that sometimes the variable (e.g. R::Database) can be on the right hand side.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.