Sqlx: Add a top-level derive for a new SQL Type ( general ideas, needs refinement )

Created on 20 Jan 2020  路  9Comments  路  Source: launchbadge/sqlx

The following are ideas and not final.

Tuple Struct (of 1)

#[derive(sqlx::Type)]
// transparent indicates that this type is purely for 
// documentation and it should act exactly as if it were the wrapped type in all aspects
#[sqlx(transparent)]
struct MyType(i32);

Enum

#[derive(sqlx::Type)]
// specify the OID explicitly for the type (optionally the OID for when its used as an array)
#[sqlx(postgres(oid = 1212, array_oid = 21341))]
// by default we encode and decode as a string and rely on first-class support
// for enumerations in the database server
#[sqlx(repr(i32))]
// by default we should snake_case the type name and look up its OID
// you should be able to use a container-level attribute to rename the type name 
#[sqlx(rename = "myenum")]
enum MyEnum {
  TheFoo,
  // By default we should snake_case rename but also allow renames
  #[sqlx(rename = "foothe")]
  FooThe
}

Struct

// In Postgres you can define your own composites or structs
// We should be able to support this
#[derive(sqlx::Type)]
struct Foo { a: i32 }

// Additionally, perhaps something like the following for strongly typed JSON
// from Postgres or MySQL
#[derive(sqlx::Type)]
#[sqlx(repr(json))]
struct Bar { b: i64 }

Basic idea of implementation is these derives would implement HasSqlType and add the Encode and Decode derives. Can proc derives be recursive (e.g., can this derive generate more derive macro invocations)?

enhancement question

Most helpful comment

Right.. It's the for T part that still won't be allowed.


Regardless, thinking about this again, the derive should probably just follow the feature flags that are being applied to the sqlx-macros crate.

If the postgres feature is present, emit an explicit for Postgres impl.

All 9 comments

This will be an excellent addition to the library, for sure!

Can proc derives be recursive (e.g., can this derive generate more derive macro invocations)?

No because they cannot emit more #[derive] attributes for the same item (their output is purely additive). However, we can simply call into the derive implementations for Encode and Decode and merge the token streams instead.

tuple structs of one and enums seem pretty straight forward. how does encode & decode work for structs though? and how would repr(json) do? add derives for serde and use serde_json?

  • [ ] Case 1 - Transparent or "New Type"
Rust
#[derive(sqlx::Type)]
#[repr(transparent)]
struct Health(i32);
SQL
INT

  • [ ] Case 2 - Strong Enum
Rust
#[derive(sqlx::Type)]
#[sqlx(postgres(oid = 101010))]
enum Direction { Up, Down, Left, Right };
Postgres
CREATE TYPE direction AS ENUM ('up', 'down', 'left', 'right');
MySQL
ENUM('up', 'down', 'left', 'right')

  • [ ] Case 3 - Weak Enum
Rust
#[derive(sqlx::Type)]
#[repr(i32)]
enum Direction { Up, Down, Left, Right };
SQL
INT

  • [ ] Case 4 - Struct (Postgres-only)
Rust
#[derive(sqlx::Type)]
#[sqlx(postgres(oid = 101010))]
struct Point { x: i16, y: i16 };
Postgres
CREATE TYPE point AS (
  x INT2,
  y INT2
);

Unresolved Questions

  • [ ] repr(i32) or sqlx(repr(i32)) for Weak Enums ?

Future Extensions

  • Strongly typed JSON
  • Remove need for explicit OID

@Freax13 Let me know if you have any other questions.

Encode and Decode for structs in Postgres is a touch complicated. The only documentation is in the source code. It's a specific binary encoding. If you don't feel comfortable tackling this, that's totally fine.

Here is the best "docs" I can find on it.

https://github.com/scrive/hpqtypes/blob/master/libpqtypes/src/record.c

turns out deriving HasSqlType for transparent types (and weak enums?) isnt as straight forward as I thought it would be. ideally we would want something like

struct Foo(i32);
impl<DB: sqlx::Database> sqlx::types::HasSqlType<Foo> for DB where DB: sqlx::types::HasSqlType<i32> {
    fn type_info() -> Self::TypeInfo {
        <Self as HasSqlType<i32>>::type_info()
    }
}

however this doesnt work due to the orphan rules (the two foreign traits being HasSqlType and Database)

so we could either

  1. derive implementations for specific Databases using derive inputs like
#[derive(HasSqlType(MySql,Postgres,OtherDB))]
struct Foo(i32);
  1. just hardcode the MySql and Postgres DBs
  2. default to MySql and Postgres unless something else is specified in the derive inputs

I like option 1. here. Users would already need to write the derive for their code, and an additional param for HasSqlType specifying the DB type being used seems pretty logical, especially given that doing so will reduce the amount of code which would need to be generated.

however this doesnt work due to the orphan rules (the two foreign traits being HasSqlType and Database)

This is actually changing in 1.41 to be allowed. Orphan rules are relaxed if the type parameter of a foreign trait is your crate-local type.

See: https://github.com/rust-lang/rust/blob/master/RELEASES.md#language


Until 1.41 is out and we depend on it, I would recommend exposing a boolean or something from sqlx as #[doc(hidden)] that we can look at and produce implementations.

some rules will be relaxed, but it still wont work
playground example

Right.. It's the for T part that still won't be allowed.


Regardless, thinking about this again, the derive should probably just follow the feature flags that are being applied to the sqlx-macros crate.

If the postgres feature is present, emit an explicit for Postgres impl.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mehcode picture mehcode  路  8Comments

DarrienG picture DarrienG  路  4Comments

sam701 picture sam701  路  6Comments

liangyongrui picture liangyongrui  路  3Comments

kunjee17 picture kunjee17  路  3Comments