From 99d31077967d90cf89a258a83f28e96fa5589563 Mon Sep 17 00:00:00 2001 From: Clancy Date: Sat, 4 Jul 2026 21:51:23 +0300 Subject: [PATCH 1/2] chore: add build-prod target to makefile for stripped release binaries --- makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index e9926d5..e458037 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -.PHONY: build run test install db-up db-down db-clean bi fmt fmt-check vet integration-gen integration-test bench race lint +.PHONY: build build-prod run test install db-up db-down db-clean bi fmt fmt-check vet integration-gen integration-test bench race lint bi: build install @@ -11,6 +11,9 @@ bench: build: go build -o bin/valkyrie +build-prod: + go build -ldflags="-s -w" -o bin/valkyrie + install: build mkdir -p $(HOME)/go/bin ln -sf $(shell pwd)/bin/valkyrie $(HOME)/go/bin/valkyrie From f29832b30eed4f81b3ba3d6bdda72a07b7934594 Mon Sep 17 00:00:00 2001 From: Clancy Date: Sun, 5 Jul 2026 16:03:18 +0300 Subject: [PATCH 2/2] Feat: Implement createMany and CreateManyAndReturn 1- define hasAnyLog() for easy imports in the main client of the log package in case any log levels were configured, to avoid verbose if condition 2- define CreateManyBuilder, CreateManyAndReturnBuilder, and their associated select and omit builders,generics in the same modular style of CreateBuilder 3- define a SupportsBulkInsert() on the client dialect, to insert in bulk if the DB supports, or to fall back to a loop in a transaction if it doesn't (Sqlite), implement the function for both PG and Sqlite 4- Refactor the duplicates between create, createMany, and createManyAndReturn: a- Consolidated all per-field insert branching logic (cuid/uuid generation, defaults, optional fields, array handling) exclusively into the generated {{Model}}InputToMap method b- Introduced a package-level runtime helper mapToColsVals in relations_runtime.gotpl to map a model input map to structured columns and values based on a generated stable column order slice ({{Model}}ColOrder) c- Replaced duplicate inline column mapping inside execute{{Model}}Create with calls to {{Model}}InputToMap and mapToColsVals d- Extracted duplicate bulk SQL generation logic (column union computation, value flattening, query param binding, and dialect-specific RETURNING clauses) into a shared runtime helper buildBulkInsertSQL e- Updated execute{{Model}}CreateMany and execute{{Model}}CreateManyAndReturn to delegate SQL construction to buildBulkInsertSQL f- Consolidated relation check template OR-chains into a single generated hasAnyRelation() helper method on *{{Model}}Select to check relation selections cleanly 5- test createMany, and the returning count, and test createManyAndReturn that it supports select too --- generator/generator.go | 8 + generator/templates/builders_create.gotpl | 52 ++++- generator/templates/client.gotpl | 3 + generator/templates/header.gotpl | 2 + generator/templates/model_create.gotpl | 203 ++++++++++++++---- generator/templates/relations_runtime.gotpl | 73 +++++++ integration/create_many_test.go | 99 +++++++++ integration/main.go | 5 + integration/schema.prisma | 2 +- integration/valkyrie.json | 4 +- integration/valkyrie/category.go | 150 ++++++++++++- integration/valkyrie/categoryToPost.go | 143 +++++++++++- integration/valkyrie/client.go | 146 +++++++++++-- integration/valkyrie/comment.go | 172 +++++++++++++-- .../valkyrie/migrations/00001_init.sql | 5 +- .../valkyrie/migrations/00002_dummies.sql | 35 --- .../00003_change_type_from_enum_to_String.sql | 38 ---- .../00004_changing_3_types_in_one_go.sql | 37 ---- .../valkyrie/migrations/00005_init.sql | 39 ---- integration/valkyrie/post.go | 172 +++++++++++++-- integration/valkyrie/profile.go | 160 ++++++++++++-- integration/valkyrie/user.go | 172 +++++++++++++-- 22 files changed, 1399 insertions(+), 321 deletions(-) create mode 100644 integration/create_many_test.go delete mode 100644 integration/valkyrie/migrations/00002_dummies.sql delete mode 100644 integration/valkyrie/migrations/00003_change_type_from_enum_to_String.sql delete mode 100644 integration/valkyrie/migrations/00004_changing_3_types_in_one_go.sql delete mode 100644 integration/valkyrie/migrations/00005_init.sql diff --git a/generator/generator.go b/generator/generator.go index 7c1b9b6..bc59575 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -39,6 +39,14 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default } return false }, + "hasAnyLog": func() bool { + for _, l := range defaultLogs { + if l != "none" { + return true + } + } + return false + }, }) tmpl, err := tmpl.ParseFS(templatesFS, "templates/*.gotpl") if err != nil { diff --git a/generator/templates/builders_create.gotpl b/generator/templates/builders_create.gotpl index 412c2d1..f40c540 100644 --- a/generator/templates/builders_create.gotpl +++ b/generator/templates/builders_create.gotpl @@ -33,6 +33,53 @@ type CreateOmitBuilder[M any, I any, S any, O any] struct { func (b *CreateOmitBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { return b.builder.execFunc(ctx, b.builder.input, nil, &b.omits) } + +type CreateManyBuilder[M any, I any] struct { + client *Queries + inputs []I + execFunc func(ctx context.Context, inputs []I) (int64, error) +} + +func (b *CreateManyBuilder[M, I]) Exec(ctx context.Context) (int64, error) { + return b.execFunc(ctx, b.inputs) +} + +type CreateManyAndReturnBuilder[M any, I any, S any, O any] struct { + client *Queries + inputs []I + execFunc func(ctx context.Context, inputs []I, s *S, o *O) ([]*M, error) +} + +func (b *CreateManyAndReturnBuilder[M, I, S, O]) Select(s S) *CreateManyAndReturnSelectBuilder[M, I, S, O] { + return &CreateManyAndReturnSelectBuilder[M, I, S, O]{builder: b, selects: s} +} + +func (b *CreateManyAndReturnBuilder[M, I, S, O]) Omit(o O) *CreateManyAndReturnOmitBuilder[M, I, S, O] { + return &CreateManyAndReturnOmitBuilder[M, I, S, O]{builder: b, omits: o} +} + +func (b *CreateManyAndReturnBuilder[M, I, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.execFunc(ctx, b.inputs, nil, nil) +} + +type CreateManyAndReturnSelectBuilder[M any, I any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, I, S, O] + selects S +} + +func (b *CreateManyAndReturnSelectBuilder[M, I, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.inputs, &b.selects, nil) +} + +type CreateManyAndReturnOmitBuilder[M any, I any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, I, S, O] + omits O +} + +func (b *CreateManyAndReturnOmitBuilder[M, I, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.inputs, nil, &b.omits) +} + func executeInsert[M any]( ctx context.Context, q *Queries, @@ -53,7 +100,7 @@ func executeInsert[M any]( if i > 0 { sb.WriteString(", ") } - sb.WriteString(col) + sb.WriteString(q.dialect.Quote(col)) } sb.WriteString(") VALUES (") for i := range cols { @@ -93,9 +140,8 @@ func executeInsert[M any]( } var idVal any - quotedIdCol := q.dialect.Quote(idCol) for i, c := range cols { - if c == quotedIdCol { + if c == idCol { idVal = vals[i] break } diff --git a/generator/templates/client.gotpl b/generator/templates/client.gotpl index 75503b4..25eb8a0 100644 --- a/generator/templates/client.gotpl +++ b/generator/templates/client.gotpl @@ -2,6 +2,7 @@ type Dialect interface { Quote(ident string) string BindVar(idx int) string SupportsReturning() bool + SupportsBulkInsert() bool } {{- if or (eq .Schema.Datasource.Provider "postgres") (eq .Schema.Datasource.Provider "postgresql") }} @@ -9,6 +10,7 @@ type postgresDialect struct{} func (postgresDialect) Quote(ident string) string { return `"` + ident + `"` } func (postgresDialect) BindVar(idx int) string { return fmt.Sprintf("$%d", idx) } func (postgresDialect) SupportsReturning() bool { return true } +func (postgresDialect) SupportsBulkInsert() bool { return true } {{- end }} {{- if or (eq .Schema.Datasource.Provider "sqlite") (eq .Schema.Datasource.Provider "sqlite3") }} @@ -16,6 +18,7 @@ type sqliteDialect struct{} func (sqliteDialect) Quote(ident string) string { return `"` + ident + `"` } func (sqliteDialect) BindVar(idx int) string { return "?" } func (sqliteDialect) SupportsReturning() bool { return true } +func (sqliteDialect) SupportsBulkInsert() bool { return false } {{- end }} type DBTX interface { diff --git a/generator/templates/header.gotpl b/generator/templates/header.gotpl index 372b531..0ce988f 100644 --- a/generator/templates/header.gotpl +++ b/generator/templates/header.gotpl @@ -9,7 +9,9 @@ import ( "embed" {{- end }} "fmt" + {{- if hasAnyLog }} "log" + {{- end }} "strconv" "strings" "time" diff --git a/generator/templates/model_create.gotpl b/generator/templates/model_create.gotpl index 82ada59..cc6d20a 100644 --- a/generator/templates/model_create.gotpl +++ b/generator/templates/model_create.gotpl @@ -1,3 +1,20 @@ +var {{ .Model.Name }}ColOrder = []string{ +{{- range $field := .Model.ScalarFields }} + "{{ $field.EffectiveColName }}", +{{- end }} +} + +func (s *{{ .Model.Name }}Select) hasAnyRelation() bool { + if s == nil { + return false + } + {{- if .Model.RelationFields }} + return {{ range $i, $rel := .Model.RelationFields }}{{ if $i }} || {{ end }}s.{{ capitalize $rel.Name }} != nil{{ end }} + {{- else }} + return false + {{- end }} +} + func (d *{{ .Model.Name }}Delegate) Create(input {{ .Model.Name }}CreateInput) *CreateBuilder[{{ .Model.Name }}, {{ .Model.Name }}CreateInput, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { return &CreateBuilder[{{ .Model.Name }}, {{ .Model.Name }}CreateInput, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ client: d.client, @@ -7,96 +24,200 @@ func (d *{{ .Model.Name }}Delegate) Create(input {{ .Model.Name }}CreateInput) * } func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, input {{ .Model.Name }}CreateInput, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) { - var cols []string - var vals []any + m := q.{{ .Model.Name }}InputToMap(input) + cols, vals := mapToColsVals(m, {{ .Model.Name }}ColOrder) + + returningCols := q.select{{ .Model.Name }}Cols(selects, omits) + + scanFunc := func(res *{{ .Model.Name }}, cols []string) []any { + return res.ScanFields(cols) + } + + idCol := "{{ range $field := .Model.ScalarFields }}{{ if $field.IsID }}{{ $field.EffectiveColName }}{{ end }}{{ end }}" + + hasRelations := selects.hasAnyRelation() + + var res *{{ .Model.Name }} + var err error + if hasRelations { + err = q.transaction(ctx, func(txQ *Queries) error { + var err error + res, err = executeInsert(ctx, txQ, "{{ .Model.EffectiveTableName }}", cols, vals, returningCols, idCol, scanFunc) + if err != nil { + return err + } + return txQ.load{{ .Model.Name }}Relations(ctx, []*{{ .Model.Name }}{res}, selects) + }) + } else { + res, err = executeInsert(ctx, q, "{{ .Model.EffectiveTableName }}", cols, vals, returningCols, idCol, scanFunc) + } + if err != nil { + return nil, err + } + + return res, nil +} +func (q *Queries) {{ .Model.Name }}InputToMap(input {{ .Model.Name }}CreateInput) map[string]any { + m := make(map[string]any) {{- range $field := .Model.ScalarFields }} {{- if $field.EnumRef }} {{- if $field.IsArray }} if input.{{ capitalize $field.Name }} != nil { - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) - vals = append(vals, input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} } {{- else }} if input.{{ capitalize $field.Name }} != nil { - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) - vals = append(vals, *input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = *input.{{ capitalize $field.Name }} } {{- end }} {{- else }} {{- if $field.IsArray }} if input.{{ capitalize $field.Name }} != nil { - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) - vals = append(vals, input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} } {{- else }} {{- if and $field.Default (eq $field.Default.Kind.String "Func") }} if input.{{ capitalize $field.Name }} != nil { - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) - vals = append(vals, *input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = *input.{{ capitalize $field.Name }} } else { - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) {{- if eq $field.Default.FuncName "cuid" }} - vals = append(vals, generateCUID()) + m["{{ $field.EffectiveColName }}"] = generateCUID() {{- else if eq $field.Default.FuncName "uuid" }} - vals = append(vals, generateUUID()) + m["{{ $field.EffectiveColName }}"] = generateUUID() {{- else if eq $field.Default.FuncName "now" }} - vals = append(vals, time.Now()) + m["{{ $field.EffectiveColName }}"] = time.Now() {{- end }} } {{- else if or $field.Optional (ne $field.Default nil) }} if input.{{ capitalize $field.Name }} != nil { - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) - vals = append(vals, *input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = *input.{{ capitalize $field.Name }} } {{- else }} {{- if and $field.IsID (eq $field.GoType "string") }} - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) if input.{{ capitalize $field.Name }} != "" { - vals = append(vals, input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} } else { - vals = append(vals, generateCUID()) + m["{{ $field.EffectiveColName }}"] = generateCUID() } {{- else }} - cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}")) - vals = append(vals, input.{{ capitalize $field.Name }}) + m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} {{- end }} {{- end }} {{- end }} {{- end }} {{- end }} + return m +} - returningCols := q.select{{ .Model.Name }}Cols(selects, omits) +func (d *{{ .Model.Name }}Delegate) CreateMany(inputs []{{ .Model.Name }}CreateInput) *CreateManyBuilder[{{ .Model.Name }}, {{ .Model.Name }}CreateInput] { + return &CreateManyBuilder[{{ .Model.Name }}, {{ .Model.Name }}CreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.execute{{ .Model.Name }}CreateMany, + } +} - scanFunc := func(res *{{ .Model.Name }}, cols []string) []any { - return res.ScanFields(cols) +func (d *{{ .Model.Name }}Delegate) CreateManyAndReturn(inputs []{{ .Model.Name }}CreateInput) *CreateManyAndReturnBuilder[{{ .Model.Name }}, {{ .Model.Name }}CreateInput, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { + return &CreateManyAndReturnBuilder[{{ .Model.Name }}, {{ .Model.Name }}CreateInput, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.execute{{ .Model.Name }}CreateManyAndReturn, } +} - idCol := "{{ range $field := .Model.ScalarFields }}{{ if $field.IsID }}{{ $field.EffectiveColName }}{{ end }}{{ end }}" +func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, inputs []{{ .Model.Name }}CreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } - {{- if .Model.RelationFields }} - hasRelations := selects != nil && ({{ range $i, $rel := .Model.RelationFields }}{{ if $i }} || {{ end }}selects.{{ capitalize $rel.Name }} != nil{{ end }}) - {{- else }} - hasRelations := false - {{- end }} + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.{{ .Model.Name }}InputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "{{ .Model.EffectiveTableName }}", rowMaps, {{ .Model.Name }}ColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } - var res *{{ .Model.Name }} - var err error - if hasRelations { - err = q.transaction(ctx, func(txQ *Queries) error { - var err error - res, err = executeInsert(ctx, txQ, "{{ .Model.EffectiveTableName }}", cols, vals, returningCols, idCol, scanFunc) + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.execute{{ .Model.Name }}Create(ctx, input, nil, nil) if err != nil { return err } - return txQ.load{{ .Model.Name }}Relations(ctx, []*{{ .Model.Name }}{res}, selects) + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Context, inputs []{{ .Model.Name }}CreateInput, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) ([]*{{ .Model.Name }}, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.select{{ .Model.Name }}Cols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.{{ .Model.Name }}InputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "{{ .Model.EffectiveTableName }}", rowMaps, {{ .Model.Name }}ColOrder, returningCols) + var records []*{{ .Model.Name }} + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record {{ .Model.Name }} + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.load{{ .Model.Name }}Relations(ctx, records, selects) + } + return nil }) - } else { - res, err = executeInsert(ctx, q, "{{ .Model.EffectiveTableName }}", cols, vals, returningCols, idCol, scanFunc) + if err != nil { + return nil, err + } + return records, nil } + + // Fallback to loop inside transaction + var records []*{{ .Model.Name }} + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.execute{{ .Model.Name }}Create(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.load{{ .Model.Name }}Relations(ctx, records, selects) + } + return nil + }) if err != nil { return nil, err } - - return res, nil + return records, nil } diff --git a/generator/templates/relations_runtime.gotpl b/generator/templates/relations_runtime.gotpl index 75e20f8..933ae0e 100644 --- a/generator/templates/relations_runtime.gotpl +++ b/generator/templates/relations_runtime.gotpl @@ -63,3 +63,76 @@ func computeCols(specs []colSpec, hasSelects, anySelected bool) []string { return cols } +func mapToColsVals(m map[string]any, colOrder []string) (cols []string, vals []any) { + for _, c := range colOrder { + if v, ok := m[c]; ok { + cols = append(cols, c) + vals = append(vals, v) + } + } + return +} + +func buildBulkInsertSQL(dialect Dialect, table string, rowMaps []map[string]any, colOrder []string, returningCols []string) (string, []any) { + colsSet := make(map[string]bool) + for _, rMap := range rowMaps { + for col := range rMap { + colsSet[col] = true + } + } + var cols []string + for _, c := range colOrder { + if colsSet[c] { + cols = append(cols, c) + } + } + + var vals []any + for _, rMap := range rowMaps { + for _, col := range cols { + vals = append(vals, rMap[col]) + } + } + + var sb strings.Builder + sb.WriteString("INSERT INTO ") + sb.WriteString(dialect.Quote(table)) + sb.WriteString(" (") + for i, col := range cols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(dialect.Quote(col)) + } + sb.WriteString(") VALUES ") + + paramIndex := 1 + for i := range rowMaps { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString("(") + for j := range cols { + if j > 0 { + sb.WriteString(", ") + } + sb.WriteString(dialect.BindVar(paramIndex)) + paramIndex++ + } + sb.WriteString(")") + } + + if dialect.SupportsReturning() && len(returningCols) > 0 { + sb.WriteString(" RETURNING ") + for i, col := range returningCols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(dialect.Quote(col)) + } + } + + return sb.String(), vals +} + + diff --git a/integration/create_many_test.go b/integration/create_many_test.go new file mode 100644 index 0000000..654aad3 --- /dev/null +++ b/integration/create_many_test.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "integration/valkyrie" + "testing" +) + +func TestCreateMany(t *testing.T) { + ctx := context.Background() + client, cleanup := setupTestDB(t) + defer cleanup() + + t.Run("CreateMany returns correct count", func(t *testing.T) { + count, err := client.User.CreateMany([]valkyrie.UserCreateInput{ + { + Email: "bulk1@example.com", + PhoneNum: "+111", + }, + { + Email: "bulk2@example.com", + PhoneNum: "+222", + }, + { + Email: "bulk3@example.com", + PhoneNum: "+333", + }, + }).Exec(ctx) + + if err != nil { + t.Fatalf("CreateMany failed: %v", err) + } + + if count != 3 { + t.Errorf("expected count 3, got %d", count) + } + + var dbCount int + err = client.Raw().QueryRowContext(ctx, `SELECT count(*) FROM "User"`).Scan(&dbCount) + if err != nil { + t.Fatalf("Raw SQL query failed: %v", err) + } + if dbCount != 3 { + t.Errorf("expected 3 users in db, got %d", dbCount) + } + }) + + t.Run("CreateManyAndReturn works and supports Select", func(t *testing.T) { + author, err := client.User.Create(valkyrie.UserCreateInput{ + Email: "author@example.com", + }).Exec(ctx) + if err != nil { + t.Fatalf("failed to create author: %v", err) + } + + posts, err := client.Post.CreateManyAndReturn([]valkyrie.PostCreateInput{ + { + Title: "Post One", + AuthorId: author.Id, + }, + { + Title: "Post Two", + AuthorId: author.Id, + }, + }).Select(valkyrie.PostSelect{ + Id: true, + Title: true, + Author: &valkyrie.UserSelect{ + Email: true, + }, + }).Exec(ctx) + + if err != nil { + t.Fatalf("CreateManyAndReturn failed: %v", err) + } + + if len(posts) != 2 { + t.Fatalf("expected 2 posts, got %d", len(posts)) + } + + if posts[0].Title != "Post One" || posts[1].Title != "Post Two" { + t.Errorf("unexpected post titles: %v, %v", posts[0].Title, posts[1].Title) + } + + for _, post := range posts { + if post.Author == nil { + t.Fatalf("expected Author to be loaded, got nil") + } + if post.Author.Email != "author@example.com" { + t.Errorf("expected author email 'author@example.com', got %s", post.Author.Email) + } + } + + bytes, _ := json.MarshalIndent(posts, "", " ") + fmt.Println(string(bytes)) + }) +} diff --git a/integration/main.go b/integration/main.go index 8caa374..b8629e1 100644 --- a/integration/main.go +++ b/integration/main.go @@ -67,6 +67,11 @@ func main() { log.Fatalf("failed to create user: %v", err) } + usersCount, err := db.User.CreateMany([]valkyrie.UserCreateInput{ + {Email: "cl@gm.com"}, {Email: "cc@gg.com"}, + }).Exec(ctx) + fmt.Printf("\nCREATED %d USERS\n", usersCount) + fmt.Println("COMMENT:") printJSON(comment) } diff --git a/integration/schema.prisma b/integration/schema.prisma index 986bdcc..0eccc03 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -13,7 +13,7 @@ enum UserRole { model User { id String @id @default(cuid()) email String @unique - phoneNum String + phoneNum String @unique role UserRole @default(STUDENT) profile Profile? posts Post[] diff --git a/integration/valkyrie.json b/integration/valkyrie.json index 72edeb4..4fa9a00 100644 --- a/integration/valkyrie.json +++ b/integration/valkyrie.json @@ -9,7 +9,5 @@ "output": { "client": "./valkyrie", "migrations": "./valkyrie/migrations" - }, - - "log": ["query", "info", "error"] + } } diff --git a/integration/valkyrie/category.go b/integration/valkyrie/category.go index 12ea2b2..71987ad 100644 --- a/integration/valkyrie/category.go +++ b/integration/valkyrie/category.go @@ -87,6 +87,19 @@ func (q *Queries) selectCategoryCols(selects *CategorySelect, omits *CategoryOmi return cols } + +var CategoryColOrder = []string{ + "id", + "name", +} + +func (s *CategorySelect) hasAnyRelation() bool { + if s == nil { + return false + } + return s.Posts != nil +} + func (d *CategoryDelegate) Create(input CategoryCreateInput) *CreateBuilder[Category, CategoryCreateInput, CategorySelect, CategoryOmit] { return &CreateBuilder[Category, CategoryCreateInput, CategorySelect, CategoryOmit]{ client: d.client, @@ -96,16 +109,8 @@ func (d *CategoryDelegate) Create(input CategoryCreateInput) *CreateBuilder[Cate } func (q *Queries) executeCategoryCreate(ctx context.Context, input CategoryCreateInput, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { - var cols []string - var vals []any - if input.Id != nil { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, *input.Id) - } else { - cols = append(cols, q.dialect.Quote("id")) - } - cols = append(cols, q.dialect.Quote("name")) - vals = append(vals, input.Name) + m := q.CategoryInputToMap(input) + cols, vals := mapToColsVals(m, CategoryColOrder) returningCols := q.selectCategoryCols(selects, omits) @@ -114,7 +119,8 @@ func (q *Queries) executeCategoryCreate(ctx context.Context, input CategoryCreat } idCol := "id" - hasRelations := selects != nil && (selects.Posts != nil) + + hasRelations := selects.hasAnyRelation() var res *Category var err error @@ -136,6 +142,128 @@ func (q *Queries) executeCategoryCreate(ctx context.Context, input CategoryCreat return res, nil } + +func (q *Queries) CategoryInputToMap(input CategoryCreateInput) map[string]any { + m := make(map[string]any) + if input.Id != nil { + m["id"] = *input.Id + } else { + } + m["name"] = input.Name + return m +} + +func (d *CategoryDelegate) CreateMany(inputs []CategoryCreateInput) *CreateManyBuilder[Category, CategoryCreateInput] { + return &CreateManyBuilder[Category, CategoryCreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeCategoryCreateMany, + } +} + +func (d *CategoryDelegate) CreateManyAndReturn(inputs []CategoryCreateInput) *CreateManyAndReturnBuilder[Category, CategoryCreateInput, CategorySelect, CategoryOmit] { + return &CreateManyAndReturnBuilder[Category, CategoryCreateInput, CategorySelect, CategoryOmit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeCategoryCreateManyAndReturn, + } +} + +func (q *Queries) executeCategoryCreateMany(ctx context.Context, inputs []CategoryCreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.CategoryInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Category", rowMaps, CategoryColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.executeCategoryCreate(ctx, input, nil, nil) + if err != nil { + return err + } + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs []CategoryCreateInput, selects *CategorySelect, omits *CategoryOmit) ([]*Category, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.selectCategoryCols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.CategoryInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Category", rowMaps, CategoryColOrder, returningCols) + var records []*Category + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record Category + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.loadCategoryRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil + } + + // Fallback to loop inside transaction + var records []*Category + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.executeCategoryCreate(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.loadCategoryRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil +} func (q *Queries) loadCategoryRelations(ctx context.Context, records []*Category, selects *CategorySelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valkyrie/categoryToPost.go b/integration/valkyrie/categoryToPost.go index 77790dd..83de630 100644 --- a/integration/valkyrie/categoryToPost.go +++ b/integration/valkyrie/categoryToPost.go @@ -90,6 +90,19 @@ func (q *Queries) selectCategoryToPostCols(selects *CategoryToPostSelect, omits return cols } + +var CategoryToPostColOrder = []string{ + "postId", + "categoryId", +} + +func (s *CategoryToPostSelect) hasAnyRelation() bool { + if s == nil { + return false + } + return s.Post != nil || s.Category != nil +} + func (d *CategoryToPostDelegate) Create(input CategoryToPostCreateInput) *CreateBuilder[CategoryToPost, CategoryToPostCreateInput, CategoryToPostSelect, CategoryToPostOmit] { return &CreateBuilder[CategoryToPost, CategoryToPostCreateInput, CategoryToPostSelect, CategoryToPostOmit]{ client: d.client, @@ -99,12 +112,8 @@ func (d *CategoryToPostDelegate) Create(input CategoryToPostCreateInput) *Create } func (q *Queries) executeCategoryToPostCreate(ctx context.Context, input CategoryToPostCreateInput, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { - var cols []string - var vals []any - cols = append(cols, q.dialect.Quote("postId")) - vals = append(vals, input.PostId) - cols = append(cols, q.dialect.Quote("categoryId")) - vals = append(vals, input.CategoryId) + m := q.CategoryToPostInputToMap(input) + cols, vals := mapToColsVals(m, CategoryToPostColOrder) returningCols := q.selectCategoryToPostCols(selects, omits) @@ -113,7 +122,8 @@ func (q *Queries) executeCategoryToPostCreate(ctx context.Context, input Categor } idCol := "" - hasRelations := selects != nil && (selects.Post != nil || selects.Category != nil) + + hasRelations := selects.hasAnyRelation() var res *CategoryToPost var err error @@ -135,6 +145,125 @@ func (q *Queries) executeCategoryToPostCreate(ctx context.Context, input Categor return res, nil } + +func (q *Queries) CategoryToPostInputToMap(input CategoryToPostCreateInput) map[string]any { + m := make(map[string]any) + m["postId"] = input.PostId + m["categoryId"] = input.CategoryId + return m +} + +func (d *CategoryToPostDelegate) CreateMany(inputs []CategoryToPostCreateInput) *CreateManyBuilder[CategoryToPost, CategoryToPostCreateInput] { + return &CreateManyBuilder[CategoryToPost, CategoryToPostCreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeCategoryToPostCreateMany, + } +} + +func (d *CategoryToPostDelegate) CreateManyAndReturn(inputs []CategoryToPostCreateInput) *CreateManyAndReturnBuilder[CategoryToPost, CategoryToPostCreateInput, CategoryToPostSelect, CategoryToPostOmit] { + return &CreateManyAndReturnBuilder[CategoryToPost, CategoryToPostCreateInput, CategoryToPostSelect, CategoryToPostOmit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeCategoryToPostCreateManyAndReturn, + } +} + +func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, inputs []CategoryToPostCreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.CategoryToPostInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "CategoryToPost", rowMaps, CategoryToPostColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.executeCategoryToPostCreate(ctx, input, nil, nil) + if err != nil { + return err + } + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, inputs []CategoryToPostCreateInput, selects *CategoryToPostSelect, omits *CategoryToPostOmit) ([]*CategoryToPost, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.selectCategoryToPostCols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.CategoryToPostInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "CategoryToPost", rowMaps, CategoryToPostColOrder, returningCols) + var records []*CategoryToPost + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record CategoryToPost + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.loadCategoryToPostRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil + } + + // Fallback to loop inside transaction + var records []*CategoryToPost + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.executeCategoryToPostCreate(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.loadCategoryToPostRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil +} func (q *Queries) loadCategoryToPostRelations(ctx context.Context, records []*CategoryToPost, selects *CategoryToPostSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valkyrie/client.go b/integration/valkyrie/client.go index 20682a3..bee6c89 100644 --- a/integration/valkyrie/client.go +++ b/integration/valkyrie/client.go @@ -7,7 +7,6 @@ import ( "embed" "encoding/json" "fmt" - "log" "strconv" "strings" "time" @@ -69,12 +68,14 @@ type Dialect interface { Quote(ident string) string BindVar(idx int) string SupportsReturning() bool + SupportsBulkInsert() bool } type sqliteDialect struct{} func (sqliteDialect) Quote(ident string) string { return `"` + ident + `"` } func (sqliteDialect) BindVar(idx int) string { return "?" } func (sqliteDialect) SupportsReturning() bool { return true } +func (sqliteDialect) SupportsBulkInsert() bool { return false } type DBTX interface { ExecContext(context.Context, string, ...any) (sql.Result, error) @@ -138,7 +139,6 @@ func (db *DB) Raw() *sql.DB { // RunMigrations runs all pending migrations from the embedded folder. func (db *DB) RunMigrations(ctx context.Context) error { - log.Println("Running migrations...") if err := goose.SetDialect(db.provider); err != nil { return err } @@ -146,10 +146,8 @@ func (db *DB) RunMigrations(ctx context.Context) error { goose.SetBaseFS(migrationsFS) err := goose.UpContext(ctx, db.sqlDB, "migrations") if err != nil { - log.Printf("Migrations failed: %v", err) return err } - log.Println("Migrations completed successfully.") return nil } @@ -169,25 +167,16 @@ func (q *Queries) bindVars(count int) string { } func (q *Queries) query(ctx context.Context, query string, args ...any) (*sql.Rows, error) { - log.Printf("[%s] SQL Query: %s | Args: %v", strings.ToUpper(q.provider), query, args) res, err := q.db.QueryContext(ctx, query, args...) - if err != nil { - log.Printf("[%s] SQL Error: %v | Query: %s | Args: %v", strings.ToUpper(q.provider), err, query, args) - } return res, err } func (q *Queries) queryRow(ctx context.Context, query string, args ...any) *sql.Row { - log.Printf("[%s] SQL QueryRow: %s | Args: %v", strings.ToUpper(q.provider), query, args) return q.db.QueryRowContext(ctx, query, args...) } func (q *Queries) exec(ctx context.Context, query string, args ...any) (sql.Result, error) { - log.Printf("[%s] SQL Exec: %s | Args: %v", strings.ToUpper(q.provider), query, args) res, err := q.db.ExecContext(ctx, query, args...) - if err != nil { - log.Printf("[%s] SQL Error: %v | Query: %s | Args: %v", strings.ToUpper(q.provider), err, query, args) - } return res, err } @@ -202,7 +191,6 @@ func (q *Queries) transaction(ctx context.Context, fn func(txQ *Queries) error) if !ok { return fn(q) } - log.Printf("[%s] SQL Begin Transaction", strings.ToUpper(q.provider)) tx, err := starter.BeginTx(ctx, nil) if err != nil { return err @@ -210,7 +198,6 @@ func (q *Queries) transaction(ctx context.Context, fn func(txQ *Queries) error) defer func() { if p := recover(); p != nil { - log.Printf("[%s] SQL Rollback Transaction", strings.ToUpper(q.provider)) _ = tx.Rollback() panic(p) } @@ -230,11 +217,9 @@ func (q *Queries) transaction(ctx context.Context, fn func(txQ *Queries) error) txQueries.CategoryToPost = &CategoryToPostDelegate{client: txQueries} if err := fn(txQueries); err != nil { - log.Printf("[%s] SQL Rollback Transaction", strings.ToUpper(q.provider)) _ = tx.Rollback() return err } - log.Printf("[%s] SQL Commit Transaction", strings.ToUpper(q.provider)) return tx.Commit() } @@ -249,7 +234,6 @@ func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { if err != nil { return nil, err } - log.Printf("[%s] SQL Begin Transaction", strings.ToUpper(db.provider)) q := &Queries{ db: sqlTx, provider: db.provider, @@ -270,13 +254,11 @@ func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { // Commit commits the transaction. func (tx *Tx) Commit() error { - log.Printf("[%s] SQL Commit Transaction", strings.ToUpper(tx.provider)) return tx.tx.Commit() } // Rollback aborts the transaction. func (tx *Tx) Rollback() error { - log.Printf("[%s] SQL Rollback Transaction", strings.ToUpper(tx.provider)) return tx.tx.Rollback() } @@ -346,6 +328,53 @@ type CreateOmitBuilder[M any, I any, S any, O any] struct { func (b *CreateOmitBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { return b.builder.execFunc(ctx, b.builder.input, nil, &b.omits) } + +type CreateManyBuilder[M any, I any] struct { + client *Queries + inputs []I + execFunc func(ctx context.Context, inputs []I) (int64, error) +} + +func (b *CreateManyBuilder[M, I]) Exec(ctx context.Context) (int64, error) { + return b.execFunc(ctx, b.inputs) +} + +type CreateManyAndReturnBuilder[M any, I any, S any, O any] struct { + client *Queries + inputs []I + execFunc func(ctx context.Context, inputs []I, s *S, o *O) ([]*M, error) +} + +func (b *CreateManyAndReturnBuilder[M, I, S, O]) Select(s S) *CreateManyAndReturnSelectBuilder[M, I, S, O] { + return &CreateManyAndReturnSelectBuilder[M, I, S, O]{builder: b, selects: s} +} + +func (b *CreateManyAndReturnBuilder[M, I, S, O]) Omit(o O) *CreateManyAndReturnOmitBuilder[M, I, S, O] { + return &CreateManyAndReturnOmitBuilder[M, I, S, O]{builder: b, omits: o} +} + +func (b *CreateManyAndReturnBuilder[M, I, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.execFunc(ctx, b.inputs, nil, nil) +} + +type CreateManyAndReturnSelectBuilder[M any, I any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, I, S, O] + selects S +} + +func (b *CreateManyAndReturnSelectBuilder[M, I, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.inputs, &b.selects, nil) +} + +type CreateManyAndReturnOmitBuilder[M any, I any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, I, S, O] + omits O +} + +func (b *CreateManyAndReturnOmitBuilder[M, I, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.inputs, nil, &b.omits) +} + func executeInsert[M any]( ctx context.Context, q *Queries, @@ -366,7 +395,7 @@ func executeInsert[M any]( if i > 0 { sb.WriteString(", ") } - sb.WriteString(col) + sb.WriteString(q.dialect.Quote(col)) } sb.WriteString(") VALUES (") for i := range cols { @@ -406,9 +435,8 @@ func executeInsert[M any]( } var idVal any - quotedIdCol := q.dialect.Quote(idCol) for i, c := range cols { - if c == quotedIdCol { + if c == idCol { idVal = vals[i] break } @@ -585,3 +613,75 @@ func computeCols(specs []colSpec, hasSelects, anySelected bool) []string { } return cols } + +func mapToColsVals(m map[string]any, colOrder []string) (cols []string, vals []any) { + for _, c := range colOrder { + if v, ok := m[c]; ok { + cols = append(cols, c) + vals = append(vals, v) + } + } + return +} + +func buildBulkInsertSQL(dialect Dialect, table string, rowMaps []map[string]any, colOrder []string, returningCols []string) (string, []any) { + colsSet := make(map[string]bool) + for _, rMap := range rowMaps { + for col := range rMap { + colsSet[col] = true + } + } + var cols []string + for _, c := range colOrder { + if colsSet[c] { + cols = append(cols, c) + } + } + + var vals []any + for _, rMap := range rowMaps { + for _, col := range cols { + vals = append(vals, rMap[col]) + } + } + + var sb strings.Builder + sb.WriteString("INSERT INTO ") + sb.WriteString(dialect.Quote(table)) + sb.WriteString(" (") + for i, col := range cols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(dialect.Quote(col)) + } + sb.WriteString(") VALUES ") + + paramIndex := 1 + for i := range rowMaps { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString("(") + for j := range cols { + if j > 0 { + sb.WriteString(", ") + } + sb.WriteString(dialect.BindVar(paramIndex)) + paramIndex++ + } + sb.WriteString(")") + } + + if dialect.SupportsReturning() && len(returningCols) > 0 { + sb.WriteString(" RETURNING ") + for i, col := range returningCols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(dialect.Quote(col)) + } + } + + return sb.String(), vals +} diff --git a/integration/valkyrie/comment.go b/integration/valkyrie/comment.go index 829eafd..90eb1f2 100644 --- a/integration/valkyrie/comment.go +++ b/integration/valkyrie/comment.go @@ -130,6 +130,24 @@ func (q *Queries) selectCommentCols(selects *CommentSelect, omits *CommentOmit, return cols } + +var CommentColOrder = []string{ + "id", + "textify", + "dummy3", + "dummy1", + "dummy2", + "postId", + "authorId", +} + +func (s *CommentSelect) hasAnyRelation() bool { + if s == nil { + return false + } + return s.Post != nil || s.Author != nil +} + func (d *CommentDelegate) Create(input CommentCreateInput) *CreateBuilder[Comment, CommentCreateInput, CommentSelect, CommentOmit] { return &CreateBuilder[Comment, CommentCreateInput, CommentSelect, CommentOmit]{ client: d.client, @@ -139,27 +157,8 @@ func (d *CommentDelegate) Create(input CommentCreateInput) *CreateBuilder[Commen } func (q *Queries) executeCommentCreate(ctx context.Context, input CommentCreateInput, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { - var cols []string - var vals []any - if input.Id != nil { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, *input.Id) - } else { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, generateCUID()) - } - cols = append(cols, q.dialect.Quote("textify")) - vals = append(vals, input.Textify) - cols = append(cols, q.dialect.Quote("dummy3")) - vals = append(vals, input.Dummy3) - cols = append(cols, q.dialect.Quote("dummy1")) - vals = append(vals, input.Dummy1) - cols = append(cols, q.dialect.Quote("dummy2")) - vals = append(vals, input.Dummy2) - cols = append(cols, q.dialect.Quote("postId")) - vals = append(vals, input.PostId) - cols = append(cols, q.dialect.Quote("authorId")) - vals = append(vals, input.AuthorId) + m := q.CommentInputToMap(input) + cols, vals := mapToColsVals(m, CommentColOrder) returningCols := q.selectCommentCols(selects, omits) @@ -168,7 +167,8 @@ func (q *Queries) executeCommentCreate(ctx context.Context, input CommentCreateI } idCol := "id" - hasRelations := selects != nil && (selects.Post != nil || selects.Author != nil) + + hasRelations := selects.hasAnyRelation() var res *Comment var err error @@ -190,6 +190,134 @@ func (q *Queries) executeCommentCreate(ctx context.Context, input CommentCreateI return res, nil } + +func (q *Queries) CommentInputToMap(input CommentCreateInput) map[string]any { + m := make(map[string]any) + if input.Id != nil { + m["id"] = *input.Id + } else { + m["id"] = generateCUID() + } + m["textify"] = input.Textify + m["dummy3"] = input.Dummy3 + m["dummy1"] = input.Dummy1 + m["dummy2"] = input.Dummy2 + m["postId"] = input.PostId + m["authorId"] = input.AuthorId + return m +} + +func (d *CommentDelegate) CreateMany(inputs []CommentCreateInput) *CreateManyBuilder[Comment, CommentCreateInput] { + return &CreateManyBuilder[Comment, CommentCreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeCommentCreateMany, + } +} + +func (d *CommentDelegate) CreateManyAndReturn(inputs []CommentCreateInput) *CreateManyAndReturnBuilder[Comment, CommentCreateInput, CommentSelect, CommentOmit] { + return &CreateManyAndReturnBuilder[Comment, CommentCreateInput, CommentSelect, CommentOmit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeCommentCreateManyAndReturn, + } +} + +func (q *Queries) executeCommentCreateMany(ctx context.Context, inputs []CommentCreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.CommentInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Comment", rowMaps, CommentColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.executeCommentCreate(ctx, input, nil, nil) + if err != nil { + return err + } + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs []CommentCreateInput, selects *CommentSelect, omits *CommentOmit) ([]*Comment, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.selectCommentCols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.CommentInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Comment", rowMaps, CommentColOrder, returningCols) + var records []*Comment + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record Comment + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.loadCommentRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil + } + + // Fallback to loop inside transaction + var records []*Comment + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.executeCommentCreate(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.loadCommentRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil +} func (q *Queries) loadCommentRelations(ctx context.Context, records []*Comment, selects *CommentSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valkyrie/migrations/00001_init.sql b/integration/valkyrie/migrations/00001_init.sql index cff6dea..32ee87c 100644 --- a/integration/valkyrie/migrations/00001_init.sql +++ b/integration/valkyrie/migrations/00001_init.sql @@ -30,7 +30,10 @@ CREATE TABLE `Post` ( ); CREATE TABLE `Comment` ( `id` text NOT NULL, - `text` text NOT NULL, + `textify` integer NOT NULL, + `dummy3` text NOT NULL, + `dummy1` integer NOT NULL, + `dummy2` text NOT NULL, `postId` text NOT NULL, `authorId` text NOT NULL, PRIMARY KEY (`id`), diff --git a/integration/valkyrie/migrations/00002_dummies.sql b/integration/valkyrie/migrations/00002_dummies.sql deleted file mode 100644 index c2cea20..0000000 --- a/integration/valkyrie/migrations/00002_dummies.sql +++ /dev/null @@ -1,35 +0,0 @@ --- +goose Up -PRAGMA foreign_keys = off; -CREATE TABLE `new_Comment` ( - `id` text NOT NULL, - `textify` text NOT NULL, - `dummy3` integer NOT NULL, - `dummy1` text NOT NULL, - `dummy2` text NOT NULL, - `postId` text NOT NULL, - `authorId` text NOT NULL, - PRIMARY KEY (`id`), - CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_textify_check` CHECK ("textify" IN ('ADMIN', 'student', 'TEACHER')) -); -INSERT INTO `new_Comment` (`id`, `textify`, `postId`, `authorId`) SELECT `id`, `text`, `postId`, `authorId` FROM `Comment`; -DROP TABLE `Comment`; -ALTER TABLE `new_Comment` RENAME TO `Comment`; -PRAGMA foreign_keys = on; - --- +goose Down -PRAGMA foreign_keys = off; -CREATE TABLE `new_Comment` ( - `id` text NOT NULL, - `text` text NOT NULL, - `postId` text NOT NULL, - `authorId` text NOT NULL, - PRIMARY KEY (`id`), - CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION -); -INSERT INTO `new_Comment` (`id`, `text`, `postId`, `authorId`) SELECT `id`, `textify`, `postId`, `authorId` FROM `Comment`; -DROP TABLE `Comment`; -ALTER TABLE `new_Comment` RENAME TO `Comment`; -PRAGMA foreign_keys = on; diff --git a/integration/valkyrie/migrations/00003_change_type_from_enum_to_String.sql b/integration/valkyrie/migrations/00003_change_type_from_enum_to_String.sql deleted file mode 100644 index 43a357d..0000000 --- a/integration/valkyrie/migrations/00003_change_type_from_enum_to_String.sql +++ /dev/null @@ -1,38 +0,0 @@ --- +goose Up -PRAGMA foreign_keys = off; -CREATE TABLE `new_Comment` ( - `id` text NOT NULL, - `textify` text NOT NULL, - `dummy3` integer NOT NULL, - `dummy1` text NOT NULL, - `dummy2` text NOT NULL, - `postId` text NOT NULL, - `authorId` text NOT NULL, - PRIMARY KEY (`id`), - CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION -); -INSERT INTO `new_Comment` (`id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId`) SELECT `id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId` FROM `Comment`; -DROP TABLE `Comment`; -ALTER TABLE `new_Comment` RENAME TO `Comment`; -PRAGMA foreign_keys = on; - --- +goose Down -PRAGMA foreign_keys = off; -CREATE TABLE `new_Comment` ( - `id` text NOT NULL, - `textify` text NOT NULL, - `dummy3` integer NOT NULL, - `dummy1` text NOT NULL, - `dummy2` text NOT NULL, - `postId` text NOT NULL, - `authorId` text NOT NULL, - PRIMARY KEY (`id`), - CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_textify_check` CHECK ("textify" IN ('ADMIN', 'student', 'TEACHER')) -); -INSERT INTO `new_Comment` (`id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId`) SELECT `id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId` FROM `Comment`; -DROP TABLE `Comment`; -ALTER TABLE `new_Comment` RENAME TO `Comment`; -PRAGMA foreign_keys = on; diff --git a/integration/valkyrie/migrations/00004_changing_3_types_in_one_go.sql b/integration/valkyrie/migrations/00004_changing_3_types_in_one_go.sql deleted file mode 100644 index e1e71f1..0000000 --- a/integration/valkyrie/migrations/00004_changing_3_types_in_one_go.sql +++ /dev/null @@ -1,37 +0,0 @@ --- +goose Up -PRAGMA foreign_keys = off; -CREATE TABLE `new_Comment` ( - `id` text NOT NULL, - `textify` integer NOT NULL, - `dummy3` text NOT NULL, - `dummy1` integer NOT NULL, - `dummy2` text NOT NULL, - `postId` text NOT NULL, - `authorId` text NOT NULL, - PRIMARY KEY (`id`), - CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION -); -INSERT INTO `new_Comment` (`id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId`) SELECT `id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId` FROM `Comment`; -DROP TABLE `Comment`; -ALTER TABLE `new_Comment` RENAME TO `Comment`; -PRAGMA foreign_keys = on; - --- +goose Down -PRAGMA foreign_keys = off; -CREATE TABLE `new_Comment` ( - `id` text NOT NULL, - `textify` text NOT NULL, - `dummy3` integer NOT NULL, - `dummy1` text NOT NULL, - `dummy2` text NOT NULL, - `postId` text NOT NULL, - `authorId` text NOT NULL, - PRIMARY KEY (`id`), - CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION -); -INSERT INTO `new_Comment` (`id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId`) SELECT `id`, `textify`, `dummy3`, `dummy1`, `dummy2`, `postId`, `authorId` FROM `Comment`; -DROP TABLE `Comment`; -ALTER TABLE `new_Comment` RENAME TO `Comment`; -PRAGMA foreign_keys = on; diff --git a/integration/valkyrie/migrations/00005_init.sql b/integration/valkyrie/migrations/00005_init.sql deleted file mode 100644 index f5ecf84..0000000 --- a/integration/valkyrie/migrations/00005_init.sql +++ /dev/null @@ -1,39 +0,0 @@ --- +goose Up -PRAGMA foreign_keys = off; -CREATE TABLE `new_User` ( - `id` text NOT NULL, - `email` text NOT NULL, - `phoneNum` text NOT NULL, - `role` text NOT NULL DEFAULT ('student'), - `referredById` text NULL, - `sponsorId` text NULL, - PRIMARY KEY (`id`), - CONSTRAINT `User_referredById_fkey` FOREIGN KEY (`referredById`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `User_sponsorId_fkey` FOREIGN KEY (`sponsorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `User_role_check` CHECK ("role" IN ('ADMIN', 'student', 'TEACHER')) -); -INSERT INTO `new_User` (`id`, `email`, `phoneNum`, `role`, `referredById`) SELECT `id`, `email`, `phoneNum`, `role`, `referredById` FROM `User`; -DROP TABLE `User`; -ALTER TABLE `new_User` RENAME TO `User`; -CREATE UNIQUE INDEX `User_email_key` ON `User` (`email`); -CREATE UNIQUE INDEX `User_email_phoneNum_key` ON `User` (`email`, `phoneNum`); -PRAGMA foreign_keys = on; - --- +goose Down -PRAGMA foreign_keys = off; -CREATE TABLE `new_User` ( - `id` text NOT NULL, - `email` text NOT NULL, - `phoneNum` text NOT NULL, - `role` text NOT NULL DEFAULT 'student', - `referredById` text NULL, - PRIMARY KEY (`id`), - CONSTRAINT `User_referredById_fkey` FOREIGN KEY (`referredById`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `User_role_check` CHECK ("role" IN ('ADMIN', 'student', 'TEACHER')) -); -INSERT INTO `new_User` (`id`, `email`, `phoneNum`, `role`, `referredById`) SELECT `id`, `email`, `phoneNum`, `role`, `referredById` FROM `User`; -DROP TABLE `User`; -ALTER TABLE `new_User` RENAME TO `User`; -CREATE UNIQUE INDEX `User_email_key` ON `User` (`email`); -CREATE UNIQUE INDEX `User_email_phoneNum_key` ON `User` (`email`, `phoneNum`); -PRAGMA foreign_keys = on; diff --git a/integration/valkyrie/post.go b/integration/valkyrie/post.go index 5b49f8a..ab2cf4b 100644 --- a/integration/valkyrie/post.go +++ b/integration/valkyrie/post.go @@ -117,6 +117,22 @@ func (q *Queries) selectPostCols(selects *PostSelect, omits *PostOmit, forceCols return cols } + +var PostColOrder = []string{ + "id", + "title", + "content", + "published", + "authorId", +} + +func (s *PostSelect) hasAnyRelation() bool { + if s == nil { + return false + } + return s.Author != nil || s.Comments != nil || s.Categories != nil +} + func (d *PostDelegate) Create(input PostCreateInput) *CreateBuilder[Post, PostCreateInput, PostSelect, PostOmit] { return &CreateBuilder[Post, PostCreateInput, PostSelect, PostOmit]{ client: d.client, @@ -126,27 +142,8 @@ func (d *PostDelegate) Create(input PostCreateInput) *CreateBuilder[Post, PostCr } func (q *Queries) executePostCreate(ctx context.Context, input PostCreateInput, selects *PostSelect, omits *PostOmit) (*Post, error) { - var cols []string - var vals []any - if input.Id != nil { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, *input.Id) - } else { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, generateCUID()) - } - cols = append(cols, q.dialect.Quote("title")) - vals = append(vals, input.Title) - if input.Content != nil { - cols = append(cols, q.dialect.Quote("content")) - vals = append(vals, *input.Content) - } - if input.Published != nil { - cols = append(cols, q.dialect.Quote("published")) - vals = append(vals, *input.Published) - } - cols = append(cols, q.dialect.Quote("authorId")) - vals = append(vals, input.AuthorId) + m := q.PostInputToMap(input) + cols, vals := mapToColsVals(m, PostColOrder) returningCols := q.selectPostCols(selects, omits) @@ -155,7 +152,8 @@ func (q *Queries) executePostCreate(ctx context.Context, input PostCreateInput, } idCol := "id" - hasRelations := selects != nil && (selects.Author != nil || selects.Comments != nil || selects.Categories != nil) + + hasRelations := selects.hasAnyRelation() var res *Post var err error @@ -177,6 +175,136 @@ func (q *Queries) executePostCreate(ctx context.Context, input PostCreateInput, return res, nil } + +func (q *Queries) PostInputToMap(input PostCreateInput) map[string]any { + m := make(map[string]any) + if input.Id != nil { + m["id"] = *input.Id + } else { + m["id"] = generateCUID() + } + m["title"] = input.Title + if input.Content != nil { + m["content"] = *input.Content + } + if input.Published != nil { + m["published"] = *input.Published + } + m["authorId"] = input.AuthorId + return m +} + +func (d *PostDelegate) CreateMany(inputs []PostCreateInput) *CreateManyBuilder[Post, PostCreateInput] { + return &CreateManyBuilder[Post, PostCreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executePostCreateMany, + } +} + +func (d *PostDelegate) CreateManyAndReturn(inputs []PostCreateInput) *CreateManyAndReturnBuilder[Post, PostCreateInput, PostSelect, PostOmit] { + return &CreateManyAndReturnBuilder[Post, PostCreateInput, PostSelect, PostOmit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executePostCreateManyAndReturn, + } +} + +func (q *Queries) executePostCreateMany(ctx context.Context, inputs []PostCreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.PostInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Post", rowMaps, PostColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.executePostCreate(ctx, input, nil, nil) + if err != nil { + return err + } + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []PostCreateInput, selects *PostSelect, omits *PostOmit) ([]*Post, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.selectPostCols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.PostInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Post", rowMaps, PostColOrder, returningCols) + var records []*Post + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record Post + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.loadPostRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil + } + + // Fallback to loop inside transaction + var records []*Post + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.executePostCreate(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.loadPostRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil +} func (q *Queries) loadPostRelations(ctx context.Context, records []*Post, selects *PostSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valkyrie/profile.go b/integration/valkyrie/profile.go index 201d0d9..261569b 100644 --- a/integration/valkyrie/profile.go +++ b/integration/valkyrie/profile.go @@ -95,6 +95,20 @@ func (q *Queries) selectProfileCols(selects *ProfileSelect, omits *ProfileOmit, return cols } + +var ProfileColOrder = []string{ + "id", + "bio", + "userId", +} + +func (s *ProfileSelect) hasAnyRelation() bool { + if s == nil { + return false + } + return s.User != nil +} + func (d *ProfileDelegate) Create(input ProfileCreateInput) *CreateBuilder[Profile, ProfileCreateInput, ProfileSelect, ProfileOmit] { return &CreateBuilder[Profile, ProfileCreateInput, ProfileSelect, ProfileOmit]{ client: d.client, @@ -104,21 +118,8 @@ func (d *ProfileDelegate) Create(input ProfileCreateInput) *CreateBuilder[Profil } func (q *Queries) executeProfileCreate(ctx context.Context, input ProfileCreateInput, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { - var cols []string - var vals []any - if input.Id != nil { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, *input.Id) - } else { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, generateCUID()) - } - if input.Bio != nil { - cols = append(cols, q.dialect.Quote("bio")) - vals = append(vals, *input.Bio) - } - cols = append(cols, q.dialect.Quote("userId")) - vals = append(vals, input.UserId) + m := q.ProfileInputToMap(input) + cols, vals := mapToColsVals(m, ProfileColOrder) returningCols := q.selectProfileCols(selects, omits) @@ -127,7 +128,8 @@ func (q *Queries) executeProfileCreate(ctx context.Context, input ProfileCreateI } idCol := "id" - hasRelations := selects != nil && (selects.User != nil) + + hasRelations := selects.hasAnyRelation() var res *Profile var err error @@ -149,6 +151,132 @@ func (q *Queries) executeProfileCreate(ctx context.Context, input ProfileCreateI return res, nil } + +func (q *Queries) ProfileInputToMap(input ProfileCreateInput) map[string]any { + m := make(map[string]any) + if input.Id != nil { + m["id"] = *input.Id + } else { + m["id"] = generateCUID() + } + if input.Bio != nil { + m["bio"] = *input.Bio + } + m["userId"] = input.UserId + return m +} + +func (d *ProfileDelegate) CreateMany(inputs []ProfileCreateInput) *CreateManyBuilder[Profile, ProfileCreateInput] { + return &CreateManyBuilder[Profile, ProfileCreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeProfileCreateMany, + } +} + +func (d *ProfileDelegate) CreateManyAndReturn(inputs []ProfileCreateInput) *CreateManyAndReturnBuilder[Profile, ProfileCreateInput, ProfileSelect, ProfileOmit] { + return &CreateManyAndReturnBuilder[Profile, ProfileCreateInput, ProfileSelect, ProfileOmit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeProfileCreateManyAndReturn, + } +} + +func (q *Queries) executeProfileCreateMany(ctx context.Context, inputs []ProfileCreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.ProfileInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Profile", rowMaps, ProfileColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.executeProfileCreate(ctx, input, nil, nil) + if err != nil { + return err + } + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs []ProfileCreateInput, selects *ProfileSelect, omits *ProfileOmit) ([]*Profile, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.selectProfileCols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.ProfileInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "Profile", rowMaps, ProfileColOrder, returningCols) + var records []*Profile + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record Profile + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.loadProfileRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil + } + + // Fallback to loop inside transaction + var records []*Profile + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.executeProfileCreate(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.loadProfileRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil +} func (q *Queries) loadProfileRelations(ctx context.Context, records []*Profile, selects *ProfileSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valkyrie/user.go b/integration/valkyrie/user.go index 0671723..58941c4 100644 --- a/integration/valkyrie/user.go +++ b/integration/valkyrie/user.go @@ -123,6 +123,22 @@ func (q *Queries) selectUserCols(selects *UserSelect, omits *UserOmit, forceCols return cols } + +var UserColOrder = []string{ + "id", + "email", + "phoneNum", + "role", + "referredById", +} + +func (s *UserSelect) hasAnyRelation() bool { + if s == nil { + return false + } + return s.Profile != nil || s.Posts != nil || s.Comments != nil || s.ReferredBy != nil || s.Referrals != nil +} + func (d *UserDelegate) Create(input UserCreateInput) *CreateBuilder[User, UserCreateInput, UserSelect, UserOmit] { return &CreateBuilder[User, UserCreateInput, UserSelect, UserOmit]{ client: d.client, @@ -132,27 +148,8 @@ func (d *UserDelegate) Create(input UserCreateInput) *CreateBuilder[User, UserCr } func (q *Queries) executeUserCreate(ctx context.Context, input UserCreateInput, selects *UserSelect, omits *UserOmit) (*User, error) { - var cols []string - var vals []any - if input.Id != nil { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, *input.Id) - } else { - cols = append(cols, q.dialect.Quote("id")) - vals = append(vals, generateCUID()) - } - cols = append(cols, q.dialect.Quote("email")) - vals = append(vals, input.Email) - cols = append(cols, q.dialect.Quote("phoneNum")) - vals = append(vals, input.PhoneNum) - if input.Role != nil { - cols = append(cols, q.dialect.Quote("role")) - vals = append(vals, *input.Role) - } - if input.ReferredById != nil { - cols = append(cols, q.dialect.Quote("referredById")) - vals = append(vals, *input.ReferredById) - } + m := q.UserInputToMap(input) + cols, vals := mapToColsVals(m, UserColOrder) returningCols := q.selectUserCols(selects, omits) @@ -161,7 +158,8 @@ func (q *Queries) executeUserCreate(ctx context.Context, input UserCreateInput, } idCol := "id" - hasRelations := selects != nil && (selects.Profile != nil || selects.Posts != nil || selects.Comments != nil || selects.ReferredBy != nil || selects.Referrals != nil) + + hasRelations := selects.hasAnyRelation() var res *User var err error @@ -183,6 +181,136 @@ func (q *Queries) executeUserCreate(ctx context.Context, input UserCreateInput, return res, nil } + +func (q *Queries) UserInputToMap(input UserCreateInput) map[string]any { + m := make(map[string]any) + if input.Id != nil { + m["id"] = *input.Id + } else { + m["id"] = generateCUID() + } + m["email"] = input.Email + m["phoneNum"] = input.PhoneNum + if input.Role != nil { + m["role"] = *input.Role + } + if input.ReferredById != nil { + m["referredById"] = *input.ReferredById + } + return m +} + +func (d *UserDelegate) CreateMany(inputs []UserCreateInput) *CreateManyBuilder[User, UserCreateInput] { + return &CreateManyBuilder[User, UserCreateInput]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeUserCreateMany, + } +} + +func (d *UserDelegate) CreateManyAndReturn(inputs []UserCreateInput) *CreateManyAndReturnBuilder[User, UserCreateInput, UserSelect, UserOmit] { + return &CreateManyAndReturnBuilder[User, UserCreateInput, UserSelect, UserOmit]{ + client: d.client, + inputs: inputs, + execFunc: d.client.executeUserCreateManyAndReturn, + } +} + +func (q *Queries) executeUserCreateMany(ctx context.Context, inputs []UserCreateInput) (int64, error) { + if len(inputs) == 0 { + return 0, nil + } + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.UserInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "User", rowMaps, UserColOrder, nil) + res, err := q.exec(ctx, query, vals...) + if err != nil { + return 0, err + } + return res.RowsAffected() + } + + var count int64 + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + _, err := txQ.executeUserCreate(ctx, input, nil, nil) + if err != nil { + return err + } + count++ + } + return nil + }) + return count, err +} + +func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []UserCreateInput, selects *UserSelect, omits *UserOmit) ([]*User, error) { + if len(inputs) == 0 { + return nil, nil + } + + hasRelations := selects.hasAnyRelation() + returningCols := q.selectUserCols(selects, omits) + + if q.dialect.SupportsBulkInsert() { + rowMaps := make([]map[string]any, len(inputs)) + for i, input := range inputs { + rowMaps[i] = q.UserInputToMap(input) + } + query, vals := buildBulkInsertSQL(q.dialect, "User", rowMaps, UserColOrder, returningCols) + var records []*User + err := q.transaction(ctx, func(txQ *Queries) error { + rows, err := txQ.query(ctx, query, vals...) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var record User + if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { + return err + } + records = append(records, &record) + } + if err := rows.Err(); err != nil { + return err + } + if hasRelations { + return txQ.loadUserRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil + } + + // Fallback to loop inside transaction + var records []*User + err := q.transaction(ctx, func(txQ *Queries) error { + for _, input := range inputs { + res, err := txQ.executeUserCreate(ctx, input, nil, nil) + if err != nil { + return err + } + records = append(records, res) + } + + if hasRelations { + return txQ.loadUserRelations(ctx, records, selects) + } + return nil + }) + if err != nil { + return nil, err + } + return records, nil +} func (q *Queries) loadUserRelations(ctx context.Context, records []*User, selects *UserSelect) error { if selects == nil || len(records) == 0 { return nil