libsql icon indicating copy to clipboard operation
libsql copied to clipboard

Joint row deserialization into structs

Open aminya opened this issue 10 months ago • 2 comments

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?

aminya avatar Apr 13 '24 06:04 aminya

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.

sveltespot avatar Apr 16 '24 15:04 sveltespot

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

codegod100 avatar Aug 25 '24 00:08 codegod100