edgedb / edgedb-go

The official Go client library for EdgeDB
https://pkg.go.dev/github.com/edgedb/edgedb-go
Apache License 2.0
167 stars 11 forks source link

Inserting nested structures with multi-links #222

Closed divan closed 2 years ago

divan commented 2 years ago

How would you suggest to insert nested structures from Go code that have multi links?

With single links it's relatively straighforward, albeit verbose.

But with multi links it's not clear how is it supposed to be handled.

Straightforward approach would be to iterate over children's slices, insert manually, keep IDs, then add ID's to the parent object. Super verbose and painful without codegeneration. Also, requires transaction.

Another approach I was drawing from the examples of bulk insert from EdgeQL docs – marshalling data into JSON and inserting from json array. But it feels super hackish and not clear how to use for more than one nesting level.

Going through code I found something called "$inline" tag and PR that enabled it, but it's unclear if it's something that can be used for the nested inserts.

Would appreaciate suggestions/advice here a lot.

Sample schema

module default {
    type Pet {
        required property name -> str;
    }

    type Person {
        required property name -> str;
        multi link pets -> Pet;
    }

    type Movie {
        required property title -> str;
        multi link actors -> Person;
    }
};

Sample data (generated by Copilot, sorry)):

ttype Pet struct {
    Name string `edgedb:"name"`
}

type Person struct {
    Name string `edgedb:"name"`
    Pets []Pet  `edgedb:"pets"`
}

type Movie struct {
    Title  string   `edgedb:"title"`
    Actors []Person `edgedb:"actors"`
}

var movie = Movie{
    Title: "The Matrix",
    Actors: []Person{
        {
            Name: "Keanu Reeves",
            Pets: []Pet{
                { Name: "Neo", },
                { Name: "Morpheus", },
            },
        },
        {
            Name: "Laurence Fishburne",
            Pets: []Pet{
                { Name: "Tank", },
                { Name: "Cypher", },
            },
        },
    },
}
main.go ```go package main import ( "context" "fmt" "log" "github.com/edgedb/edgedb-go" ) type Pet struct { Name string `edgedb:"name"` } type Person struct { Name string `edgedb:"name"` Pets []Pet `edgedb:"pets"` } type Movie struct { Title string `edgedb:"title"` Actors []Person `edgedb:"actors"` } var movie = Movie{ Title: "The Matrix", Actors: []Person{ { Name: "Keanu Reeves", Pets: []Pet{ {Name: "Neo"}, {Name: "Morpheus"}, }, }, { Name: "Laurence Fishburne", Pets: []Pet{ {Name: "Tank"}, {Name: "Cypher"}, }, }, }, } func main() { ctx := context.TODO() client, err := edgedb.CreateClient(ctx, edgedb.Options{}) if err != nil { log.Fatal(err) } defer client.Close() var result struct { id edgedb.UUID `edgedb:"id"` } err = client.QuerySingle(ctx, `INSERT Movie { title := $1, actors := ?, }`, &result, movie.Title, movie.Actors, ) if err != nil { log.Fatal(err) } fmt.Println(result) } ```
fmoor commented 2 years ago

using the JSON approach looks something like this

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    "github.com/edgedb/edgedb-go"
)

type Pet struct {
    Name string `edgedb:"name"`
}

type Person struct {
    Name string `edgedb:"name"`
    Pets []Pet  `edgedb:"pets"`
}

type Movie struct {
    Title  string   `edgedb:"title"`
    Actors []Person `edgedb:"actors"`
}

var movie = Movie{
    Title: "The Matrix",
    Actors: []Person{
        {
            Name: "Keanu Reeves",
            Pets: []Pet{
                {Name: "Neo"},
                {Name: "Morpheus"},
            },
        },
        {
            Name: "Laurence Fishburne",
            Pets: []Pet{
                {Name: "Tank"},
                {Name: "Cypher"},
            },
        },
    },
}

func main() {
    ctx := context.TODO()
    client, err := edgedb.CreateClient(ctx, edgedb.Options{})
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    var result struct {
        id edgedb.UUID `edgedb:"id"`
    }

    data, err := json.Marshal(movie.Actors)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))

    err = client.QuerySingle(ctx, `
        INSERT Movie {
            title := <str>$0,
            actors := (FOR actor IN json_array_unpack(to_json(<str>$1)) UNION (
                INSERT Person {
                    name := <str>actor['Name'],
                    pets := (FOR pet in json_array_unpack(actor['Pets']) UNION (
                        INSERT Pet {
                            name := <str>pet['Name'],
                        }
                    ))
                } 
            ))
        }
        `,
        &result,
        movie.Title,
        string(data),
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(result)
}

Going through code I found something called "$inline" tag and PR that enabled it, but it's unclear if it's something that can be used for the nested inserts.

The $inline tag is for promoting the fields of an embedded struct up to the embeder struct, but this is only relevant for decoding query results. There is currently no way to send objects or tuples as query arguments.

divan commented 2 years ago

Thanks for the example @fmoor. JSON approach looks quite okay, suprisingly. What about FOR ... json_array_unpack... performance? I mean compared to sending equivalent pregenerated query? Is there any data or guesstimates available?

Also, are there any ideas or plans of query builder? https://github.com/edgedb/edgedb-go/issues/182

elprans commented 2 years ago

What about FOR ... json_array_unpack... performance? I mean compared to sending equivalent pregenerated query?

json_array_unpack would be faster than a bunch of pregenerated text, because the query itself is smaller.

divan commented 2 years ago

Thank you! Closing.