From 85d023220de455863898f01e1397819c9ba90794 Mon Sep 17 00:00:00 2001 From: Clancy Date: Thu, 9 Jul 2026 16:15:51 +0300 Subject: [PATCH] Chore: refactor the create struct-based API to be function based for consistency with the reads 1- define FieldAssignment (col+val) and RecordInput (slice of FieldAssignment) in header template 2- add .Set() on FieldT, UniqueFieldT, StringField, StringUniqueField in client template, field vars produce FieldAssignments 3- drop per-field SetFieldName() from model_predicate template, replaced by generic Set() 4- refactor create builders M,I,S,O to M,S,O, I param dead (and we killed him), since it's the same slice of FieldAssignment every time, now it lives on the struct itself 5- add per-model validateCreate() that iterates the assignment slice, checks required/string-safety/enum-range per field 6- add assignmentsToModelCreate converter, from []FieldAssignment to ModelCreate struct, falls naturally with the beforeCreate Hook that takes the same struct and allows mutuation 7- rebuild executeModelCreate to accept []FieldAssignment instead of ModelCreate, converts to struct internally for hooks, validates original assignments, reads mutated struct fields for cols/vals 8- add Record() helper on delegates, wraps []FieldAssignment into RecordInput for CreateMany 9- add trimPrefix to template FuncMap, strips (*) from GoType for type assertions on optional/defaulted fields 10- update integration tests to use the new function-based API 11- fix @default(autoincrement()) skip col when nil instead of leaving val slot empty, caught during test run 12- add doc comments on Queries delegate fields listing Create fields with types/constraints for hover DX 14- add enum doc comments on const values and namespace struct fields showing DB mapping on hover --- generator/generator.go | 1 + generator/generator_test.go | 8 +- generator/templates/builders_create.gotpl | 80 ++--- generator/templates/client.gotpl | 28 ++ generator/templates/enums.gotpl | 8 + generator/templates/header.gotpl | 9 + generator/templates/model_create.gotpl | 286 +++++++++++------ generator/templates/model_predicate.gotpl | 4 +- generator/templates/model_structs.gotpl | 127 +------- integration/benchmark_test.go | 17 +- integration/create_many_test.go | 43 +-- integration/create_test.go | 67 ++-- integration/main.go | 160 ++++++---- integration/read_test.go | 97 +++--- integration/selection_test.go | 122 ++++---- integration/validation_test.go | 163 +++++----- integration/valk/category.go | 169 +++++----- integration/valk/category/category.go | 5 +- integration/valk/categoryToPost.go | 164 ++++++---- .../valk/categoryToPost/categoryToPost.go | 5 +- integration/valk/client.go | 175 ++++++++--- integration/valk/comment.go | 293 +++++++++++------- integration/valk/comment/comment.go | 5 +- integration/valk/post.go | 231 ++++++++------ integration/valk/post/post.go | 5 +- integration/valk/profile.go | 194 +++++++----- integration/valk/profile/profile.go | 5 +- integration/valk/user.go | 263 +++++++++------- integration/valk/user/user.go | 5 +- 29 files changed, 1547 insertions(+), 1192 deletions(-) diff --git a/generator/generator.go b/generator/generator.go index 9d6d781..db97d18 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -129,6 +129,7 @@ func GenerateClient(sch schema.Schema, pkgName string, parentImportPath string, } return false }, + "trimPrefix": strings.TrimPrefix, "hasStringField": func(m *schema.Model) bool { for _, sf := range m.ScalarFields { if sf.GoType == "string" || strings.Contains(sf.GoType, "string") { diff --git a/generator/generator_test.go b/generator/generator_test.go index 4a7d7c6..10c4f90 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -51,13 +51,13 @@ func TestGenerateClient_NativeDBConstraints(t *testing.T) { t.Fatal("expected item.go in outputs") } - // Verify length checks are generated - if !strings.Contains(itemCode, "utf8.RuneCountInString(input.Code) > 8") { + // 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 - if !strings.Contains(itemCode, "input.Count < -32768 || input.Count > 32767") { + // 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 f40c540..556547f 100644 --- a/generator/templates/builders_create.gotpl +++ b/generator/templates/builders_create.gotpl @@ -1,83 +1,83 @@ -type CreateBuilder[M any, I any, S any, O any] struct { - client *Queries - input I - execFunc func(ctx context.Context, input I, s *S, o *O) (*M, error) +type CreateBuilder[M any, S any, O any] struct { + client *Queries + assignments []FieldAssignment + execFunc func(ctx context.Context, assignments []FieldAssignment, s *S, o *O) (*M, error) } -func (b *CreateBuilder[M, I, S, O]) Select(s S) *CreateSelectBuilder[M, I, S, O] { - return &CreateSelectBuilder[M, I, S, O]{builder: b, selects: s} +func (b *CreateBuilder[M, S, O]) Select(s S) *CreateSelectBuilder[M, S, O] { + return &CreateSelectBuilder[M, S, O]{builder: b, selects: s} } -func (b *CreateBuilder[M, I, S, O]) Omit(o O) *CreateOmitBuilder[M, I, S, O] { - return &CreateOmitBuilder[M, I, S, O]{builder: b, omits: o} +func (b *CreateBuilder[M, S, O]) Omit(o O) *CreateOmitBuilder[M, S, O] { + return &CreateOmitBuilder[M, S, O]{builder: b, omits: o} } -func (b *CreateBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { - return b.execFunc(ctx, b.input, nil, nil) +func (b *CreateBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.execFunc(ctx, b.assignments, nil, nil) } -type CreateSelectBuilder[M any, I any, S any, O any] struct { - builder *CreateBuilder[M, I, S, O] +type CreateSelectBuilder[M any, S any, O any] struct { + builder *CreateBuilder[M, S, O] selects S } -func (b *CreateSelectBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { - return b.builder.execFunc(ctx, b.builder.input, &b.selects, nil) +func (b *CreateSelectBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.assignments, &b.selects, nil) } -type CreateOmitBuilder[M any, I any, S any, O any] struct { - builder *CreateBuilder[M, I, S, O] +type CreateOmitBuilder[M any, S any, O any] struct { + builder *CreateBuilder[M, S, O] omits O } -func (b *CreateOmitBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { - return b.builder.execFunc(ctx, b.builder.input, nil, &b.omits) +func (b *CreateOmitBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.assignments, nil, &b.omits) } -type CreateManyBuilder[M any, I any] struct { +type CreateManyBuilder[M any] struct { client *Queries - inputs []I - execFunc func(ctx context.Context, inputs []I) (int64, error) + records []RecordInput + execFunc func(ctx context.Context, records []RecordInput) (int64, error) } -func (b *CreateManyBuilder[M, I]) Exec(ctx context.Context) (int64, error) { - return b.execFunc(ctx, b.inputs) +func (b *CreateManyBuilder[M]) Exec(ctx context.Context) (int64, error) { + return b.execFunc(ctx, b.records) } -type CreateManyAndReturnBuilder[M any, I any, S any, O any] struct { +type CreateManyAndReturnBuilder[M any, S any, O any] struct { client *Queries - inputs []I - execFunc func(ctx context.Context, inputs []I, s *S, o *O) ([]*M, error) + records []RecordInput + execFunc func(ctx context.Context, records []RecordInput, 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, S, O]) Select(s S) *CreateManyAndReturnSelectBuilder[M, S, O] { + return &CreateManyAndReturnSelectBuilder[M, 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, S, O]) Omit(o O) *CreateManyAndReturnOmitBuilder[M, S, O] { + return &CreateManyAndReturnOmitBuilder[M, 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) +func (b *CreateManyAndReturnBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.execFunc(ctx, b.records, nil, nil) } -type CreateManyAndReturnSelectBuilder[M any, I any, S any, O any] struct { - builder *CreateManyAndReturnBuilder[M, I, S, O] +type CreateManyAndReturnSelectBuilder[M any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, 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) +func (b *CreateManyAndReturnSelectBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.records, &b.selects, nil) } -type CreateManyAndReturnOmitBuilder[M any, I any, S any, O any] struct { - builder *CreateManyAndReturnBuilder[M, I, S, O] +type CreateManyAndReturnOmitBuilder[M any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, 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 (b *CreateManyAndReturnOmitBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.records, nil, &b.omits) } func executeInsert[M any]( diff --git a/generator/templates/client.gotpl b/generator/templates/client.gotpl index de8d822..1df8137 100644 --- a/generator/templates/client.gotpl +++ b/generator/templates/client.gotpl @@ -33,6 +33,18 @@ type Queries struct { provider string dialect Dialect {{- range $model := .Schema.Models }} + // {{ $model.Name }} provides CRUD operations for {{ $model.Name }}. + // + {{- $maxName := 0 }}{{ $maxType := 0 }} + {{- range $f := $model.ScalarFields }} + {{- if gt (len $f.Name) $maxName }}{{ $maxName = len $f.Name }}{{ end }} + {{- $t := "" }}{{ if $f.EnumRef }}{{ $t = $f.EnumRef.Name }}{{ else }}{{ $t = trimPrefix $f.GoType "*" }}{{ end }} + {{- if gt (len $t) $maxType }}{{ $maxType = len $t }}{{ end }} + {{- end }} + {{- range $field := $model.ScalarFields }} + {{- $typeStr := "" }}{{ if $field.EnumRef }}{{ $typeStr = $field.EnumRef.Name }}{{ else }}{{ $typeStr = trimPrefix $field.GoType "*" }}{{ end }} + // {{ printf "%-*s" $maxName $field.Name }} {{ printf "%-*s" $maxType $typeStr }} {{ if and (eq $field.Default nil) (not $field.Optional) }}required{{ else if $field.Optional }}optional{{ else if $field.Default }}default: {{ $field.Default.Value }}{{ end }} + {{- end }} {{ $model.Name }} *{{ $model.Name }}Delegate {{- end }} {{- range $enum := .Schema.Enums }} @@ -370,6 +382,10 @@ type Field[T any] struct { Column string } +func (f Field[T]) Set(val T) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + func (f Field[T]) EQ(val T) Predicate { return StandardPredicate{ Data: PredicateData{ @@ -462,6 +478,10 @@ type UniqueField[T any] struct { Column string } +func (f UniqueField[T]) Set(val T) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + type UniqueFieldPredicate struct { StandardPredicate } @@ -569,6 +589,10 @@ type StringField struct { Column string } +func (f StringField) Set(val string) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + func (f StringField) EQ(val string) Predicate { return StandardPredicate{ Data: PredicateData{ @@ -681,6 +705,10 @@ type StringUniqueField struct { Column string } +func (f StringUniqueField) Set(val string) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + func (f StringUniqueField) EQ(val string) UniquePredicate { return UniqueFieldPredicate{ StandardPredicate: StandardPredicate{ diff --git a/generator/templates/enums.gotpl b/generator/templates/enums.gotpl index 301d9d8..e6df804 100644 --- a/generator/templates/enums.gotpl +++ b/generator/templates/enums.gotpl @@ -3,16 +3,24 @@ type {{ $enum.Name }}Type string const ( {{- range $val := $enum.ValueMap }} + // {{ capitalize $val.Name }} maps to "{{ $val.DBName }}" {{ $enum.Name }}Type{{ capitalize $val.Name }} {{ $enum.Name }}Type = "{{ $val.DBName }}" {{- end }} ) type {{ lowercase $enum.Name }}Namespace struct { {{- range $val := $enum.ValueMap }} + // {{ capitalize $val.Name }} maps to "{{ $val.DBName }}" {{ capitalize $val.Name }} {{ $enum.Name }}Type {{- end }} } +// {{ $enum.Name }} enum values: +// +{{- $maxVal := 0 }}{{ range $v := $enum.ValueMap }}{{ if gt (len $v.Name) $maxVal }}{{ $maxVal = len $v.Name }}{{ end }}{{ end }} +{{- range $v := $enum.ValueMap }} +// {{ printf "%-*s" $maxVal $v.Name }} {{ $v.DBName }} +{{- end }} var {{ $enum.Name }} = {{ lowercase $enum.Name }}Namespace{ {{- range $val := $enum.ValueMap }} {{ capitalize $val.Name }}: {{ $enum.Name }}Type{{ capitalize $val.Name }}, diff --git a/generator/templates/header.gotpl b/generator/templates/header.gotpl index c25c6bd..db344c6 100644 --- a/generator/templates/header.gotpl +++ b/generator/templates/header.gotpl @@ -90,3 +90,12 @@ func (e *ValidationError) HasErrors() bool { return len(e.Errors) > 0 } +type FieldAssignment struct { + Col string + Val any +} + +type RecordInput struct { + Assignments []FieldAssignment +} + diff --git a/generator/templates/model_create.gotpl b/generator/templates/model_create.gotpl index cd71c40..039f703 100644 --- a/generator/templates/model_create.gotpl +++ b/generator/templates/model_create.gotpl @@ -15,24 +15,152 @@ func (s *{{ .Model.Name }}Select) hasAnyRelation() bool { {{- end }} } -func (d *{{ .Model.Name }}Delegate) Create(input {{ .Model.Name }}Create) *CreateBuilder[{{ .Model.Name }}, {{ .Model.Name }}Create, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { - return &CreateBuilder[{{ .Model.Name }}, {{ .Model.Name }}Create, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ - client: d.client, - input: input, - execFunc: d.client.execute{{ .Model.Name }}Create, +func (d *{{ .Model.Name }}Delegate) Create(assignments ...FieldAssignment) *CreateBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { + return &CreateBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.execute{{ .Model.Name }}Create, + } +} + +func validate{{ .Model.Name }}Create(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + {{- range $field := .Model.ScalarFields }} + {{- $col := $field.EffectiveColName }} + case "{{ $col }}": + {{- if $field.EnumRef }} + {{- $enumType := printf "%sType" $field.EnumRef.Name }} + {{- if $field.IsArray }} + if v, ok := a.Val.([]{{ $enumType }}); ok { + for i, val := range v { + if !val.IsValid() { + errs.Add(fmt.Sprintf("{{ $field.Name }}[%d]", i), val, "enum", fmt.Sprintf("invalid enum value %q for field {{ $field.Name }}", val)) + } + } + } + {{- else }} + if v, ok := a.Val.({{ $enumType }}); ok && !v.IsValid() { + errs.Add("{{ $field.Name }}", v, "enum", fmt.Sprintf("invalid enum value %q for field {{ $field.Name }}", v)) + } + {{- end }} + {{- else if eq $field.GoType "string" }} + if v, ok := a.Val.(string); ok { + {{- if and (eq $field.Default nil) (not $field.Optional) (not $field.IsArray) }} + if v == "" { + errs.Add("{{ $field.Name }}", v, "required", "field {{ $field.Name }} is required") + } + {{- end }} + if strings.Contains(v, "\x00") { + errs.Add("{{ $field.Name }}", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("{{ $field.Name }}", v, "safety", "string must be valid UTF-8") + } + {{- if $field.NativeType }} + {{- if or (eq $field.NativeType.Name "VarChar") (eq $field.NativeType.Name "Char") }} + {{- $limit := index $field.NativeType.Args 0 }} + if utf8.RuneCountInString(v) > {{ $limit }} { + errs.Add("{{ $field.Name }}", v, "length", "string exceeds maximum length of {{ $limit }} characters") + } + {{- end }} + {{- end }} + } + {{- else if or (eq $field.GoType "int32") (eq $field.GoType "int64") (eq $field.GoType "int") }} + {{- if $field.NativeType }} + {{- if eq $field.NativeType.Name "SmallInt" }} + if v, ok := a.Val.({{ $field.GoType }}); ok && (v < -32768 || v > 32767) { + errs.Add("{{ $field.Name }}", v, "range", "value is out of range for SmallInt (-32768 to 32767)") + } + {{- else if eq $field.NativeType.Name "TinyInt" }} + if v, ok := a.Val.({{ $field.GoType }}); ok && (v < -128 || v > 127) { + errs.Add("{{ $field.Name }}", v, "range", "value is out of range for TinyInt (-128 to 127)") + } + {{- end }} + {{- end }} + {{- end }} + {{- end }} + } + } + + {{- range $field := .Model.ScalarFields }} + {{- $col := $field.EffectiveColName }} + {{- $fieldName := capitalize $field.Name }} + {{- if $field.EnumRef }} + {{- if and (eq $field.Default nil) (not $field.Optional) (not $field.IsArray) }} + if !provided["{{ $col }}"] { + errs.Add("{{ $field.Name }}", nil, "required", "field {{ $fieldName }} is required") + } + {{- end }} + {{- else if eq $field.GoType "string" }} + {{- if and (eq $field.Default nil) (not $field.Optional) (not $field.IsArray) }} + if !provided["{{ $col }}"] { + errs.Add("{{ $field.Name }}", "", "required", "field {{ $fieldName }} is required") + } + {{- end }} + {{- end }} + {{- end }} + + if errs.HasErrors() { + return *errs + } + return nil +} + +func assignmentsTo{{ .Model.Name }}Create(assignments []FieldAssignment) {{ .Model.Name }}Create { + var input {{ .Model.Name }}Create + for _, a := range assignments { + switch a.Col { + {{- range $field := .Model.ScalarFields }} + case "{{ $field.EffectiveColName }}": + {{- if $field.EnumRef }} + {{- if $field.IsArray }} + if v, ok := a.Val.([]{{ $field.EnumRef.Name }}Type); ok { + input.{{ capitalize $field.Name }} = v + } + {{- else }} + if v, ok := a.Val.({{ $field.EnumRef.Name }}Type); ok { + input.{{ capitalize $field.Name }} = &v + } + {{- end }} + {{- else if $field.IsArray }} + if v, ok := a.Val.({{ trimPrefix $field.GoType "*" }}); ok { + input.{{ capitalize $field.Name }} = v + } + {{- else }} + {{- if or (ne $field.Default nil) $field.Optional }} + if v, ok := a.Val.({{ trimPrefix $field.GoType "*" }}); ok { + input.{{ capitalize $field.Name }} = &v + } + {{- else }} + if v, ok := a.Val.({{ $field.GoType }}); ok { + input.{{ capitalize $field.Name }} = v + } + {{- end }} + {{- end }} + {{- end }} + } } + return input } -func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, input {{ .Model.Name }}Create, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) { +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) + if q.{{ .Model.Name }}.beforeCreate != nil { if err := q.{{ .Model.Name }}.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validate{{ .Model.Name }}Create(assignments); err != nil { return nil, err } + var cols []string var vals []any @@ -59,6 +187,12 @@ func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, input {{ . } {{- else }} {{- if and $field.Default (eq $field.Default.Kind.String "Func") }} + {{- if eq $field.Default.FuncName "autoincrement" }} + if input.{{ $fieldName }} != nil { + cols = append(cols, "{{ $col }}") + vals = append(vals, *input.{{ $fieldName }}) + } + {{- else }} if input.{{ $fieldName }} != nil { cols = append(cols, "{{ $col }}") vals = append(vals, *input.{{ $fieldName }}) @@ -72,6 +206,7 @@ func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, input {{ . vals = append(vals, time.Now()) {{- end }} } + {{- end }} {{- else if or $field.Optional (ne $field.Default nil) }} if input.{{ $fieldName }} != nil { cols = append(cols, "{{ $col }}") @@ -131,89 +266,64 @@ func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, input {{ . return res, nil } -func (q *Queries) {{ .Model.Name }}InputToMap(input {{ .Model.Name }}Create) map[string]any { - m := make(map[string]any) - {{- range $field := .Model.ScalarFields }} - {{- if $field.EnumRef }} - {{- if $field.IsArray }} - if input.{{ capitalize $field.Name }} != nil { - m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} - } - {{- else }} - if input.{{ capitalize $field.Name }} != nil { - m["{{ $field.EffectiveColName }}"] = *input.{{ capitalize $field.Name }} - } - {{- end }} - {{- else }} - {{- if $field.IsArray }} - if input.{{ capitalize $field.Name }} != nil { - m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} - } - {{- else }} - {{- if and $field.Default (eq $field.Default.Kind.String "Func") }} - if input.{{ capitalize $field.Name }} != nil { - m["{{ $field.EffectiveColName }}"] = *input.{{ capitalize $field.Name }} - } else { - {{- if eq $field.Default.FuncName "cuid" }} - m["{{ $field.EffectiveColName }}"] = generateCUID() - {{- else if eq $field.Default.FuncName "uuid" }} - m["{{ $field.EffectiveColName }}"] = generateUUID() - {{- else if eq $field.Default.FuncName "now" }} - m["{{ $field.EffectiveColName }}"] = time.Now() - {{- end }} - } - {{- else if or $field.Optional (ne $field.Default nil) }} - if input.{{ capitalize $field.Name }} != nil { - m["{{ $field.EffectiveColName }}"] = *input.{{ capitalize $field.Name }} - } - {{- else }} - {{- if and $field.IsID (eq $field.GoType "string") }} - if input.{{ capitalize $field.Name }} != "" { - m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} - } else { - m["{{ $field.EffectiveColName }}"] = generateCUID() - } - {{- else }} - m["{{ $field.EffectiveColName }}"] = input.{{ capitalize $field.Name }} - {{- end }} - {{- end }} +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 }} - return m + {{- end }} + rowMaps[i] = m + } + return rowMaps } -func (d *{{ .Model.Name }}Delegate) CreateMany(inputs []{{ .Model.Name }}Create) *CreateManyBuilder[{{ .Model.Name }}, {{ .Model.Name }}Create] { - return &CreateManyBuilder[{{ .Model.Name }}, {{ .Model.Name }}Create]{ +func (d *{{ .Model.Name }}Delegate) CreateMany(records ...RecordInput) *CreateManyBuilder[{{ .Model.Name }}] { + return &CreateManyBuilder[{{ .Model.Name }}]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.execute{{ .Model.Name }}CreateMany, } } -func (d *{{ .Model.Name }}Delegate) CreateManyAndReturn(inputs []{{ .Model.Name }}Create) *CreateManyAndReturnBuilder[{{ .Model.Name }}, {{ .Model.Name }}Create, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { - return &CreateManyAndReturnBuilder[{{ .Model.Name }}, {{ .Model.Name }}Create, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ +func (d *{{ .Model.Name }}Delegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { + return &CreateManyAndReturnBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.execute{{ .Model.Name }}CreateManyAndReturn, } } -func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, inputs []{{ .Model.Name }}Create) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + 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) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.{{ .Model.Name }}InputToMap(input) - } + rowMaps := {{ lowercase .Model.Name }}RecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "{{ .Model.EffectiveTableName }}", rowMaps, {{ .Model.Name }}ColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -224,8 +334,8 @@ func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, inputs 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) + for _, rec := range records { + _, err := txQ.execute{{ .Model.Name }}Create(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -236,12 +346,12 @@ func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, inputs return count, err } -func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Context, inputs []{{ .Model.Name }}Create, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) ([]*{{ .Model.Name }}, error) { - if len(inputs) == 0 { +func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Context, records []RecordInput, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) ([]*{{ .Model.Name }}, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + 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) } } @@ -250,12 +360,9 @@ func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Contex 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) - } + rowMaps := {{ lowercase .Model.Name }}RecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "{{ .Model.EffectiveTableName }}", rowMaps, {{ .Model.Name }}ColOrder, returningCols) - records := make([]*{{ .Model.Name }}, 0) + recordsOut := make([]*{{ .Model.Name }}, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -267,40 +374,39 @@ func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Contex if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.load{{ .Model.Name }}Relations(ctx, records, selects) + return txQ.load{{ .Model.Name }}Relations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*{{ .Model.Name }}, 0) + recordsOut := make([]*{{ .Model.Name }}, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.execute{{ .Model.Name }}Create(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.execute{{ .Model.Name }}Create(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.load{{ .Model.Name }}Relations(ctx, records, selects) + return txQ.load{{ .Model.Name }}Relations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } diff --git a/generator/templates/model_predicate.gotpl b/generator/templates/model_predicate.gotpl index 6878963..7a865b8 100644 --- a/generator/templates/model_predicate.gotpl +++ b/generator/templates/model_predicate.gotpl @@ -28,7 +28,9 @@ func (p UniquePredicate) Validate() error { type Select = {{ .ParentPackageName }}.{{ .Model.Name }}Select type Omit = {{ .ParentPackageName }}.{{ .Model.Name }}Omit -type Create = {{ .ParentPackageName }}.{{ .Model.Name }}Create +func Record(assignments ...{{ .ParentPackageName }}.FieldAssignment) {{ .ParentPackageName }}.RecordInput { + return {{ .ParentPackageName }}.RecordInput{Assignments: assignments} +} func And(preds ...{{ .ParentPackageName }}.Predicate) {{ .ParentPackageName }}.Predicate { return {{ .ParentPackageName }}.And(preds...) diff --git a/generator/templates/model_structs.gotpl b/generator/templates/model_structs.gotpl index e54e760..c1eac27 100644 --- a/generator/templates/model_structs.gotpl +++ b/generator/templates/model_structs.gotpl @@ -8,7 +8,7 @@ type {{ .Model.Name }} struct { {{- end }} } -// {{ .Model.Name }}Create represents the input structure for creation +// {{ .Model.Name }}Create is used for hooks only — the Create API uses FieldAssignment type {{ .Model.Name }}Create struct { {{- range $field := .Model.ScalarFields }} {{ capitalize $field.Name }} {{ if $field.EnumRef }}{{ if $field.IsArray }}[]{{ $field.EnumRef.Name }}Type{{ else }}*{{ $field.EnumRef.Name }}Type{{ end }}{{ else }}{{ if $field.IsArray }}{{ $field.GoType }}{{ else }}{{ if and (ne $field.Default nil) (not $field.Optional) }}*{{ end }}{{ $field.GoType }}{{ end }}{{ end }} `json:"{{ $field.Name }}"` @@ -95,132 +95,7 @@ func (q *Queries) select{{ .Model.Name }}Cols(selects *{{ .Model.Name }}Select, return cols } -func (input {{ .Model.Name }}Create) Validate() error { - errs := &ValidationError{} - {{- range $field := .Model.ScalarFields }} - {{- $fieldName := capitalize $field.Name }} - {{- if $field.EnumRef }} - {{- if $field.IsArray }} - for i, val := range input.{{ $fieldName }} { - if !val.IsValid() { - errs.Add(fmt.Sprintf("{{ $field.Name }}[%d]", i), val, "enum", fmt.Sprintf("invalid enum value %q for field {{ $fieldName }}", val)) - } - } - {{- else if and (eq $field.Default nil) (not $field.Optional) }} - if input.{{ $fieldName }} == nil { - errs.Add("{{ $field.Name }}", nil, "required", "field {{ $fieldName }} is required") - } else if !input.{{ $fieldName }}.IsValid() { - errs.Add("{{ $field.Name }}", *input.{{ $fieldName }}, "enum", fmt.Sprintf("invalid enum value %q for field {{ $fieldName }}", *input.{{ $fieldName }})) - } - {{- else }} - if input.{{ $fieldName }} != nil { - if !input.{{ $fieldName }}.IsValid() { - errs.Add("{{ $field.Name }}", *input.{{ $fieldName }}, "enum", fmt.Sprintf("invalid enum value %q for field {{ $fieldName }}", *input.{{ $fieldName }})) - } - } - {{- end }} - {{- else }} - {{- if eq $field.GoType "string" }} - {{- if and (eq $field.Default nil) (not $field.Optional) (not $field.IsArray) }} - if input.{{ $fieldName }} == "" { - errs.Add("{{ $field.Name }}", input.{{ $fieldName }}, "required", "field {{ $fieldName }} is required") - } - {{- end }} - - {{- if and (ne $field.Default nil) (not $field.Optional) }} - if input.{{ $fieldName }} != nil { - val := *input.{{ $fieldName }} - if strings.Contains(val, "\x00") { - errs.Add("{{ $field.Name }}", val, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(val) { - errs.Add("{{ $field.Name }}", val, "safety", "string must be valid UTF-8") - } - {{- if $field.NativeType }} - {{- if or (eq $field.NativeType.Name "VarChar") (eq $field.NativeType.Name "Char") }} - {{- $limit := index $field.NativeType.Args 0 }} - if utf8.RuneCountInString(val) > {{ $limit }} { - errs.Add("{{ $field.Name }}", val, "length", "string exceeds maximum length of {{ $limit }} characters") - } - {{- end }} - {{- end }} - } - {{- else if $field.Optional }} - if input.{{ $fieldName }} != nil { - val := *input.{{ $fieldName }} - if strings.Contains(val, "\x00") { - errs.Add("{{ $field.Name }}", val, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(val) { - errs.Add("{{ $field.Name }}", val, "safety", "string must be valid UTF-8") - } - {{- if $field.NativeType }} - {{- if or (eq $field.NativeType.Name "VarChar") (eq $field.NativeType.Name "Char") }} - {{- $limit := index $field.NativeType.Args 0 }} - if utf8.RuneCountInString(val) > {{ $limit }} { - errs.Add("{{ $field.Name }}", val, "length", "string exceeds maximum length of {{ $limit }} characters") - } - {{- end }} - {{- end }} - } - {{- else }} - if strings.Contains(input.{{ $fieldName }}, "\x00") { - errs.Add("{{ $field.Name }}", input.{{ $fieldName }}, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.{{ $fieldName }}) { - errs.Add("{{ $field.Name }}", input.{{ $fieldName }}, "safety", "string must be valid UTF-8") - } - {{- if $field.NativeType }} - {{- if or (eq $field.NativeType.Name "VarChar") (eq $field.NativeType.Name "Char") }} - {{- $limit := index $field.NativeType.Args 0 }} - if utf8.RuneCountInString(input.{{ $fieldName }}) > {{ $limit }} { - errs.Add("{{ $field.Name }}", input.{{ $fieldName }}, "length", "string exceeds maximum length of {{ $limit }} characters") - } - {{- end }} - {{- end }} - {{- end }} - {{- else if or (eq $field.GoType "int32") (eq $field.GoType "int64") (eq $field.GoType "int") }} - {{- if $field.NativeType }} - {{- if eq $field.NativeType.Name "SmallInt" }} - {{- if and (ne $field.Default nil) (not $field.Optional) }} - if input.{{ $fieldName }} != nil && (*input.{{ $fieldName }} < -32768 || *input.{{ $fieldName }} > 32767) { - errs.Add("{{ $field.Name }}", *input.{{ $fieldName }}, "range", "value is out of range for SmallInt (-32768 to 32767)") - } - {{- else if $field.Optional }} - if input.{{ $fieldName }} != nil && (*input.{{ $fieldName }} < -32768 || *input.{{ $fieldName }} > 32767) { - errs.Add("{{ $field.Name }}", *input.{{ $fieldName }}, "range", "value is out of range for SmallInt (-32768 to 32767)") - } - {{- else }} - if input.{{ $fieldName }} < -32768 || input.{{ $fieldName }} > 32767 { - errs.Add("{{ $field.Name }}", input.{{ $fieldName }}, "range", "value is out of range for SmallInt (-32768 to 32767)") - } - {{- end }} - {{- else if eq $field.NativeType.Name "TinyInt" }} - {{- if and (ne $field.Default nil) (not $field.Optional) }} - if input.{{ $fieldName }} != nil && (*input.{{ $fieldName }} < -128 || *input.{{ $fieldName }} > 127) { - errs.Add("{{ $field.Name }}", *input.{{ $fieldName }}, "range", "value is out of range for TinyInt (-128 to 127)") - } - {{- else if $field.Optional }} - if input.{{ $fieldName }} != nil && (*input.{{ $fieldName }} < -128 || *input.{{ $fieldName }} > 127) { - errs.Add("{{ $field.Name }}", *input.{{ $fieldName }}, "range", "value is out of range for TinyInt (-128 to 127)") - } - {{- else }} - if input.{{ $fieldName }} < -128 || input.{{ $fieldName }} > 127 { - errs.Add("{{ $field.Name }}", input.{{ $fieldName }}, "range", "value is out of range for TinyInt (-128 to 127)") - } - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - - if errs.HasErrors() { - return *errs - } - return nil -} diff --git a/integration/benchmark_test.go b/integration/benchmark_test.go index 5df2b69..46ae27e 100644 --- a/integration/benchmark_test.go +++ b/integration/benchmark_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "fmt" "integration/valk" + "integration/valk/user" "strconv" "testing" "time" @@ -59,10 +60,10 @@ func TestCreationBenchmark(t *testing.T) { t.Logf("Running %d iterations of ORM Create...", iterations) startORM := time.Now() for i := range iterations { - _, err := db.User.Create(valk.UserCreate{ - Email: fmt.Sprintf("orm-%d@example.com", i), - PhoneNum: fmt.Sprintf("+54321%d", i), - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set(fmt.Sprintf("orm-%d@example.com", i)), + user.PhoneNum.Set(fmt.Sprintf("+54321%d", i)), + ).Exec(ctx) if err != nil { t.Fatalf("ORM create failed: %v", err) } @@ -112,10 +113,10 @@ func BenchmarkORMCreate(b *testing.B) { ctx := context.Background() for i := 0; b.Loop(); i++ { - _, err := db.User.Create(valk.UserCreate{ - Email: fmt.Sprintf("bench-orm-%d@example.com", i), - PhoneNum: fmt.Sprintf("+98765%d", i), - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set(fmt.Sprintf("bench-orm-%d@example.com", i)), + user.PhoneNum.Set(fmt.Sprintf("+98765%d", i)), + ).Exec(ctx) if err != nil { b.Fatalf("ORM create failed: %v", err) } diff --git a/integration/create_many_test.go b/integration/create_many_test.go index 8db74d4..cca50dd 100644 --- a/integration/create_many_test.go +++ b/integration/create_many_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "integration/valk" + "integration/valk/post" + "integration/valk/user" "testing" ) @@ -14,20 +16,11 @@ func TestCreateMany(t *testing.T) { defer cleanup() t.Run("CreateMany returns correct count", func(t *testing.T) { - count, err := client.User.CreateMany([]valk.UserCreate{ - { - Email: "bulk1@example.com", - PhoneNum: "+111", - }, - { - Email: "bulk2@example.com", - PhoneNum: "+222", - }, - { - Email: "bulk3@example.com", - PhoneNum: "+333", - }, - }).Exec(ctx) + count, err := client.User.CreateMany( + user.Record(user.Email.Set("bulk1@example.com"), user.PhoneNum.Set("+111")), + user.Record(user.Email.Set("bulk2@example.com"), user.PhoneNum.Set("+222")), + user.Record(user.Email.Set("bulk3@example.com"), user.PhoneNum.Set("+333")), + ).Exec(ctx) if err != nil { t.Fatalf("CreateMany failed: %v", err) @@ -48,24 +41,18 @@ func TestCreateMany(t *testing.T) { }) t.Run("CreateManyAndReturn works and supports Select", func(t *testing.T) { - author, err := client.User.Create(valk.UserCreate{ - Email: "author@example.com", - PhoneNum: "+444", - }).Exec(ctx) + author, err := client.User.Create( + user.Email.Set("author@example.com"), + user.PhoneNum.Set("+444"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create author: %v", err) } - posts, err := client.Post.CreateManyAndReturn([]valk.PostCreate{ - { - Title: "Post One", - AuthorId: author.Id, - }, - { - Title: "Post Two", - AuthorId: author.Id, - }, - }).Select(valk.PostSelect{ + posts, err := client.Post.CreateManyAndReturn( + post.Record(post.Title.Set("Post One"), post.AuthorId.Set(author.Id)), + post.Record(post.Title.Set("Post Two"), post.AuthorId.Set(author.Id)), + ).Select(valk.PostSelect{ Id: true, Title: true, Author: &valk.UserSelect{ diff --git a/integration/create_test.go b/integration/create_test.go index 60945e6..1c0beff 100644 --- a/integration/create_test.go +++ b/integration/create_test.go @@ -3,6 +3,7 @@ package main import ( "context" "integration/valk" + "integration/valk/user" "strings" "testing" ) @@ -12,10 +13,10 @@ func TestCreateBasic(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "test@example.com", - PhoneNum: "+123456789", - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("test@example.com"), + user.PhoneNum.Set("+123456789"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -54,10 +55,9 @@ func TestCreateWithSelect(t *testing.T) { ctx := context.Background() u, err := db.User.Create( - valk.UserCreate{ - Email: "select@example.com", - PhoneNum: "+999999999", - }).Select(valk.UserSelect{ + user.Email.Set("select@example.com"), + user.PhoneNum.Set("+999999999"), + ).Select(valk.UserSelect{ Id: true, Email: true, Profile: &valk.ProfileSelect{ @@ -89,10 +89,10 @@ func TestCreateWithOmit(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "omit@example.com", - PhoneNum: "+888888888", - }).Omit(valk.UserOmit{ + u, err := db.User.Create( + user.Email.Set("omit@example.com"), + user.PhoneNum.Set("+888888888"), + ).Omit(valk.UserOmit{ PhoneNum: true, }).Exec(ctx) @@ -119,13 +119,11 @@ func TestCreateWithCustomEnum(t *testing.T) { defer cleanup() ctx := context.Background() - adminRole := valk.UserRole.Admin - - u, err := db.User.Create(valk.UserCreate{ - Email: "admin@example.com", - PhoneNum: "+000000000", - Role: &adminRole, - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("admin@example.com"), + user.PhoneNum.Set("+000000000"), + user.Role.Set(valk.UserRole.Admin), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -142,10 +140,10 @@ func TestCreateValidation(t *testing.T) { ctx := context.Background() // basic required check - _, err := db.User.Create(valk.UserCreate{ + _, err := db.User.Create( // no email - PhoneNum: "+123456789", - }).Exec(ctx) + user.PhoneNum.Set("+123456789"), + ).Exec(ctx) if err == nil { t.Fatal("expected error creating user with empty required email, got nil") } @@ -166,12 +164,11 @@ func TestCreateValidation(t *testing.T) { } // invalid enum - invalidRole := valk.UserRoleType("INVALID_ROLE") - _, err = db.User.Create(valk.UserCreate{ - Email: "invalid_role@example.com", - PhoneNum: "+123456789", - Role: &invalidRole, - }).Exec(ctx) + _, err = db.User.Create( + user.Email.Set("invalid_role@example.com"), + user.PhoneNum.Set("+123456789"), + user.Role.Set(valk.UserRoleType("INVALID_ROLE")), + ).Exec(ctx) if err == nil { t.Fatal("expected error creating user with invalid enum role, got nil") } @@ -190,10 +187,10 @@ func TestCreateValidation(t *testing.T) { } // Multi-error (no email + null-byte) - _, err = db.User.Create(valk.UserCreate{ + _, err = db.User.Create( // no email - PhoneNum: "phone\x00num", - }).Exec(ctx) + user.PhoneNum.Set("phone\x00num"), + ).Exec(ctx) if err == nil { t.Fatal("expected error, got nil") } @@ -206,10 +203,10 @@ func TestCreateValidation(t *testing.T) { } // UTF-8 validation - _, err = db.User.Create(valk.UserCreate{ - Email: "utf8@example.com", - PhoneNum: "invalid\xffutf8", - }).Exec(ctx) + _, err = db.User.Create( + user.Email.Set("utf8@example.com"), + user.PhoneNum.Set("invalid\xffutf8"), + ).Exec(ctx) if err == nil { t.Fatal("expected error for invalid UTF-8, got nil") } diff --git a/integration/main.go b/integration/main.go index a63ccc7..7d615c1 100644 --- a/integration/main.go +++ b/integration/main.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/json" - "errors" "fmt" "integration/valk" "integration/valk/category" @@ -27,88 +26,117 @@ type SeedData struct { } func seed(db *valk.DB, ctx context.Context) *SeedData { - db.User.BeforeCreate(func(ctx context.Context, uc *user.Create) error { - return errors.New("AAAAAAAAH") + 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) + } + return nil }) - referrer, err := db.User.Create(user.Create{ - Email: "referrer@example.com", - PhoneNum: "555-0001", - Password: new("pass123"), - Role: &valk.UserRole.Admin, - }).Exec(ctx) + var usersToCreate []valk.RecordInput + + for i := range 20 { + usersToCreate = append(usersToCreate, user.Record( + user.Email.Set(fmt.Sprintf("email-%d", i)), + user.PhoneNum.Set(fmt.Sprintf("555-%d", i)), + user.Password.Set(fmt.Sprintf("password-%d", i)), + )) + } + + _, err := db.User.CreateMany(usersToCreate...).Exec(ctx) + if err != nil { + log.Fatalf("failed to create users: %v", err) + } + + _, err = db.User.CreateMany( + user.Record( + user.Email.Set("test"), + user.Password.Set("passwd"), + ), + user.Record( + user.Email.Set("again"), + user.Password.Set("123456"), + ), + ).Exec(ctx) + referrer, err := db.User.Create( + user.Email.Set("referrer@example.com"), + user.PhoneNum.Set("555-0001"), + user.Password.Set("pass123"), + user.Role.Set(valk.UserRole.Student), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create referrer: %v", err) } - referred, err := db.User.Create(user.Create{ - Email: "referred@example.com", - PhoneNum: "555-0002", - Password: new("pass456"), - Role: &valk.UserRole.Student, - ReferredById: &referrer.Id, - }).Exec(ctx) + referred, err := db.User.Create( + user.Email.Set("referred@example.com"), + user.PhoneNum.Set("555-0002"), + user.Password.Set("pass456"), + user.Role.Set(valk.UserRole.Student), + user.ReferredById.Set(referrer.Id), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create referred: %v", err) } - prof, err := db.Profile.Create(profile.Create{ - Bio: new("BLEH"), - UserId: referred.Id, - }).Exec(ctx) + prof, err := db.Profile.Create( + profile.Bio.Set("BLEH"), + profile.UserId.Set(referred.Id), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create profile: %v", err) } _ = prof - p, err := db.Post.Create(post.Create{ - Title: "Valkyrie ORM Deep Dive", - Content: new("skrrrt"), - AuthorId: referred.Id, - }).Exec(ctx) + p, err := db.Post.Create( + post.Title.Set("Valkyrie ORM Deep Dive"), + post.Content.Set("skrrrt"), + post.AuthorId.Set(referred.Id), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create post: %v", err) } - cat, err := db.Category.Create(category.Create{ - Name: "Programming", - }).Exec(ctx) + cat, err := db.Category.Create( + category.Name.Set("Programming"), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create category: %v", err) } - _, err = db.CategoryToPost.Create(categoryToPost.Create{ - PostId: p.Id, - CategoryId: cat.Id, - }).Exec(ctx) + _, err = db.CategoryToPost.Create( + categoryToPost.PostId.Set(p.Id), + categoryToPost.CategoryId.Set(cat.Id), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create CategoryToPost: %v", err) } meta1 := json.RawMessage(`{"rating":5,"verified":true}`) - _, err = db.Comment.Create(comment.Create{ - Textify: 100, - Dummy3: "dummy_val_1", - Dummy1: 42, - Dummy2: "dummy_val_2", - PostId: p.Id, - AuthorId: referrer.Id, - Meta: &meta1, - }).Exec(ctx) + _, err = db.Comment.Create( + comment.Textify.Set(100), + comment.Dummy3.Set("dummy_val_1"), + comment.Dummy1.Set(42), + comment.Dummy2.Set("dummy_val_2"), + comment.PostId.Set(p.Id), + comment.AuthorId.Set(referrer.Id), + comment.Meta.Set(meta1), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create comment 1: %v", err) } meta2 := json.RawMessage(`{"rating":4,"verified":false}`) - _, err = db.Comment.Create(comment.Create{ - Textify: 200, - Dummy3: "dummy_val_3", - Dummy1: 84, - Dummy2: "dummy_val_4", - PostId: p.Id, - AuthorId: referred.Id, - Meta: &meta2, - }).Exec(ctx) + _, err = db.Comment.Create( + comment.Textify.Set(200), + comment.Dummy3.Set("dummy_val_3"), + comment.Dummy1.Set(84), + comment.Dummy2.Set("dummy_val_4"), + comment.PostId.Set(p.Id), + comment.AuthorId.Set(referred.Id), + comment.Meta.Set(meta2), + ).Exec(ctx) if err != nil { log.Fatalf("failed to create comment 2: %v", err) } @@ -249,19 +277,19 @@ func runManualTransaction(db *valk.DB, ctx context.Context) { defer tx.Rollback() fmt.Println("Manual Transaction: started successfully") - author, err := tx.User.Create(user.Create{ - Email: "clancySizer@gmail.com", - PhoneNum: "+1234567890", - }).Exec(ctx) + author, err := tx.User.Create( + user.Email.Set("clancySizer@gmail.com"), + user.PhoneNum.Set("+1234567890"), + ).Exec(ctx) if err != nil { fmt.Printf("failed to create user: %+v", err) return } - postWithAuthor, err := tx.Post.Create(post.Create{ - Title: "A Post", - AuthorId: author.Id, - }).Select(post.Select{ + postWithAuthor, err := tx.Post.Create( + post.Title.Set("A Post"), + post.AuthorId.Set(author.Id), + ).Select(post.Select{ Id: true, Title: true, Author: &user.Select{ @@ -287,18 +315,18 @@ func runBlockBasedTransaction(db *valk.DB, ctx context.Context) { err := db.Transaction(ctx, func(tx *valk.Tx) error { fmt.Println("Block-based Transaction: started successfully") - author, err := tx.User.Create(user.Create{ - Email: "clancySizer@gmail.com", - PhoneNum: "+1234567890", - }).Exec(ctx) + author, err := tx.User.Create( + user.Email.Set("clancySizer@gmail.com"), + user.PhoneNum.Set("+1234567890"), + ).Exec(ctx) if err != nil { return err } - postWithAuthor, err := tx.Post.Create(post.Create{ - Title: "A Post", - AuthorId: author.Id, - }).Select(post.Select{ + postWithAuthor, err := tx.Post.Create( + post.Title.Set("A Post"), + post.AuthorId.Set(author.Id), + ).Select(post.Select{ Id: true, Title: true, Author: &user.Select{ diff --git a/integration/read_test.go b/integration/read_test.go index 53cbeed..4989ad9 100644 --- a/integration/read_test.go +++ b/integration/read_test.go @@ -18,10 +18,7 @@ func TestFindUniqueWithNoFieldsSet(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{ - Email: "onlyuser@example.com", - PhoneNum: "000", - }).Exec(ctx) + _, err := db.User.Create(user.Email.Set("onlyuser@example.com"), user.PhoneNum.Set("000")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -37,11 +34,11 @@ func TestFindUniqueConflictingCompoundFields(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "a@example.com", PhoneNum: "111"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("a@example.com"), user.PhoneNum.Set("111")).Exec(ctx) if err != nil { t.Fatalf("seed a failed: %v", err) } - _, err = db.User.Create(valk.UserCreate{Email: "b@example.com", PhoneNum: "222"}).Exec(ctx) + _, err = db.User.Create(user.Email.Set("b@example.com"), user.PhoneNum.Set("222")).Exec(ctx) if err != nil { t.Fatalf("seed b failed: %v", err) } @@ -60,7 +57,7 @@ func TestSelectWithNoFieldsSet(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "empty_select@example.com", PhoneNum: "333"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("empty_select@example.com"), user.PhoneNum.Set("333")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -79,7 +76,7 @@ func TestOmitAllFields(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "omit_all@example.com", PhoneNum: "334"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("omit_all@example.com"), user.PhoneNum.Set("334")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -104,7 +101,7 @@ func TestOmitIdFieldStillAllowsFilterById(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{Email: "omit_id@example.com", PhoneNum: "335"}).Exec(ctx) + u, err := db.User.Create(user.Email.Set("omit_id@example.com"), user.PhoneNum.Set("335")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -126,7 +123,7 @@ func TestRelationLoadWithNoRelatedRows(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "noposts@example.com", PhoneNum: "444"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("noposts@example.com"), user.PhoneNum.Set("444")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -151,11 +148,11 @@ func TestFindUniqueRelationLoad(t *testing.T) { defer cleanup() ctx := context.Background() - author, err := db.User.Create(valk.UserCreate{Email: "unique_rel@example.com", PhoneNum: "445"}).Exec(ctx) + author, err := db.User.Create(user.Email.Set("unique_rel@example.com"), user.PhoneNum.Set("445")).Exec(ctx) if err != nil { t.Fatalf("seed author failed: %v", err) } - _, err = db.Post.Create(valk.PostCreate{Title: "Unique Rel Post", AuthorId: author.Id}).Exec(ctx) + _, err = db.Post.Create(post.Title.Set("Unique Rel Post"), post.AuthorId.Set(author.Id)).Exec(ctx) if err != nil { t.Fatalf("seed post failed: %v", err) } @@ -177,11 +174,11 @@ func TestFindFirstRelationLoad(t *testing.T) { defer cleanup() ctx := context.Background() - author, err := db.User.Create(valk.UserCreate{Email: "first_rel@example.com", PhoneNum: "446"}).Exec(ctx) + author, err := db.User.Create(user.Email.Set("first_rel@example.com"), user.PhoneNum.Set("446")).Exec(ctx) if err != nil { t.Fatalf("seed author failed: %v", err) } - _, err = db.Post.Create(valk.PostCreate{Title: "First Rel Post", AuthorId: author.Id}).Exec(ctx) + _, err = db.Post.Create(post.Title.Set("First Rel Post"), post.AuthorId.Set(author.Id)).Exec(ctx) if err != nil { t.Fatalf("seed post failed: %v", err) } @@ -230,12 +227,12 @@ func TestDuplicateUniqueCreateFails(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "dup@example.com", PhoneNum: "555"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("dup@example.com"), user.PhoneNum.Set("555")).Exec(ctx) if err != nil { t.Fatalf("first create failed: %v", err) } - _, err = db.User.Create(valk.UserCreate{Email: "dup@example.com", PhoneNum: "556"}).Exec(ctx) + _, err = db.User.Create(user.Email.Set("dup@example.com"), user.PhoneNum.Set("556")).Exec(ctx) if err == nil { t.Error("expected a unique constraint violation on duplicate email, got nil error") } @@ -256,10 +253,7 @@ func TestConcurrentDuplicateCreateOnlyOneSucceeds(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - _, err := db.User.Create(valk.UserCreate{ - Email: "race@example.com", - PhoneNum: "race-phone", - }).Exec(ctx) + _, err := db.User.Create(user.Email.Set("race@example.com"), user.PhoneNum.Set("race-phone")).Exec(ctx) mu.Lock() defer mu.Unlock() if err == nil { @@ -281,11 +275,11 @@ func TestWhitespacePaddedEmailNotTreatedAsDuplicate(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "dup2@example.com", PhoneNum: "601"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("dup2@example.com"), user.PhoneNum.Set("601")).Exec(ctx) if err != nil { t.Fatalf("first create failed: %v", err) } - _, err = db.User.Create(valk.UserCreate{Email: " dup2@example.com", PhoneNum: "602"}).Exec(ctx) + _, err = db.User.Create(user.Email.Set(" dup2@example.com"), user.PhoneNum.Set("602")).Exec(ctx) if err != nil { t.Fatalf("expected leading-whitespace email to be treated as a distinct value, create failed: %v", err) } @@ -304,7 +298,7 @@ func TestEmailCaseSensitivity(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "CaseTest@Example.com", PhoneNum: "603"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("CaseTest@Example.com"), user.PhoneNum.Set("603")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -339,7 +333,7 @@ func TestVeryLongEmailValue(t *testing.T) { longEmail := strings.Repeat("a", 5000) + "@example.com" - _, err := db.User.Create(valk.UserCreate{Email: longEmail, PhoneNum: "999"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set(longEmail), user.PhoneNum.Set("999")).Exec(ctx) if err != nil { t.Fatalf("create with a very long email failed: %v", err) } @@ -362,10 +356,7 @@ func TestCreateWithEmptyStringEmail(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{ - Email: "", - PhoneNum: "800", - }).Exec(ctx) + _, err := db.User.Create(user.Email.Set(""), user.PhoneNum.Set("800")).Exec(ctx) if err != nil { return } @@ -385,19 +376,12 @@ func TestOptionalEnumNullVsValueFilter(t *testing.T) { ctx := context.Background() adminRole := valk.UserRole.Admin - _, err := db.User.Create(valk.UserCreate{ - Email: "role_set@example.com", - PhoneNum: "700", - RoleOptional: &adminRole, - }).Exec(ctx) + _, err := db.User.Create(user.Email.Set("role_set@example.com"), user.PhoneNum.Set("700"), user.RoleOptional.Set(adminRole)).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } - _, err = db.User.Create(valk.UserCreate{ - Email: "role_unset@example.com", - PhoneNum: "701", - }).Exec(ctx) + _, err = db.User.Create(user.Email.Set("role_unset@example.com"), user.PhoneNum.Set("701")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -424,7 +408,7 @@ func TestSQLInjectionVariants(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "injection_target@example.com", PhoneNum: "900"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("injection_target@example.com"), user.PhoneNum.Set("900")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -476,11 +460,11 @@ func TestCompoundUniqueWithOneFieldMatchingWrongRow(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{Email: "compound_a@example.com", PhoneNum: "701a"}).Exec(ctx) + _, err := db.User.Create(user.Email.Set("compound_a@example.com"), user.PhoneNum.Set("701a")).Exec(ctx) if err != nil { t.Fatalf("seed a failed: %v", err) } - _, err = db.User.Create(valk.UserCreate{Email: "compound_b@example.com", PhoneNum: "701b"}).Exec(ctx) + _, err = db.User.Create(user.Email.Set("compound_b@example.com"), user.PhoneNum.Set("701b")).Exec(ctx) if err != nil { t.Fatalf("seed b failed: %v", err) } @@ -499,10 +483,7 @@ func TestCompoundUniqueConstraintEdgeCases(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{ - Email: "compound_edge@example.com", - PhoneNum: "800a", - }).Exec(ctx) + _, err := db.User.Create(user.Email.Set("compound_edge@example.com"), user.PhoneNum.Set("800a")).Exec(ctx) if err != nil { t.Fatalf("seed failed: %v", err) } @@ -563,32 +544,26 @@ func TestJsonField(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(user.Create{ - Email: "json_test_user@example.com", - PhoneNum: "555-json", - }).Exec(ctx) + u, err := db.User.Create(user.Email.Set("json_test_user@example.com"), + user.PhoneNum.Set("555-json")).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) } - p, err := db.Post.Create(post.Create{ - Title: "JSON Post", - AuthorId: u.Id, - }).Exec(ctx) + p, err := db.Post.Create(post.Title.Set("JSON Post"), + post.AuthorId.Set(u.Id)).Exec(ctx) if err != nil { t.Fatalf("failed to create post: %v", err) } metaVal := json.RawMessage(`{"tags":["valkyrie","orm"],"version":1}`) - c, err := db.Comment.Create(valk.CommentCreate{ - Textify: 1, - Dummy3: "dummy3", - Dummy1: 10, - Dummy2: "dummy2", - PostId: p.Id, - AuthorId: u.Id, - Meta: &metaVal, - }).Exec(ctx) + c, err := db.Comment.Create(comment.Textify.Set(1), + comment.Dummy3.Set("dummy3"), + comment.Dummy1.Set(10), + comment.Dummy2.Set("dummy2"), + comment.PostId.Set(p.Id), + comment.AuthorId.Set(u.Id), + comment.Meta.Set(metaVal)).Exec(ctx) if err != nil { t.Fatalf("failed to create comment with JSON: %v", err) } diff --git a/integration/selection_test.go b/integration/selection_test.go index d083446..072bd6f 100644 --- a/integration/selection_test.go +++ b/integration/selection_test.go @@ -5,6 +5,10 @@ import ( "encoding/json" "fmt" "integration/valk" + "integration/valk/comment" + "integration/valk/post" + "integration/valk/profile" + "integration/valk/user" "testing" _ "modernc.org/sqlite" @@ -15,43 +19,43 @@ func TestRelationLoadChildHoldsFK(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "parent@example.com", - PhoneNum: "+111111111", - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("parent@example.com"), + user.PhoneNum.Set("+111111111"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) } - _, err = db.Post.Create(valk.PostCreate{ - Title: "Post 1", - Content: new("Content 1"), - AuthorId: u.Id, - }).Exec(ctx) + _, err = db.Post.Create( + post.Title.Set("Post 1"), + post.Content.Set("Content 1"), + post.AuthorId.Set(u.Id), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create post 1: %v", err) } - _, err = db.Post.Create(valk.PostCreate{ - Title: "Post 2", - Content: new("Content 2"), - AuthorId: u.Id, - }).Exec(ctx) + _, err = db.Post.Create( + post.Title.Set("Post 2"), + post.Content.Set("Content 2"), + post.AuthorId.Set(u.Id), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create post 2: %v", err) } - _, err = db.Profile.Create(valk.ProfileCreate{ - Bio: new("My bio"), - UserId: u.Id, - }).Exec(ctx) + _, err = db.Profile.Create( + profile.Bio.Set("My bio"), + profile.UserId.Set(u.Id), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create profile: %v", err) } - u2, err := db.User.Create(valk.UserCreate{ - Email: "parent2@example.com", - PhoneNum: "+222222222", - }).Select(valk.UserSelect{ + u2, err := db.User.Create( + user.Email.Set("parent2@example.com"), + user.PhoneNum.Set("+222222222"), + ).Select(valk.UserSelect{ Id: true, Email: true, Posts: &valk.PostSelect{ @@ -83,18 +87,18 @@ func TestRelationLoadParentHoldsFK(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "author@example.com", - PhoneNum: "+222222222", - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("author@example.com"), + user.PhoneNum.Set("+222222222"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) } - p, err := db.Post.Create(valk.PostCreate{ - Title: "My Post", - AuthorId: u.Id, - }).Select(valk.PostSelect{ + p, err := db.Post.Create( + post.Title.Set("My Post"), + post.AuthorId.Set(u.Id), + ).Select(valk.PostSelect{ Id: true, Title: true, AuthorId: true, @@ -129,19 +133,19 @@ func TestRelationLoadSelfRelation(t *testing.T) { defer cleanup() ctx := context.Background() - referrer, err := db.User.Create(valk.UserCreate{ - Email: "referrer@example.com", - PhoneNum: "+333333333", - }).Exec(ctx) + referrer, err := db.User.Create( + user.Email.Set("referrer@example.com"), + user.PhoneNum.Set("+333333333"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create referrer: %v", err) } - referred, err := db.User.Create(valk.UserCreate{ - Email: "referred@example.com", - PhoneNum: "+444444444", - ReferredById: &referrer.Id, - }).Select(valk.UserSelect{ + referred, err := db.User.Create( + user.Email.Set("referred@example.com"), + user.PhoneNum.Set("+444444444"), + user.ReferredById.Set(referrer.Id), + ).Select(valk.UserSelect{ Id: true, Email: true, ReferredById: true, @@ -173,38 +177,38 @@ func TestRelationLoadDeepNesting(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "deep@example.com", - PhoneNum: "+555555555", - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("deep@example.com"), + user.PhoneNum.Set("+555555555"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) } - p, err := db.Post.Create(valk.PostCreate{ - Title: "Deep Post", - AuthorId: u.Id, - }).Exec(ctx) + p, err := db.Post.Create( + post.Title.Set("Deep Post"), + post.AuthorId.Set(u.Id), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create post: %v", err) } - _, err = db.Comment.Create(valk.CommentCreate{ - Textify: 42, - Dummy3: "d3", - Dummy1: 1, - Dummy2: "d2", - PostId: p.Id, - AuthorId: u.Id, - }).Exec(ctx) + _, err = db.Comment.Create( + comment.Textify.Set(42), + comment.Dummy3.Set("d3"), + comment.Dummy1.Set(1), + comment.Dummy2.Set("d2"), + comment.PostId.Set(p.Id), + comment.AuthorId.Set(u.Id), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create comment: %v", err) } - p2, err := db.Post.Create(valk.PostCreate{ - Title: "Another Post", - AuthorId: u.Id, - }).Select(valk.PostSelect{ + p2, err := db.Post.Create( + post.Title.Set("Another Post"), + post.AuthorId.Set(u.Id), + ).Select(valk.PostSelect{ Id: true, Title: true, Author: &valk.UserSelect{ diff --git a/integration/validation_test.go b/integration/validation_test.go index d70c5a8..5e2170b 100644 --- a/integration/validation_test.go +++ b/integration/validation_test.go @@ -13,6 +13,7 @@ import ( "unicode/utf8" "integration/valk" + "integration/valk/user" ) func TestCreate_DuplicateEmail_Rejected(t *testing.T) { @@ -20,15 +21,12 @@ func TestCreate_DuplicateEmail_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - input := valk.UserCreate{Email: "dupe@example.com", PhoneNum: "+100000001"} - - if _, err := db.User.Create(input).Exec(ctx); err != nil { + if _, err := db.User.Create(user.Email.Set("dupe@example.com"), user.PhoneNum.Set("+100000001")).Exec(ctx); err != nil { t.Fatalf("first insert should succeed, got: %v", err) } // Same email, different phoneNum, must still fail if email is unique - dupe := valk.UserCreate{Email: "dupe@example.com", PhoneNum: "+100000002"} - if _, err := db.User.Create(dupe).Exec(ctx); err == nil { + if _, err := db.User.Create(user.Email.Set("dupe@example.com"), user.PhoneNum.Set("+100000002")).Exec(ctx); err == nil { t.Fatal("expected unique constraint violation on duplicate email, got nil error") } @@ -47,15 +45,15 @@ func TestCreate_DuplicateEmail_CaseVariants(t *testing.T) { defer cleanup() ctx := context.Background() - if _, err := db.User.Create(valk.UserCreate{ - Email: "Case@Example.com", PhoneNum: "+100000003", - }).Exec(ctx); err != nil { + if _, err := db.User.Create( + user.Email.Set("Case@Example.com"), user.PhoneNum.Set("+100000003"), + ).Exec(ctx); err != nil { t.Fatalf("failed initial insert: %v", err) } - _, err := db.User.Create(valk.UserCreate{ - Email: "case@example.com", PhoneNum: "+100000004", - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("case@example.com"), user.PhoneNum.Set("+100000004"), + ).Exec(ctx) t.Logf("case-variant email insert result: err=%v (confirm this matches intended uniqueness semantics)", err) } @@ -65,16 +63,16 @@ func TestCreate_CompoundUnique_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - if _, err := db.User.Create(valk.UserCreate{ - Email: "a@example.com", PhoneNum: "+199999999", - }).Exec(ctx); err != nil { + if _, err := db.User.Create( + user.Email.Set("a@example.com"), user.PhoneNum.Set("+199999999"), + ).Exec(ctx); err != nil { t.Fatalf("first insert failed: %v", err) } // Same email + same phoneNum hits @@unique([email, phoneNum]). - if _, err := db.User.Create(valk.UserCreate{ - Email: "a@example.com", PhoneNum: "+199999999", - }).Exec(ctx); err == nil { + if _, err := db.User.Create( + user.Email.Set("a@example.com"), user.PhoneNum.Set("+199999999"), + ).Exec(ctx); err == nil { t.Fatal("expected unique constraint violation on duplicate (email, phoneNum)") } } @@ -84,7 +82,7 @@ func TestCreate_ZeroValueInput_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{}).Exec(ctx) + _, err := db.User.Create().Exec(ctx) if err == nil { t.Fatal("expected error creating user with entirely zero-value input (missing required email)") } @@ -95,9 +93,9 @@ func TestCreate_EmptyStringEmail_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{ - Email: "", PhoneNum: "+100000005", - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set(""), user.PhoneNum.Set("+100000005"), + ).Exec(ctx) if err == nil { t.Fatal("expected error creating user with empty-string email") } @@ -108,9 +106,9 @@ func TestCreate_WhitespaceOnlyEmail_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - _, err := db.User.Create(valk.UserCreate{ - Email: " ", PhoneNum: "+100000006", - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set(" "), user.PhoneNum.Set("+100000006"), + ).Exec(ctx) if err == nil { t.Log("WARNING: whitespace-only email was accepted confirm this is intentional, not an oversight") @@ -122,12 +120,11 @@ func TestCreate_ReferredBy_NonexistentID_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - fakeID := "clnonexistent00000000000" - _, err := db.User.Create(valk.UserCreate{ - Email: "orphan@example.com", - PhoneNum: "+100000007", - ReferredById: &fakeID, - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("orphan@example.com"), + user.PhoneNum.Set("+100000007"), + user.ReferredById.Set("clnonexistent00000000000"), + ).Exec(ctx) if err == nil { t.Fatal("expected FK violation when referredById points to a nonexistent user") @@ -140,17 +137,16 @@ func TestCreate_ReferredBy_SelfReference_Rejected(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "self@example.com", PhoneNum: "+100000008", - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("self@example.com"), user.PhoneNum.Set("+100000008"), + ).Exec(ctx) if err != nil { t.Fatalf("setup insert failed: %v", err) } - bReferrer := u.Id - b, err := db.User.Create(valk.UserCreate{ - Email: "referred@example.com", PhoneNum: "+100000009", ReferredById: &bReferrer, - }).Exec(ctx) + b, err := db.User.Create( + user.Email.Set("referred@example.com"), user.PhoneNum.Set("+100000009"), user.ReferredById.Set(u.Id), + ).Exec(ctx) if err != nil { t.Fatalf("valid referral chain should succeed: %v", err) } @@ -166,9 +162,9 @@ func TestCreate_InvalidEnumValue_BypassingTypeSystem(t *testing.T) { pffff := valk.UserRoleType("totallyNotARole") - _, err := db.User.Create(valk.UserCreate{ - Email: "pffff-role@example.com", PhoneNum: "+100000010", Role: &pffff, - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("pffff-role@example.com"), user.PhoneNum.Set("+100000010"), user.Role.Set(pffff), + ).Exec(ctx) if err == nil { t.Fatal("expected rejection of an enum value outside the declared domain") @@ -180,9 +176,9 @@ func TestCreate_DefaultEnumAppliedWhenNil(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "noRole@example.com", PhoneNum: "+100000011", Role: nil, - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("noRole@example.com"), user.PhoneNum.Set("+100000011"), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create: %v", err) } @@ -213,9 +209,9 @@ func TestCreate_StringEdgeCases(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: tc.email, PhoneNum: tc.phone, - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set(tc.email), user.PhoneNum.Set(tc.phone), + ).Exec(ctx) if tc.expectError && err == nil { t.Fatalf("expected error for input %q, got success (id=%s)", tc.email, u.Id) @@ -245,17 +241,17 @@ func TestCreate_Select_ForceIncludesFK_EvenWhenNotExplicitlySelected(t *testing. defer cleanup() ctx := context.Background() - referrer, err := db.User.Create(valk.UserCreate{ - Email: "referrer@example.com", PhoneNum: "+300000002", - }).Exec(ctx) + referrer, err := db.User.Create( + user.Email.Set("referrer@example.com"), user.PhoneNum.Set("+300000002"), + ).Exec(ctx) if err != nil { t.Fatalf("setup failed: %v", err) } rid := referrer.Id - u, err := db.User.Create(valk.UserCreate{ - Email: "referredfk@example.com", PhoneNum: "+300000003", ReferredById: &rid, - }).Select(valk.UserSelect{ + u, err := db.User.Create( + user.Email.Set("referredfk@example.com"), user.PhoneNum.Set("+300000003"), user.ReferredById.Set(rid), + ).Select(valk.UserSelect{ Id: true, ReferredBy: &valk.UserSelect{Id: true}, // ReferredById itself intenionally not selected @@ -278,9 +274,9 @@ func TestCreate_Select_EmptyStruct_ReturnsEverything(t *testing.T) { defer cleanup() ctx := context.Background() - u, err := db.User.Create(valk.UserCreate{ - Email: "empty-select@example.com", PhoneNum: "+300000004", - }).Select(valk.UserSelect{}).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("empty-select@example.com"), user.PhoneNum.Set("+300000004"), + ).Select(valk.UserSelect{}).Exec(ctx) if err != nil { t.Fatalf("create failed: %v", err) @@ -297,9 +293,9 @@ func TestCreate_ContextAlreadyCancelled(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel before the call even starts - _, err := db.User.Create(valk.UserCreate{ - Email: "cancelled@example.com", PhoneNum: "+400000001", - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("cancelled@example.com"), user.PhoneNum.Set("+400000001"), + ).Exec(ctx) if err == nil { t.Fatal("expected error when context is already cancelled") @@ -324,9 +320,9 @@ func TestCreate_ContextTimeout_DuringExec(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) defer cancel() - _, err := db.User.Create(valk.UserCreate{ - Email: "timeout@example.com", PhoneNum: "+400000002", - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("timeout@example.com"), user.PhoneNum.Set("+400000002"), + ).Exec(ctx) if err == nil { t.Log("create succeeded despite near-zero timeout likely fine if driver executes faster than ctx propagation, but worth a second look under load") @@ -347,10 +343,10 @@ func TestCreate_ConcurrentDuplicateEmail_ExactlyOneWins(t *testing.T) { wg.Add(1) go func(n int) { defer wg.Done() - _, err := db.User.Create(valk.UserCreate{ - Email: "race@example.com", - PhoneNum: fmt.Sprintf("+50000%04d", n), // distinct phones so only email collides - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("race@example.com"), + user.PhoneNum.Set(fmt.Sprintf("+50000%04d", n)), // distinct phones so only email collides + ).Exec(ctx) if err != nil { atomic.AddInt64(&failures, 1) } else { @@ -389,10 +385,10 @@ func TestCreate_ConcurrentUniqueIDs_NoCollision(t *testing.T) { wg.Add(1) go func(idx int) { defer wg.Done() - u, err := db.User.Create(valk.UserCreate{ - Email: fmt.Sprintf("bulk%d@example.com", idx), - PhoneNum: fmt.Sprintf("+600%06d", idx), - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set(fmt.Sprintf("bulk%d@example.com", idx)), + user.PhoneNum.Set(fmt.Sprintf("+600%06d", idx)), + ).Exec(ctx) errs[idx] = err if err == nil { ids[idx] = u.Id @@ -425,11 +421,11 @@ func TestCreate_FailurePartway_LeavesNoPartialRow(t *testing.T) { fakeReferrer := "clDoesNotExist00000000000" before := countAllUsers(t, ctx, db) - _, err := db.User.Create(valk.UserCreate{ - Email: "partial@example.com", - PhoneNum: "+700000001", - ReferredById: &fakeReferrer, - }).Exec(ctx) + _, err := db.User.Create( + user.Email.Set("partial@example.com"), + user.PhoneNum.Set("+700000001"), + user.ReferredById.Set(fakeReferrer), + ).Exec(ctx) if err == nil { t.Fatal("expected FK failure") @@ -470,10 +466,10 @@ func TestCreate_Hooks(t *testing.T) { return nil }) - u, err := db.User.Create(valk.UserCreate{ - Email: "hook@example.com", - PhoneNum: "+100000000", // Will be modified by hook - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("hook@example.com"), + user.PhoneNum.Set("+100000000"), // Will be modified by hook + ).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -495,7 +491,6 @@ func TestCreate_Hooks_PasswordHashing(t *testing.T) { db.User.BeforeCreate(func(ctx context.Context, input *valk.UserCreate) error { if input.Email == "hash@example.com" && input.Password != nil { - h := sha256.Sum256([]byte(*input.Password)) hashed := hex.EncodeToString(h[:]) input.Password = &hashed @@ -505,11 +500,11 @@ func TestCreate_Hooks_PasswordHashing(t *testing.T) { rawPassword := "12345678" - u, err := db.User.Create(valk.UserCreate{ - Email: "hash@example.com", - PhoneNum: "+199999999", - Password: &rawPassword, - }).Exec(ctx) + u, err := db.User.Create( + user.Email.Set("hash@example.com"), + user.PhoneNum.Set("+199999999"), + user.Password.Set(rawPassword), + ).Exec(ctx) if err != nil { t.Fatalf("failed to create user: %v", err) diff --git a/integration/valk/category.go b/integration/valk/category.go index c632b95..e1771bb 100644 --- a/integration/valk/category.go +++ b/integration/valk/category.go @@ -15,7 +15,7 @@ type Category struct { Posts []*CategoryToPost `json:"posts,omitempty"` } -// CategoryCreate represents the input structure for creation +// CategoryCreate is used for hooks only — the Create API uses FieldAssignment type CategoryCreate struct { Id *int32 `json:"id"` Name string `json:"name"` @@ -90,24 +90,6 @@ func (q *Queries) selectCategoryCols(selects *CategorySelect, omits *CategoryOmi return cols } -func (input CategoryCreate) Validate() error { - errs := &ValidationError{} - if input.Name == "" { - errs.Add("name", input.Name, "required", "field Name is required") - } - if strings.Contains(input.Name, "\x00") { - errs.Add("name", input.Name, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.Name) { - errs.Add("name", input.Name, "safety", "string must be valid UTF-8") - } - - if errs.HasErrors() { - return *errs - } - return nil -} - var CategoryColOrder = []string{ "id", "name", @@ -120,31 +102,81 @@ func (s *CategorySelect) hasAnyRelation() bool { return s.Posts != nil } -func (d *CategoryDelegate) Create(input CategoryCreate) *CreateBuilder[Category, CategoryCreate, CategorySelect, CategoryOmit] { - return &CreateBuilder[Category, CategoryCreate, CategorySelect, CategoryOmit]{ - client: d.client, - input: input, - execFunc: d.client.executeCategoryCreate, +func (d *CategoryDelegate) Create(assignments ...FieldAssignment) *CreateBuilder[Category, CategorySelect, CategoryOmit] { + return &CreateBuilder[Category, CategorySelect, CategoryOmit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.executeCategoryCreate, + } +} + +func validateCategoryCreate(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + case "id": + case "name": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("name", v, "required", "field name is required") + } + if strings.Contains(v, "\x00") { + errs.Add("name", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("name", v, "safety", "string must be valid UTF-8") + } + } + } } + if !provided["name"] { + errs.Add("name", "", "required", "field Name is required") + } + + if errs.HasErrors() { + return *errs + } + return nil +} + +func assignmentsToCategoryCreate(assignments []FieldAssignment) CategoryCreate { + var input CategoryCreate + for _, a := range assignments { + switch a.Col { + case "id": + if v, ok := a.Val.(int32); ok { + input.Id = &v + } + case "name": + if v, ok := a.Val.(string); ok { + input.Name = v + } + } + } + return input } -func (q *Queries) executeCategoryCreate(ctx context.Context, input CategoryCreate, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { +func (q *Queries) executeCategoryCreate(ctx context.Context, assignments []FieldAssignment, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { + input := assignmentsToCategoryCreate(assignments) + if q.Category.beforeCreate != nil { if err := q.Category.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validateCategoryCreate(assignments); err != nil { return nil, err } + var cols []string var vals []any if input.Id != nil { cols = append(cols, "id") vals = append(vals, *input.Id) - } else { - cols = append(cols, "id") } cols = append(cols, "name") vals = append(vals, input.Name) @@ -186,47 +218,48 @@ func (q *Queries) executeCategoryCreate(ctx context.Context, input CategoryCreat return res, nil } -func (q *Queries) CategoryInputToMap(input CategoryCreate) map[string]any { - m := make(map[string]any) - if input.Id != nil { - m["id"] = *input.Id - } else { +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 } - m["name"] = input.Name - return m + return rowMaps } -func (d *CategoryDelegate) CreateMany(inputs []CategoryCreate) *CreateManyBuilder[Category, CategoryCreate] { - return &CreateManyBuilder[Category, CategoryCreate]{ +func (d *CategoryDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Category] { + return &CreateManyBuilder[Category]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeCategoryCreateMany, } } -func (d *CategoryDelegate) CreateManyAndReturn(inputs []CategoryCreate) *CreateManyAndReturnBuilder[Category, CategoryCreate, CategorySelect, CategoryOmit] { - return &CreateManyAndReturnBuilder[Category, CategoryCreate, CategorySelect, CategoryOmit]{ +func (d *CategoryDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[Category, CategorySelect, CategoryOmit] { + return &CreateManyAndReturnBuilder[Category, CategorySelect, CategoryOmit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeCategoryCreateManyAndReturn, } } -func (q *Queries) executeCategoryCreateMany(ctx context.Context, inputs []CategoryCreate) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) executeCategoryCreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateCategoryCreate(rec.Assignments); err != nil { return 0, fmt.Errorf("validation failed at index %d: %w", i, err) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.CategoryInputToMap(input) - } + rowMaps := categoryRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Category", rowMaps, CategoryColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -237,8 +270,8 @@ func (q *Queries) executeCategoryCreateMany(ctx context.Context, inputs []Catego var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - _, err := txQ.executeCategoryCreate(ctx, input, nil, nil) + for _, rec := range records { + _, err := txQ.executeCategoryCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -249,12 +282,12 @@ func (q *Queries) executeCategoryCreateMany(ctx context.Context, inputs []Catego return count, err } -func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs []CategoryCreate, selects *CategorySelect, omits *CategoryOmit) ([]*Category, error) { - if len(inputs) == 0 { +func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *CategorySelect, omits *CategoryOmit) ([]*Category, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateCategoryCreate(rec.Assignments); err != nil { return nil, fmt.Errorf("validation failed at index %d: %w", i, err) } } @@ -263,12 +296,9 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs 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) - } + rowMaps := categoryRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Category", rowMaps, CategoryColOrder, returningCols) - records := make([]*Category, 0) + recordsOut := make([]*Category, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -280,42 +310,41 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.loadCategoryRelations(ctx, records, selects) + return txQ.loadCategoryRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*Category, 0) + recordsOut := make([]*Category, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.executeCategoryCreate(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.executeCategoryCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.loadCategoryRelations(ctx, records, selects) + return txQ.loadCategoryRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } func (d *CategoryDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Category, CategorySelect, CategoryOmit] { return &FindUniqueBuilder[Category, CategorySelect, CategoryOmit]{ diff --git a/integration/valk/category/category.go b/integration/valk/category/category.go index ae59571..59f8085 100644 --- a/integration/valk/category/category.go +++ b/integration/valk/category/category.go @@ -20,7 +20,10 @@ func (p UniquePredicate) Validate() error { type Select = valk.CategorySelect type Omit = valk.CategoryOmit -type Create = valk.CategoryCreate + +func Record(assignments ...valk.FieldAssignment) valk.RecordInput { + return valk.RecordInput{Assignments: assignments} +} func And(preds ...valk.Predicate) valk.Predicate { return valk.And(preds...) diff --git a/integration/valk/categoryToPost.go b/integration/valk/categoryToPost.go index 3f613f7..516743b 100644 --- a/integration/valk/categoryToPost.go +++ b/integration/valk/categoryToPost.go @@ -16,7 +16,7 @@ type CategoryToPost struct { Category *Category `json:"category,omitempty"` } -// CategoryToPostCreate represents the input structure for creation +// CategoryToPostCreate is used for hooks only — the Create API uses FieldAssignment type CategoryToPostCreate struct { PostId string `json:"postId"` CategoryId int32 `json:"categoryId"` @@ -93,24 +93,6 @@ func (q *Queries) selectCategoryToPostCols(selects *CategoryToPostSelect, omits return cols } -func (input CategoryToPostCreate) Validate() error { - errs := &ValidationError{} - if input.PostId == "" { - errs.Add("postId", input.PostId, "required", "field PostId is required") - } - if strings.Contains(input.PostId, "\x00") { - errs.Add("postId", input.PostId, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.PostId) { - errs.Add("postId", input.PostId, "safety", "string must be valid UTF-8") - } - - if errs.HasErrors() { - return *errs - } - return nil -} - var CategoryToPostColOrder = []string{ "postId", "categoryId", @@ -123,24 +105,76 @@ func (s *CategoryToPostSelect) hasAnyRelation() bool { return s.Post != nil || s.Category != nil } -func (d *CategoryToPostDelegate) Create(input CategoryToPostCreate) *CreateBuilder[CategoryToPost, CategoryToPostCreate, CategoryToPostSelect, CategoryToPostOmit] { - return &CreateBuilder[CategoryToPost, CategoryToPostCreate, CategoryToPostSelect, CategoryToPostOmit]{ - client: d.client, - input: input, - execFunc: d.client.executeCategoryToPostCreate, +func (d *CategoryToPostDelegate) Create(assignments ...FieldAssignment) *CreateBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { + return &CreateBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.executeCategoryToPostCreate, + } +} + +func validateCategoryToPostCreate(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + case "postId": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("postId", v, "required", "field postId is required") + } + if strings.Contains(v, "\x00") { + errs.Add("postId", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("postId", v, "safety", "string must be valid UTF-8") + } + } + case "categoryId": + } + } + if !provided["postId"] { + errs.Add("postId", "", "required", "field PostId is required") + } + + if errs.HasErrors() { + return *errs + } + return nil +} + +func assignmentsToCategoryToPostCreate(assignments []FieldAssignment) CategoryToPostCreate { + var input CategoryToPostCreate + for _, a := range assignments { + switch a.Col { + case "postId": + if v, ok := a.Val.(string); ok { + input.PostId = v + } + case "categoryId": + if v, ok := a.Val.(int32); ok { + input.CategoryId = v + } + } } + return input } -func (q *Queries) executeCategoryToPostCreate(ctx context.Context, input CategoryToPostCreate, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { +func (q *Queries) executeCategoryToPostCreate(ctx context.Context, assignments []FieldAssignment, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { + input := assignmentsToCategoryToPostCreate(assignments) + if q.CategoryToPost.beforeCreate != nil { if err := q.CategoryToPost.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validateCategoryToPostCreate(assignments); err != nil { return nil, err } + var cols []string var vals []any cols = append(cols, "postId") @@ -185,44 +219,46 @@ func (q *Queries) executeCategoryToPostCreate(ctx context.Context, input Categor return res, nil } -func (q *Queries) CategoryToPostInputToMap(input CategoryToPostCreate) map[string]any { - m := make(map[string]any) - m["postId"] = input.PostId - m["categoryId"] = input.CategoryId - return m +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(inputs []CategoryToPostCreate) *CreateManyBuilder[CategoryToPost, CategoryToPostCreate] { - return &CreateManyBuilder[CategoryToPost, CategoryToPostCreate]{ +func (d *CategoryToPostDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[CategoryToPost] { + return &CreateManyBuilder[CategoryToPost]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeCategoryToPostCreateMany, } } -func (d *CategoryToPostDelegate) CreateManyAndReturn(inputs []CategoryToPostCreate) *CreateManyAndReturnBuilder[CategoryToPost, CategoryToPostCreate, CategoryToPostSelect, CategoryToPostOmit] { - return &CreateManyAndReturnBuilder[CategoryToPost, CategoryToPostCreate, CategoryToPostSelect, CategoryToPostOmit]{ +func (d *CategoryToPostDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { + return &CreateManyAndReturnBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeCategoryToPostCreateManyAndReturn, } } -func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, inputs []CategoryToPostCreate) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateCategoryToPostCreate(rec.Assignments); err != nil { return 0, fmt.Errorf("validation failed at index %d: %w", i, err) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.CategoryToPostInputToMap(input) - } + rowMaps := categoryToPostRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "CategoryToPost", rowMaps, CategoryToPostColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -233,8 +269,8 @@ func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, inputs [] var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - _, err := txQ.executeCategoryToPostCreate(ctx, input, nil, nil) + for _, rec := range records { + _, err := txQ.executeCategoryToPostCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -245,12 +281,12 @@ func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, inputs [] return count, err } -func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, inputs []CategoryToPostCreate, selects *CategoryToPostSelect, omits *CategoryToPostOmit) ([]*CategoryToPost, error) { - if len(inputs) == 0 { +func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *CategoryToPostSelect, omits *CategoryToPostOmit) ([]*CategoryToPost, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateCategoryToPostCreate(rec.Assignments); err != nil { return nil, fmt.Errorf("validation failed at index %d: %w", i, err) } } @@ -259,12 +295,9 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, 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) - } + rowMaps := categoryToPostRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "CategoryToPost", rowMaps, CategoryToPostColOrder, returningCols) - records := make([]*CategoryToPost, 0) + recordsOut := make([]*CategoryToPost, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -276,42 +309,41 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.loadCategoryToPostRelations(ctx, records, selects) + return txQ.loadCategoryToPostRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*CategoryToPost, 0) + recordsOut := make([]*CategoryToPost, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.executeCategoryToPostCreate(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.executeCategoryToPostCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.loadCategoryToPostRelations(ctx, records, selects) + return txQ.loadCategoryToPostRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } func (d *CategoryToPostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { return &FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ diff --git a/integration/valk/categoryToPost/categoryToPost.go b/integration/valk/categoryToPost/categoryToPost.go index a7f484f..fc763a3 100644 --- a/integration/valk/categoryToPost/categoryToPost.go +++ b/integration/valk/categoryToPost/categoryToPost.go @@ -20,7 +20,10 @@ func (p UniquePredicate) Validate() error { type Select = valk.CategoryToPostSelect type Omit = valk.CategoryToPostOmit -type Create = valk.CategoryToPostCreate + +func Record(assignments ...valk.FieldAssignment) valk.RecordInput { + return valk.RecordInput{Assignments: assignments} +} func And(preds ...valk.Predicate) valk.Predicate { return valk.And(preds...) diff --git a/integration/valk/client.go b/integration/valk/client.go index afd544c..6f2d944 100644 --- a/integration/valk/client.go +++ b/integration/valk/client.go @@ -83,20 +83,40 @@ func (e *ValidationError) HasErrors() bool { return len(e.Errors) > 0 } +type FieldAssignment struct { + Col string + Val any +} + +type RecordInput struct { + Assignments []FieldAssignment +} + type UserRoleType string const ( - UserRoleTypeAdmin UserRoleType = "ADMIN" + // Admin maps to "ADMIN" + UserRoleTypeAdmin UserRoleType = "ADMIN" + // Student maps to "student" UserRoleTypeStudent UserRoleType = "student" + // Teacher maps to "TEACHER" UserRoleTypeTeacher UserRoleType = "TEACHER" ) type userRoleNamespace struct { - Admin UserRoleType + // Admin maps to "ADMIN" + Admin UserRoleType + // Student maps to "student" Student UserRoleType + // Teacher maps to "TEACHER" Teacher UserRoleType } +// UserRole enum values: +// +// ADMIN ADMIN +// STUDENT student +// TEACHER TEACHER var UserRole = userRoleNamespace{ Admin: UserRoleTypeAdmin, Student: UserRoleTypeStudent, @@ -132,14 +152,53 @@ type DBTX interface { } type Queries struct { - db DBTX - provider string - dialect Dialect - User *UserDelegate - Profile *ProfileDelegate - Post *PostDelegate - Comment *CommentDelegate - Category *CategoryDelegate + db DBTX + provider string + dialect Dialect + // User provides CRUD operations for User. + // + // id string default: cuid() + // email string required + // phoneNum string required + // password string optional + // role UserRole default: STUDENT + // roleOptional UserRole optional + // referredById string optional + User *UserDelegate + // Profile provides CRUD operations for Profile. + // + // id string default: cuid() + // bio string optional + // userId string required + Profile *ProfileDelegate + // Post provides CRUD operations for Post. + // + // id string default: cuid() + // title string required + // content string optional + // published bool default: false + // authorId string required + Post *PostDelegate + // Comment provides CRUD operations for Comment. + // + // id string default: cuid() + // textify int32 required + // dummy3 string required + // dummy1 int32 required + // dummy2 string required + // postId string required + // authorId string required + // meta json.RawMessage optional + Comment *CommentDelegate + // Category provides CRUD operations for Category. + // + // id int32 default: autoincrement() + // name string required + Category *CategoryDelegate + // CategoryToPost provides CRUD operations for CategoryToPost. + // + // postId string required + // categoryId int32 required CategoryToPost *CategoryToPostDelegate UserRole userRoleNamespace } @@ -411,6 +470,10 @@ type Field[T any] struct { Column string } +func (f Field[T]) Set(val T) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + func (f Field[T]) EQ(val T) Predicate { return StandardPredicate{ Data: PredicateData{ @@ -503,6 +566,10 @@ type UniqueField[T any] struct { Column string } +func (f UniqueField[T]) Set(val T) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + type UniqueFieldPredicate struct { StandardPredicate } @@ -610,6 +677,10 @@ type StringField struct { Column string } +func (f StringField) Set(val string) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + func (f StringField) EQ(val string) Predicate { return StandardPredicate{ Data: PredicateData{ @@ -722,6 +793,10 @@ type StringUniqueField struct { Column string } +func (f StringUniqueField) Set(val string) FieldAssignment { + return FieldAssignment{Col: f.Column, Val: val} +} + func (f StringUniqueField) EQ(val string) UniquePredicate { return UniqueFieldPredicate{ StandardPredicate: StandardPredicate{ @@ -1064,86 +1139,86 @@ func (db *DB) Transaction(ctx context.Context, fn func(tx *Tx) error) error { return tx.Commit() } -type CreateBuilder[M any, I any, S any, O any] struct { - client *Queries - input I - execFunc func(ctx context.Context, input I, s *S, o *O) (*M, error) +type CreateBuilder[M any, S any, O any] struct { + client *Queries + assignments []FieldAssignment + execFunc func(ctx context.Context, assignments []FieldAssignment, s *S, o *O) (*M, error) } -func (b *CreateBuilder[M, I, S, O]) Select(s S) *CreateSelectBuilder[M, I, S, O] { - return &CreateSelectBuilder[M, I, S, O]{builder: b, selects: s} +func (b *CreateBuilder[M, S, O]) Select(s S) *CreateSelectBuilder[M, S, O] { + return &CreateSelectBuilder[M, S, O]{builder: b, selects: s} } -func (b *CreateBuilder[M, I, S, O]) Omit(o O) *CreateOmitBuilder[M, I, S, O] { - return &CreateOmitBuilder[M, I, S, O]{builder: b, omits: o} +func (b *CreateBuilder[M, S, O]) Omit(o O) *CreateOmitBuilder[M, S, O] { + return &CreateOmitBuilder[M, S, O]{builder: b, omits: o} } -func (b *CreateBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { - return b.execFunc(ctx, b.input, nil, nil) +func (b *CreateBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.execFunc(ctx, b.assignments, nil, nil) } -type CreateSelectBuilder[M any, I any, S any, O any] struct { - builder *CreateBuilder[M, I, S, O] +type CreateSelectBuilder[M any, S any, O any] struct { + builder *CreateBuilder[M, S, O] selects S } -func (b *CreateSelectBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { - return b.builder.execFunc(ctx, b.builder.input, &b.selects, nil) +func (b *CreateSelectBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.assignments, &b.selects, nil) } -type CreateOmitBuilder[M any, I any, S any, O any] struct { - builder *CreateBuilder[M, I, S, O] +type CreateOmitBuilder[M any, S any, O any] struct { + builder *CreateBuilder[M, S, O] omits O } -func (b *CreateOmitBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) { - return b.builder.execFunc(ctx, b.builder.input, nil, &b.omits) +func (b *CreateOmitBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.assignments, nil, &b.omits) } -type CreateManyBuilder[M any, I any] struct { +type CreateManyBuilder[M any] struct { client *Queries - inputs []I - execFunc func(ctx context.Context, inputs []I) (int64, error) + records []RecordInput + execFunc func(ctx context.Context, records []RecordInput) (int64, error) } -func (b *CreateManyBuilder[M, I]) Exec(ctx context.Context) (int64, error) { - return b.execFunc(ctx, b.inputs) +func (b *CreateManyBuilder[M]) Exec(ctx context.Context) (int64, error) { + return b.execFunc(ctx, b.records) } -type CreateManyAndReturnBuilder[M any, I any, S any, O any] struct { +type CreateManyAndReturnBuilder[M any, S any, O any] struct { client *Queries - inputs []I - execFunc func(ctx context.Context, inputs []I, s *S, o *O) ([]*M, error) + records []RecordInput + execFunc func(ctx context.Context, records []RecordInput, 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, S, O]) Select(s S) *CreateManyAndReturnSelectBuilder[M, S, O] { + return &CreateManyAndReturnSelectBuilder[M, 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, S, O]) Omit(o O) *CreateManyAndReturnOmitBuilder[M, S, O] { + return &CreateManyAndReturnOmitBuilder[M, 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) +func (b *CreateManyAndReturnBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.execFunc(ctx, b.records, nil, nil) } -type CreateManyAndReturnSelectBuilder[M any, I any, S any, O any] struct { - builder *CreateManyAndReturnBuilder[M, I, S, O] +type CreateManyAndReturnSelectBuilder[M any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, 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) +func (b *CreateManyAndReturnSelectBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.records, &b.selects, nil) } -type CreateManyAndReturnOmitBuilder[M any, I any, S any, O any] struct { - builder *CreateManyAndReturnBuilder[M, I, S, O] +type CreateManyAndReturnOmitBuilder[M any, S any, O any] struct { + builder *CreateManyAndReturnBuilder[M, 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 (b *CreateManyAndReturnOmitBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.records, nil, &b.omits) } func executeInsert[M any]( diff --git a/integration/valk/comment.go b/integration/valk/comment.go index e19df42..74f7308 100644 --- a/integration/valk/comment.go +++ b/integration/valk/comment.go @@ -23,7 +23,7 @@ type Comment struct { Author *User `json:"author,omitempty"` } -// CommentCreate represents the input structure for creation +// CommentCreate is used for hooks only — the Create API uses FieldAssignment type CommentCreate struct { Id *string `json:"id"` Textify int32 `json:"textify"` @@ -142,60 +142,6 @@ func (q *Queries) selectCommentCols(selects *CommentSelect, omits *CommentOmit, return cols } -func (input CommentCreate) Validate() error { - errs := &ValidationError{} - if input.Id != nil { - val := *input.Id - if strings.Contains(val, "\x00") { - errs.Add("id", val, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(val) { - errs.Add("id", val, "safety", "string must be valid UTF-8") - } - } - if input.Dummy3 == "" { - errs.Add("dummy3", input.Dummy3, "required", "field Dummy3 is required") - } - if strings.Contains(input.Dummy3, "\x00") { - errs.Add("dummy3", input.Dummy3, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.Dummy3) { - errs.Add("dummy3", input.Dummy3, "safety", "string must be valid UTF-8") - } - if input.Dummy2 == "" { - errs.Add("dummy2", input.Dummy2, "required", "field Dummy2 is required") - } - if strings.Contains(input.Dummy2, "\x00") { - errs.Add("dummy2", input.Dummy2, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.Dummy2) { - errs.Add("dummy2", input.Dummy2, "safety", "string must be valid UTF-8") - } - if input.PostId == "" { - errs.Add("postId", input.PostId, "required", "field PostId is required") - } - if strings.Contains(input.PostId, "\x00") { - errs.Add("postId", input.PostId, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.PostId) { - errs.Add("postId", input.PostId, "safety", "string must be valid UTF-8") - } - if input.AuthorId == "" { - errs.Add("authorId", input.AuthorId, "required", "field AuthorId is required") - } - if strings.Contains(input.AuthorId, "\x00") { - errs.Add("authorId", input.AuthorId, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.AuthorId) { - errs.Add("authorId", input.AuthorId, "safety", "string must be valid UTF-8") - } - - if errs.HasErrors() { - return *errs - } - return nil -} - var CommentColOrder = []string{ "id", "textify", @@ -214,24 +160,156 @@ func (s *CommentSelect) hasAnyRelation() bool { return s.Post != nil || s.Author != nil } -func (d *CommentDelegate) Create(input CommentCreate) *CreateBuilder[Comment, CommentCreate, CommentSelect, CommentOmit] { - return &CreateBuilder[Comment, CommentCreate, CommentSelect, CommentOmit]{ - client: d.client, - input: input, - execFunc: d.client.executeCommentCreate, +func (d *CommentDelegate) Create(assignments ...FieldAssignment) *CreateBuilder[Comment, CommentSelect, CommentOmit] { + return &CreateBuilder[Comment, CommentSelect, CommentOmit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.executeCommentCreate, + } +} + +func validateCommentCreate(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + if strings.Contains(v, "\x00") { + errs.Add("id", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("id", v, "safety", "string must be valid UTF-8") + } + } + case "textify": + case "dummy3": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("dummy3", v, "required", "field dummy3 is required") + } + if strings.Contains(v, "\x00") { + errs.Add("dummy3", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("dummy3", v, "safety", "string must be valid UTF-8") + } + } + case "dummy1": + case "dummy2": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("dummy2", v, "required", "field dummy2 is required") + } + if strings.Contains(v, "\x00") { + errs.Add("dummy2", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("dummy2", v, "safety", "string must be valid UTF-8") + } + } + case "postId": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("postId", v, "required", "field postId is required") + } + if strings.Contains(v, "\x00") { + errs.Add("postId", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("postId", v, "safety", "string must be valid UTF-8") + } + } + case "authorId": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("authorId", v, "required", "field authorId is required") + } + if strings.Contains(v, "\x00") { + errs.Add("authorId", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("authorId", v, "safety", "string must be valid UTF-8") + } + } + case "meta": + } + } + if !provided["dummy3"] { + errs.Add("dummy3", "", "required", "field Dummy3 is required") + } + if !provided["dummy2"] { + errs.Add("dummy2", "", "required", "field Dummy2 is required") + } + if !provided["postId"] { + errs.Add("postId", "", "required", "field PostId is required") + } + if !provided["authorId"] { + errs.Add("authorId", "", "required", "field AuthorId is required") + } + + if errs.HasErrors() { + return *errs + } + return nil +} + +func assignmentsToCommentCreate(assignments []FieldAssignment) CommentCreate { + var input CommentCreate + for _, a := range assignments { + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + input.Id = &v + } + case "textify": + if v, ok := a.Val.(int32); ok { + input.Textify = v + } + case "dummy3": + if v, ok := a.Val.(string); ok { + input.Dummy3 = v + } + case "dummy1": + if v, ok := a.Val.(int32); ok { + input.Dummy1 = v + } + case "dummy2": + if v, ok := a.Val.(string); ok { + input.Dummy2 = v + } + case "postId": + if v, ok := a.Val.(string); ok { + input.PostId = v + } + case "authorId": + if v, ok := a.Val.(string); ok { + input.AuthorId = v + } + case "meta": + if v, ok := a.Val.(json.RawMessage); ok { + input.Meta = &v + } + } } + return input } -func (q *Queries) executeCommentCreate(ctx context.Context, input CommentCreate, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { +func (q *Queries) executeCommentCreate(ctx context.Context, assignments []FieldAssignment, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { + input := assignmentsToCommentCreate(assignments) + if q.Comment.beforeCreate != nil { if err := q.Comment.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validateCommentCreate(assignments); err != nil { return nil, err } + var cols []string var vals []any if input.Id != nil { @@ -295,56 +373,49 @@ func (q *Queries) executeCommentCreate(ctx context.Context, input CommentCreate, return res, nil } -func (q *Queries) CommentInputToMap(input CommentCreate) 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 - if input.Meta != nil { - m["meta"] = *input.Meta +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 m + return rowMaps } -func (d *CommentDelegate) CreateMany(inputs []CommentCreate) *CreateManyBuilder[Comment, CommentCreate] { - return &CreateManyBuilder[Comment, CommentCreate]{ +func (d *CommentDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Comment] { + return &CreateManyBuilder[Comment]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeCommentCreateMany, } } -func (d *CommentDelegate) CreateManyAndReturn(inputs []CommentCreate) *CreateManyAndReturnBuilder[Comment, CommentCreate, CommentSelect, CommentOmit] { - return &CreateManyAndReturnBuilder[Comment, CommentCreate, CommentSelect, CommentOmit]{ +func (d *CommentDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[Comment, CommentSelect, CommentOmit] { + return &CreateManyAndReturnBuilder[Comment, CommentSelect, CommentOmit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeCommentCreateManyAndReturn, } } -func (q *Queries) executeCommentCreateMany(ctx context.Context, inputs []CommentCreate) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) executeCommentCreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateCommentCreate(rec.Assignments); err != nil { return 0, fmt.Errorf("validation failed at index %d: %w", i, err) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.CommentInputToMap(input) - } + rowMaps := commentRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Comment", rowMaps, CommentColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -355,8 +426,8 @@ func (q *Queries) executeCommentCreateMany(ctx context.Context, inputs []Comment var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - _, err := txQ.executeCommentCreate(ctx, input, nil, nil) + for _, rec := range records { + _, err := txQ.executeCommentCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -367,12 +438,12 @@ func (q *Queries) executeCommentCreateMany(ctx context.Context, inputs []Comment return count, err } -func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs []CommentCreate, selects *CommentSelect, omits *CommentOmit) ([]*Comment, error) { - if len(inputs) == 0 { +func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *CommentSelect, omits *CommentOmit) ([]*Comment, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateCommentCreate(rec.Assignments); err != nil { return nil, fmt.Errorf("validation failed at index %d: %w", i, err) } } @@ -381,12 +452,9 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs 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) - } + rowMaps := commentRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Comment", rowMaps, CommentColOrder, returningCols) - records := make([]*Comment, 0) + recordsOut := make([]*Comment, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -398,42 +466,41 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.loadCommentRelations(ctx, records, selects) + return txQ.loadCommentRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*Comment, 0) + recordsOut := make([]*Comment, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.executeCommentCreate(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.executeCommentCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.loadCommentRelations(ctx, records, selects) + return txQ.loadCommentRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } func (d *CommentDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Comment, CommentSelect, CommentOmit] { return &FindUniqueBuilder[Comment, CommentSelect, CommentOmit]{ diff --git a/integration/valk/comment/comment.go b/integration/valk/comment/comment.go index 330277d..859f433 100644 --- a/integration/valk/comment/comment.go +++ b/integration/valk/comment/comment.go @@ -21,7 +21,10 @@ func (p UniquePredicate) Validate() error { type Select = valk.CommentSelect type Omit = valk.CommentOmit -type Create = valk.CommentCreate + +func Record(assignments ...valk.FieldAssignment) valk.RecordInput { + return valk.RecordInput{Assignments: assignments} +} func And(preds ...valk.Predicate) valk.Predicate { return valk.And(preds...) diff --git a/integration/valk/post.go b/integration/valk/post.go index 5445602..f4df486 100644 --- a/integration/valk/post.go +++ b/integration/valk/post.go @@ -20,7 +20,7 @@ type Post struct { Categories []*CategoryToPost `json:"categories,omitempty"` } -// PostCreate represents the input structure for creation +// PostCreate is used for hooks only — the Create API uses FieldAssignment type PostCreate struct { Id *string `json:"id"` Title string `json:"title"` @@ -120,42 +120,6 @@ func (q *Queries) selectPostCols(selects *PostSelect, omits *PostOmit, forceCols return cols } -func (input PostCreate) Validate() error { - errs := &ValidationError{} - if input.Id != nil { - val := *input.Id - if strings.Contains(val, "\x00") { - errs.Add("id", val, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(val) { - errs.Add("id", val, "safety", "string must be valid UTF-8") - } - } - if input.Title == "" { - errs.Add("title", input.Title, "required", "field Title is required") - } - if strings.Contains(input.Title, "\x00") { - errs.Add("title", input.Title, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.Title) { - errs.Add("title", input.Title, "safety", "string must be valid UTF-8") - } - if input.AuthorId == "" { - errs.Add("authorId", input.AuthorId, "required", "field AuthorId is required") - } - if strings.Contains(input.AuthorId, "\x00") { - errs.Add("authorId", input.AuthorId, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.AuthorId) { - errs.Add("authorId", input.AuthorId, "safety", "string must be valid UTF-8") - } - - if errs.HasErrors() { - return *errs - } - return nil -} - var PostColOrder = []string{ "id", "title", @@ -171,24 +135,113 @@ func (s *PostSelect) hasAnyRelation() bool { return s.Author != nil || s.Comments != nil || s.Categories != nil } -func (d *PostDelegate) Create(input PostCreate) *CreateBuilder[Post, PostCreate, PostSelect, PostOmit] { - return &CreateBuilder[Post, PostCreate, PostSelect, PostOmit]{ - client: d.client, - input: input, - execFunc: d.client.executePostCreate, +func (d *PostDelegate) Create(assignments ...FieldAssignment) *CreateBuilder[Post, PostSelect, PostOmit] { + return &CreateBuilder[Post, PostSelect, PostOmit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.executePostCreate, + } +} + +func validatePostCreate(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + if strings.Contains(v, "\x00") { + errs.Add("id", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("id", v, "safety", "string must be valid UTF-8") + } + } + case "title": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("title", v, "required", "field title is required") + } + if strings.Contains(v, "\x00") { + errs.Add("title", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("title", v, "safety", "string must be valid UTF-8") + } + } + case "content": + case "published": + case "authorId": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("authorId", v, "required", "field authorId is required") + } + if strings.Contains(v, "\x00") { + errs.Add("authorId", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("authorId", v, "safety", "string must be valid UTF-8") + } + } + } + } + if !provided["title"] { + errs.Add("title", "", "required", "field Title is required") + } + if !provided["authorId"] { + errs.Add("authorId", "", "required", "field AuthorId is required") + } + + if errs.HasErrors() { + return *errs } + return nil } -func (q *Queries) executePostCreate(ctx context.Context, input PostCreate, selects *PostSelect, omits *PostOmit) (*Post, error) { +func assignmentsToPostCreate(assignments []FieldAssignment) PostCreate { + var input PostCreate + for _, a := range assignments { + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + input.Id = &v + } + case "title": + if v, ok := a.Val.(string); ok { + input.Title = v + } + case "content": + if v, ok := a.Val.(string); ok { + input.Content = &v + } + case "published": + if v, ok := a.Val.(bool); ok { + input.Published = &v + } + case "authorId": + if v, ok := a.Val.(string); ok { + input.AuthorId = v + } + } + } + return input +} + +func (q *Queries) executePostCreate(ctx context.Context, assignments []FieldAssignment, selects *PostSelect, omits *PostOmit) (*Post, error) { + input := assignmentsToPostCreate(assignments) + if q.Post.beforeCreate != nil { if err := q.Post.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validatePostCreate(assignments); err != nil { return nil, err } + var cols []string var vals []any if input.Id != nil { @@ -248,55 +301,49 @@ func (q *Queries) executePostCreate(ctx context.Context, input PostCreate, selec return res, nil } -func (q *Queries) PostInputToMap(input PostCreate) 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 +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 } - m["authorId"] = input.AuthorId - return m + return rowMaps } -func (d *PostDelegate) CreateMany(inputs []PostCreate) *CreateManyBuilder[Post, PostCreate] { - return &CreateManyBuilder[Post, PostCreate]{ +func (d *PostDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Post] { + return &CreateManyBuilder[Post]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executePostCreateMany, } } -func (d *PostDelegate) CreateManyAndReturn(inputs []PostCreate) *CreateManyAndReturnBuilder[Post, PostCreate, PostSelect, PostOmit] { - return &CreateManyAndReturnBuilder[Post, PostCreate, PostSelect, PostOmit]{ +func (d *PostDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[Post, PostSelect, PostOmit] { + return &CreateManyAndReturnBuilder[Post, PostSelect, PostOmit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executePostCreateManyAndReturn, } } -func (q *Queries) executePostCreateMany(ctx context.Context, inputs []PostCreate) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) executePostCreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validatePostCreate(rec.Assignments); err != nil { return 0, fmt.Errorf("validation failed at index %d: %w", i, err) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.PostInputToMap(input) - } + rowMaps := postRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Post", rowMaps, PostColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -307,8 +354,8 @@ func (q *Queries) executePostCreateMany(ctx context.Context, inputs []PostCreate var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - _, err := txQ.executePostCreate(ctx, input, nil, nil) + for _, rec := range records { + _, err := txQ.executePostCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -319,12 +366,12 @@ func (q *Queries) executePostCreateMany(ctx context.Context, inputs []PostCreate return count, err } -func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []PostCreate, selects *PostSelect, omits *PostOmit) ([]*Post, error) { - if len(inputs) == 0 { +func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *PostSelect, omits *PostOmit) ([]*Post, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validatePostCreate(rec.Assignments); err != nil { return nil, fmt.Errorf("validation failed at index %d: %w", i, err) } } @@ -333,12 +380,9 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []P 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) - } + rowMaps := postRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Post", rowMaps, PostColOrder, returningCols) - records := make([]*Post, 0) + recordsOut := make([]*Post, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -350,42 +394,41 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []P if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.loadPostRelations(ctx, records, selects) + return txQ.loadPostRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*Post, 0) + recordsOut := make([]*Post, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.executePostCreate(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.executePostCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.loadPostRelations(ctx, records, selects) + return txQ.loadPostRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } func (d *PostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Post, PostSelect, PostOmit] { return &FindUniqueBuilder[Post, PostSelect, PostOmit]{ diff --git a/integration/valk/post/post.go b/integration/valk/post/post.go index 706009c..d5249e0 100644 --- a/integration/valk/post/post.go +++ b/integration/valk/post/post.go @@ -20,7 +20,10 @@ func (p UniquePredicate) Validate() error { type Select = valk.PostSelect type Omit = valk.PostOmit -type Create = valk.PostCreate + +func Record(assignments ...valk.FieldAssignment) valk.RecordInput { + return valk.RecordInput{Assignments: assignments} +} func And(preds ...valk.Predicate) valk.Predicate { return valk.And(preds...) diff --git a/integration/valk/profile.go b/integration/valk/profile.go index ab4afac..14a9011 100644 --- a/integration/valk/profile.go +++ b/integration/valk/profile.go @@ -16,7 +16,7 @@ type Profile struct { User *User `json:"user,omitempty"` } -// ProfileCreate represents the input structure for creation +// ProfileCreate is used for hooks only — the Create API uses FieldAssignment type ProfileCreate struct { Id *string `json:"id"` Bio *string `json:"bio"` @@ -98,33 +98,6 @@ func (q *Queries) selectProfileCols(selects *ProfileSelect, omits *ProfileOmit, return cols } -func (input ProfileCreate) Validate() error { - errs := &ValidationError{} - if input.Id != nil { - val := *input.Id - if strings.Contains(val, "\x00") { - errs.Add("id", val, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(val) { - errs.Add("id", val, "safety", "string must be valid UTF-8") - } - } - if input.UserId == "" { - errs.Add("userId", input.UserId, "required", "field UserId is required") - } - if strings.Contains(input.UserId, "\x00") { - errs.Add("userId", input.UserId, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.UserId) { - errs.Add("userId", input.UserId, "safety", "string must be valid UTF-8") - } - - if errs.HasErrors() { - return *errs - } - return nil -} - var ProfileColOrder = []string{ "id", "bio", @@ -138,24 +111,89 @@ func (s *ProfileSelect) hasAnyRelation() bool { return s.User != nil } -func (d *ProfileDelegate) Create(input ProfileCreate) *CreateBuilder[Profile, ProfileCreate, ProfileSelect, ProfileOmit] { - return &CreateBuilder[Profile, ProfileCreate, ProfileSelect, ProfileOmit]{ - client: d.client, - input: input, - execFunc: d.client.executeProfileCreate, +func (d *ProfileDelegate) Create(assignments ...FieldAssignment) *CreateBuilder[Profile, ProfileSelect, ProfileOmit] { + return &CreateBuilder[Profile, ProfileSelect, ProfileOmit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.executeProfileCreate, + } +} + +func validateProfileCreate(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + if strings.Contains(v, "\x00") { + errs.Add("id", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("id", v, "safety", "string must be valid UTF-8") + } + } + case "bio": + case "userId": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("userId", v, "required", "field userId is required") + } + if strings.Contains(v, "\x00") { + errs.Add("userId", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("userId", v, "safety", "string must be valid UTF-8") + } + } + } + } + if !provided["userId"] { + errs.Add("userId", "", "required", "field UserId is required") + } + + if errs.HasErrors() { + return *errs } + return nil } -func (q *Queries) executeProfileCreate(ctx context.Context, input ProfileCreate, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { +func assignmentsToProfileCreate(assignments []FieldAssignment) ProfileCreate { + var input ProfileCreate + for _, a := range assignments { + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + input.Id = &v + } + case "bio": + if v, ok := a.Val.(string); ok { + input.Bio = &v + } + case "userId": + if v, ok := a.Val.(string); ok { + input.UserId = v + } + } + } + return input +} + +func (q *Queries) executeProfileCreate(ctx context.Context, assignments []FieldAssignment, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { + input := assignmentsToProfileCreate(assignments) + if q.Profile.beforeCreate != nil { if err := q.Profile.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validateProfileCreate(assignments); err != nil { return nil, err } + var cols []string var vals []any if input.Id != nil { @@ -209,51 +247,49 @@ func (q *Queries) executeProfileCreate(ctx context.Context, input ProfileCreate, return res, nil } -func (q *Queries) ProfileInputToMap(input ProfileCreate) 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 +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 } - m["userId"] = input.UserId - return m + return rowMaps } -func (d *ProfileDelegate) CreateMany(inputs []ProfileCreate) *CreateManyBuilder[Profile, ProfileCreate] { - return &CreateManyBuilder[Profile, ProfileCreate]{ +func (d *ProfileDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[Profile] { + return &CreateManyBuilder[Profile]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeProfileCreateMany, } } -func (d *ProfileDelegate) CreateManyAndReturn(inputs []ProfileCreate) *CreateManyAndReturnBuilder[Profile, ProfileCreate, ProfileSelect, ProfileOmit] { - return &CreateManyAndReturnBuilder[Profile, ProfileCreate, ProfileSelect, ProfileOmit]{ +func (d *ProfileDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[Profile, ProfileSelect, ProfileOmit] { + return &CreateManyAndReturnBuilder[Profile, ProfileSelect, ProfileOmit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeProfileCreateManyAndReturn, } } -func (q *Queries) executeProfileCreateMany(ctx context.Context, inputs []ProfileCreate) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) executeProfileCreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateProfileCreate(rec.Assignments); err != nil { return 0, fmt.Errorf("validation failed at index %d: %w", i, err) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.ProfileInputToMap(input) - } + rowMaps := profileRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Profile", rowMaps, ProfileColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -264,8 +300,8 @@ func (q *Queries) executeProfileCreateMany(ctx context.Context, inputs []Profile var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - _, err := txQ.executeProfileCreate(ctx, input, nil, nil) + for _, rec := range records { + _, err := txQ.executeProfileCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -276,12 +312,12 @@ func (q *Queries) executeProfileCreateMany(ctx context.Context, inputs []Profile return count, err } -func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs []ProfileCreate, selects *ProfileSelect, omits *ProfileOmit) ([]*Profile, error) { - if len(inputs) == 0 { +func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *ProfileSelect, omits *ProfileOmit) ([]*Profile, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateProfileCreate(rec.Assignments); err != nil { return nil, fmt.Errorf("validation failed at index %d: %w", i, err) } } @@ -290,12 +326,9 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs 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) - } + rowMaps := profileRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "Profile", rowMaps, ProfileColOrder, returningCols) - records := make([]*Profile, 0) + recordsOut := make([]*Profile, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -307,42 +340,41 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.loadProfileRelations(ctx, records, selects) + return txQ.loadProfileRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*Profile, 0) + recordsOut := make([]*Profile, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.executeProfileCreate(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.executeProfileCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.loadProfileRelations(ctx, records, selects) + return txQ.loadProfileRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } func (d *ProfileDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit] { return &FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit]{ diff --git a/integration/valk/profile/profile.go b/integration/valk/profile/profile.go index 46e03df..33e0c00 100644 --- a/integration/valk/profile/profile.go +++ b/integration/valk/profile/profile.go @@ -20,7 +20,10 @@ func (p UniquePredicate) Validate() error { type Select = valk.ProfileSelect type Omit = valk.ProfileOmit -type Create = valk.ProfileCreate + +func Record(assignments ...valk.FieldAssignment) valk.RecordInput { + return valk.RecordInput{Assignments: assignments} +} func And(preds ...valk.Predicate) valk.Predicate { return valk.And(preds...) diff --git a/integration/valk/user.go b/integration/valk/user.go index 44d7c49..4aad02e 100644 --- a/integration/valk/user.go +++ b/integration/valk/user.go @@ -24,7 +24,7 @@ type User struct { Referrals []*User `json:"referrals,omitempty"` } -// UserCreate represents the input structure for creation +// UserCreate is used for hooks only — the Create API uses FieldAssignment type UserCreate struct { Id *string `json:"id"` Email string `json:"email"` @@ -142,52 +142,6 @@ func (q *Queries) selectUserCols(selects *UserSelect, omits *UserOmit, forceCols return cols } -func (input UserCreate) Validate() error { - errs := &ValidationError{} - if input.Id != nil { - val := *input.Id - if strings.Contains(val, "\x00") { - errs.Add("id", val, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(val) { - errs.Add("id", val, "safety", "string must be valid UTF-8") - } - } - if input.Email == "" { - errs.Add("email", input.Email, "required", "field Email is required") - } - if strings.Contains(input.Email, "\x00") { - errs.Add("email", input.Email, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.Email) { - errs.Add("email", input.Email, "safety", "string must be valid UTF-8") - } - if input.PhoneNum == "" { - errs.Add("phoneNum", input.PhoneNum, "required", "field PhoneNum is required") - } - if strings.Contains(input.PhoneNum, "\x00") { - errs.Add("phoneNum", input.PhoneNum, "safety", "string cannot contain null bytes") - } - if !utf8.ValidString(input.PhoneNum) { - errs.Add("phoneNum", input.PhoneNum, "safety", "string must be valid UTF-8") - } - if input.Role != nil { - if !input.Role.IsValid() { - errs.Add("role", *input.Role, "enum", fmt.Sprintf("invalid enum value %q for field Role", *input.Role)) - } - } - if input.RoleOptional != nil { - if !input.RoleOptional.IsValid() { - errs.Add("roleOptional", *input.RoleOptional, "enum", fmt.Sprintf("invalid enum value %q for field RoleOptional", *input.RoleOptional)) - } - } - - if errs.HasErrors() { - return *errs - } - return nil -} - var UserColOrder = []string{ "id", "email", @@ -205,24 +159,129 @@ func (s *UserSelect) hasAnyRelation() bool { return s.Profile != nil || s.Posts != nil || s.Comments != nil || s.ReferredBy != nil || s.Referrals != nil } -func (d *UserDelegate) Create(input UserCreate) *CreateBuilder[User, UserCreate, UserSelect, UserOmit] { - return &CreateBuilder[User, UserCreate, UserSelect, UserOmit]{ - client: d.client, - input: input, - execFunc: d.client.executeUserCreate, +func (d *UserDelegate) Create(assignments ...FieldAssignment) *CreateBuilder[User, UserSelect, UserOmit] { + return &CreateBuilder[User, UserSelect, UserOmit]{ + client: d.client, + assignments: assignments, + execFunc: d.client.executeUserCreate, + } +} + +func validateUserCreate(assignments []FieldAssignment) error { + errs := &ValidationError{} + + provided := make(map[string]bool) + for _, a := range assignments { + provided[a.Col] = true + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + if strings.Contains(v, "\x00") { + errs.Add("id", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("id", v, "safety", "string must be valid UTF-8") + } + } + case "email": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("email", v, "required", "field email is required") + } + if strings.Contains(v, "\x00") { + errs.Add("email", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("email", v, "safety", "string must be valid UTF-8") + } + } + case "phoneNum": + if v, ok := a.Val.(string); ok { + if v == "" { + errs.Add("phoneNum", v, "required", "field phoneNum is required") + } + if strings.Contains(v, "\x00") { + errs.Add("phoneNum", v, "safety", "string cannot contain null bytes") + } + if !utf8.ValidString(v) { + errs.Add("phoneNum", v, "safety", "string must be valid UTF-8") + } + } + case "password": + case "role": + if v, ok := a.Val.(UserRoleType); ok && !v.IsValid() { + errs.Add("role", v, "enum", fmt.Sprintf("invalid enum value %q for field role", v)) + } + case "roleOptional": + if v, ok := a.Val.(UserRoleType); ok && !v.IsValid() { + errs.Add("roleOptional", v, "enum", fmt.Sprintf("invalid enum value %q for field roleOptional", v)) + } + case "referredById": + } + } + if !provided["email"] { + errs.Add("email", "", "required", "field Email is required") + } + if !provided["phoneNum"] { + errs.Add("phoneNum", "", "required", "field PhoneNum is required") + } + + if errs.HasErrors() { + return *errs + } + return nil +} + +func assignmentsToUserCreate(assignments []FieldAssignment) UserCreate { + var input UserCreate + for _, a := range assignments { + switch a.Col { + case "id": + if v, ok := a.Val.(string); ok { + input.Id = &v + } + case "email": + if v, ok := a.Val.(string); ok { + input.Email = v + } + case "phoneNum": + if v, ok := a.Val.(string); ok { + input.PhoneNum = v + } + case "password": + if v, ok := a.Val.(string); ok { + input.Password = &v + } + case "role": + if v, ok := a.Val.(UserRoleType); ok { + input.Role = &v + } + case "roleOptional": + if v, ok := a.Val.(UserRoleType); ok { + input.RoleOptional = &v + } + case "referredById": + if v, ok := a.Val.(string); ok { + input.ReferredById = &v + } + } } + return input } -func (q *Queries) executeUserCreate(ctx context.Context, input UserCreate, selects *UserSelect, omits *UserOmit) (*User, error) { +func (q *Queries) executeUserCreate(ctx context.Context, assignments []FieldAssignment, selects *UserSelect, omits *UserOmit) (*User, error) { + input := assignmentsToUserCreate(assignments) + if q.User.beforeCreate != nil { if err := q.User.beforeCreate(ctx, &input); err != nil { return nil, err } } - if err := input.Validate(); err != nil { + if err := validateUserCreate(assignments); err != nil { return nil, err } + var cols []string var vals []any if input.Id != nil { @@ -290,61 +349,49 @@ func (q *Queries) executeUserCreate(ctx context.Context, input UserCreate, selec return res, nil } -func (q *Queries) UserInputToMap(input UserCreate) 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.Password != nil { - m["password"] = *input.Password - } - if input.Role != nil { - m["role"] = *input.Role - } - if input.RoleOptional != nil { - m["roleOptional"] = *input.RoleOptional - } - if input.ReferredById != nil { - m["referredById"] = *input.ReferredById +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 m + return rowMaps } -func (d *UserDelegate) CreateMany(inputs []UserCreate) *CreateManyBuilder[User, UserCreate] { - return &CreateManyBuilder[User, UserCreate]{ +func (d *UserDelegate) CreateMany(records ...RecordInput) *CreateManyBuilder[User] { + return &CreateManyBuilder[User]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeUserCreateMany, } } -func (d *UserDelegate) CreateManyAndReturn(inputs []UserCreate) *CreateManyAndReturnBuilder[User, UserCreate, UserSelect, UserOmit] { - return &CreateManyAndReturnBuilder[User, UserCreate, UserSelect, UserOmit]{ +func (d *UserDelegate) CreateManyAndReturn(records ...RecordInput) *CreateManyAndReturnBuilder[User, UserSelect, UserOmit] { + return &CreateManyAndReturnBuilder[User, UserSelect, UserOmit]{ client: d.client, - inputs: inputs, + records: records, execFunc: d.client.executeUserCreateManyAndReturn, } } -func (q *Queries) executeUserCreateMany(ctx context.Context, inputs []UserCreate) (int64, error) { - if len(inputs) == 0 { +func (q *Queries) executeUserCreateMany(ctx context.Context, records []RecordInput) (int64, error) { + if len(records) == 0 { return 0, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateUserCreate(rec.Assignments); err != nil { return 0, fmt.Errorf("validation failed at index %d: %w", i, err) } } if q.dialect.SupportsBulkInsert() { - rowMaps := make([]map[string]any, len(inputs)) - for i, input := range inputs { - rowMaps[i] = q.UserInputToMap(input) - } + rowMaps := userRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "User", rowMaps, UserColOrder, nil) res, err := q.exec(ctx, query, vals...) if err != nil { @@ -355,8 +402,8 @@ func (q *Queries) executeUserCreateMany(ctx context.Context, inputs []UserCreate var count int64 err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - _, err := txQ.executeUserCreate(ctx, input, nil, nil) + for _, rec := range records { + _, err := txQ.executeUserCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } @@ -367,12 +414,12 @@ func (q *Queries) executeUserCreateMany(ctx context.Context, inputs []UserCreate return count, err } -func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []UserCreate, selects *UserSelect, omits *UserOmit) ([]*User, error) { - if len(inputs) == 0 { +func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, records []RecordInput, selects *UserSelect, omits *UserOmit) ([]*User, error) { + if len(records) == 0 { return nil, nil } - for i, input := range inputs { - if err := input.Validate(); err != nil { + for i, rec := range records { + if err := validateUserCreate(rec.Assignments); err != nil { return nil, fmt.Errorf("validation failed at index %d: %w", i, err) } } @@ -381,12 +428,9 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []U 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) - } + rowMaps := userRecordsToRowMaps(records) query, vals := buildBulkInsertSQL(q.dialect, "User", rowMaps, UserColOrder, returningCols) - records := make([]*User, 0) + recordsOut := make([]*User, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -398,42 +442,41 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []U if err := rows.Scan(record.ScanFields(returningCols)...); err != nil { return err } - records = append(records, &record) + recordsOut = append(recordsOut, &record) } if err := rows.Err(); err != nil { return err } if hasRelations { - return txQ.loadUserRelations(ctx, records, selects) + return txQ.loadUserRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } - // Fallback to loop inside transaction - records := make([]*User, 0) + recordsOut := make([]*User, 0) err := q.transaction(ctx, func(txQ *Queries) error { - for _, input := range inputs { - res, err := txQ.executeUserCreate(ctx, input, nil, nil) + for _, rec := range records { + res, err := txQ.executeUserCreate(ctx, rec.Assignments, nil, nil) if err != nil { return err } - records = append(records, res) + recordsOut = append(recordsOut, res) } if hasRelations { - return txQ.loadUserRelations(ctx, records, selects) + return txQ.loadUserRelations(ctx, recordsOut, selects) } return nil }) if err != nil { return nil, err } - return records, nil + return recordsOut, nil } func (d *UserDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[User, UserSelect, UserOmit] { return &FindUniqueBuilder[User, UserSelect, UserOmit]{ diff --git a/integration/valk/user/user.go b/integration/valk/user/user.go index 10dacdd..2517fc4 100644 --- a/integration/valk/user/user.go +++ b/integration/valk/user/user.go @@ -20,7 +20,10 @@ func (p UniquePredicate) Validate() error { type Select = valk.UserSelect type Omit = valk.UserOmit -type Create = valk.UserCreate + +func Record(assignments ...valk.FieldAssignment) valk.RecordInput { + return valk.RecordInput{Assignments: assignments} +} func And(preds ...valk.Predicate) valk.Predicate { return valk.And(preds...)