From abcdaed1effcecda42e5d6d1efe3d10bb611e4b6 Mon Sep 17 00:00:00 2001 From: Clancy Date: Thu, 9 Jul 2026 20:41:47 +0300 Subject: [PATCH 1/4] Feat: Allow Pre-save hooks on the create operations 1- Add beforeCreate hook to CreateMany and CreateManyAndReturn wrappers to allow mutation on the struct before it's serialized to SQL rows 2- Add ToRowMap() on {Model}Create as the single exit point from the struct representation, with per field nil checks, default injection, and CUID/UUID generation (if present) 3- establish a flow of create api takes the FieldAssignment spread and generates a struct via assignmentsTo{Model}Create, and the pre-save hook mutates that struct if preset, then, the base struct is converted to to a map via ToRowMap() that's used to generate the SQL rows in create operations 4- Remove RecordsToRowMaps generator function because ToRowMap() now owns the struct to map conversion with proper defaults, eliminating the duplicated default/CUID logic between RecordsToRowMaps and the single-create path 5- Remove Assignments() on {Model}Create because ToRowMap() replaces it as the single back-conversion path structs no longer round-trip through Field assignment 6- Remove validateFn, rowMapFn, and singleCreateFn params from executeCreateMany/AndReturn since validation is handled at FieldAssignment level in the wrapper and map conversion is inline via ToRowMap(), the fallback per-row path uses mapToColsVals + executeInsert instead of re-entering singleCreateFn (which would double-fire hooks, and it did) --- generator/generator_test.go | 63 ----------- generator/templates/builders_create.gotpl | 39 ++----- generator/templates/model_create.gotpl | 131 +++++++++++++++------- integration/create_many_test.go | 96 ++++++++++++++++ integration/main.go | 20 +++- integration/valk/category.go | 66 ++++++----- integration/valk/categoryToPost.go | 62 +++++----- integration/valk/client.go | 41 +++---- integration/valk/comment.go | 77 ++++++++----- integration/valk/post.go | 76 ++++++++----- integration/valk/profile.go | 72 +++++++----- integration/valk/user.go | 84 +++++++++----- 12 files changed, 496 insertions(+), 331 deletions(-) delete mode 100644 generator/generator_test.go diff --git a/generator/generator_test.go b/generator/generator_test.go deleted file mode 100644 index 10c4f90..0000000 --- a/generator/generator_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package generator - -import ( - "strings" - "testing" - "github.com/voidclancy/valk/schema" -) - -func TestGenerateClient_NativeDBConstraints(t *testing.T) { - sch := schema.Schema{ - Models: []*schema.Model{ - { - Name: "Item", - TableName: "items", - ScalarFields: []*schema.ScalarField{ - { - Name: "id", - Type: "String", - GoType: "string", - IsID: true, - }, - { - Name: "code", - Type: "String", - GoType: "string", - NativeType: &schema.NativeType{ - Name: "VarChar", - Args: []string{"8"}, - }, - }, - { - Name: "count", - Type: "Int", - GoType: "int32", - NativeType: &schema.NativeType{ - Name: "SmallInt", - }, - }, - }, - }, - }, - } - - outputs, err := GenerateClient(sch, "valk", "github.com/voidclancy/valk", "", "", nil) - if err != nil { - t.Fatalf("failed to generate client: %v", err) - } - - itemCode, ok := outputs["item.go"] - if !ok { - t.Fatal("expected item.go in outputs") - } - - // Verify length checks are generated in validate function - if !strings.Contains(itemCode, `utf8.RuneCountInString(v) > 8`) { - t.Errorf("expected generated code to contain VarChar limit check, got:\n%s", itemCode) - } - - // Verify SmallInt range checks are generated in validate function - if !strings.Contains(itemCode, "v < -32768 || v > 32767") { - t.Errorf("expected generated code to contain SmallInt limit check, got:\n%s", itemCode) - } -} diff --git a/generator/templates/builders_create.gotpl b/generator/templates/builders_create.gotpl index 0cb1874..e3e1e88 100644 --- a/generator/templates/builders_create.gotpl +++ b/generator/templates/builders_create.gotpl @@ -178,27 +178,18 @@ func executeInsert[M any]( } -func executeCreateMany[M any]( +func executeCreateMany( ctx context.Context, q *Queries, - records []RecordInput, + rowMaps []map[string]any, tableName string, colOrder []string, - validateFn func([]FieldAssignment) error, - rowMapFn func([]RecordInput) []map[string]any, - singleCreateFn func(context.Context, []FieldAssignment) (*M, error), ) (int64, error) { - if len(records) == 0 { + if len(rowMaps) == 0 { return 0, nil } - for i, rec := range records { - if err := validateFn(rec.Assignments); err != nil { - return 0, fmt.Errorf("validation failed at index %d: %w", i, err) - } - } if q.dialect.SupportsBulkInsert() { - rowMaps := rowMapFn(records) query, vals := buildBulkInsertSQL(q.dialect, tableName, rowMaps, colOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -209,8 +200,9 @@ func executeCreateMany[M any]( var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, rec := range records { - _, err := singleCreateFn(ctx, rec.Assignments) + for _, rowMap := range rowMaps { + query, vals := buildBulkInsertSQL(txQ.dialect, tableName, []map[string]any{rowMap}, colOrder, nil) + _, err := txQ.exec(ctx, query, vals...) if err != nil { return err } @@ -224,33 +216,25 @@ func executeCreateMany[M any]( func executeCreateManyAndReturn[M any, S any, O any]( ctx context.Context, q *Queries, - records []RecordInput, + rowMaps []map[string]any, tableName string, colOrder []string, selects *S, omits *O, - validateFn func([]FieldAssignment) error, - rowMapFn func([]RecordInput) []map[string]any, selectColsFn func(*S, *O, ...string) []string, loadRelationsFn func(context.Context, []*M, *S) error, scanFunc func(*M, []string) []any, - singleCreateFn func(context.Context, []FieldAssignment) (*M, error), hasRelationsFn func(*S) bool, + idCol string, ) ([]*M, error) { - if len(records) == 0 { + if len(rowMaps) == 0 { return nil, nil } - for i, rec := range records { - if err := validateFn(rec.Assignments); err != nil { - return nil, fmt.Errorf("validation failed at index %d: %w", i, err) - } - } hasRelations := selects != nil && hasRelationsFn(selects) returningCols := selectColsFn(selects, omits) if q.dialect.SupportsBulkInsert() { - rowMaps := rowMapFn(records) query, vals := buildBulkInsertSQL(q.dialect, tableName, rowMaps, colOrder, returningCols) recordsOut := make([]*M, 0) err := q.transaction(ctx, func(txQ *Queries) error { @@ -282,8 +266,9 @@ func executeCreateManyAndReturn[M any, S any, O any]( recordsOut := make([]*M, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, rec := range records { - res, err := singleCreateFn(ctx, rec.Assignments) + for _, rowMap := range rowMaps { + cols, vals := mapToColsVals(rowMap, colOrder) + res, err := executeInsert(ctx, txQ, tableName, cols, vals, returningCols, idCol, scanFunc) if err != nil { return err } diff --git a/generator/templates/model_create.gotpl b/generator/templates/model_create.gotpl index 4535bc2..3267de2 100644 --- a/generator/templates/model_create.gotpl +++ b/generator/templates/model_create.gotpl @@ -148,6 +148,64 @@ func assignmentsTo{{ .Model.Name }}Create(assignments []FieldAssignment) {{ .Mod return input } +func (s *{{ .Model.Name }}Create) ToRowMap() map[string]any { + m := make(map[string]any, {{ len .Model.ScalarFields }}) + {{- range $field := .Model.ScalarFields }} + {{- $col := $field.EffectiveColName }} + {{- $fieldName := capitalize $field.Name }} + {{- if $field.EnumRef }} + {{- if $field.IsArray }} + if s.{{ $fieldName }} != nil { + m["{{ $col }}"] = s.{{ $fieldName }} + } + {{- else }} + if s.{{ $fieldName }} != nil { + m["{{ $col }}"] = *s.{{ $fieldName }} + } + {{- end }} + {{- else if $field.IsArray }} + if s.{{ $fieldName }} != nil { + m["{{ $col }}"] = s.{{ $fieldName }} + } + {{- else }} + {{- if and $field.Default (eq $field.Default.Kind.String "Func") }} + {{- if eq $field.Default.FuncName "autoincrement" }} + if s.{{ $fieldName }} != nil { + m["{{ $col }}"] = *s.{{ $fieldName }} + } + {{- else }} + if s.{{ $fieldName }} != nil { + m["{{ $col }}"] = *s.{{ $fieldName }} + } else { + {{- if eq $field.Default.FuncName "cuid" }} + m["{{ $col }}"] = generateCUID() + {{- else if eq $field.Default.FuncName "uuid" }} + m["{{ $col }}"] = generateUUID() + {{- else if eq $field.Default.FuncName "now" }} + m["{{ $col }}"] = time.Now() + {{- end }} + } + {{- end }} + {{- else if or $field.Optional (ne $field.Default nil) }} + if s.{{ $fieldName }} != nil { + m["{{ $col }}"] = *s.{{ $fieldName }} + } + {{- else }} + {{- if and $field.IsID (eq $field.GoType "string") }} + if s.{{ $fieldName }} != "" { + m["{{ $col }}"] = s.{{ $fieldName }} + } else { + m["{{ $col }}"] = generateCUID() + } + {{- else }} + m["{{ $col }}"] = s.{{ $fieldName }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + return m +} + func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, assignments []FieldAssignment, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) { input := assignmentsTo{{ .Model.Name }}Create(assignments) @@ -266,36 +324,6 @@ func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, assignment return res, nil } -func {{ lowercase .Model.Name }}RecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - {{- range $field := .Model.ScalarFields }} - {{- $col := $field.EffectiveColName }} - {{- if and $field.Default (eq $field.Default.Kind.String "Func") }} - if _, ok := m["{{ $col }}"]; !ok { - {{- if eq $field.Default.FuncName "cuid" }} - m["{{ $col }}"] = generateCUID() - {{- else if eq $field.Default.FuncName "uuid" }} - m["{{ $col }}"] = generateUUID() - {{- else if eq $field.Default.FuncName "now" }} - m["{{ $col }}"] = time.Now() - {{- end }} - } - {{- else if and $field.IsID (eq $field.GoType "string") }} - if _, ok := m["{{ $col }}"]; !ok { - m["{{ $col }}"] = generateCUID() - } - {{- end }} - {{- end }} - rowMaps[i] = m - } - return rowMaps -} - func (d *{{ .Model.Name }}Delegate) CreateMany(records ...RecordInput) *CreateManyBuilder[{{ .Model.Name }}] { return &CreateManyBuilder[{{ .Model.Name }}]{ client: d.client, @@ -313,25 +341,42 @@ func (d *{{ .Model.Name }}Delegate) CreateManyAndReturn(records ...RecordInput) } func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "{{ .Model.EffectiveTableName }}", {{ .Model.Name }}ColOrder, - validate{{ .Model.Name }}Create, - {{ lowercase .Model.Name }}RecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*{{ .Model.Name }}, error) { - return q.execute{{ .Model.Name }}Create(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validate{{ .Model.Name }}Create(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsTo{{ .Model.Name }}Create(rec.Assignments) + if q.{{ .Model.Name }}.beforeCreate != nil { + if err := q.{{ .Model.Name }}.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "{{ .Model.EffectiveTableName }}", {{ .Model.Name }}ColOrder) } func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Context, records []RecordInput, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) ([]*{{ .Model.Name }}, error) { - return executeCreateManyAndReturn(ctx, q, records, "{{ .Model.EffectiveTableName }}", {{ .Model.Name }}ColOrder, selects, omits, - validate{{ .Model.Name }}Create, - {{ lowercase .Model.Name }}RecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "{{ range $field := .Model.ScalarFields }}{{ if $field.IsID }}{{ $field.EffectiveColName }}{{ end }}{{ end }}" + for i, rec := range records { + if err := validate{{ .Model.Name }}Create(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsTo{{ .Model.Name }}Create(rec.Assignments) + if q.{{ .Model.Name }}.beforeCreate != nil { + if err := q.{{ .Model.Name }}.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "{{ .Model.EffectiveTableName }}", {{ .Model.Name }}ColOrder, selects, omits, q.select{{ .Model.Name }}Cols, q.load{{ .Model.Name }}Relations, (*{{ .Model.Name }}).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*{{ .Model.Name }}, error) { - return q.execute{{ .Model.Name }}Create(ctx, assignments, nil, nil) - }, (*{{ .Model.Name }}Select).hasAnyRelation, + idCol, ) } diff --git a/integration/create_many_test.go b/integration/create_many_test.go index cca50dd..7a84e51 100644 --- a/integration/create_many_test.go +++ b/integration/create_many_test.go @@ -10,6 +10,102 @@ import ( "testing" ) +func TestCreateMany_Hooks(t *testing.T) { + ctx := context.Background() + + t.Run("BeforeCreate hook mutates input during CreateMany", func(t *testing.T) { + client, cleanup := setupTestDB(t) + defer cleanup() + + client.User.BeforeCreate(func(ctx context.Context, input *valk.UserCreate) error { + if input.Email == "hooked@example.com" { + input.PhoneNum = "+188888888" + } + return nil + }) + + count, err := client.User.CreateMany( + user.Record(user.Email.Set("hooked@example.com"), user.PhoneNum.Set("+100000000")), + user.Record(user.Email.Set("normal@example.com"), user.PhoneNum.Set("+200000000")), + ).Exec(ctx) + + if err != nil { + t.Fatalf("CreateMany failed: %v", err) + } + if count != 2 { + t.Fatalf("expected count 2, got %d", count) + } + + var phone string + err = client.Raw().QueryRowContext(ctx, + `SELECT phoneNum FROM "User" WHERE email = 'hooked@example.com'`, + ).Scan(&phone) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if phone != "+188888888" { + t.Errorf("expected PhoneNum to be mutated to '+188888888', got %q", phone) + } + }) + + t.Run("BeforeCreate hook mutates input during CreateManyAndReturn", func(t *testing.T) { + client, cleanup := setupTestDB(t) + defer cleanup() + + client.User.BeforeCreate(func(ctx context.Context, input *valk.UserCreate) error { + if input.Email == "hooked@example.com" { + input.PhoneNum = "+188888888" + } + return nil + }) + + users, err := client.User.CreateManyAndReturn( + user.Record(user.Email.Set("hooked@example.com"), user.PhoneNum.Set("+100000001")), + user.Record(user.Email.Set("normal@example.com"), user.PhoneNum.Set("+200000001")), + ).Exec(ctx) + + if err != nil { + t.Fatalf("CreateManyAndReturn failed: %v", err) + } + if len(users) != 2 { + t.Fatalf("expected 2 users, got %d", len(users)) + } + + for _, u := range users { + if u.Email == "hooked@example.com" && u.PhoneNum != "+188888888" { + t.Errorf("expected hooked user PhoneNum to be '+188888888', got %q", u.PhoneNum) + } + } + }) + + t.Run("BeforeCreate hook error aborts CreateMany", func(t *testing.T) { + client, cleanup := setupTestDB(t) + defer cleanup() + + client.User.BeforeCreate(func(ctx context.Context, input *valk.UserCreate) error { + if input.Email == "reject@example.com" { + return fmt.Errorf("hook rejected: %s", input.Email) + } + return nil + }) + + _, err := client.User.CreateMany( + user.Record(user.Email.Set("good@example.com"), user.PhoneNum.Set("+300000000")), + user.Record(user.Email.Set("reject@example.com"), user.PhoneNum.Set("+300000001")), + ).Exec(ctx) + + if err == nil { + t.Fatal("expected error from hook rejection, got nil") + } + + var count int + client.Raw().QueryRowContext(ctx, `SELECT count(*) FROM "User"`).Scan(&count) + if count != 0 { + t.Fatalf("expected 0 rows after aborted CreateMany, got %d", count) + } + }) +} + func TestCreateMany(t *testing.T) { ctx := context.Background() client, cleanup := setupTestDB(t) diff --git a/integration/main.go b/integration/main.go index 7d615c1..5f49f72 100644 --- a/integration/main.go +++ b/integration/main.go @@ -27,10 +27,15 @@ type SeedData struct { func seed(db *valk.DB, ctx context.Context) *SeedData { db.User.BeforeCreate(func(ctx context.Context, user *valk.UserCreate) error { - if user.Email == "referrer@example.com" { - user.Role = new(valk.UserRole.Admin) - fmt.Println(user.Role) - } + + user.Role = new(valk.UserRole.Admin) + + return nil + }) + + db.User.AfterCreate(func(ctx context.Context, user *valk.User) error { + fmt.Println("CREATED USER WITH ID: ", user.Id) + fmt.Println("USER ROLE: ", user.Role) return nil }) @@ -44,11 +49,16 @@ func seed(db *valk.DB, ctx context.Context) *SeedData { )) } - _, err := db.User.CreateMany(usersToCreate...).Exec(ctx) + users, err := db.User.CreateManyAndReturn(usersToCreate...).Exec(ctx) if err != nil { log.Fatalf("failed to create users: %v", err) } + for _, u := range users { + + fmt.Println("USER ROLE: ", u.Role) + } + _, err = db.User.CreateMany( user.Record( user.Email.Set("test"), diff --git a/integration/valk/category.go b/integration/valk/category.go index 44d5d5e..d642cfb 100644 --- a/integration/valk/category.go +++ b/integration/valk/category.go @@ -159,6 +159,15 @@ func assignmentsToCategoryCreate(assignments []FieldAssignment) CategoryCreate { return input } +func (s *CategoryCreate) ToRowMap() map[string]any { + m := make(map[string]any, 2) + if s.Id != nil { + m["id"] = *s.Id + } + m["name"] = s.Name + return m +} + func (q *Queries) executeCategoryCreate(ctx context.Context, assignments []FieldAssignment, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { input := assignmentsToCategoryCreate(assignments) @@ -218,20 +227,6 @@ func (q *Queries) executeCategoryCreate(ctx context.Context, assignments []Field return res, nil } -func categoryRecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - if _, ok := m["id"]; !ok { - } - rowMaps[i] = m - } - return rowMaps -} - func (d *CategoryDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Category] { return &CreateManyBuilder[Category]{ client: d.client, @@ -249,26 +244,43 @@ func (d *CategoryDelegate) CreateManyAndReturn(records ...RecordInput) *CreateMa } func (q *Queries) executeCategoryCreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "Category", CategoryColOrder, - validateCategoryCreate, - categoryRecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*Category, error) { - return q.executeCategoryCreate(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validateCategoryCreate(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToCategoryCreate(rec.Assignments) + if q.Category.beforeCreate != nil { + if err := q.Category.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "Category", CategoryColOrder) } func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *CategorySelect, omits *CategoryOmit) ([]*Category, error) { - return executeCreateManyAndReturn(ctx, q, records, "Category", CategoryColOrder, selects, omits, - validateCategoryCreate, - categoryRecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "id" + for i, rec := range records { + if err := validateCategoryCreate(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToCategoryCreate(rec.Assignments) + if q.Category.beforeCreate != nil { + if err := q.Category.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "Category", CategoryColOrder, selects, omits, q.selectCategoryCols, q.loadCategoryRelations, (*Category).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*Category, error) { - return q.executeCategoryCreate(ctx, assignments, nil, nil) - }, (*CategorySelect).hasAnyRelation, + idCol, ) } func (d *CategoryDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Category, CategorySelect, CategoryOmit] { diff --git a/integration/valk/categoryToPost.go b/integration/valk/categoryToPost.go index d517141..598171f 100644 --- a/integration/valk/categoryToPost.go +++ b/integration/valk/categoryToPost.go @@ -162,6 +162,13 @@ func assignmentsToCategoryToPostCreate(assignments []FieldAssignment) CategoryTo return input } +func (s *CategoryToPostCreate) ToRowMap() map[string]any { + m := make(map[string]any, 2) + m["postId"] = s.PostId + m["categoryId"] = s.CategoryId + return m +} + func (q *Queries) executeCategoryToPostCreate(ctx context.Context, assignments []FieldAssignment, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { input := assignmentsToCategoryToPostCreate(assignments) @@ -219,18 +226,6 @@ func (q *Queries) executeCategoryToPostCreate(ctx context.Context, assignments [ return res, nil } -func categoryToPostRecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - rowMaps[i] = m - } - return rowMaps -} - func (d *CategoryToPostDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[CategoryToPost] { return &CreateManyBuilder[CategoryToPost]{ client: d.client, @@ -248,26 +243,43 @@ func (d *CategoryToPostDelegate) CreateManyAndReturn(records ...RecordInput) *Cr } func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "CategoryToPost", CategoryToPostColOrder, - validateCategoryToPostCreate, - categoryToPostRecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*CategoryToPost, error) { - return q.executeCategoryToPostCreate(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validateCategoryToPostCreate(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToCategoryToPostCreate(rec.Assignments) + if q.CategoryToPost.beforeCreate != nil { + if err := q.CategoryToPost.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "CategoryToPost", CategoryToPostColOrder) } func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *CategoryToPostSelect, omits *CategoryToPostOmit) ([]*CategoryToPost, error) { - return executeCreateManyAndReturn(ctx, q, records, "CategoryToPost", CategoryToPostColOrder, selects, omits, - validateCategoryToPostCreate, - categoryToPostRecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "" + for i, rec := range records { + if err := validateCategoryToPostCreate(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToCategoryToPostCreate(rec.Assignments) + if q.CategoryToPost.beforeCreate != nil { + if err := q.CategoryToPost.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "CategoryToPost", CategoryToPostColOrder, selects, omits, q.selectCategoryToPostCols, q.loadCategoryToPostRelations, (*CategoryToPost).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*CategoryToPost, error) { - return q.executeCategoryToPostCreate(ctx, assignments, nil, nil) - }, (*CategoryToPostSelect).hasAnyRelation, + idCol, ) } func (d *CategoryToPostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { diff --git a/integration/valk/client.go b/integration/valk/client.go index 31b10af..416671c 100644 --- a/integration/valk/client.go +++ b/integration/valk/client.go @@ -157,7 +157,7 @@ type Queries struct { dialect Dialect // User provides CRUD operations for User. // - // id string default: cuid() + // id string default: uuid() // email string required // phoneNum string required // password string optional @@ -1318,27 +1318,18 @@ func executeInsert[M any]( return &res, nil } -func executeCreateMany[M any]( +func executeCreateMany( ctx context.Context, q *Queries, - records []RecordInput, + rowMaps []map[string]any, tableName string, colOrder []string, - validateFn func([]FieldAssignment) error, - rowMapFn func([]RecordInput) []map[string]any, - singleCreateFn func(context.Context, []FieldAssignment) (*M, error), ) (int64, error) { - if len(records) == 0 { + if len(rowMaps) == 0 { return 0, nil } - for i, rec := range records { - if err := validateFn(rec.Assignments); err != nil { - return 0, fmt.Errorf("validation failed at index %d: %w", i, err) - } - } if q.dialect.SupportsBulkInsert() { - rowMaps := rowMapFn(records) query, vals := buildBulkInsertSQL(q.dialect, tableName, rowMaps, colOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -1349,8 +1340,9 @@ func executeCreateMany[M any]( var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, rec := range records { - _, err := singleCreateFn(ctx, rec.Assignments) + for _, rowMap := range rowMaps { + query, vals := buildBulkInsertSQL(txQ.dialect, tableName, []map[string]any{rowMap}, colOrder, nil) + _, err := txQ.exec(ctx, query, vals...) if err != nil { return err } @@ -1364,33 +1356,25 @@ func executeCreateMany[M any]( func executeCreateManyAndReturn[M any, S any, O any]( ctx context.Context, q *Queries, - records []RecordInput, + rowMaps []map[string]any, tableName string, colOrder []string, selects *S, omits *O, - validateFn func([]FieldAssignment) error, - rowMapFn func([]RecordInput) []map[string]any, selectColsFn func(*S, *O, ...string) []string, loadRelationsFn func(context.Context, []*M, *S) error, scanFunc func(*M, []string) []any, - singleCreateFn func(context.Context, []FieldAssignment) (*M, error), hasRelationsFn func(*S) bool, + idCol string, ) ([]*M, error) { - if len(records) == 0 { + if len(rowMaps) == 0 { return nil, nil } - for i, rec := range records { - if err := validateFn(rec.Assignments); err != nil { - return nil, fmt.Errorf("validation failed at index %d: %w", i, err) - } - } hasRelations := selects != nil && hasRelationsFn(selects) returningCols := selectColsFn(selects, omits) if q.dialect.SupportsBulkInsert() { - rowMaps := rowMapFn(records) query, vals := buildBulkInsertSQL(q.dialect, tableName, rowMaps, colOrder, returningCols) recordsOut := make([]*M, 0) err := q.transaction(ctx, func(txQ *Queries) error { @@ -1422,8 +1406,9 @@ func executeCreateManyAndReturn[M any, S any, O any]( recordsOut := make([]*M, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, rec := range records { - res, err := singleCreateFn(ctx, rec.Assignments) + for _, rowMap := range rowMaps { + cols, vals := mapToColsVals(rowMap, colOrder) + res, err := executeInsert(ctx, txQ, tableName, cols, vals, returningCols, idCol, scanFunc) if err != nil { return err } diff --git a/integration/valk/comment.go b/integration/valk/comment.go index ef60cd3..938dee4 100644 --- a/integration/valk/comment.go +++ b/integration/valk/comment.go @@ -297,6 +297,25 @@ func assignmentsToCommentCreate(assignments []FieldAssignment) CommentCreate { return input } +func (s *CommentCreate) ToRowMap() map[string]any { + m := make(map[string]any, 8) + if s.Id != nil { + m["id"] = *s.Id + } else { + m["id"] = generateCUID() + } + m["textify"] = s.Textify + m["dummy3"] = s.Dummy3 + m["dummy1"] = s.Dummy1 + m["dummy2"] = s.Dummy2 + m["postId"] = s.PostId + m["authorId"] = s.AuthorId + if s.Meta != nil { + m["meta"] = *s.Meta + } + return m +} + func (q *Queries) executeCommentCreate(ctx context.Context, assignments []FieldAssignment, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { input := assignmentsToCommentCreate(assignments) @@ -373,21 +392,6 @@ func (q *Queries) executeCommentCreate(ctx context.Context, assignments []FieldA return res, nil } -func commentRecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - if _, ok := m["id"]; !ok { - m["id"] = generateCUID() - } - rowMaps[i] = m - } - return rowMaps -} - func (d *CommentDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Comment] { return &CreateManyBuilder[Comment]{ client: d.client, @@ -405,26 +409,43 @@ func (d *CommentDelegate) CreateManyAndReturn(records ...RecordInput) *CreateMan } func (q *Queries) executeCommentCreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "Comment", CommentColOrder, - validateCommentCreate, - commentRecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*Comment, error) { - return q.executeCommentCreate(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validateCommentCreate(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToCommentCreate(rec.Assignments) + if q.Comment.beforeCreate != nil { + if err := q.Comment.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "Comment", CommentColOrder) } func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *CommentSelect, omits *CommentOmit) ([]*Comment, error) { - return executeCreateManyAndReturn(ctx, q, records, "Comment", CommentColOrder, selects, omits, - validateCommentCreate, - commentRecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "id" + for i, rec := range records { + if err := validateCommentCreate(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToCommentCreate(rec.Assignments) + if q.Comment.beforeCreate != nil { + if err := q.Comment.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "Comment", CommentColOrder, selects, omits, q.selectCommentCols, q.loadCommentRelations, (*Comment).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*Comment, error) { - return q.executeCommentCreate(ctx, assignments, nil, nil) - }, (*CommentSelect).hasAnyRelation, + idCol, ) } func (d *CommentDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Comment, CommentSelect, CommentOmit] { diff --git a/integration/valk/post.go b/integration/valk/post.go index 99a7d45..b5b863f 100644 --- a/integration/valk/post.go +++ b/integration/valk/post.go @@ -229,6 +229,24 @@ func assignmentsToPostCreate(assignments []FieldAssignment) PostCreate { return input } +func (s *PostCreate) ToRowMap() map[string]any { + m := make(map[string]any, 5) + if s.Id != nil { + m["id"] = *s.Id + } else { + m["id"] = generateCUID() + } + m["title"] = s.Title + if s.Content != nil { + m["content"] = *s.Content + } + if s.Published != nil { + m["published"] = *s.Published + } + m["authorId"] = s.AuthorId + return m +} + func (q *Queries) executePostCreate(ctx context.Context, assignments []FieldAssignment, selects *PostSelect, omits *PostOmit) (*Post, error) { input := assignmentsToPostCreate(assignments) @@ -301,21 +319,6 @@ func (q *Queries) executePostCreate(ctx context.Context, assignments []FieldAssi return res, nil } -func postRecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - if _, ok := m["id"]; !ok { - m["id"] = generateCUID() - } - rowMaps[i] = m - } - return rowMaps -} - func (d *PostDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Post] { return &CreateManyBuilder[Post]{ client: d.client, @@ -333,26 +336,43 @@ func (d *PostDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAn } func (q *Queries) executePostCreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "Post", PostColOrder, - validatePostCreate, - postRecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*Post, error) { - return q.executePostCreate(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validatePostCreate(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToPostCreate(rec.Assignments) + if q.Post.beforeCreate != nil { + if err := q.Post.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "Post", PostColOrder) } func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *PostSelect, omits *PostOmit) ([]*Post, error) { - return executeCreateManyAndReturn(ctx, q, records, "Post", PostColOrder, selects, omits, - validatePostCreate, - postRecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "id" + for i, rec := range records { + if err := validatePostCreate(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToPostCreate(rec.Assignments) + if q.Post.beforeCreate != nil { + if err := q.Post.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "Post", PostColOrder, selects, omits, q.selectPostCols, q.loadPostRelations, (*Post).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*Post, error) { - return q.executePostCreate(ctx, assignments, nil, nil) - }, (*PostSelect).hasAnyRelation, + idCol, ) } func (d *PostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Post, PostSelect, PostOmit] { diff --git a/integration/valk/profile.go b/integration/valk/profile.go index e276134..69aefaa 100644 --- a/integration/valk/profile.go +++ b/integration/valk/profile.go @@ -181,6 +181,20 @@ func assignmentsToProfileCreate(assignments []FieldAssignment) ProfileCreate { return input } +func (s *ProfileCreate) ToRowMap() map[string]any { + m := make(map[string]any, 3) + if s.Id != nil { + m["id"] = *s.Id + } else { + m["id"] = generateCUID() + } + if s.Bio != nil { + m["bio"] = *s.Bio + } + m["userId"] = s.UserId + return m +} + func (q *Queries) executeProfileCreate(ctx context.Context, assignments []FieldAssignment, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { input := assignmentsToProfileCreate(assignments) @@ -247,21 +261,6 @@ func (q *Queries) executeProfileCreate(ctx context.Context, assignments []FieldA return res, nil } -func profileRecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - if _, ok := m["id"]; !ok { - m["id"] = generateCUID() - } - rowMaps[i] = m - } - return rowMaps -} - func (d *ProfileDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Profile] { return &CreateManyBuilder[Profile]{ client: d.client, @@ -279,26 +278,43 @@ func (d *ProfileDelegate) CreateManyAndReturn(records ...RecordInput) *CreateMan } func (q *Queries) executeProfileCreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "Profile", ProfileColOrder, - validateProfileCreate, - profileRecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*Profile, error) { - return q.executeProfileCreate(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validateProfileCreate(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToProfileCreate(rec.Assignments) + if q.Profile.beforeCreate != nil { + if err := q.Profile.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "Profile", ProfileColOrder) } func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *ProfileSelect, omits *ProfileOmit) ([]*Profile, error) { - return executeCreateManyAndReturn(ctx, q, records, "Profile", ProfileColOrder, selects, omits, - validateProfileCreate, - profileRecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "id" + for i, rec := range records { + if err := validateProfileCreate(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToProfileCreate(rec.Assignments) + if q.Profile.beforeCreate != nil { + if err := q.Profile.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "Profile", ProfileColOrder, selects, omits, q.selectProfileCols, q.loadProfileRelations, (*Profile).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*Profile, error) { - return q.executeProfileCreate(ctx, assignments, nil, nil) - }, (*ProfileSelect).hasAnyRelation, + idCol, ) } func (d *ProfileDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit] { diff --git a/integration/valk/user.go b/integration/valk/user.go index b81b81b..7b02d93 100644 --- a/integration/valk/user.go +++ b/integration/valk/user.go @@ -269,6 +269,30 @@ func assignmentsToUserCreate(assignments []FieldAssignment) UserCreate { return input } +func (s *UserCreate) ToRowMap() map[string]any { + m := make(map[string]any, 7) + if s.Id != nil { + m["id"] = *s.Id + } else { + m["id"] = generateUUID() + } + m["email"] = s.Email + m["phoneNum"] = s.PhoneNum + if s.Password != nil { + m["password"] = *s.Password + } + if s.Role != nil { + m["role"] = *s.Role + } + if s.RoleOptional != nil { + m["roleOptional"] = *s.RoleOptional + } + if s.ReferredById != nil { + m["referredById"] = *s.ReferredById + } + return m +} + func (q *Queries) executeUserCreate(ctx context.Context, assignments []FieldAssignment, selects *UserSelect, omits *UserOmit) (*User, error) { input := assignmentsToUserCreate(assignments) @@ -289,7 +313,7 @@ func (q *Queries) executeUserCreate(ctx context.Context, assignments []FieldAssi vals = append(vals, *input.Id) } else { cols = append(cols, "id") - vals = append(vals, generateCUID()) + vals = append(vals, generateUUID()) } cols = append(cols, "email") vals = append(vals, input.Email) @@ -349,21 +373,6 @@ func (q *Queries) executeUserCreate(ctx context.Context, assignments []FieldAssi return res, nil } -func userRecordsToRowMaps(records []RecordInput) []map[string]any { - rowMaps := make([]map[string]any, len(records)) - for i, rec := range records { - m := make(map[string]any, len(rec.Assignments)) - for _, a := range rec.Assignments { - m[a.Col] = a.Val - } - if _, ok := m["id"]; !ok { - m["id"] = generateCUID() - } - rowMaps[i] = m - } - return rowMaps -} - func (d *UserDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[User] { return &CreateManyBuilder[User]{ client: d.client, @@ -381,26 +390,43 @@ func (d *UserDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAn } func (q *Queries) executeUserCreateMany(ctx context.Context, records []RecordInput) (int64, error) { - return executeCreateMany(ctx, q, records, "User", UserColOrder, - validateUserCreate, - userRecordsToRowMaps, - func(ctx context.Context, assignments []FieldAssignment) (*User, error) { - return q.executeUserCreate(ctx, assignments, nil, nil) - }, - ) + rowMaps := make([]map[string]any, len(records)) + for i, rec := range records { + if err := validateUserCreate(rec.Assignments); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToUserCreate(rec.Assignments) + if q.User.beforeCreate != nil { + if err := q.User.beforeCreate(ctx, &input); err != nil { + return 0, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateMany(ctx, q, rowMaps, "User", UserColOrder) } func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *UserSelect, omits *UserOmit) ([]*User, error) { - return executeCreateManyAndReturn(ctx, q, records, "User", UserColOrder, selects, omits, - validateUserCreate, - userRecordsToRowMaps, + rowMaps := make([]map[string]any, len(records)) + idCol := "id" + for i, rec := range records { + if err := validateUserCreate(rec.Assignments); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + input := assignmentsToUserCreate(rec.Assignments) + if q.User.beforeCreate != nil { + if err := q.User.beforeCreate(ctx, &input); err != nil { + return nil, err + } + } + rowMaps[i] = input.ToRowMap() + } + return executeCreateManyAndReturn(ctx, q, rowMaps, "User", UserColOrder, selects, omits, q.selectUserCols, q.loadUserRelations, (*User).ScanFields, - func(ctx context.Context, assignments []FieldAssignment) (*User, error) { - return q.executeUserCreate(ctx, assignments, nil, nil) - }, (*UserSelect).hasAnyRelation, + idCol, ) } func (d *UserDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[User, UserSelect, UserOmit] { From e4a8e9fa9e03fb78c607d7d88ea263ca8e2d9393 Mon Sep 17 00:00:00 2001 From: Clancy Date: Thu, 9 Jul 2026 20:42:58 +0300 Subject: [PATCH 2/4] revert back to cuid --- integration/valk/client.go | 2 +- integration/valk/user.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/valk/client.go b/integration/valk/client.go index 416671c..02e302a 100644 --- a/integration/valk/client.go +++ b/integration/valk/client.go @@ -157,7 +157,7 @@ type Queries struct { dialect Dialect // User provides CRUD operations for User. // - // id string default: uuid() + // id string default: cuid() // email string required // phoneNum string required // password string optional diff --git a/integration/valk/user.go b/integration/valk/user.go index 7b02d93..46ad3ae 100644 --- a/integration/valk/user.go +++ b/integration/valk/user.go @@ -274,7 +274,7 @@ func (s *UserCreate) ToRowMap() map[string]any { if s.Id != nil { m["id"] = *s.Id } else { - m["id"] = generateUUID() + m["id"] = generateCUID() } m["email"] = s.Email m["phoneNum"] = s.PhoneNum @@ -313,7 +313,7 @@ func (q *Queries) executeUserCreate(ctx context.Context, assignments []FieldAssi vals = append(vals, *input.Id) } else { cols = append(cols, "id") - vals = append(vals, generateUUID()) + vals = append(vals, generateCUID()) } cols = append(cols, "email") vals = append(vals, input.Email) From 8ff9644e28b182e6a79657f9a016f6a773beab37 Mon Sep 17 00:00:00 2001 From: Clancy Date: Thu, 9 Jul 2026 20:56:09 +0300 Subject: [PATCH 3/4] fix testCreateMany_hooks, using the unique query api instead of dialect specific which caused Postgres to fail in the workflow run --- integration/create_many_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/integration/create_many_test.go b/integration/create_many_test.go index 7a84e51..9777c03 100644 --- a/integration/create_many_test.go +++ b/integration/create_many_test.go @@ -35,16 +35,13 @@ func TestCreateMany_Hooks(t *testing.T) { if count != 2 { t.Fatalf("expected count 2, got %d", count) } + usr, err := client.User.FindUnique(user.Email.EQ("hooked@example.com")).Exec(ctx) - var phone string - err = client.Raw().QueryRowContext(ctx, - `SELECT phoneNum FROM "User" WHERE email = 'hooked@example.com'`, - ).Scan(&phone) if err != nil { t.Fatalf("query failed: %v", err) } - if phone != "+188888888" { - t.Errorf("expected PhoneNum to be mutated to '+188888888', got %q", phone) + if usr.PhoneNum != "+188888888" { + t.Errorf("expected PhoneNum to be mutated to '+188888888', got %q", usr.PhoneNum) } }) From 61b8209ce4e70f3ac669344cde66060e70dcab1a Mon Sep 17 00:00:00 2001 From: Clancy Date: Thu, 9 Jul 2026 21:26:44 +0300 Subject: [PATCH 4/4] Feat: add support to AfterCreate hook on createManyAndReturn 1- add the if {model}.afterCreate check to the CreateManyAndReturn in model_create template 2- update TestCreateMany_Hooks in create_many_tests to test the afterCreate hook on bulk inserts (CreateManyAndReturn for now) --- generator/templates/model_create.gotpl | 13 +++++- integration/create_many_test.go | 60 ++++++++++++++++++++++++++ integration/valk/category.go | 13 +++++- integration/valk/categoryToPost.go | 13 +++++- integration/valk/comment.go | 13 +++++- integration/valk/post.go | 13 +++++- integration/valk/profile.go | 13 +++++- integration/valk/user.go | 13 +++++- 8 files changed, 144 insertions(+), 7 deletions(-) diff --git a/generator/templates/model_create.gotpl b/generator/templates/model_create.gotpl index 3267de2..9ccd885 100644 --- a/generator/templates/model_create.gotpl +++ b/generator/templates/model_create.gotpl @@ -372,11 +372,22 @@ func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Contex } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "{{ .Model.EffectiveTableName }}", {{ .Model.Name }}ColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "{{ .Model.EffectiveTableName }}", {{ .Model.Name }}ColOrder, selects, omits, q.select{{ .Model.Name }}Cols, q.load{{ .Model.Name }}Relations, (*{{ .Model.Name }}).ScanFields, (*{{ .Model.Name }}Select).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.{{ .Model.Name }}.afterCreate != nil { + for _, r := range results { + if err := q.{{ .Model.Name }}.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } diff --git a/integration/create_many_test.go b/integration/create_many_test.go index 9777c03..1e8c2d2 100644 --- a/integration/create_many_test.go +++ b/integration/create_many_test.go @@ -7,6 +7,7 @@ import ( "integration/valk" "integration/valk/post" "integration/valk/user" + "strings" "testing" ) @@ -101,6 +102,65 @@ func TestCreateMany_Hooks(t *testing.T) { t.Fatalf("expected 0 rows after aborted CreateMany, got %d", count) } }) + + t.Run("AfterCreate hook fires during CreateManyAndReturn", func(t *testing.T) { + client, cleanup := setupTestDB(t) + defer cleanup() + + var afterCalled bool + var gotRole valk.UserRoleType + client.User.AfterCreate(func(ctx context.Context, user *valk.User) error { + afterCalled = true + gotRole = user.Role + return nil + }) + + users, err := client.User.CreateManyAndReturn( + user.Record(user.Email.Set("after@example.com"), user.PhoneNum.Set("+500000000")), + ).Exec(ctx) + + if err != nil { + t.Fatalf("CreateManyAndReturn failed: %v", err) + } + if len(users) != 1 { + t.Fatalf("expected 1 user, got %d", len(users)) + } + if !afterCalled { + t.Fatal("expected AfterCreate hook to be called") + } + if gotRole != users[0].Role { + t.Errorf("AfterCreate received role %v, expected %v", gotRole, users[0].Role) + } + }) + + t.Run("AfterCreate hook error aborts CreateManyAndReturn", func(t *testing.T) { + client, cleanup := setupTestDB(t) + defer cleanup() + + client.User.AfterCreate(func(ctx context.Context, user *valk.User) error { + return fmt.Errorf("after hook rejected") + }) + + var count int + client.Raw().QueryRowContext(ctx, `SELECT count(*) FROM "User"`).Scan(&count) + prevCount := count + + _, err := client.User.CreateManyAndReturn( + user.Record(user.Email.Set("aftershoot@example.com"), user.PhoneNum.Set("+600000000")), + ).Exec(ctx) + + if err == nil { + t.Fatal("expected error from AfterCreate rejection, got nil") + } + if !strings.Contains(err.Error(), "after hook rejected") { + t.Errorf("expected 'after hook rejected' in error, got %v", err) + } + + client.Raw().QueryRowContext(ctx, `SELECT count(*) FROM "User"`).Scan(&count) + if count != prevCount+1 { + t.Fatalf("expected %d rows (insert still committed), got %d", prevCount+1, count) + } + }) } func TestCreateMany(t *testing.T) { diff --git a/integration/valk/category.go b/integration/valk/category.go index d642cfb..b53993e 100644 --- a/integration/valk/category.go +++ b/integration/valk/category.go @@ -275,13 +275,24 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, record } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "Category", CategoryColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "Category", CategoryColOrder, selects, omits, q.selectCategoryCols, q.loadCategoryRelations, (*Category).ScanFields, (*CategorySelect).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.Category.afterCreate != nil { + for _, r := range results { + if err := q.Category.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } func (d *CategoryDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Category, CategorySelect, CategoryOmit] { return &FindUniqueBuilder[Category, CategorySelect, CategoryOmit]{ diff --git a/integration/valk/categoryToPost.go b/integration/valk/categoryToPost.go index 598171f..6261c7e 100644 --- a/integration/valk/categoryToPost.go +++ b/integration/valk/categoryToPost.go @@ -274,13 +274,24 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "CategoryToPost", CategoryToPostColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "CategoryToPost", CategoryToPostColOrder, selects, omits, q.selectCategoryToPostCols, q.loadCategoryToPostRelations, (*CategoryToPost).ScanFields, (*CategoryToPostSelect).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.CategoryToPost.afterCreate != nil { + for _, r := range results { + if err := q.CategoryToPost.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } func (d *CategoryToPostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { return &FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ diff --git a/integration/valk/comment.go b/integration/valk/comment.go index 938dee4..ec2497f 100644 --- a/integration/valk/comment.go +++ b/integration/valk/comment.go @@ -440,13 +440,24 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, records } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "Comment", CommentColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "Comment", CommentColOrder, selects, omits, q.selectCommentCols, q.loadCommentRelations, (*Comment).ScanFields, (*CommentSelect).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.Comment.afterCreate != nil { + for _, r := range results { + if err := q.Comment.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } func (d *CommentDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Comment, CommentSelect, CommentOmit] { return &FindUniqueBuilder[Comment, CommentSelect, CommentOmit]{ diff --git a/integration/valk/post.go b/integration/valk/post.go index b5b863f..bd56914 100644 --- a/integration/valk/post.go +++ b/integration/valk/post.go @@ -367,13 +367,24 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, records [] } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "Post", PostColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "Post", PostColOrder, selects, omits, q.selectPostCols, q.loadPostRelations, (*Post).ScanFields, (*PostSelect).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.Post.afterCreate != nil { + for _, r := range results { + if err := q.Post.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } func (d *PostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Post, PostSelect, PostOmit] { return &FindUniqueBuilder[Post, PostSelect, PostOmit]{ diff --git a/integration/valk/profile.go b/integration/valk/profile.go index 69aefaa..84aa63f 100644 --- a/integration/valk/profile.go +++ b/integration/valk/profile.go @@ -309,13 +309,24 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, records } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "Profile", ProfileColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "Profile", ProfileColOrder, selects, omits, q.selectProfileCols, q.loadProfileRelations, (*Profile).ScanFields, (*ProfileSelect).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.Profile.afterCreate != nil { + for _, r := range results { + if err := q.Profile.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } func (d *ProfileDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit] { return &FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit]{ diff --git a/integration/valk/user.go b/integration/valk/user.go index 46ad3ae..49ec04a 100644 --- a/integration/valk/user.go +++ b/integration/valk/user.go @@ -421,13 +421,24 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, records [] } rowMaps[i] = input.ToRowMap() } - return executeCreateManyAndReturn(ctx, q, rowMaps, "User", UserColOrder, selects, omits, + results, err := executeCreateManyAndReturn(ctx, q, rowMaps, "User", UserColOrder, selects, omits, q.selectUserCols, q.loadUserRelations, (*User).ScanFields, (*UserSelect).hasAnyRelation, idCol, ) + if err != nil { + return nil, err + } + if q.User.afterCreate != nil { + for _, r := range results { + if err := q.User.afterCreate(ctx, r); err != nil { + return nil, err + } + } + } + return results, nil } func (d *UserDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[User, UserSelect, UserOmit] { return &FindUniqueBuilder[User, UserSelect, UserOmit]{