tursodatabase / libsql

libSQL is a fork of SQLite that is both Open Source, and Open Contributions.
https://turso.tech/libsql
MIT License
9.41k stars 246 forks source link

Joint row deserialization into structs #1314

Open aminya opened 5 months ago

aminya commented 5 months ago

I have this common use case where I join two tables and I want to deserialize a row into separate structs. However, each row cannot be easily deserialized into separate structs via from_row.

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
pub struct User {
  pub id: i32,
  pub date: DateTime<Utc>,
  pub group_id: i32,
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
pub struct Group {
  pub id: i32,
  pub name: String,
}

The query is like

let rows = db_conn
    .query(
      "SELECT users.*, groups.*
        FROM users
        JOIN groups ON users.group_id = groups.id
        WHERE users.user_id = ?1 LIMIT ?2
    ",
      params![user_id.clone(), 10],
    )
    .await?;

while let Some(row) = rows.next().await? {
    // there's no such thing:
    let (users, group) = from_row<(User, Group)>(&row)?;
}

I have to do things like the following to be able to convert a Row into separate structs:


#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
pub struct UserWithGroup {
  pub user: User,
  pub group: Group,
}

// Implement from_row for UserWithGroup to deserialize from a database row
impl UserWithGroup {
  pub fn from_row<'de>(row: &'de Row) -> Result<UserWithGroup, DeError> {
    return Ok(Self {
      user: User {
        id: row.get(0).expect("id"),
        date: DateTime::deserialize(
          row.get_value(1).expect("date").into_deserializer(),
        )?,
        group_id: row.get(2).expect("group_id"),
      },
      group: Group {
        id: row.get(3).expect("group_id"),
        name: row.get(4).expect("name"),
      },
    });
  }
}

To be able to do something like

while let Some(row) = rows.next().await? {
    let users_with_group = UsersWithGroup::from_row(&row)?;
}

Is there a way to support from_row for the JOIN use cases maybe via tuples so that this manual work is not needed anymore?

sveltespot commented 5 months ago

We do face the same issues. Currently, our work around for this is to use #[serde(alias = 'some_unique_name')] for each of the fields of the struct. So considering your use case, it will look something like:

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
pub struct User {
  #[serde(alias='user_table_id')]
  pub id: i32,
  #[serde(alias='user_table_date')]
  pub date: DateTime<Utc>,
  #[serde(alias='user_table_group_id')]
  pub group_id: i32,
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
pub struct Group {
  #[serde(alias='group_table_id')]
  pub id: i32,
  #[serde(alias='group_table_name')]
  pub name: String,
}

And then modify your query to include those names, like so:

let rows = db_conn
    .query(
      "SELECT users.id as user_table_id,
            users.date as user_table_date,
            users.group_id as user_table_group_id,
            groups.id as group_table_id,
            groups.name as group_table_name
          FROM users
          JOIN groups ON users.group_id = groups.id
          WHERE users.user_id = ?1 LIMIT ?2
    ",
      params![user_id.clone(), 10],
    )
    .await?;

while let Some(row) = rows.next().await? {
    let user = from_row<User>(&row)?;
    let group = from_row<Group>(&row)?;
}

Although this seems like a lot of work, but since the query is mostly generated programmatically, this is a fair bit easier to work with. But I totally agree with you that if there was some solution to this directly from libsql, it would be a huge help.

codegod100 commented 3 weeks ago

i think it's ironic that this lib is written in rust but doesn't have builtin mechanism for struct building