diff --git a/cli/handleGenerate.go b/cli/handleGenerate.go index 5b6e149..8668fa7 100644 --- a/cli/handleGenerate.go +++ b/cli/handleGenerate.go @@ -47,14 +47,25 @@ func handleGenerate() { pkgName = "valk" } - outputs, err := generator.GenerateClient(*schemaDef, pkgName, embedRelDir, config.Output.Migrations, config.Log) + parentImportPath, err := generator.ResolveImportPath(config.Output.Client) + if err != nil { + fmt.Printf("failed to resolve parent import path: %v\n", err) + return + } + + outputs, err := generator.GenerateClient(*schemaDef, pkgName, parentImportPath, embedRelDir, config.Output.Migrations, config.Log) if err != nil { fmt.Printf("failed to generate client: %v\n", err) return } for filename, content := range outputs { - if err := os.WriteFile(filepath.Join(config.Output.Client, filename), []byte(content), 0644); err != nil { + outPath := filepath.Join(config.Output.Client, filename) + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + fmt.Println(err) + return + } + if err := os.WriteFile(outPath, []byte(content), 0644); err != nil { fmt.Println(err) return } diff --git a/generator/generator.go b/generator/generator.go index 869cf88..9d6d781 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -3,9 +3,13 @@ package generator import ( "bytes" "embed" + "fmt" "go/format" + "os" "path/filepath" + "strings" "text/template" + "github.com/voidclancy/valk/schema" ) @@ -22,15 +26,77 @@ type templateData struct { } type modelTemplateData struct { - PackageName string - Model *schema.Model + PackageName string + Model *schema.Model + ParentImportPath string + ParentPackageName string +} + +func ResolveImportPath(clientDir string) (string, error) { + absClientDir, err := filepath.Abs(clientDir) + if err != nil { + return "", err + } + + current := absClientDir + for { + modFile := filepath.Join(current, "go.mod") + if _, err := os.Stat(modFile); err == nil { + content, err := os.ReadFile(modFile) + if err != nil { + return "", err + } + var modName string + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "module ") { + modName = strings.TrimSpace(strings.TrimPrefix(line, "module")) + break + } + } + if modName == "" { + return "", fmt.Errorf("go.mod found but no module declaration found") + } + + rel, err := filepath.Rel(current, absClientDir) + if err != nil { + return "", err + } + if rel == "." { + return modName, nil + } + return filepath.ToSlash(filepath.Join(modName, rel)), nil + } + + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } + + return filepath.Base(clientDir), nil } -func GenerateClient(sch schema.Schema, pkgName string, embedPath string, defaultDiskPath string, defaultLogs []string) (map[string]string, error) { +func GenerateClient(sch schema.Schema, pkgName string, parentImportPath string, embedPath string, defaultDiskPath string, defaultLogs []string) (map[string]string, error) { tmpl := template.New("").Funcs(template.FuncMap{ "capitalize": capitalize, "lowercase": lowercase, "fkForRelation": fkForRelation, + "fieldPredType": func(f *schema.ScalarField, parentPkg string) string { + if f.EnumRef != nil { + if f.IsArray { + return "[]" + parentPkg + "." + f.EnumRef.Name + "Type" + } + return parentPkg + "." + f.EnumRef.Name + "Type" + } + t := f.GoType + if f.Optional { + t = strings.TrimPrefix(t, "*") + } + return t + }, "hasLog": func(level string) bool { for _, l := range defaultLogs { if l == "all" || l == level { @@ -47,6 +113,30 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default } return false }, + "hasJsonField": func(m *schema.Model) bool { + for _, sf := range m.ScalarFields { + if sf.Type == "Json" || strings.Contains(sf.GoType, "json.RawMessage") { + return true + } + } + return false + }, + "hasTimeField": func(m *schema.Model) bool { + for _, sf := range m.ScalarFields { + if sf.Type == "DateTime" || strings.Contains(sf.GoType, "time.Time") { + return true + } + } + return false + }, + "hasStringField": func(m *schema.Model) bool { + for _, sf := range m.ScalarFields { + if sf.GoType == "string" || strings.Contains(sf.GoType, "string") { + return true + } + } + return false + }, }) tmpl, err := tmpl.ParseFS(templatesFS, "templates/*.gotpl") if err != nil { @@ -76,6 +166,7 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default "client.gotpl", "tx.gotpl", "builders_create.gotpl", + "builders_read.gotpl", "relations_runtime.gotpl", } for _, file := range files { @@ -93,8 +184,10 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default for _, m := range sch.Models { var mBuf bytes.Buffer mData := modelTemplateData{ - PackageName: pkgName, - Model: m, + PackageName: pkgName, + Model: m, + ParentImportPath: parentImportPath, + ParentPackageName: pkgName, } if err := tmpl.ExecuteTemplate(&mBuf, "model_header.gotpl", mData); err != nil { @@ -104,6 +197,7 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default mFiles := []string{ "model_structs.gotpl", "model_create.gotpl", + "model_read.gotpl", "model_relations.gotpl", } for _, file := range mFiles { @@ -117,6 +211,23 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default return nil, err } outputs[lowercase(m.Name)+".go"] = string(mFormatted) + + // Generate the sub-package predicate file (e.g. user/user.go) + var pBuf bytes.Buffer + pData := modelTemplateData{ + PackageName: lowercase(m.Name), + Model: m, + ParentImportPath: parentImportPath, + ParentPackageName: pkgName, + } + if err := tmpl.ExecuteTemplate(&pBuf, "model_predicate.gotpl", pData); err != nil { + return nil, err + } + pFormatted, err := format.Source(pBuf.Bytes()) + if err != nil { + return nil, err + } + outputs[lowercase(m.Name)+"/"+lowercase(m.Name)+".go"] = string(pFormatted) } return outputs, nil diff --git a/generator/generator_test.go b/generator/generator_test.go index 77a6db6..4a7d7c6 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -41,7 +41,7 @@ func TestGenerateClient_NativeDBConstraints(t *testing.T) { }, } - outputs, err := GenerateClient(sch, "valk", "", "", nil) + outputs, err := GenerateClient(sch, "valk", "github.com/voidclancy/valk", "", "", nil) if err != nil { t.Fatalf("failed to generate client: %v", err) } diff --git a/generator/templates/builders_read.gotpl b/generator/templates/builders_read.gotpl new file mode 100644 index 0000000..3ffe385 --- /dev/null +++ b/generator/templates/builders_read.gotpl @@ -0,0 +1,247 @@ +type FindUniqueBuilder[M any, S any, O any] struct { + client *Queries + where UniquePredicate + execFunc func(ctx context.Context, where UniquePredicate, s *S, o *O) (*M, error) +} + +func (b *FindUniqueBuilder[M, S, O]) Select(s S) *FindUniqueSelectBuilder[M, S, O] { + return &FindUniqueSelectBuilder[M, S, O]{builder: b, selects: s} +} + +func (b *FindUniqueBuilder[M, S, O]) Omit(o O) *FindUniqueOmitBuilder[M, S, O] { + return &FindUniqueOmitBuilder[M, S, O]{builder: b, omits: o} +} + +func (b *FindUniqueBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.execFunc(ctx, b.where, nil, nil) +} + +type FindUniqueSelectBuilder[M any, S any, O any] struct { + builder *FindUniqueBuilder[M, S, O] + selects S +} + +func (b *FindUniqueSelectBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, &b.selects, nil) +} + +type FindUniqueOmitBuilder[M any, S any, O any] struct { + builder *FindUniqueBuilder[M, S, O] + omits O +} + +func (b *FindUniqueOmitBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, nil, &b.omits) +} + +type FindFirstBuilder[M any, S any, O any] struct { + client *Queries + where []Predicate + execFunc func(ctx context.Context, where []Predicate, s *S, o *O) (*M, error) +} + +func (b *FindFirstBuilder[M, S, O]) Select(s S) *FindFirstSelectBuilder[M, S, O] { + return &FindFirstSelectBuilder[M, S, O]{builder: b, selects: s} +} + +func (b *FindFirstBuilder[M, S, O]) Omit(o O) *FindFirstOmitBuilder[M, S, O] { + return &FindFirstOmitBuilder[M, S, O]{builder: b, omits: o} +} + +func (b *FindFirstBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.execFunc(ctx, b.where, nil, nil) +} + +type FindFirstSelectBuilder[M any, S any, O any] struct { + builder *FindFirstBuilder[M, S, O] + selects S +} + +func (b *FindFirstSelectBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, &b.selects, nil) +} + +type FindFirstOmitBuilder[M any, S any, O any] struct { + builder *FindFirstBuilder[M, S, O] + omits O +} + +func (b *FindFirstOmitBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, nil, &b.omits) +} + +type FindManyBuilder[M any, S any, O any] struct { + client *Queries + where []Predicate + execFunc func(ctx context.Context, where []Predicate, s *S, o *O) ([]*M, error) +} + +func (b *FindManyBuilder[M, S, O]) Select(s S) *FindManySelectBuilder[M, S, O] { + return &FindManySelectBuilder[M, S, O]{builder: b, selects: s} +} + +func (b *FindManyBuilder[M, S, O]) Omit(o O) *FindManyOmitBuilder[M, S, O] { + return &FindManyOmitBuilder[M, S, O]{builder: b, omits: o} +} + +func (b *FindManyBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.execFunc(ctx, b.where, nil, nil) +} + +type FindManySelectBuilder[M any, S any, O any] struct { + builder *FindManyBuilder[M, S, O] + selects S +} + +func (b *FindManySelectBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.where, &b.selects, nil) +} + +type FindManyOmitBuilder[M any, S any, O any] struct { + builder *FindManyBuilder[M, S, O] + omits O +} + +func (b *FindManyOmitBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.where, nil, &b.omits) +} + +func executeFindOne[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(record *M, cols []string) []any, +) (*M, error) { + var sb strings.Builder + sb.Grow(64 + len(returningCols)*15 + len(table) + len(whereClause)) + sb.WriteString("SELECT ") + for i, col := range returningCols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(q.dialect.Quote(col)) + } + sb.WriteString(" FROM ") + sb.WriteString(q.dialect.Quote(table)) + sb.WriteString(whereClause) + sb.WriteString(" LIMIT 1") + + var res M + row := q.queryRow(ctx, sb.String(), whereVals...) + scanTargets := scanFunc(&res, returningCols) + if err := row.Scan(scanTargets...); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &res, nil +} + +func executeFindMany[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(record *M, cols []string) []any, +) ([]*M, error) { + var sb strings.Builder + sb.Grow(64 + len(returningCols)*15 + len(table) + len(whereClause)) + sb.WriteString("SELECT ") + for i, col := range returningCols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(q.dialect.Quote(col)) + } + sb.WriteString(" FROM ") + sb.WriteString(q.dialect.Quote(table)) + sb.WriteString(whereClause) + + rows, err := q.query(ctx, sb.String(), whereVals...) + if err != nil { + return nil, err + } + defer rows.Close() + + results := make([]*M, 0) + for rows.Next() { + var res M + scanTargets := scanFunc(&res, returningCols) + if err := rows.Scan(scanTargets...); err != nil { + return nil, err + } + results = append(results, &res) + } + if err := rows.Err(); err != nil { + return nil, err + } + return results, nil +} + +func executeSingleWithRelations[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(*M, []string) []any, + hasRelations bool, + loadRelations func(ctx context.Context, txQ *Queries, results []*M) error, +) (*M, error) { + if !hasRelations { + return executeFindOne(ctx, q, table, whereClause, whereVals, returningCols, scanFunc) + } + + var res *M + err := q.transaction(ctx, func(txQ *Queries) error { + var err error + res, err = executeFindOne(ctx, txQ, table, whereClause, whereVals, returningCols, scanFunc) + if err != nil || res == nil { + return err + } + return loadRelations(ctx, txQ, []*M{res}) + }) + if err != nil { + return nil, err + } + return res, nil +} + +func executeManyWithRelations[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(*M, []string) []any, + hasRelations bool, + loadRelations func(ctx context.Context, txQ *Queries, results []*M) error, +) ([]*M, error) { + if !hasRelations { + return executeFindMany(ctx, q, table, whereClause, whereVals, returningCols, scanFunc) + } + + results := make([]*M, 0) + err := q.transaction(ctx, func(txQ *Queries) error { + var err error + results, err = executeFindMany(ctx, txQ, table, whereClause, whereVals, returningCols, scanFunc) + if err != nil || len(results) == 0 { + return err + } + return loadRelations(ctx, txQ, results) + }) + if err != nil { + return nil, err + } + return results, nil +} + + diff --git a/generator/templates/client.gotpl b/generator/templates/client.gotpl index 207ea2b..de8d822 100644 --- a/generator/templates/client.gotpl +++ b/generator/templates/client.gotpl @@ -243,3 +243,718 @@ func (q *Queries) transaction(ctx context.Context, fn func(txQ *Queries) error) {{- end }} return tx.Commit() } + +type PredicateData struct { + Column string + Operator string + Value any + IsLogical bool + Children []PredicateData +} + +type Predicate interface { + ToPredicateData() PredicateData + Validate() error +} + +type UniquePredicate interface { + Predicate + IsUnique() + Validate() error +} + +type StandardPredicate struct { + Data PredicateData +} + +func (sp StandardPredicate) ToPredicateData() PredicateData { + return sp.Data +} + +func validateValue(col string, val any) error { + switch v := val.(type) { + case string: + if strings.Contains(v, "\x00") { + return &ValidationError{ + Errors: []FieldError{ + {Field: col, Value: v, Rule: "safety", Msg: "string cannot contain null bytes"}, + }, + } + } + if !utf8.ValidString(v) { + return &ValidationError{ + Errors: []FieldError{ + {Field: col, Value: v, Rule: "safety", Msg: "string must be valid UTF-8"}, + }, + } + } + case []string: + for _, s := range v { + if err := validateValue(col, s); err != nil { + return err + } + } + case []any: + for _, item := range v { + if err := validateValue(col, item); err != nil { + return err + } + } + } + return nil +} + +func (pd PredicateData) Validate() error { + if pd.IsLogical { + for _, child := range pd.Children { + if err := child.Validate(); err != nil { + return err + } + } + return nil + } + return validateValue(pd.Column, pd.Value) +} + +func (sp StandardPredicate) Validate() error { + return sp.Data.Validate() +} + +func And(preds ...Predicate) Predicate { + var children []PredicateData + for _, p := range preds { + if p != nil { + children = append(children, p.ToPredicateData()) + } + } + return StandardPredicate{ + Data: PredicateData{ + IsLogical: true, + Operator: "AND", + Children: children, + }, + } +} + +func Or(preds ...Predicate) Predicate { + var children []PredicateData + for _, p := range preds { + if p != nil { + children = append(children, p.ToPredicateData()) + } + } + return StandardPredicate{ + Data: PredicateData{ + IsLogical: true, + Operator: "OR", + Children: children, + }, + } +} + +func Not(pred Predicate) Predicate { + var children []PredicateData + if pred != nil { + children = append(children, pred.ToPredicateData()) + } + return StandardPredicate{ + Data: PredicateData{ + IsLogical: true, + Operator: "NOT", + Children: children, + }, + } +} + +type Field[T any] struct { + Column string +} + +func (f Field[T]) EQ(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + } +} + +func (f Field[T]) NEQ(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f Field[T]) GT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f Field[T]) GTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f Field[T]) LT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f Field[T]) LTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f Field[T]) In(vals []T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f Field[T]) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f Field[T]) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +type UniqueField[T any] struct { + Column string +} + +type UniqueFieldPredicate struct { + StandardPredicate +} + +func (UniqueFieldPredicate) IsUnique() {} + +func (p UniqueFieldPredicate) Validate() error { + if p.Data.Column == "" { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +func (f UniqueField[T]) EQ(val T) UniquePredicate { + return UniqueFieldPredicate{ + StandardPredicate: StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + }, + } +} + +func (f UniqueField[T]) NEQ(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f UniqueField[T]) GT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f UniqueField[T]) GTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f UniqueField[T]) LT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f UniqueField[T]) LTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f UniqueField[T]) In(vals []T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f UniqueField[T]) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f UniqueField[T]) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +type StringField struct { + Column string +} + +func (f StringField) EQ(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + } +} + +func (f StringField) NEQ(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f StringField) GT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f StringField) GTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f StringField) LT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f StringField) LTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f StringField) In(vals []string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f StringField) Like(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: val, + }, + } +} + +func (f StringField) Contains(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: "%" + val + "%", + }, + } +} + +func (f StringField) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f StringField) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +type StringUniqueField struct { + Column string +} + +func (f StringUniqueField) EQ(val string) UniquePredicate { + return UniqueFieldPredicate{ + StandardPredicate: StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + }, + } +} + +func (f StringUniqueField) NEQ(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f StringUniqueField) GT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f StringUniqueField) GTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f StringUniqueField) LT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f StringUniqueField) LTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f StringUniqueField) In(vals []string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f StringUniqueField) Like(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: val, + }, + } +} + +func (f StringUniqueField) Contains(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: "%" + val + "%", + }, + } +} + +func (f StringUniqueField) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f StringUniqueField) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +func CompilePredicates(dialect Dialect, preds []Predicate) (string, []any) { + if len(preds) == 0 { + return "", nil + } + var data []PredicateData + for _, p := range preds { + if p != nil { + data = append(data, p.ToPredicateData()) + } + } + return CompilePredicateData(dialect, data) +} + +func CompilePredicateData(dialect Dialect, data []PredicateData) (string, []any) { + if len(data) == 0 { + return "", nil + } + var parts []string + var args []any + var bindIdx = 1 + + var compile func(p PredicateData) string + compile = func(p PredicateData) string { + if p.IsLogical { + if len(p.Children) == 0 { + return "" + } + if p.Operator == "NOT" { + sub := compile(p.Children[0]) + if sub == "" { + return "" + } + return fmt.Sprintf("NOT (%s)", sub) + } + var subParts []string + for _, child := range p.Children { + sub := compile(child) + if sub != "" { + subParts = append(subParts, sub) + } + } + if len(subParts) == 0 { + return "" + } + if len(subParts) == 1 { + return subParts[0] + } + return fmt.Sprintf("(%s)", strings.Join(subParts, " "+p.Operator+" ")) + } + + switch p.Operator { + case "IS NULL", "IS NOT NULL": + return fmt.Sprintf("%s %s", dialect.Quote(p.Column), p.Operator) + case "IN": + valSlice := unpackSlice(p.Value) + if len(valSlice) == 0 { + return "1=0" + } + var placeHolders []string + for range valSlice { + placeHolders = append(placeHolders, dialect.BindVar(bindIdx)) + bindIdx++ + } + for _, val := range valSlice { + args = append(args, val) + } + return fmt.Sprintf("%s IN (%s)", dialect.Quote(p.Column), strings.Join(placeHolders, ", ")) + default: + placeholder := dialect.BindVar(bindIdx) + bindIdx++ + args = append(args, p.Value) + return fmt.Sprintf("%s %s %s", dialect.Quote(p.Column), p.Operator, placeholder) + } + } + + for _, p := range data { + part := compile(p) + if part != "" { + parts = append(parts, part) + } + } + + if len(parts) == 0 { + return "", nil + } + return strings.Join(parts, " AND "), args +} + +func unpackSlice(val any) []any { + if val == nil { + return nil + } + switch v := val.(type) { + case []string: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []int: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []int32: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []int64: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []float32: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []float64: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []bool: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []time.Time: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case [][]byte: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []json.RawMessage: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []any: + return v + {{- range .Schema.Enums }} + case []{{ .Name }}Type: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + {{- end }} + default: + return []any{val} + } +} diff --git a/generator/templates/header.gotpl b/generator/templates/header.gotpl index f5b7cda..c25c6bd 100644 --- a/generator/templates/header.gotpl +++ b/generator/templates/header.gotpl @@ -15,6 +15,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "github.com/google/uuid" "github.com/pressly/goose/v3" diff --git a/generator/templates/model_create.gotpl b/generator/templates/model_create.gotpl index e3b0c5f..6fa5c53 100644 --- a/generator/templates/model_create.gotpl +++ b/generator/templates/model_create.gotpl @@ -197,7 +197,7 @@ func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Contex rowMaps[i] = q.{{ .Model.Name }}InputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "{{ .Model.EffectiveTableName }}", rowMaps, {{ .Model.Name }}ColOrder, returningCols) - var records []*{{ .Model.Name }} + records := make([]*{{ .Model.Name }}, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -226,7 +226,7 @@ func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Contex } // Fallback to loop inside transaction - var records []*{{ .Model.Name }} + records := 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) diff --git a/generator/templates/model_header.gotpl b/generator/templates/model_header.gotpl index ee878fa..45180f8 100644 --- a/generator/templates/model_header.gotpl +++ b/generator/templates/model_header.gotpl @@ -2,19 +2,19 @@ package {{ .PackageName }} import ( "context" - "database/sql" + {{- if hasJsonField .Model }} + "encoding/json" + {{- end }} "fmt" "slices" + {{- if hasStringField .Model }} "strings" + {{- end }} + {{- if hasTimeField .Model }} "time" + {{- end }} + {{- if hasStringField .Model }} "unicode/utf8" + {{- end }} ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - diff --git a/generator/templates/model_predicate.gotpl b/generator/templates/model_predicate.gotpl new file mode 100644 index 0000000..6878963 --- /dev/null +++ b/generator/templates/model_predicate.gotpl @@ -0,0 +1,100 @@ +package {{ .PackageName }} + +import ( + "fmt" + {{- if hasJsonField .Model }} + "encoding/json" + {{- end }} + {{- if hasTimeField .Model }} + "time" + {{- end }} + "{{ .ParentImportPath }}" +) + + + +type UniquePredicate struct { + {{ .ParentPackageName }}.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = {{ .ParentPackageName }}.{{ .Model.Name }}Select +type Omit = {{ .ParentPackageName }}.{{ .Model.Name }}Omit +type Create = {{ .ParentPackageName }}.{{ .Model.Name }}Create + +func And(preds ...{{ .ParentPackageName }}.Predicate) {{ .ParentPackageName }}.Predicate { + return {{ .ParentPackageName }}.And(preds...) +} + +func Or(preds ...{{ .ParentPackageName }}.Predicate) {{ .ParentPackageName }}.Predicate { + return {{ .ParentPackageName }}.Or(preds...) +} + +func Not(pred {{ .ParentPackageName }}.Predicate) {{ .ParentPackageName }}.Predicate { + return {{ .ParentPackageName }}.Not(pred) +} + +{{ range $field := .Model.ScalarFields -}} +{{- $isUnique := or $field.IsID $field.IsUnique -}} +{{- $fieldType := fieldPredType $field $.ParentPackageName -}} +{{- $col := $field.EffectiveColName -}} +{{- if eq $field.Type "String" }} + {{- if $isUnique }} +var {{ capitalize $field.Name }} = {{ $.ParentPackageName }}.StringUniqueField{Column: "{{ $col }}"} + {{- else }} +var {{ capitalize $field.Name }} = {{ $.ParentPackageName }}.StringField{Column: "{{ $col }}"} + {{- end }} +{{- else }} + {{- if $isUnique }} +var {{ capitalize $field.Name }} = {{ $.ParentPackageName }}.UniqueField[{{ $fieldType }}]{Column: "{{ $col }}"} + {{- else }} +var {{ capitalize $field.Name }} = {{ $.ParentPackageName }}.Field[{{ $fieldType }}]{Column: "{{ $col }}"} + {{- end }} +{{- end }} +{{ end }} + +{{- range $index := .Model.CompositeUnique }} +{{- $constraintName := $index.Name }} +{{- if eq $constraintName "" }} + {{- range $i, $f := $index.Fields }} + {{- if $i }} + {{- $constraintName = printf "%s_%s" $constraintName (capitalize $f) }} + {{- else }} + {{- $constraintName = capitalize $f }} + {{- end }} + {{- end }} +{{- end }} +// Helper for compound unique constraint: {{ $constraintName }} +func {{ capitalize $constraintName }}Unique( + {{- range $i, $fName := $index.Fields -}} + {{- $field := $.Model.GetField $fName -}} + {{- if $i }}, {{ end -}} + {{- lowercase $field.Name }} {{ fieldPredType $field $.ParentPackageName -}} + {{- end -}} +) UniquePredicate { + return UniquePredicate{ + StandardPredicate: {{ $.ParentPackageName }}.StandardPredicate{ + Data: {{ $.ParentPackageName }}.And( + {{- range $fName := $index.Fields }} + {{- $field := $.Model.GetField $fName }} + {{ $.ParentPackageName }}.StandardPredicate{ + Data: {{ $.ParentPackageName }}.PredicateData{ + Column: "{{ $field.EffectiveColName }}", + Operator: "=", + Value: {{ lowercase $field.Name }}, + }, + }, + {{- end }} + ).ToPredicateData(), + }, + } +} +{{- end }} diff --git a/generator/templates/model_read.gotpl b/generator/templates/model_read.gotpl new file mode 100644 index 0000000..e2ff964 --- /dev/null +++ b/generator/templates/model_read.gotpl @@ -0,0 +1,88 @@ +func (d *{{ .Model.Name }}Delegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { + return &FindUniqueBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ + client: d.client, + where: where, + execFunc: d.client.execute{{ .Model.Name }}FindUnique, + } +} + +func (d *{{ .Model.Name }}Delegate) FindFirst(preds ...Predicate) *FindFirstBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { + return &FindFirstBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ + client: d.client, + where: preds, + execFunc: d.client.execute{{ .Model.Name }}FindFirst, + } +} + +func (d *{{ .Model.Name }}Delegate) FindMany(preds ...Predicate) *FindManyBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] { + return &FindManyBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{ + client: d.client, + where: preds, + execFunc: d.client.execute{{ .Model.Name }}FindMany, + } +} + +func (q *Queries) execute{{ .Model.Name }}FindUnique(ctx context.Context, where UniquePredicate, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.select{{ .Model.Name }}Cols(selects, omits) + return executeSingleWithRelations(ctx, q, "{{ .Model.EffectiveTableName }}", whereClause, vals, returningCols, + func(res *{{ .Model.Name }}, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*{{ .Model.Name }}) error { + return txQ.load{{ .Model.Name }}Relations(ctx, results, selects) + }, + ) +} + +func (q *Queries) execute{{ .Model.Name }}FindFirst(ctx context.Context, where []Predicate, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.select{{ .Model.Name }}Cols(selects, omits) + return executeSingleWithRelations(ctx, q, "{{ .Model.EffectiveTableName }}", whereClause, vals, returningCols, + func(res *{{ .Model.Name }}, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*{{ .Model.Name }}) error { + return txQ.load{{ .Model.Name }}Relations(ctx, results, selects) + }, + ) +} + +func (q *Queries) execute{{ .Model.Name }}FindMany(ctx context.Context, where []Predicate, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) ([]*{{ .Model.Name }}, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.select{{ .Model.Name }}Cols(selects, omits) + return executeManyWithRelations(ctx, q, "{{ .Model.EffectiveTableName }}", whereClause, vals, returningCols, + func(res *{{ .Model.Name }}, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*{{ .Model.Name }}) error { + return txQ.load{{ .Model.Name }}Relations(ctx, results, selects) + }, + ) +} diff --git a/generator/templates/model_structs.gotpl b/generator/templates/model_structs.gotpl index be723b5..e54e760 100644 --- a/generator/templates/model_structs.gotpl +++ b/generator/templates/model_structs.gotpl @@ -1,7 +1,7 @@ // {{ .Model.Name }} represents the database model type {{ .Model.Name }} struct { {{- range $field := .Model.ScalarFields }} - {{ capitalize $field.Name }} {{ if $field.EnumRef }}{{ if $field.IsArray }}[]{{ $field.EnumRef.Name }}Type{{ else }}{{ if $field.Optional }}*{{ end }}{{ $field.EnumRef.Name }}Type{{ end }}{{ else }}{{ $field.GoType }}{{ end }} `db:"{{ $field.EffectiveColName }}" json:"{{ $field.Name }}"` + {{ capitalize $field.Name }} {{ if $field.EnumRef }}{{ if $field.IsArray }}[]{{ $field.EnumRef.Name }}Type{{ else }}{{ if $field.Optional }}*{{ end }}{{ $field.EnumRef.Name }}Type{{ end }}{{ else }}{{ $field.GoType }}{{ end }} `db:"{{ $field.EffectiveColName }}" json:"{{ $field.Name }}{{ if $field.Optional }},omitempty{{ end }}"` {{- end }} {{- range $relation := .Model.RelationFields }} {{ capitalize $relation.Name }} {{ if $relation.IsArray }}[]*{{ $relation.TargetModelName }}{{ else }}*{{ $relation.TargetModelName }}{{ end }} `json:"{{ $relation.Name }},omitempty"` @@ -78,8 +78,9 @@ func (q *Queries) select{{ .Model.Name }}Cols(selects *{{ .Model.Name }}Select, specs := []colSpec{ {{- range $field := .Model.ScalarFields }} {"{{ $field.EffectiveColName }}", selects != nil && selects.{{ capitalize $field.Name }}, omits != nil && omits.{{ capitalize $field.Name }}, + {{- if $field.IsID }} selects != nil && selects.hasAnyRelation(){{ else }} {{- $relName := fkForRelation $.Model $field }} - {{- if ne $relName "" }} selects != nil && selects.{{ capitalize $relName }} != nil{{ else }} false{{ end }}}, + {{- if ne $relName "" }} selects != nil && selects.{{ capitalize $relName }} != nil{{ else }} false{{ end }}{{ end }}}, {{- end }} } @@ -220,3 +221,6 @@ func (input {{ .Model.Name }}Create) Validate() error { } return nil } + + + diff --git a/integration/integration.test b/integration/integration.test deleted file mode 100644 index 52f1d84..0000000 Binary files a/integration/integration.test and /dev/null differ diff --git a/integration/main.go b/integration/main.go index 53a52db..f65a6d8 100644 --- a/integration/main.go +++ b/integration/main.go @@ -2,100 +2,215 @@ package main import ( "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "fmt" "integration/valk" + "integration/valk/category" + "integration/valk/categoryToPost" + "integration/valk/comment" + "integration/valk/post" + "integration/valk/profile" + "integration/valk/user" "log" _ "modernc.org/sqlite" ) -func hashPassword(pass string) string { - h := sha256.Sum256([]byte(pass)) - return hex.EncodeToString(h[:]) +type SeedData struct { + ReferrerId string + ReferredId string + PostId string + Meta1 json.RawMessage + Meta2 json.RawMessage +} + +func seed(db *valk.DB, ctx context.Context) *SeedData { + referrer, err := db.User.Create(user.Create{ + Email: "referrer@example.com", + PhoneNum: "555-0001", + Password: new("pass123"), + Role: &valk.UserRole.Admin, + }).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) + 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) + 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) + if err != nil { + log.Fatalf("failed to create post: %v", err) + } + + cat, err := db.Category.Create(category.Create{ + Name: "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) + 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) + 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) + if err != nil { + log.Fatalf("failed to create comment 2: %v", err) + } + + return &SeedData{ + ReferrerId: referrer.Id, + ReferredId: referred.Id, + PostId: p.Id, + Meta1: meta1, + Meta2: meta2, + } } -func main() { +func main() { db := openConn() defer db.Close() rawDB := db.Raw() rawDB.SetMaxOpenConns(10) - ctx := context.Background() runMigrations(db, ctx) - db.User.BeforeCreate(func(ctx context.Context, i *valk.UserCreate) error { - if i.Password != nil { - hash := hashPassword(*i.Password) - i.Password = &hash - return nil - } - return nil - }) - - db.User.AfterCreate(func(ctx context.Context, u *valk.User) error { - fmt.Printf("HASHED PASS: %s \n", *u.Password) - return nil - }) - author, err := db.User.Create(valk.UserCreate{ - Email: "clancySizer@gmail.com", - PhoneNum: "+1234567890", - Password: new("veryStrongPassword"), - Role: &valk.UserRole.Admin, - }).Exec(ctx) + fmt.Println("=== Seeding Data ===") + data := seed(db, ctx) + fmt.Println("Seeding complete.") + fmt.Println() + all, err := db.User.FindMany().Exec(ctx) if err != nil { - log.Fatalf("failed to create user: %v", err) + log.Fatalf("failed to get all users: %v", err) } + fmt.Println("=== ALL ===") - post, err := db.Post.Create(valk.PostCreate{ + printJSON(all) - Content: new("eheheh"), - Title: "some post", - AuthorId: author.Id, + fmt.Println("=== QUERY 1: Deep Nested Select ===") + resUser, err := db.User.FindFirst( + user.Email.EQ("referred@example.com"), + ).Select(user.Select{ + Email: true, + Profile: &profile.Select{ + Bio: true, + }, + ReferredBy: &user.Select{ + Email: true, + PhoneNum: true, + }, + Posts: &post.Select{ + Title: true, + Comments: &comment.Select{ + Textify: true, + Meta: true, + Author: &user.Select{ + Email: true, + }, + }, + }, }).Exec(ctx) if err != nil { - log.Fatalf("failed to create user: %v", err) + log.Fatalf("Query 1 failed: %v", err) + } + printJSON(resUser) + fmt.Println() + + fmt.Println("=== QUERY 2: Omit Nested Fields ===") + resPost, err := db.Post.FindFirst( + post.Title.Like("%Valkyrie%"), + ). + Select(post.Select{ + Title: true, + Published: true, + Comments: &comment.Select{ + Textify: true, + Meta: true, + }, + }). + Exec(ctx) + if err != nil { + log.Fatalf("Query 2 failed: %v", err) } + printJSON(resPost) + fmt.Println() - comment, err := db.Comment.Create(valk.CommentCreate{ - Textify: 42, - Dummy3: "d3", - Dummy1: 1, - Dummy2: "d2", - PostId: post.Id, - AuthorId: author.Id, - }).Select(valk.CommentSelect{ - Id: true, + fmt.Println("=== QUERY 3: Filtering with Relations ===") + resComments, err := db.Comment.FindMany( + comment.Meta.EQ(data.Meta1), + ).Select(comment.Select{ Textify: true, - Dummy3: true, - Dummy1: true, - Dummy2: true, - PostId: true, - Author: &valk.UserSelect{}, - Post: &valk.PostSelect{}, + Meta: true, + Post: &post.Select{ + Title: true, + Author: &user.Select{ + Email: true, + }, + }, }).Exec(ctx) if err != nil { - log.Fatalf("failed to create user: %v", err) + log.Fatalf("Query 3 failed: %v", err) } - - usersCount, err := db.User.CreateMany([]valk.UserCreate{ - {Email: "cl@gm.com"}, {Email: "cc@gg.com"}, - }).Exec(ctx) - fmt.Printf("\nCREATED %d USERS\n", usersCount) - fmt.Println("COMMENT:") - printJSON(comment) - + printJSON(resComments) + fmt.Println() } + func openConn() *valk.DB { db, err := valk.Open("sqlite", "file::memory:?_pragma=foreign_keys(1)") if err != nil { @@ -103,12 +218,12 @@ func openConn() *valk.DB { } return db } - func runMigrations(db *valk.DB, ctx context.Context) { if err := db.RunMigrations(ctx); err != nil { log.Fatalf("failed to run migrations: %v", err) } } + func runManualTransaction(db *valk.DB, ctx context.Context) { tx, err := db.BeginTx(ctx, nil) if err != nil { @@ -118,31 +233,28 @@ func runManualTransaction(db *valk.DB, ctx context.Context) { defer tx.Rollback() fmt.Println("Manual Transaction: started successfully") - author, err := tx.User.Create(valk.UserCreate{ + author, err := tx.User.Create(user.Create{ Email: "clancySizer@gmail.com", PhoneNum: "+1234567890", }).Exec(ctx) if err != nil { fmt.Printf("failed to create user: %+v", err) return - } - postWithAuthor, err := tx.Post.Create(valk.PostCreate{ + postWithAuthor, err := tx.Post.Create(post.Create{ Title: "A Post", AuthorId: author.Id, - }).Select(valk.PostSelect{ + }).Select(post.Select{ Id: true, Title: true, - Author: &valk.UserSelect{ - + Author: &user.Select{ Email: true, }, }).Exec(ctx) if err != nil { fmt.Printf("failed to create Post: %+v", err) return - } b, _ := json.MarshalIndent(postWithAuthor, "", " ") @@ -151,17 +263,15 @@ func runManualTransaction(db *valk.DB, ctx context.Context) { if err := tx.Commit(); err != nil { log.Printf("Manual Transaction: commit failed: %v", err) return - } fmt.Println("Manual Transaction: committed successfully") - } 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(valk.UserCreate{ + author, err := tx.User.Create(user.Create{ Email: "clancySizer@gmail.com", PhoneNum: "+1234567890", }).Exec(ctx) @@ -169,14 +279,13 @@ func runBlockBasedTransaction(db *valk.DB, ctx context.Context) { return err } - postWithAuthor, err := tx.Post.Create(valk.PostCreate{ + postWithAuthor, err := tx.Post.Create(post.Create{ Title: "A Post", AuthorId: author.Id, - }).Select(valk.PostSelect{ + }).Select(post.Select{ Id: true, Title: true, - Author: &valk.UserSelect{ - + Author: &user.Select{ Email: true, }, }).Exec(ctx) @@ -192,8 +301,8 @@ func runBlockBasedTransaction(db *valk.DB, ctx context.Context) { fmt.Printf("Block-based Transaction failed: %v", err) } fmt.Println("Block-based Transaction: committed successfully") - } + func printJSON(v any) { b, _ := json.MarshalIndent(v, "", " ") fmt.Println(string(b)) diff --git a/integration/read_test.go b/integration/read_test.go new file mode 100644 index 0000000..b793c6d --- /dev/null +++ b/integration/read_test.go @@ -0,0 +1,612 @@ +package main + +import ( + "context" + "encoding/json" + "integration/valk" + "integration/valk/comment" + "integration/valk/user" + "strings" + "sync" + "testing" + "time" +) + +func TestFindUniqueWithNoFieldsSet(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{ + Email: "onlyuser@example.com", + PhoneNum: "000", + }).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + res, err := db.User.FindUnique(nil).Exec(ctx) + if err == nil && res != nil { + t.Errorf("FindUnique with a zero-value where matched a row unexpectedly (%+v); it should require at least one unique field or return an error", res) + } +} + +func TestFindUniqueConflictingCompoundFields(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "a@example.com", PhoneNum: "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) + if err != nil { + t.Fatalf("seed b failed: %v", err) + } + + res, err := db.User.FindUnique(user.EmailPhoneUnique("a@example.com", "222")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res != nil { + t.Errorf("expected nil since no single row matches both email a@example.com and phone 222, got: %+v", res) + } +} + +func TestSelectWithNoFieldsSet(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "empty_select@example.com", PhoneNum: "333"}).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ("empty_select@example.com")).Select(valk.UserSelect{}).Exec(ctx) + if err != nil { + t.Fatalf("empty select produced an error instead of degrading gracefully: %v", err) + } + if res == nil { + t.Fatal("expected a non-nil result even with no fields selected") + } +} + +func TestOmitAllFields(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "omit_all@example.com", PhoneNum: "334"}).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ("omit_all@example.com")).Omit(valk.UserOmit{ + Id: true, + Email: true, + PhoneNum: true, + Password: true, + Role: true, + }).Exec(ctx) + if err != nil { + t.Fatalf("omitting every field produced an error instead of degrading gracefully: %v", err) + } + if res == nil { + t.Fatal("expected a non-nil result even with everything omitted") + } +} + +func TestOmitIdFieldStillAllowsFilterById(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + u, err := db.User.Create(valk.UserCreate{Email: "omit_id@example.com", PhoneNum: "335"}).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + res, err := db.User.FindUnique(user.Id.EQ(u.Id)).Omit(valk.UserOmit{Id: true}).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res == nil { + t.Fatal("expected to find the user by id even though id is omitted from the returned columns") + } + if res.Id != "" { + t.Errorf("expected omitted Id field to be zero-value, got %q", res.Id) + } +} + +func TestRelationLoadWithNoRelatedRows(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "noposts@example.com", PhoneNum: "444"}).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ("noposts@example.com")).Select(valk.UserSelect{ + Email: true, + Posts: &valk.PostSelect{Title: true}, + }).Exec(ctx) + if err != nil { + t.Fatalf("failed to find user with empty relation: %v", err) + } + if res == nil { + t.Fatal("expected non-nil user") + } + if len(res.Posts) != 0 { + t.Errorf("expected 0 related posts, got %d", len(res.Posts)) + } +} + +func TestFindUniqueRelationLoad(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + author, err := db.User.Create(valk.UserCreate{Email: "unique_rel@example.com", PhoneNum: "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) + if err != nil { + t.Fatalf("seed post failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ("unique_rel@example.com")).Select(valk.UserSelect{ + Email: true, + Posts: &valk.PostSelect{Title: true}, + }).Exec(ctx) + if err != nil { + t.Fatalf("FindUnique with relation load failed: %v", err) + } + if res == nil || len(res.Posts) != 1 || res.Posts[0].Title != "Unique Rel Post" { + t.Errorf("expected FindUnique to load the relation the same way FindMany does, got: %+v", res) + } +} + +func TestFindFirstRelationLoad(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + author, err := db.User.Create(valk.UserCreate{Email: "first_rel@example.com", PhoneNum: "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) + if err != nil { + t.Fatalf("seed post failed: %v", err) + } + + res, err := db.User.FindFirst(user.Email.EQ("first_rel@example.com")).Select(valk.UserSelect{ + Email: true, + Posts: &valk.PostSelect{Title: true}, + }).Exec(ctx) + if err != nil { + t.Fatalf("FindFirst with relation load failed: %v", err) + } + if res == nil || len(res.Posts) != 1 || res.Posts[0].Title != "First Rel Post" { + t.Errorf("expected FindFirst to load the relation the same way FindMany does, got: %+v", res) + } +} + +func TestContextAlreadyCancelled(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := db.User.FindMany().Exec(ctx) + if err == nil { + t.Error("expected error when querying with an already-cancelled context, got nil") + } +} + +func TestContextDeadlineExceeded(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) + defer cancel() + time.Sleep(time.Millisecond) + + _, err := db.User.FindMany().Exec(ctx) + if err == nil { + t.Error("expected error for an expired context deadline, got nil") + } +} + +func TestDuplicateUniqueCreateFails(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "dup@example.com", PhoneNum: "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) + if err == nil { + t.Error("expected a unique constraint violation on duplicate email, got nil error") + } +} + +func TestConcurrentDuplicateCreateOnlyOneSucceeds(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + const n = 10 + var wg sync.WaitGroup + var mu sync.Mutex + successCount := 0 + errCount := 0 + + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, err := db.User.Create(valk.UserCreate{ + Email: "race@example.com", + PhoneNum: "race-phone", + }).Exec(ctx) + mu.Lock() + defer mu.Unlock() + if err == nil { + successCount++ + } else { + errCount++ + } + }() + } + wg.Wait() + + if successCount != 1 { + t.Errorf("expected exactly 1 concurrent create to succeed under the unique constraint, got %d successes and %d errors", successCount, errCount) + } +} + +func TestWhitespacePaddedEmailNotTreatedAsDuplicate(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "dup2@example.com", PhoneNum: "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) + if err != nil { + t.Fatalf("expected leading-whitespace email to be treated as a distinct value, create failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ(" dup2@example.com")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res == nil || res.PhoneNum != "602" { + t.Errorf("expected exact match including leading whitespace, since no normalization should be silently applied, got: %+v", res) + } +} + +func TestEmailCaseSensitivity(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "CaseTest@Example.com", PhoneNum: "603"}).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ("casetest@example.com")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res != nil { + t.Errorf("query matched a differently-cased email (%+v); confirm this is an intentional case-insensitive collation and not an accidental DB default", res) + } +} + +func TestControlCharacterInFilter(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + res, err := db.User.FindFirst(user.Email.EQ("test\x00null@example.com")).Exec(ctx) + if err == nil { + t.Fatalf("expected validation error for query with an embedded null byte, got nil") + } + if res != nil { + t.Errorf("expected no match for a value containing a null byte, got: %+v", res) + } +} + +func TestVeryLongEmailValue(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + longEmail := strings.Repeat("a", 5000) + "@example.com" + + _, err := db.User.Create(valk.UserCreate{Email: longEmail, PhoneNum: "999"}).Exec(ctx) + if err != nil { + t.Fatalf("create with a very long email failed: %v", err) + } + + res, err := db.User.FindUnique(user.Email.EQ(longEmail)).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res == nil || res.Email != longEmail { + gotLen := 0 + if res != nil { + gotLen = len(res.Email) + } + t.Errorf("long email value was not stored/retrieved exactly, expected len=%d got len=%d", len(longEmail), gotLen) + } +} + +func TestCreateWithEmptyStringEmail(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{ + Email: "", + PhoneNum: "800", + }).Exec(ctx) + if err != nil { + return + } + + res, err := db.User.FindUnique(user.Email.EQ("")).Exec(ctx) + if err != nil { + t.Fatalf("query for empty-string email failed: %v", err) + } + if res == nil { + t.Error("empty string email was accepted on create but cannot be queried back via FindUnique") + } +} + +func TestOptionalEnumNullVsValueFilter(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + adminRole := valk.UserRole.Admin + _, err := db.User.Create(valk.UserCreate{ + Email: "role_set@example.com", + PhoneNum: "700", + RoleOptional: &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) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + nullRoleUsers, err := db.User.FindMany(user.RoleOptional.IsNull()).Exec(ctx) + if err != nil { + t.Fatalf("query for null role failed: %v", err) + } + if len(nullRoleUsers) != 1 || nullRoleUsers[0].Email != "role_unset@example.com" { + t.Errorf("expected exactly role_unset@example.com for a null-role filter, got: %+v", nullRoleUsers) + } + + setRoleUsers, err := db.User.FindMany(user.RoleOptional.EQ(adminRole)).Exec(ctx) + if err != nil { + t.Fatalf("query for admin role failed: %v", err) + } + if len(setRoleUsers) != 1 || setRoleUsers[0].Email != "role_set@example.com" { + t.Errorf("expected exactly role_set@example.com for an admin-role filter, got: %+v", setRoleUsers) + } +} + +func TestSQLInjectionVariants(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "injection_target@example.com", PhoneNum: "900"}).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + payloads := []string{ + "' OR '1'='1", + "'; DROP TABLE \"User\"; --", + "' UNION SELECT * FROM \"User\" --", + "\\'; --", + "%' OR '1'='1", + "injection_target@example.com'--", + "' OR 1=1#", + } + + for _, payload := range payloads { + t.Run(payload, func(t *testing.T) { + res, err := db.User.FindFirst(user.Email.EQ(payload)).Exec(ctx) + if err != nil { + t.Fatalf("query crashed on payload %q: %v", payload, err) + } + if res != nil { + t.Errorf("payload %q unexpectedly matched a row: %+v", payload, res) + } + }) + } + + sanity, err := db.User.FindUnique(user.Email.EQ("injection_target@example.com")).Exec(ctx) + if err != nil || sanity == nil { + t.Fatalf("sanity check failed after injection attempts: the seed row should still exist, err=%v res=%+v", err, sanity) + } +} + +func TestFindManyReturnsEmptySliceNotNilWhenNoMatches(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + res, err := db.User.FindMany(user.Email.EQ("definitely_not_present@example.com")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if len(res) != 0 { + t.Errorf("expected 0 results, got %d", len(res)) + } +} + +func TestCompoundUniqueWithOneFieldMatchingWrongRow(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{Email: "compound_a@example.com", PhoneNum: "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) + if err != nil { + t.Fatalf("seed b failed: %v", err) + } + + res, err := db.User.FindUnique(user.EmailPhoneUnique("compound_a@example.com", "701b")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res != nil { + t.Errorf("expected nil since email and phone belong to different rows, got: %+v", res) + } +} + +func TestCompoundUniqueConstraintEdgeCases(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valk.UserCreate{ + Email: "compound_edge@example.com", + PhoneNum: "800a", + }).Exec(ctx) + if err != nil { + t.Fatalf("seed failed: %v", err) + } + + t.Run("Happy path", func(t *testing.T) { + res, err := db.User.FindUnique(user.EmailPhoneUnique("compound_edge@example.com", "800a")).Exec(ctx) + if err != nil { + t.Fatalf("happy path failed: %v", err) + } + if res == nil || res.Email != "compound_edge@example.com" { + t.Errorf("expected to retrieve seeded row, got: %+v", res) + } + }) + + t.Run("SQL Injection in one compound field", func(t *testing.T) { + res, err := db.User.FindUnique(user.EmailPhoneUnique("compound_edge@example.com", "800a' OR '1'='1")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res != nil { + t.Errorf("expected no match due to SQL injection payload, but got row: %+v", res) + } + }) + + t.Run("Partial mismatch", func(t *testing.T) { + res, err := db.User.FindUnique(user.EmailPhoneUnique("compound_edge@example.com", "wrong_phone")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res != nil { + t.Errorf("expected nil on partial mismatch, got: %+v", res) + } + }) + + t.Run("Empty strings in both compound fields", func(t *testing.T) { + res, err := db.User.FindUnique(user.EmailPhoneUnique("", "")).Exec(ctx) + if err != nil { + t.Fatalf("query failed: %v", err) + } + if res != nil { + t.Errorf("expected nil for empty strings, got: %+v", res) + } + }) + + t.Run("Control characters", func(t *testing.T) { + res, err := db.User.FindUnique(user.EmailPhoneUnique("compound_edge@example.com\x00", "800a\r\n")).Exec(ctx) + if err == nil { + t.Fatalf("expected validation error for control-character mutated fields, got nil") + } + if res != nil { + t.Errorf("expected nil for control-character mutated fields, got: %+v", res) + } + }) +} + +func TestJsonField(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + u, err := db.User.Create(user.Create{ + Email: "json_test_user@example.com", + PhoneNum: "555-json", + }).Exec(ctx) + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + p, err := db.Post.Create(valk.PostCreate{ + Title: "JSON Post", + AuthorId: 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) + if err != nil { + t.Fatalf("failed to create comment with JSON: %v", err) + } + + // 1. FindFirst by exact JSON matching + found, err := db.Comment.FindFirst(comment.Meta.EQ(metaVal)).Exec(ctx) + if err != nil { + t.Fatalf("failed to find comment by json EQ: %v", err) + } + if found == nil || found.Id != c.Id { + t.Errorf("expected to find comment %s, got %v", c.Id, found) + } + + // 2. FindMany using IN operator with JSON slices + foundMany, err := db.Comment.FindMany(comment.Meta.In([]json.RawMessage{metaVal})).Exec(ctx) + if err != nil { + t.Fatalf("failed to find comments by json IN: %v", err) + } + if len(foundMany) != 1 || foundMany[0].Id != c.Id { + t.Errorf("expected 1 comment, got %d comments", len(foundMany)) + } +} diff --git a/integration/schema.prisma b/integration/schema.prisma index 9ced9bb..de0bd98 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -11,21 +11,21 @@ enum UserRole { } model User { - id String @id @default(cuid()) - email String @unique - ///test this bitch - phoneNum String @unique - password String? - role UserRole @default(STUDENT) - profile Profile? - posts Post[] - comments Comment[] + id String @id @default(cuid()) + email String @unique + phoneNum String @unique + password String? + role UserRole @default(STUDENT) + roleOptional UserRole? + profile Profile? + posts Post[] + comments Comment[] referredById String? referredBy User? @relation("UserReferrals", fields: [referredById], references: [id]) referrals User[] @relation("UserReferrals") - @@unique([email, phoneNum]) + @@unique([email, phoneNum], name: "emailPhone") } model Profile { @@ -56,6 +56,7 @@ model Comment { post Post @relation(fields: [postId], references: [id]) authorId String author User @relation(fields: [authorId], references: [id]) + meta Json? } model Category { diff --git a/integration/valk/category.go b/integration/valk/category.go index 25e80e9..63ee181 100644 --- a/integration/valk/category.go +++ b/integration/valk/category.go @@ -2,22 +2,12 @@ package valk import ( "context" - "database/sql" "fmt" "slices" "strings" - "time" "unicode/utf8" ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - // Category represents the database model type Category struct { Id int32 `db:"id" json:"id"` @@ -85,7 +75,7 @@ func (q *Queries) selectCategoryCols(selects *CategorySelect, omits *CategoryOmi anySelected := selects != nil && (selects.Id || selects.Name || selects.Posts != nil) specs := []colSpec{ - {"id", selects != nil && selects.Id, omits != nil && omits.Id, false}, + {"id", selects != nil && selects.Id, omits != nil && omits.Id, selects != nil && selects.hasAnyRelation()}, {"name", selects != nil && selects.Name, omits != nil && omits.Name, false}, } @@ -270,7 +260,7 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs rowMaps[i] = q.CategoryInputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "Category", rowMaps, CategoryColOrder, returningCols) - var records []*Category + records := make([]*Category, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -299,7 +289,7 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs } // Fallback to loop inside transaction - var records []*Category + records := make([]*Category, 0) err := q.transaction(ctx, func(txQ *Queries) error { for _, input := range inputs { res, err := txQ.executeCategoryCreate(ctx, input, nil, nil) @@ -319,6 +309,94 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs } return records, nil } +func (d *CategoryDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Category, CategorySelect, CategoryOmit] { + return &FindUniqueBuilder[Category, CategorySelect, CategoryOmit]{ + client: d.client, + where: where, + execFunc: d.client.executeCategoryFindUnique, + } +} + +func (d *CategoryDelegate) FindFirst(preds ...Predicate) *FindFirstBuilder[Category, CategorySelect, CategoryOmit] { + return &FindFirstBuilder[Category, CategorySelect, CategoryOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeCategoryFindFirst, + } +} + +func (d *CategoryDelegate) FindMany(preds ...Predicate) *FindManyBuilder[Category, CategorySelect, CategoryOmit] { + return &FindManyBuilder[Category, CategorySelect, CategoryOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeCategoryFindMany, + } +} + +func (q *Queries) executeCategoryFindUnique(ctx context.Context, where UniquePredicate, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCategoryCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Category", whereClause, vals, returningCols, + func(res *Category, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Category) error { + return txQ.loadCategoryRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeCategoryFindFirst(ctx context.Context, where []Predicate, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCategoryCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Category", whereClause, vals, returningCols, + func(res *Category, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Category) error { + return txQ.loadCategoryRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeCategoryFindMany(ctx context.Context, where []Predicate, selects *CategorySelect, omits *CategoryOmit) ([]*Category, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCategoryCols(selects, omits) + return executeManyWithRelations(ctx, q, "Category", whereClause, vals, returningCols, + func(res *Category, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Category) error { + return txQ.loadCategoryRelations(ctx, results, selects) + }, + ) +} func (q *Queries) loadCategoryRelations(ctx context.Context, records []*Category, selects *CategorySelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valk/category/category.go b/integration/valk/category/category.go new file mode 100644 index 0000000..ae59571 --- /dev/null +++ b/integration/valk/category/category.go @@ -0,0 +1,39 @@ +package category + +import ( + "fmt" + "integration/valk" +) + +type UniquePredicate struct { + valk.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = valk.CategorySelect +type Omit = valk.CategoryOmit +type Create = valk.CategoryCreate + +func And(preds ...valk.Predicate) valk.Predicate { + return valk.And(preds...) +} + +func Or(preds ...valk.Predicate) valk.Predicate { + return valk.Or(preds...) +} + +func Not(pred valk.Predicate) valk.Predicate { + return valk.Not(pred) +} + +var Id = valk.UniqueField[int32]{Column: "id"} + +var Name = valk.StringUniqueField{Column: "name"} diff --git a/integration/valk/categoryToPost.go b/integration/valk/categoryToPost.go index 1f33045..a0279b6 100644 --- a/integration/valk/categoryToPost.go +++ b/integration/valk/categoryToPost.go @@ -2,22 +2,12 @@ package valk import ( "context" - "database/sql" "fmt" "slices" "strings" - "time" "unicode/utf8" ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - // CategoryToPost represents the database model type CategoryToPost struct { PostId string `db:"postId" json:"postId"` @@ -270,7 +260,7 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, rowMaps[i] = q.CategoryToPostInputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "CategoryToPost", rowMaps, CategoryToPostColOrder, returningCols) - var records []*CategoryToPost + records := make([]*CategoryToPost, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -299,7 +289,7 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, } // Fallback to loop inside transaction - var records []*CategoryToPost + records := make([]*CategoryToPost, 0) err := q.transaction(ctx, func(txQ *Queries) error { for _, input := range inputs { res, err := txQ.executeCategoryToPostCreate(ctx, input, nil, nil) @@ -319,6 +309,94 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, } return records, nil } +func (d *CategoryToPostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { + return &FindUniqueBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ + client: d.client, + where: where, + execFunc: d.client.executeCategoryToPostFindUnique, + } +} + +func (d *CategoryToPostDelegate) FindFirst(preds ...Predicate) *FindFirstBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { + return &FindFirstBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeCategoryToPostFindFirst, + } +} + +func (d *CategoryToPostDelegate) FindMany(preds ...Predicate) *FindManyBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit] { + return &FindManyBuilder[CategoryToPost, CategoryToPostSelect, CategoryToPostOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeCategoryToPostFindMany, + } +} + +func (q *Queries) executeCategoryToPostFindUnique(ctx context.Context, where UniquePredicate, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCategoryToPostCols(selects, omits) + return executeSingleWithRelations(ctx, q, "CategoryToPost", whereClause, vals, returningCols, + func(res *CategoryToPost, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*CategoryToPost) error { + return txQ.loadCategoryToPostRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeCategoryToPostFindFirst(ctx context.Context, where []Predicate, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCategoryToPostCols(selects, omits) + return executeSingleWithRelations(ctx, q, "CategoryToPost", whereClause, vals, returningCols, + func(res *CategoryToPost, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*CategoryToPost) error { + return txQ.loadCategoryToPostRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeCategoryToPostFindMany(ctx context.Context, where []Predicate, selects *CategoryToPostSelect, omits *CategoryToPostOmit) ([]*CategoryToPost, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCategoryToPostCols(selects, omits) + return executeManyWithRelations(ctx, q, "CategoryToPost", whereClause, vals, returningCols, + func(res *CategoryToPost, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*CategoryToPost) error { + return txQ.loadCategoryToPostRelations(ctx, results, selects) + }, + ) +} func (q *Queries) loadCategoryToPostRelations(ctx context.Context, records []*CategoryToPost, selects *CategoryToPostSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valk/categoryToPost/categoryToPost.go b/integration/valk/categoryToPost/categoryToPost.go new file mode 100644 index 0000000..a7f484f --- /dev/null +++ b/integration/valk/categoryToPost/categoryToPost.go @@ -0,0 +1,39 @@ +package categoryToPost + +import ( + "fmt" + "integration/valk" +) + +type UniquePredicate struct { + valk.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = valk.CategoryToPostSelect +type Omit = valk.CategoryToPostOmit +type Create = valk.CategoryToPostCreate + +func And(preds ...valk.Predicate) valk.Predicate { + return valk.And(preds...) +} + +func Or(preds ...valk.Predicate) valk.Predicate { + return valk.Or(preds...) +} + +func Not(pred valk.Predicate) valk.Predicate { + return valk.Not(pred) +} + +var PostId = valk.StringField{Column: "postId"} + +var CategoryId = valk.Field[int32]{Column: "categoryId"} diff --git a/integration/valk/client.go b/integration/valk/client.go index 2ec6e16..afd544c 100644 --- a/integration/valk/client.go +++ b/integration/valk/client.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "github.com/google/uuid" "github.com/pressly/goose/v3" @@ -284,6 +285,719 @@ func (q *Queries) transaction(ctx context.Context, fn func(txQ *Queries) error) return tx.Commit() } +type PredicateData struct { + Column string + Operator string + Value any + IsLogical bool + Children []PredicateData +} + +type Predicate interface { + ToPredicateData() PredicateData + Validate() error +} + +type UniquePredicate interface { + Predicate + IsUnique() + Validate() error +} + +type StandardPredicate struct { + Data PredicateData +} + +func (sp StandardPredicate) ToPredicateData() PredicateData { + return sp.Data +} + +func validateValue(col string, val any) error { + switch v := val.(type) { + case string: + if strings.Contains(v, "\x00") { + return &ValidationError{ + Errors: []FieldError{ + {Field: col, Value: v, Rule: "safety", Msg: "string cannot contain null bytes"}, + }, + } + } + if !utf8.ValidString(v) { + return &ValidationError{ + Errors: []FieldError{ + {Field: col, Value: v, Rule: "safety", Msg: "string must be valid UTF-8"}, + }, + } + } + case []string: + for _, s := range v { + if err := validateValue(col, s); err != nil { + return err + } + } + case []any: + for _, item := range v { + if err := validateValue(col, item); err != nil { + return err + } + } + } + return nil +} + +func (pd PredicateData) Validate() error { + if pd.IsLogical { + for _, child := range pd.Children { + if err := child.Validate(); err != nil { + return err + } + } + return nil + } + return validateValue(pd.Column, pd.Value) +} + +func (sp StandardPredicate) Validate() error { + return sp.Data.Validate() +} + +func And(preds ...Predicate) Predicate { + var children []PredicateData + for _, p := range preds { + if p != nil { + children = append(children, p.ToPredicateData()) + } + } + return StandardPredicate{ + Data: PredicateData{ + IsLogical: true, + Operator: "AND", + Children: children, + }, + } +} + +func Or(preds ...Predicate) Predicate { + var children []PredicateData + for _, p := range preds { + if p != nil { + children = append(children, p.ToPredicateData()) + } + } + return StandardPredicate{ + Data: PredicateData{ + IsLogical: true, + Operator: "OR", + Children: children, + }, + } +} + +func Not(pred Predicate) Predicate { + var children []PredicateData + if pred != nil { + children = append(children, pred.ToPredicateData()) + } + return StandardPredicate{ + Data: PredicateData{ + IsLogical: true, + Operator: "NOT", + Children: children, + }, + } +} + +type Field[T any] struct { + Column string +} + +func (f Field[T]) EQ(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + } +} + +func (f Field[T]) NEQ(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f Field[T]) GT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f Field[T]) GTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f Field[T]) LT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f Field[T]) LTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f Field[T]) In(vals []T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f Field[T]) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f Field[T]) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +type UniqueField[T any] struct { + Column string +} + +type UniqueFieldPredicate struct { + StandardPredicate +} + +func (UniqueFieldPredicate) IsUnique() {} + +func (p UniqueFieldPredicate) Validate() error { + if p.Data.Column == "" { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +func (f UniqueField[T]) EQ(val T) UniquePredicate { + return UniqueFieldPredicate{ + StandardPredicate: StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + }, + } +} + +func (f UniqueField[T]) NEQ(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f UniqueField[T]) GT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f UniqueField[T]) GTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f UniqueField[T]) LT(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f UniqueField[T]) LTE(val T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f UniqueField[T]) In(vals []T) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f UniqueField[T]) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f UniqueField[T]) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +type StringField struct { + Column string +} + +func (f StringField) EQ(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + } +} + +func (f StringField) NEQ(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f StringField) GT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f StringField) GTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f StringField) LT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f StringField) LTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f StringField) In(vals []string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f StringField) Like(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: val, + }, + } +} + +func (f StringField) Contains(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: "%" + val + "%", + }, + } +} + +func (f StringField) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f StringField) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +type StringUniqueField struct { + Column string +} + +func (f StringUniqueField) EQ(val string) UniquePredicate { + return UniqueFieldPredicate{ + StandardPredicate: StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "=", + Value: val, + }, + }, + } +} + +func (f StringUniqueField) NEQ(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "!=", + Value: val, + }, + } +} + +func (f StringUniqueField) GT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">", + Value: val, + }, + } +} + +func (f StringUniqueField) GTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: ">=", + Value: val, + }, + } +} + +func (f StringUniqueField) LT(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<", + Value: val, + }, + } +} + +func (f StringUniqueField) LTE(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "<=", + Value: val, + }, + } +} + +func (f StringUniqueField) In(vals []string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IN", + Value: vals, + }, + } +} + +func (f StringUniqueField) Like(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: val, + }, + } +} + +func (f StringUniqueField) Contains(val string) Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "LIKE", + Value: "%" + val + "%", + }, + } +} + +func (f StringUniqueField) IsNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NULL", + }, + } +} + +func (f StringUniqueField) IsNotNull() Predicate { + return StandardPredicate{ + Data: PredicateData{ + Column: f.Column, + Operator: "IS NOT NULL", + }, + } +} + +func CompilePredicates(dialect Dialect, preds []Predicate) (string, []any) { + if len(preds) == 0 { + return "", nil + } + var data []PredicateData + for _, p := range preds { + if p != nil { + data = append(data, p.ToPredicateData()) + } + } + return CompilePredicateData(dialect, data) +} + +func CompilePredicateData(dialect Dialect, data []PredicateData) (string, []any) { + if len(data) == 0 { + return "", nil + } + var parts []string + var args []any + var bindIdx = 1 + + var compile func(p PredicateData) string + compile = func(p PredicateData) string { + if p.IsLogical { + if len(p.Children) == 0 { + return "" + } + if p.Operator == "NOT" { + sub := compile(p.Children[0]) + if sub == "" { + return "" + } + return fmt.Sprintf("NOT (%s)", sub) + } + var subParts []string + for _, child := range p.Children { + sub := compile(child) + if sub != "" { + subParts = append(subParts, sub) + } + } + if len(subParts) == 0 { + return "" + } + if len(subParts) == 1 { + return subParts[0] + } + return fmt.Sprintf("(%s)", strings.Join(subParts, " "+p.Operator+" ")) + } + + switch p.Operator { + case "IS NULL", "IS NOT NULL": + return fmt.Sprintf("%s %s", dialect.Quote(p.Column), p.Operator) + case "IN": + valSlice := unpackSlice(p.Value) + if len(valSlice) == 0 { + return "1=0" + } + var placeHolders []string + for range valSlice { + placeHolders = append(placeHolders, dialect.BindVar(bindIdx)) + bindIdx++ + } + for _, val := range valSlice { + args = append(args, val) + } + return fmt.Sprintf("%s IN (%s)", dialect.Quote(p.Column), strings.Join(placeHolders, ", ")) + default: + placeholder := dialect.BindVar(bindIdx) + bindIdx++ + args = append(args, p.Value) + return fmt.Sprintf("%s %s %s", dialect.Quote(p.Column), p.Operator, placeholder) + } + } + + for _, p := range data { + part := compile(p) + if part != "" { + parts = append(parts, part) + } + } + + if len(parts) == 0 { + return "", nil + } + return strings.Join(parts, " AND "), args +} + +func unpackSlice(val any) []any { + if val == nil { + return nil + } + switch v := val.(type) { + case []string: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []int: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []int32: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []int64: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []float32: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []float64: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []bool: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []time.Time: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case [][]byte: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []json.RawMessage: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + case []any: + return v + case []UserRoleType: + res := make([]any, len(v)) + for i, x := range v { + res[i] = x + } + return res + default: + return []any{val} + } +} + type Tx struct { *Queries tx *sql.Tx @@ -606,6 +1320,253 @@ func loadRelation[P any, C any]( return allChildren, nil } + +type FindUniqueBuilder[M any, S any, O any] struct { + client *Queries + where UniquePredicate + execFunc func(ctx context.Context, where UniquePredicate, s *S, o *O) (*M, error) +} + +func (b *FindUniqueBuilder[M, S, O]) Select(s S) *FindUniqueSelectBuilder[M, S, O] { + return &FindUniqueSelectBuilder[M, S, O]{builder: b, selects: s} +} + +func (b *FindUniqueBuilder[M, S, O]) Omit(o O) *FindUniqueOmitBuilder[M, S, O] { + return &FindUniqueOmitBuilder[M, S, O]{builder: b, omits: o} +} + +func (b *FindUniqueBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.execFunc(ctx, b.where, nil, nil) +} + +type FindUniqueSelectBuilder[M any, S any, O any] struct { + builder *FindUniqueBuilder[M, S, O] + selects S +} + +func (b *FindUniqueSelectBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, &b.selects, nil) +} + +type FindUniqueOmitBuilder[M any, S any, O any] struct { + builder *FindUniqueBuilder[M, S, O] + omits O +} + +func (b *FindUniqueOmitBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, nil, &b.omits) +} + +type FindFirstBuilder[M any, S any, O any] struct { + client *Queries + where []Predicate + execFunc func(ctx context.Context, where []Predicate, s *S, o *O) (*M, error) +} + +func (b *FindFirstBuilder[M, S, O]) Select(s S) *FindFirstSelectBuilder[M, S, O] { + return &FindFirstSelectBuilder[M, S, O]{builder: b, selects: s} +} + +func (b *FindFirstBuilder[M, S, O]) Omit(o O) *FindFirstOmitBuilder[M, S, O] { + return &FindFirstOmitBuilder[M, S, O]{builder: b, omits: o} +} + +func (b *FindFirstBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.execFunc(ctx, b.where, nil, nil) +} + +type FindFirstSelectBuilder[M any, S any, O any] struct { + builder *FindFirstBuilder[M, S, O] + selects S +} + +func (b *FindFirstSelectBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, &b.selects, nil) +} + +type FindFirstOmitBuilder[M any, S any, O any] struct { + builder *FindFirstBuilder[M, S, O] + omits O +} + +func (b *FindFirstOmitBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) { + return b.builder.execFunc(ctx, b.builder.where, nil, &b.omits) +} + +type FindManyBuilder[M any, S any, O any] struct { + client *Queries + where []Predicate + execFunc func(ctx context.Context, where []Predicate, s *S, o *O) ([]*M, error) +} + +func (b *FindManyBuilder[M, S, O]) Select(s S) *FindManySelectBuilder[M, S, O] { + return &FindManySelectBuilder[M, S, O]{builder: b, selects: s} +} + +func (b *FindManyBuilder[M, S, O]) Omit(o O) *FindManyOmitBuilder[M, S, O] { + return &FindManyOmitBuilder[M, S, O]{builder: b, omits: o} +} + +func (b *FindManyBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.execFunc(ctx, b.where, nil, nil) +} + +type FindManySelectBuilder[M any, S any, O any] struct { + builder *FindManyBuilder[M, S, O] + selects S +} + +func (b *FindManySelectBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.where, &b.selects, nil) +} + +type FindManyOmitBuilder[M any, S any, O any] struct { + builder *FindManyBuilder[M, S, O] + omits O +} + +func (b *FindManyOmitBuilder[M, S, O]) Exec(ctx context.Context) ([]*M, error) { + return b.builder.execFunc(ctx, b.builder.where, nil, &b.omits) +} + +func executeFindOne[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(record *M, cols []string) []any, +) (*M, error) { + var sb strings.Builder + sb.Grow(64 + len(returningCols)*15 + len(table) + len(whereClause)) + sb.WriteString("SELECT ") + for i, col := range returningCols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(q.dialect.Quote(col)) + } + sb.WriteString(" FROM ") + sb.WriteString(q.dialect.Quote(table)) + sb.WriteString(whereClause) + sb.WriteString(" LIMIT 1") + + var res M + row := q.queryRow(ctx, sb.String(), whereVals...) + scanTargets := scanFunc(&res, returningCols) + if err := row.Scan(scanTargets...); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &res, nil +} + +func executeFindMany[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(record *M, cols []string) []any, +) ([]*M, error) { + var sb strings.Builder + sb.Grow(64 + len(returningCols)*15 + len(table) + len(whereClause)) + sb.WriteString("SELECT ") + for i, col := range returningCols { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(q.dialect.Quote(col)) + } + sb.WriteString(" FROM ") + sb.WriteString(q.dialect.Quote(table)) + sb.WriteString(whereClause) + + rows, err := q.query(ctx, sb.String(), whereVals...) + if err != nil { + return nil, err + } + defer rows.Close() + + results := make([]*M, 0) + for rows.Next() { + var res M + scanTargets := scanFunc(&res, returningCols) + if err := rows.Scan(scanTargets...); err != nil { + return nil, err + } + results = append(results, &res) + } + if err := rows.Err(); err != nil { + return nil, err + } + return results, nil +} + +func executeSingleWithRelations[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(*M, []string) []any, + hasRelations bool, + loadRelations func(ctx context.Context, txQ *Queries, results []*M) error, +) (*M, error) { + if !hasRelations { + return executeFindOne(ctx, q, table, whereClause, whereVals, returningCols, scanFunc) + } + + var res *M + err := q.transaction(ctx, func(txQ *Queries) error { + var err error + res, err = executeFindOne(ctx, txQ, table, whereClause, whereVals, returningCols, scanFunc) + if err != nil || res == nil { + return err + } + return loadRelations(ctx, txQ, []*M{res}) + }) + if err != nil { + return nil, err + } + return res, nil +} + +func executeManyWithRelations[M any]( + ctx context.Context, + q *Queries, + table string, + whereClause string, + whereVals []any, + returningCols []string, + scanFunc func(*M, []string) []any, + hasRelations bool, + loadRelations func(ctx context.Context, txQ *Queries, results []*M) error, +) ([]*M, error) { + if !hasRelations { + return executeFindMany(ctx, q, table, whereClause, whereVals, returningCols, scanFunc) + } + + results := make([]*M, 0) + err := q.transaction(ctx, func(txQ *Queries) error { + var err error + results, err = executeFindMany(ctx, txQ, table, whereClause, whereVals, returningCols, scanFunc) + if err != nil || len(results) == 0 { + return err + } + return loadRelations(ctx, txQ, results) + }) + if err != nil { + return nil, err + } + return results, nil +} + func directKey[T any, K any](get func(*T) K) func(*T) (string, bool) { return func(t *T) (string, bool) { return fmt.Sprint(get(t)), true diff --git a/integration/valk/comment.go b/integration/valk/comment.go index de7f5dc..19bf0e6 100644 --- a/integration/valk/comment.go +++ b/integration/valk/comment.go @@ -2,44 +2,37 @@ package valk import ( "context" - "database/sql" + "encoding/json" "fmt" "slices" "strings" - "time" "unicode/utf8" ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - // Comment represents the database model type Comment struct { - Id string `db:"id" json:"id"` - Textify int32 `db:"textify" json:"textify"` - Dummy3 string `db:"dummy3" json:"dummy3"` - Dummy1 int32 `db:"dummy1" json:"dummy1"` - Dummy2 string `db:"dummy2" json:"dummy2"` - PostId string `db:"postId" json:"postId"` - AuthorId string `db:"authorId" json:"authorId"` - Post *Post `json:"post,omitempty"` - Author *User `json:"author,omitempty"` + Id string `db:"id" json:"id"` + Textify int32 `db:"textify" json:"textify"` + Dummy3 string `db:"dummy3" json:"dummy3"` + Dummy1 int32 `db:"dummy1" json:"dummy1"` + Dummy2 string `db:"dummy2" json:"dummy2"` + PostId string `db:"postId" json:"postId"` + AuthorId string `db:"authorId" json:"authorId"` + Meta *json.RawMessage `db:"meta" json:"meta,omitempty"` + Post *Post `json:"post,omitempty"` + Author *User `json:"author,omitempty"` } // CommentCreate represents the input structure for creation type CommentCreate struct { - Id *string `json:"id"` - Textify int32 `json:"textify"` - Dummy3 string `json:"dummy3"` - Dummy1 int32 `json:"dummy1"` - Dummy2 string `json:"dummy2"` - PostId string `json:"postId"` - AuthorId string `json:"authorId"` + Id *string `json:"id"` + Textify int32 `json:"textify"` + Dummy3 string `json:"dummy3"` + Dummy1 int32 `json:"dummy1"` + Dummy2 string `json:"dummy2"` + PostId string `json:"postId"` + AuthorId string `json:"authorId"` + Meta *json.RawMessage `json:"meta"` } // CommentSelect specifies which fields to include @@ -51,6 +44,7 @@ type CommentSelect struct { Dummy2 bool `json:"dummy2"` PostId bool `json:"postId"` AuthorId bool `json:"authorId"` + Meta bool `json:"meta"` Post *PostSelect `json:"post,omitempty"` Author *UserSelect `json:"author,omitempty"` } @@ -64,6 +58,7 @@ type CommentOmit struct { Dummy2 bool `json:"dummy2"` PostId bool `json:"postId"` AuthorId bool `json:"authorId"` + Meta bool `json:"meta"` Post *PostOmit `json:"post,omitempty"` Author *UserOmit `json:"author,omitempty"` } @@ -100,6 +95,8 @@ func (m *Comment) ScanFields(cols []string) []any { targets[i] = &m.PostId case "authorId": targets[i] = &m.AuthorId + case "meta": + targets[i] = &m.Meta } } return targets @@ -113,6 +110,7 @@ var commentDefaultCols = []string{ "dummy2", "postId", "authorId", + "meta", } func (q *Queries) selectCommentCols(selects *CommentSelect, omits *CommentOmit, forceCols ...string) []string { @@ -120,16 +118,17 @@ func (q *Queries) selectCommentCols(selects *CommentSelect, omits *CommentOmit, return commentDefaultCols } - anySelected := selects != nil && (selects.Id || selects.Textify || selects.Dummy3 || selects.Dummy1 || selects.Dummy2 || selects.PostId || selects.AuthorId || selects.Post != nil || selects.Author != nil) + anySelected := selects != nil && (selects.Id || selects.Textify || selects.Dummy3 || selects.Dummy1 || selects.Dummy2 || selects.PostId || selects.AuthorId || selects.Meta || selects.Post != nil || selects.Author != nil) specs := []colSpec{ - {"id", selects != nil && selects.Id, omits != nil && omits.Id, false}, + {"id", selects != nil && selects.Id, omits != nil && omits.Id, selects != nil && selects.hasAnyRelation()}, {"textify", selects != nil && selects.Textify, omits != nil && omits.Textify, false}, {"dummy3", selects != nil && selects.Dummy3, omits != nil && omits.Dummy3, false}, {"dummy1", selects != nil && selects.Dummy1, omits != nil && omits.Dummy1, false}, {"dummy2", selects != nil && selects.Dummy2, omits != nil && omits.Dummy2, false}, {"postId", selects != nil && selects.PostId, omits != nil && omits.PostId, selects != nil && selects.Post != nil}, {"authorId", selects != nil && selects.AuthorId, omits != nil && omits.AuthorId, selects != nil && selects.Author != nil}, + {"meta", selects != nil && selects.Meta, omits != nil && omits.Meta, false}, } cols := computeCols(specs, selects != nil, anySelected) @@ -205,6 +204,7 @@ var CommentColOrder = []string{ "dummy2", "postId", "authorId", + "meta", } func (s *CommentSelect) hasAnyRelation() bool { @@ -285,6 +285,9 @@ func (q *Queries) CommentInputToMap(input CommentCreate) map[string]any { m["dummy2"] = input.Dummy2 m["postId"] = input.PostId m["authorId"] = input.AuthorId + if input.Meta != nil { + m["meta"] = *input.Meta + } return m } @@ -360,7 +363,7 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs rowMaps[i] = q.CommentInputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "Comment", rowMaps, CommentColOrder, returningCols) - var records []*Comment + records := make([]*Comment, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -389,7 +392,7 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs } // Fallback to loop inside transaction - var records []*Comment + records := make([]*Comment, 0) err := q.transaction(ctx, func(txQ *Queries) error { for _, input := range inputs { res, err := txQ.executeCommentCreate(ctx, input, nil, nil) @@ -409,6 +412,94 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs } return records, nil } +func (d *CommentDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Comment, CommentSelect, CommentOmit] { + return &FindUniqueBuilder[Comment, CommentSelect, CommentOmit]{ + client: d.client, + where: where, + execFunc: d.client.executeCommentFindUnique, + } +} + +func (d *CommentDelegate) FindFirst(preds ...Predicate) *FindFirstBuilder[Comment, CommentSelect, CommentOmit] { + return &FindFirstBuilder[Comment, CommentSelect, CommentOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeCommentFindFirst, + } +} + +func (d *CommentDelegate) FindMany(preds ...Predicate) *FindManyBuilder[Comment, CommentSelect, CommentOmit] { + return &FindManyBuilder[Comment, CommentSelect, CommentOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeCommentFindMany, + } +} + +func (q *Queries) executeCommentFindUnique(ctx context.Context, where UniquePredicate, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCommentCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Comment", whereClause, vals, returningCols, + func(res *Comment, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Comment) error { + return txQ.loadCommentRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeCommentFindFirst(ctx context.Context, where []Predicate, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCommentCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Comment", whereClause, vals, returningCols, + func(res *Comment, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Comment) error { + return txQ.loadCommentRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeCommentFindMany(ctx context.Context, where []Predicate, selects *CommentSelect, omits *CommentOmit) ([]*Comment, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectCommentCols(selects, omits) + return executeManyWithRelations(ctx, q, "Comment", whereClause, vals, returningCols, + func(res *Comment, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Comment) error { + return txQ.loadCommentRelations(ctx, results, selects) + }, + ) +} func (q *Queries) loadCommentRelations(ctx context.Context, records []*Comment, selects *CommentSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valk/comment/comment.go b/integration/valk/comment/comment.go new file mode 100644 index 0000000..330277d --- /dev/null +++ b/integration/valk/comment/comment.go @@ -0,0 +1,52 @@ +package comment + +import ( + "encoding/json" + "fmt" + "integration/valk" +) + +type UniquePredicate struct { + valk.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = valk.CommentSelect +type Omit = valk.CommentOmit +type Create = valk.CommentCreate + +func And(preds ...valk.Predicate) valk.Predicate { + return valk.And(preds...) +} + +func Or(preds ...valk.Predicate) valk.Predicate { + return valk.Or(preds...) +} + +func Not(pred valk.Predicate) valk.Predicate { + return valk.Not(pred) +} + +var Id = valk.StringUniqueField{Column: "id"} + +var Textify = valk.Field[int32]{Column: "textify"} + +var Dummy3 = valk.StringField{Column: "dummy3"} + +var Dummy1 = valk.Field[int32]{Column: "dummy1"} + +var Dummy2 = valk.StringField{Column: "dummy2"} + +var PostId = valk.StringField{Column: "postId"} + +var AuthorId = valk.StringField{Column: "authorId"} + +var Meta = valk.Field[json.RawMessage]{Column: "meta"} diff --git a/integration/valk/migrations/00001_init.sql b/integration/valk/migrations/00001_init.sql index e9ec288..6b84e8c 100644 --- a/integration/valk/migrations/00001_init.sql +++ b/integration/valk/migrations/00001_init.sql @@ -3,15 +3,18 @@ CREATE TABLE `User` ( `id` text NOT NULL, `email` text NOT NULL, `phoneNum` text NOT NULL, + `password` text NULL, `role` text NOT NULL DEFAULT ('student'), + `roleOptional` text NULL, `referredById` text NULL, PRIMARY KEY (`id`), CONSTRAINT `User_referredById_fkey` FOREIGN KEY (`referredById`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `User_role_check` CHECK ("role" IN ('ADMIN', 'student', 'TEACHER')) + CONSTRAINT `User_role_check` CHECK ("role" IN ('ADMIN', 'student', 'TEACHER')), + CONSTRAINT `User_roleOptional_check` CHECK ("roleOptional" IN ('ADMIN', 'student', 'TEACHER')) ); CREATE UNIQUE INDEX `User_email_key` ON `User` (`email`); CREATE UNIQUE INDEX `User_phoneNum_key` ON `User` (`phoneNum`); -CREATE UNIQUE INDEX `User_email_phoneNum_key` ON `User` (`email`, `phoneNum`); +CREATE UNIQUE INDEX `emailPhone` ON `User` (`email`, `phoneNum`); CREATE TABLE `Profile` ( `id` text NOT NULL, `bio` text NULL, @@ -37,6 +40,7 @@ CREATE TABLE `Comment` ( `dummy2` text NOT NULL, `postId` text NOT NULL, `authorId` text NOT NULL, + `meta` blob NULL, PRIMARY KEY (`id`), CONSTRAINT `Comment_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT `Comment_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION diff --git a/integration/valk/migrations/00002_add_password.sql b/integration/valk/migrations/00002_add_password.sql deleted file mode 100644 index 8a1fc3e..0000000 --- a/integration/valk/migrations/00002_add_password.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose Up -ALTER TABLE `User` ADD COLUMN `password` text NULL; - --- +goose Down -PRAGMA foreign_keys = off; -CREATE TABLE `new_User` ( - `id` text NOT NULL, - `email` text NOT NULL, - `phoneNum` text NOT NULL, - `role` text NOT NULL DEFAULT 'student', - `referredById` text NULL, - PRIMARY KEY (`id`), - CONSTRAINT `User_referredById_fkey` FOREIGN KEY (`referredById`) REFERENCES `User` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT `User_role_check` CHECK ("role" IN ('ADMIN', 'student', 'TEACHER')) -); -INSERT INTO `new_User` (`id`, `email`, `phoneNum`, `role`, `referredById`) SELECT `id`, `email`, `phoneNum`, `role`, `referredById` FROM `User`; -DROP TABLE `User`; -ALTER TABLE `new_User` RENAME TO `User`; -CREATE UNIQUE INDEX `User_email_key` ON `User` (`email`); -CREATE UNIQUE INDEX `User_phoneNum_key` ON `User` (`phoneNum`); -CREATE UNIQUE INDEX `User_email_phoneNum_key` ON `User` (`email`, `phoneNum`); -PRAGMA foreign_keys = on; diff --git a/integration/valk/post.go b/integration/valk/post.go index fa73291..576b0b3 100644 --- a/integration/valk/post.go +++ b/integration/valk/post.go @@ -2,27 +2,17 @@ package valk import ( "context" - "database/sql" "fmt" "slices" "strings" - "time" "unicode/utf8" ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - // Post represents the database model type Post struct { Id string `db:"id" json:"id"` Title string `db:"title" json:"title"` - Content *string `db:"content" json:"content"` + Content *string `db:"content" json:"content,omitempty"` Published bool `db:"published" json:"published"` AuthorId string `db:"authorId" json:"authorId"` Author *User `json:"author,omitempty"` @@ -112,7 +102,7 @@ func (q *Queries) selectPostCols(selects *PostSelect, omits *PostOmit, forceCols anySelected := selects != nil && (selects.Id || selects.Title || selects.Content || selects.Published || selects.AuthorId || selects.Author != nil || selects.Comments != nil || selects.Categories != nil) specs := []colSpec{ - {"id", selects != nil && selects.Id, omits != nil && omits.Id, false}, + {"id", selects != nil && selects.Id, omits != nil && omits.Id, selects != nil && selects.hasAnyRelation()}, {"title", selects != nil && selects.Title, omits != nil && omits.Title, false}, {"content", selects != nil && selects.Content, omits != nil && omits.Content, false}, {"published", selects != nil && selects.Published, omits != nil && omits.Published, false}, @@ -329,7 +319,7 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []P rowMaps[i] = q.PostInputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "Post", rowMaps, PostColOrder, returningCols) - var records []*Post + records := make([]*Post, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -358,7 +348,7 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []P } // Fallback to loop inside transaction - var records []*Post + records := make([]*Post, 0) err := q.transaction(ctx, func(txQ *Queries) error { for _, input := range inputs { res, err := txQ.executePostCreate(ctx, input, nil, nil) @@ -378,6 +368,94 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []P } return records, nil } +func (d *PostDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Post, PostSelect, PostOmit] { + return &FindUniqueBuilder[Post, PostSelect, PostOmit]{ + client: d.client, + where: where, + execFunc: d.client.executePostFindUnique, + } +} + +func (d *PostDelegate) FindFirst(preds ...Predicate) *FindFirstBuilder[Post, PostSelect, PostOmit] { + return &FindFirstBuilder[Post, PostSelect, PostOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executePostFindFirst, + } +} + +func (d *PostDelegate) FindMany(preds ...Predicate) *FindManyBuilder[Post, PostSelect, PostOmit] { + return &FindManyBuilder[Post, PostSelect, PostOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executePostFindMany, + } +} + +func (q *Queries) executePostFindUnique(ctx context.Context, where UniquePredicate, selects *PostSelect, omits *PostOmit) (*Post, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectPostCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Post", whereClause, vals, returningCols, + func(res *Post, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Post) error { + return txQ.loadPostRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executePostFindFirst(ctx context.Context, where []Predicate, selects *PostSelect, omits *PostOmit) (*Post, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectPostCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Post", whereClause, vals, returningCols, + func(res *Post, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Post) error { + return txQ.loadPostRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executePostFindMany(ctx context.Context, where []Predicate, selects *PostSelect, omits *PostOmit) ([]*Post, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectPostCols(selects, omits) + return executeManyWithRelations(ctx, q, "Post", whereClause, vals, returningCols, + func(res *Post, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Post) error { + return txQ.loadPostRelations(ctx, results, selects) + }, + ) +} func (q *Queries) loadPostRelations(ctx context.Context, records []*Post, selects *PostSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valk/post/post.go b/integration/valk/post/post.go new file mode 100644 index 0000000..706009c --- /dev/null +++ b/integration/valk/post/post.go @@ -0,0 +1,45 @@ +package post + +import ( + "fmt" + "integration/valk" +) + +type UniquePredicate struct { + valk.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = valk.PostSelect +type Omit = valk.PostOmit +type Create = valk.PostCreate + +func And(preds ...valk.Predicate) valk.Predicate { + return valk.And(preds...) +} + +func Or(preds ...valk.Predicate) valk.Predicate { + return valk.Or(preds...) +} + +func Not(pred valk.Predicate) valk.Predicate { + return valk.Not(pred) +} + +var Id = valk.StringUniqueField{Column: "id"} + +var Title = valk.StringField{Column: "title"} + +var Content = valk.StringField{Column: "content"} + +var Published = valk.Field[bool]{Column: "published"} + +var AuthorId = valk.StringField{Column: "authorId"} diff --git a/integration/valk/profile.go b/integration/valk/profile.go index b85da18..557d6fe 100644 --- a/integration/valk/profile.go +++ b/integration/valk/profile.go @@ -2,26 +2,16 @@ package valk import ( "context" - "database/sql" "fmt" "slices" "strings" - "time" "unicode/utf8" ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - // Profile represents the database model type Profile struct { Id string `db:"id" json:"id"` - Bio *string `db:"bio" json:"bio"` + Bio *string `db:"bio" json:"bio,omitempty"` UserId string `db:"userId" json:"userId"` User *User `json:"user,omitempty"` } @@ -92,7 +82,7 @@ func (q *Queries) selectProfileCols(selects *ProfileSelect, omits *ProfileOmit, anySelected := selects != nil && (selects.Id || selects.Bio || selects.UserId || selects.User != nil) specs := []colSpec{ - {"id", selects != nil && selects.Id, omits != nil && omits.Id, false}, + {"id", selects != nil && selects.Id, omits != nil && omits.Id, selects != nil && selects.hasAnyRelation()}, {"bio", selects != nil && selects.Bio, omits != nil && omits.Bio, false}, {"userId", selects != nil && selects.UserId, omits != nil && omits.UserId, selects != nil && selects.User != nil}, } @@ -292,7 +282,7 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs rowMaps[i] = q.ProfileInputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "Profile", rowMaps, ProfileColOrder, returningCols) - var records []*Profile + records := make([]*Profile, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -321,7 +311,7 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs } // Fallback to loop inside transaction - var records []*Profile + records := make([]*Profile, 0) err := q.transaction(ctx, func(txQ *Queries) error { for _, input := range inputs { res, err := txQ.executeProfileCreate(ctx, input, nil, nil) @@ -341,6 +331,94 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs } return records, nil } +func (d *ProfileDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit] { + return &FindUniqueBuilder[Profile, ProfileSelect, ProfileOmit]{ + client: d.client, + where: where, + execFunc: d.client.executeProfileFindUnique, + } +} + +func (d *ProfileDelegate) FindFirst(preds ...Predicate) *FindFirstBuilder[Profile, ProfileSelect, ProfileOmit] { + return &FindFirstBuilder[Profile, ProfileSelect, ProfileOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeProfileFindFirst, + } +} + +func (d *ProfileDelegate) FindMany(preds ...Predicate) *FindManyBuilder[Profile, ProfileSelect, ProfileOmit] { + return &FindManyBuilder[Profile, ProfileSelect, ProfileOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeProfileFindMany, + } +} + +func (q *Queries) executeProfileFindUnique(ctx context.Context, where UniquePredicate, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectProfileCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Profile", whereClause, vals, returningCols, + func(res *Profile, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Profile) error { + return txQ.loadProfileRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeProfileFindFirst(ctx context.Context, where []Predicate, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectProfileCols(selects, omits) + return executeSingleWithRelations(ctx, q, "Profile", whereClause, vals, returningCols, + func(res *Profile, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Profile) error { + return txQ.loadProfileRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeProfileFindMany(ctx context.Context, where []Predicate, selects *ProfileSelect, omits *ProfileOmit) ([]*Profile, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectProfileCols(selects, omits) + return executeManyWithRelations(ctx, q, "Profile", whereClause, vals, returningCols, + func(res *Profile, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*Profile) error { + return txQ.loadProfileRelations(ctx, results, selects) + }, + ) +} func (q *Queries) loadProfileRelations(ctx context.Context, records []*Profile, selects *ProfileSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valk/profile/profile.go b/integration/valk/profile/profile.go new file mode 100644 index 0000000..46e03df --- /dev/null +++ b/integration/valk/profile/profile.go @@ -0,0 +1,41 @@ +package profile + +import ( + "fmt" + "integration/valk" +) + +type UniquePredicate struct { + valk.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = valk.ProfileSelect +type Omit = valk.ProfileOmit +type Create = valk.ProfileCreate + +func And(preds ...valk.Predicate) valk.Predicate { + return valk.And(preds...) +} + +func Or(preds ...valk.Predicate) valk.Predicate { + return valk.Or(preds...) +} + +func Not(pred valk.Predicate) valk.Predicate { + return valk.Not(pred) +} + +var Id = valk.StringUniqueField{Column: "id"} + +var Bio = valk.StringField{Column: "bio"} + +var UserId = valk.StringUniqueField{Column: "userId"} diff --git a/integration/valk/user.go b/integration/valk/user.go index 423fd47..3c4b679 100644 --- a/integration/valk/user.go +++ b/integration/valk/user.go @@ -2,35 +2,26 @@ package valk import ( "context" - "database/sql" "fmt" "slices" "strings" - "time" "unicode/utf8" ) -var _ = time.Time{} -var _ = fmt.Sprintf -var _ = strings.Join -var _ = context.Background -var _ = sql.LevelDefault -var _ = slices.Contains[[]string, string] -var _ = utf8.ValidString - // User represents the database model type User struct { - Id string `db:"id" json:"id"` - Email string `db:"email" json:"email"` - PhoneNum string `db:"phoneNum" json:"phoneNum"` - Password *string `db:"password" json:"password"` - Role UserRoleType `db:"role" json:"role"` - ReferredById *string `db:"referredById" json:"referredById"` - Profile *Profile `json:"profile,omitempty"` - Posts []*Post `json:"posts,omitempty"` - Comments []*Comment `json:"comments,omitempty"` - ReferredBy *User `json:"referredBy,omitempty"` - Referrals []*User `json:"referrals,omitempty"` + Id string `db:"id" json:"id"` + Email string `db:"email" json:"email"` + PhoneNum string `db:"phoneNum" json:"phoneNum"` + Password *string `db:"password" json:"password,omitempty"` + Role UserRoleType `db:"role" json:"role"` + RoleOptional *UserRoleType `db:"roleOptional" json:"roleOptional,omitempty"` + ReferredById *string `db:"referredById" json:"referredById,omitempty"` + Profile *Profile `json:"profile,omitempty"` + Posts []*Post `json:"posts,omitempty"` + Comments []*Comment `json:"comments,omitempty"` + ReferredBy *User `json:"referredBy,omitempty"` + Referrals []*User `json:"referrals,omitempty"` } // UserCreate represents the input structure for creation @@ -40,6 +31,7 @@ type UserCreate struct { PhoneNum string `json:"phoneNum"` Password *string `json:"password"` Role *UserRoleType `json:"role"` + RoleOptional *UserRoleType `json:"roleOptional"` ReferredById *string `json:"referredById"` } @@ -50,6 +42,7 @@ type UserSelect struct { PhoneNum bool `json:"phoneNum"` Password bool `json:"password"` Role bool `json:"role"` + RoleOptional bool `json:"roleOptional"` ReferredById bool `json:"referredById"` Profile *ProfileSelect `json:"profile,omitempty"` Posts *PostSelect `json:"posts,omitempty"` @@ -65,6 +58,7 @@ type UserOmit struct { PhoneNum bool `json:"phoneNum"` Password bool `json:"password"` Role bool `json:"role"` + RoleOptional bool `json:"roleOptional"` ReferredById bool `json:"referredById"` Profile *ProfileOmit `json:"profile,omitempty"` Posts *PostOmit `json:"posts,omitempty"` @@ -101,6 +95,8 @@ func (m *User) ScanFields(cols []string) []any { targets[i] = &m.Password case "role": targets[i] = &m.Role + case "roleOptional": + targets[i] = &m.RoleOptional case "referredById": targets[i] = &m.ReferredById } @@ -114,6 +110,7 @@ var userDefaultCols = []string{ "phoneNum", "password", "role", + "roleOptional", "referredById", } @@ -122,14 +119,15 @@ func (q *Queries) selectUserCols(selects *UserSelect, omits *UserOmit, forceCols return userDefaultCols } - anySelected := selects != nil && (selects.Id || selects.Email || selects.PhoneNum || selects.Password || selects.Role || selects.ReferredById || selects.Profile != nil || selects.Posts != nil || selects.Comments != nil || selects.ReferredBy != nil || selects.Referrals != nil) + anySelected := selects != nil && (selects.Id || selects.Email || selects.PhoneNum || selects.Password || selects.Role || selects.RoleOptional || selects.ReferredById || selects.Profile != nil || selects.Posts != nil || selects.Comments != nil || selects.ReferredBy != nil || selects.Referrals != nil) specs := []colSpec{ - {"id", selects != nil && selects.Id, omits != nil && omits.Id, false}, + {"id", selects != nil && selects.Id, omits != nil && omits.Id, selects != nil && selects.hasAnyRelation()}, {"email", selects != nil && selects.Email, omits != nil && omits.Email, false}, {"phoneNum", selects != nil && selects.PhoneNum, omits != nil && omits.PhoneNum, false}, {"password", selects != nil && selects.Password, omits != nil && omits.Password, false}, {"role", selects != nil && selects.Role, omits != nil && omits.Role, false}, + {"roleOptional", selects != nil && selects.RoleOptional, omits != nil && omits.RoleOptional, false}, {"referredById", selects != nil && selects.ReferredById, omits != nil && omits.ReferredById, selects != nil && selects.ReferredBy != nil}, } @@ -178,6 +176,11 @@ func (input UserCreate) Validate() error { 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 @@ -191,6 +194,7 @@ var UserColOrder = []string{ "phoneNum", "password", "role", + "roleOptional", "referredById", } @@ -274,6 +278,9 @@ func (q *Queries) UserInputToMap(input UserCreate) map[string]any { if input.Role != nil { m["role"] = *input.Role } + if input.RoleOptional != nil { + m["roleOptional"] = *input.RoleOptional + } if input.ReferredById != nil { m["referredById"] = *input.ReferredById } @@ -352,7 +359,7 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []U rowMaps[i] = q.UserInputToMap(input) } query, vals := buildBulkInsertSQL(q.dialect, "User", rowMaps, UserColOrder, returningCols) - var records []*User + records := make([]*User, 0) err := q.transaction(ctx, func(txQ *Queries) error { rows, err := txQ.query(ctx, query, vals...) if err != nil { @@ -381,7 +388,7 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []U } // Fallback to loop inside transaction - var records []*User + records := make([]*User, 0) err := q.transaction(ctx, func(txQ *Queries) error { for _, input := range inputs { res, err := txQ.executeUserCreate(ctx, input, nil, nil) @@ -401,6 +408,94 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []U } return records, nil } +func (d *UserDelegate) FindUnique(where UniquePredicate) *FindUniqueBuilder[User, UserSelect, UserOmit] { + return &FindUniqueBuilder[User, UserSelect, UserOmit]{ + client: d.client, + where: where, + execFunc: d.client.executeUserFindUnique, + } +} + +func (d *UserDelegate) FindFirst(preds ...Predicate) *FindFirstBuilder[User, UserSelect, UserOmit] { + return &FindFirstBuilder[User, UserSelect, UserOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeUserFindFirst, + } +} + +func (d *UserDelegate) FindMany(preds ...Predicate) *FindManyBuilder[User, UserSelect, UserOmit] { + return &FindManyBuilder[User, UserSelect, UserOmit]{ + client: d.client, + where: preds, + execFunc: d.client.executeUserFindMany, + } +} + +func (q *Queries) executeUserFindUnique(ctx context.Context, where UniquePredicate, selects *UserSelect, omits *UserOmit) (*User, error) { + if where == nil { + return nil, fmt.Errorf("at least one unique field must be set for FindUnique") + } + if err := where.Validate(); err != nil { + return nil, err + } + whereClause, vals := CompilePredicates(q.dialect, []Predicate{where}) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectUserCols(selects, omits) + return executeSingleWithRelations(ctx, q, "User", whereClause, vals, returningCols, + func(res *User, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*User) error { + return txQ.loadUserRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeUserFindFirst(ctx context.Context, where []Predicate, selects *UserSelect, omits *UserOmit) (*User, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectUserCols(selects, omits) + return executeSingleWithRelations(ctx, q, "User", whereClause, vals, returningCols, + func(res *User, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*User) error { + return txQ.loadUserRelations(ctx, results, selects) + }, + ) +} + +func (q *Queries) executeUserFindMany(ctx context.Context, where []Predicate, selects *UserSelect, omits *UserOmit) ([]*User, error) { + for _, p := range where { + if p != nil { + if err := p.Validate(); err != nil { + return nil, err + } + } + } + whereClause, vals := CompilePredicates(q.dialect, where) + if whereClause != "" { + whereClause = " WHERE " + whereClause + } + returningCols := q.selectUserCols(selects, omits) + return executeManyWithRelations(ctx, q, "User", whereClause, vals, returningCols, + func(res *User, cols []string) []any { return res.ScanFields(cols) }, + selects.hasAnyRelation(), + func(ctx context.Context, txQ *Queries, results []*User) error { + return txQ.loadUserRelations(ctx, results, selects) + }, + ) +} func (q *Queries) loadUserRelations(ctx context.Context, records []*User, selects *UserSelect) error { if selects == nil || len(records) == 0 { return nil diff --git a/integration/valk/user/user.go b/integration/valk/user/user.go new file mode 100644 index 0000000..10dacdd --- /dev/null +++ b/integration/valk/user/user.go @@ -0,0 +1,73 @@ +package user + +import ( + "fmt" + "integration/valk" +) + +type UniquePredicate struct { + valk.StandardPredicate +} + +func (UniquePredicate) IsUnique() {} + +func (p UniquePredicate) Validate() error { + if p.StandardPredicate.Data.Column == "" && len(p.StandardPredicate.Data.Children) == 0 { + return fmt.Errorf("at least one unique field must be set for FindUnique") + } + return p.StandardPredicate.Validate() +} + +type Select = valk.UserSelect +type Omit = valk.UserOmit +type Create = valk.UserCreate + +func And(preds ...valk.Predicate) valk.Predicate { + return valk.And(preds...) +} + +func Or(preds ...valk.Predicate) valk.Predicate { + return valk.Or(preds...) +} + +func Not(pred valk.Predicate) valk.Predicate { + return valk.Not(pred) +} + +var Id = valk.StringUniqueField{Column: "id"} + +var Email = valk.StringUniqueField{Column: "email"} + +var PhoneNum = valk.StringUniqueField{Column: "phoneNum"} + +var Password = valk.StringField{Column: "password"} + +var Role = valk.Field[valk.UserRoleType]{Column: "role"} + +var RoleOptional = valk.Field[valk.UserRoleType]{Column: "roleOptional"} + +var ReferredById = valk.StringField{Column: "referredById"} + +// Helper for compound unique constraint: emailPhone +func EmailPhoneUnique(email string, phoneNum string) UniquePredicate { + return UniquePredicate{ + StandardPredicate: valk.StandardPredicate{ + Data: valk.And( + valk.StandardPredicate{ + Data: valk.PredicateData{ + Column: "email", + Operator: "=", + Value: email, + }, + }, + valk.StandardPredicate{ + Data: valk.PredicateData{ + Column: "phoneNum", + Operator: "=", + Value: phoneNum, + }, + }, + ).ToPredicateData(), + }, + } +} diff --git a/schema/schema.go b/schema/schema.go index 4b8aa4c..fde35fd 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -96,6 +96,15 @@ func (m *Model) EffectiveTableName() string { return m.Name } +func (m *Model) GetField(name string) *ScalarField { + for _, f := range m.ScalarFields { + if f.Name == name { + return f + } + } + return nil +} + type UniqueConstraint struct { Fields []string Name string