-
-
Notifications
You must be signed in to change notification settings - Fork 258
feat: add support for scanning slices of sql.Scanner structs #1244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
57e2ecd to
af62a39
Compare
|
After reflection, the kind of ULID should be Array([16]byte) rather than Struct. The example I constructed manually works. ids := make([]ulid.ULID, 0)
err := db.NewSelect().Model((*User)(nil)).Column("ulid").Scan(ctx, &ids)
if err != nil {
panic(err)
}
fmt.Println(ids) |
|
You're right, it does work when using type UID struct{ ulid.ULID }
ids := make([]UID, 0)
if err := s.db.NewSelect().Model((*model.User)(nil)).Column("id").Scan(ctx, &ids); err != nil {
panic(err)
}
fmt.Println(ids) // succeeds but all ids are zeroMy |
|
@NathanBaulch In my local environment, using a type alias works, but when using embed, |
|
I don't get any errors, only empty (zero) values. I've tested against type UID struct{ ulid.ULID }
ids := make([]UID, 0)
err := db.NewSelect().ColumnExpr(`'\x0198a84c62c66fa11f7d332e78de72dc'::bytea`).Scan(ctx, &ids)
fmt.Println(err) // nil
fmt.Println(ids) // [00000000000000000000000000]I should point out that scanning into a single value works: type UID struct{ ulid.ULID }
id := UID{}
err := db.NewSelect().ColumnExpr(`'\x0198a84c62c66fa11f7d332e78de72dc'::bytea`).Scan(ctx, &id)
fmt.Println(err) // nil
fmt.Println(id) // 01K2M4RRP6DYGHYZ9K5SWDWWPWThe goal of this PR is to get slices of my ID struct working just like single values already do. |
|
I switched to v1.2.15 and added the relevant code based on example/basic. That’s consistent with my direct understanding of the related code. example/basic/main.go package main
import (
"context"
"database/sql"
"fmt"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bun/extra/bundebug"
)
type IDAlias ulid.ULID
func (id *IDAlias) Scan(src any) error {
uid := (*ulid.ULID)(id)
return uid.Scan(src)
}
type IDEmbed struct {
ulid.ULID
}
type IDField struct {
ULID ulid.ULID `bun:"ulid"`
}
func (id *IDField) Scan(src any) error {
return id.ULID.Scan(src)
}
var _ sql.Scanner = (*IDAlias)(nil)
var _ sql.Scanner = (*IDEmbed)(nil)
var _ sql.Scanner = (*IDField)(nil)
func main() {
ctx := context.Background()
sqlite, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
if err != nil {
panic(err)
}
sqlite.SetMaxOpenConns(1)
db := bun.NewDB(sqlite, sqlitedialect.New())
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"),
))
if err := resetSchema(ctx, db); err != nil {
panic(err)
}
// Select all users.
users := make([]User, 0)
if err := db.NewSelect().Model(&users).OrderExpr("id ASC").Scan(ctx); err != nil {
panic(err)
}
fmt.Printf("all users: %v\n\n", users)
// Select one user by primary key.
user1 := new(User)
if err := db.NewSelect().Model(user1).Where("id = ?", 1).Scan(ctx); err != nil {
panic(err)
}
fmt.Printf("user1: %v\n\n", user1)
// Select a story and the associated author in a single query.
story := new(Story)
if err := db.NewSelect().
Model(story).
Relation("Author").
Limit(1).
Scan(ctx); err != nil {
panic(err)
}
fmt.Printf("story and the author: %v\n\n", story)
// Select a user into a map.
var m map[string]interface{}
if err := db.NewSelect().
Model((*User)(nil)).
Limit(1).
Scan(ctx, &m); err != nil {
panic(err)
}
fmt.Printf("user map: %v\n\n", m)
// Select all users scanning each column into a separate slice.
var ids []int64
var names []string
if err := db.NewSelect().
ColumnExpr("id, name").
Model((*User)(nil)).
OrderExpr("id ASC").
Scan(ctx, &ids, &names); err != nil {
panic(err)
}
fmt.Printf("users columns: %v %v\n\n", ids, names)
{
ids := make([]IDAlias, 0)
err := db.NewSelect().Model((*User)(nil)).Column("ulid").Scan(ctx, &ids)
if err != nil {
panic(err)
}
fmt.Println("scan into alias", ids)
}
{
ids := make([]IDField, 0)
err := db.NewSelect().Model((*User)(nil)).Column("ulid").Scan(ctx, &ids)
if err != nil {
panic(err)
}
fmt.Println("scan into struct field", ids)
}
{
ids := make([]IDEmbed, 0)
err := db.NewSelect().Model((*User)(nil)).Column("ulid").Scan(ctx, &ids)
if err != nil {
panic(err)
}
fmt.Println("scan into embed", ids)
}
}
type User struct {
ID int64 `bun:",pk,autoincrement"`
ULID ulid.ULID `bun:",default:'01D78XZ44G0000000000000000'"`
Name string
Emails []string
}
func (u User) String() string {
return fmt.Sprintf("User<%d %s %v>", u.ID, u.Name, u.Emails)
}
type Story struct {
ID int64 `bun:",pk,autoincrement"`
Title string
AuthorID int64
Author *User `bun:"rel:belongs-to,join:author_id=id"`
}
func resetSchema(ctx context.Context, db *bun.DB) error {
if err := db.ResetModel(ctx, (*User)(nil), (*Story)(nil)); err != nil {
return err
}
users := []User{
{
Name: "admin",
Emails: []string{"admin1@admin", "admin2@admin"},
},
{
Name: "root",
Emails: []string{"root1@root", "root2@root"},
},
}
if _, err := db.NewInsert().Model(&users).Exec(ctx); err != nil {
return err
}
stories := []Story{
{
Title: "Cool story",
AuthorID: users[0].ID,
},
}
if _, err := db.NewInsert().Model(&stories).Exec(ctx); err != nil {
return err
}
return nil
}git diff in go.mod |
|
Yes, I see exactly the same behavior. There is obviously some difference between SQLite (your example program) and PostgreSQL (what I'm using). |
|
When I switched to PostgreSQL, the error I encountered was still that the embed field was not recognized, dsn := "postgres://j2gg0s:@localhost:5432/postgres?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
db := bun.NewDB(sqldb, pgdialect.New())
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"),
))
type User struct {
ID int64 `bun:",pk,autoincrement"`
ULID ulid.ULID `bun:"type:varchar,default:'01K47E3YF67THMBD2WHFV3PB5J'"`
Name string
Emails []string
}Sorry for the many rounds of communication. |
|
OK, I've created a minimal example that builds upon your example here: https://gist.github.com/NathanBaulch/d7e88b6b8586a59d628bbdfb87dc65f5 |
|
This pull request has been automatically marked as stale because it has not had activity in the last 30 days. If there is no update within the next 7 days, this pr will be closed. Please feel free to give a status update now, ping for review, when it's ready. Thank you for your contributions! |
af62a39 to
e1604ae
Compare
|
Bump! |
I often find myself needing to select a single column of IDs. However in my project all IDs are of type ULID which bun interprets as a model struct rather than a simple value. Looking at the
_newModelfunction, I can see thattime.Timeis the only exception to this rule.I'd like to propose that any slice of structs that implement
sql.Scannershould be treated as a slice of values, not models. This already works for single values and slices of built-in types.In other words, I'd like the ability to do this:
This PR adds support for this simple scenario by checking for
sql.Scannerin addition totime.Time. It also adds a unit test in theinternal/dbtestpackage to validate.