Suppose you have the following SQL query:
SELECT
Artist.ArtistId as "artist.artist_id", Artist.Name as "artist.name",
Album.AlbumId as "artist.albums.album_id", Album.Title as "artist.albums.title",
Track.TrackId as "artist.albums.track.track_id", Track.Name as "artist.albums.tracks.name"
FROM Artist
JOIN Album USING(ArtistId)
JOIN Track USING(AlbumId)
You are trying to load the result of that query into a vector of rust struct: Vec<Artist>
. Note that result column names contain information on how to load the data. For example, "artist.albums.title"
tells us that the data for that particular column should go into Artist -> Optional<Vec<Album>> -> title
struct Artist {
name: String,
artist_id: i32,
albums: Option<Vec<Album>>,
}
struct Album {
title: String,
album_id: i32,
tracks: Vec<Track>,
}
struct Track {
track_id: i32,
name: String,
}
I was trying to figure out what is the most performant way of doing this in Rust.
My Current Solution
My current solution involves loading the query result into a tree like structure(Node
) and a serde Deserializer will convert that tree like structure into Vec<Artist>
. It works but not very fast.
pub enum Node {
Leaf(Value),
Internal(BTreeMap<Vec<Value>, HashMap<String, Node>>),
Root(HashMap<String, Node>), // I am not fully convinced yet if I need this variant yet
}
pub enum Value {
I64(i64),
F64(u64),
Str(String),
Blob(Vec<u8>),
Null,
}
Please uncollapse this to see how the relational data will be loaded into Node enum above
Internal(
{
[
I64(
1,
),
Str(
"AC/DC",
),
]: {
"artist.artist_id": Leaf(
I64(
1,
),
),
"artist.name": Leaf(
Str(
"AC/DC",
),
),
"artist.albums": Internal(
{
[
I64(
1,
),
Str(
"For Those About To Rock We Salute You",
),
]: {
"artist.albums.tracks": Internal(
{
[
I64(
1,
),
Str(
"For Those About To Rock (We Salute You)",
),
]: {
"artist.albums.track.track_id": Leaf(
I64(
1,
),
),
"artist.albums.tracks.name": Leaf(
Str(
"For Those About To Rock (We Salute You)",
),
),
},
},
),
"artist.albums.album_id": Leaf(
I64(
1,
),
),
"artist.albums.title": Leaf(
Str(
"For Those About To Rock We Salute You",
),
),
"artist.albums.track": Internal(
{
[
I64(
1,
),
Str(
"For Those About To Rock (We Salute You)",
),
]: {
"artist.albums.tracks.name": Leaf(
Str(
"For Those About To Rock (We Salute You)",
),
),
"artist.albums.track.track_id": Leaf(
I64(
1,
),
),
},
},
),
},
},
),
},
[
I64(
2,
),
Str(
"Accept",
),
]: {
"artist.albums": Internal(
{
[
I64(
2,
),
Str(
"Balls to the Wall",
),
]: {
"artist.albums.album_id": Leaf(
I64(
2,
),
),
"artist.albums.track": Internal(
{
[
I64(
2,
),
Str(
"Balls to the Wall",
),
]: {
"artist.albums.tracks.name": Leaf(
Str(
"Balls to the Wall",
),
),
"artist.albums.track.track_id": Leaf(
I64(
2,
),
),
},
},
),
"artist.albums.tracks": Internal(
{
[
I64(
2,
),
Str(
"Balls to the Wall",
),
]: {
"artist.albums.track.track_id": Leaf(
I64(
2,
),
),
"artist.albums.tracks.name": Leaf(
Str(
"Balls to the Wall",
),
),
},
},
),
"artist.albums.title": Leaf(
Str(
"Balls to the Wall",
),
),
},
},
),
"artist.name": Leaf(
Str(
"Accept",
),
),
"artist.artist_id": Leaf(
I64(
2,
),
),
},
},
)
Is the above the best way of going about this? I think there are some optimizations I can do to make loading the relational data into the Node
enum more efficient but I just wanted to make sure if this is even the right approach before I did anything like that.
Sidenote: I am aware Diesel (and maybe even other ORMs that I haven't tried) is capable of loading queries like this into Rust structs very fast but I am interested in the general problem of loading relational query results into arbitrarily nested Rust structures.