Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func GenerateClient(sch schema.Schema, pkgName string, embedPath string, default
"enums.gotpl",
"client.gotpl",
"tx.gotpl",
"builders_create.gotpl",
"delegates.gotpl",
}
for _, file := range files {
Expand Down
7 changes: 4 additions & 3 deletions generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ func TestGenerateClient(t *testing.T) {
t.Fatalf("failed to generate client: %v\nCode output:\n%s", err, code)
}

// Verify package name
if !strings.Contains(code, "type CreateBuilder[M any, I any, S any, O any] struct {") {
t.Errorf("expected CreateBuilder struct in code, got:\n%s", code)
}

if !strings.Contains(code, "package client") {
t.Errorf("expected package client, got:\n%s", code)
}

// Verify client and delegates
if !strings.Contains(code, "type DB struct {") {
t.Errorf("expected DB struct, got:\n%s", code)
}
Expand All @@ -66,7 +68,6 @@ func TestGenerateClient(t *testing.T) {
t.Errorf("expected PostDelegate struct, got:\n%s", code)
}

// Verify enums and namespaces (Step 2)
if !strings.Contains(code, "type RoleType string") {
t.Errorf("expected RoleType type definition, got:\n%s", code)
}
Expand Down
106 changes: 106 additions & 0 deletions generator/templates/builders_create.gotpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
type CreateBuilder[M any, I any, S any, O any] struct {
client *Queries
input I
execFunc func(ctx context.Context, input I, s *S, o *O) (*M, error)
}

func (b *CreateBuilder[M, I, S, O]) Select(s S) *CreateSelectBuilder[M, I, S, O] {
return &CreateSelectBuilder[M, I, S, O]{builder: b, selects: s}
}

func (b *CreateBuilder[M, I, S, O]) Omit(o O) *CreateOmitBuilder[M, I, S, O] {
return &CreateOmitBuilder[M, I, S, O]{builder: b, omits: o}
}

func (b *CreateBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) {
return b.execFunc(ctx, b.input, nil, nil)
}

type CreateSelectBuilder[M any, I any, S any, O any] struct {
builder *CreateBuilder[M, I, S, O]
selects S
}

func (b *CreateSelectBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) {
return b.builder.execFunc(ctx, b.builder.input, &b.selects, nil)
}

type CreateOmitBuilder[M any, I any, S any, O any] struct {
builder *CreateBuilder[M, I, S, O]
omits O
}

func (b *CreateOmitBuilder[M, I, S, O]) Exec(ctx context.Context) (*M, error) {
return b.builder.execFunc(ctx, b.builder.input, nil, &b.omits)
}
func executeInsert[M any](
ctx context.Context,
q *Queries,
table string,
cols []string,
vals []any,
returningCols []string,
idCol string,
scanFunc func(record *M, cols []string) []any,
) (*M, error) {
placeholders := make([]string, len(cols))
for i := range cols {
placeholders[i] = q.dialect.BindVar(i + 1)
}

var res M
quotedReturningCols := make([]string, len(returningCols))
for i, col := range returningCols {
quotedReturningCols[i] = q.dialect.Quote(col)
}

query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
q.dialect.Quote(table),
strings.Join(cols, ", "),
strings.Join(placeholders, ", "),
)

if q.dialect.SupportsReturning() {
query += " RETURNING " + strings.Join(quotedReturningCols, ", ")
row := q.db.QueryRowContext(ctx, query, vals...)

scanTargets := scanFunc(&res, returningCols)
if err := row.Scan(scanTargets...); err != nil {
return nil, err
}
return &res, nil
}

// Fallback for dialects without RETURNING (MySQL)
result, err := q.db.ExecContext(ctx, query, vals...)
if err != nil {
return nil, err
}

var idVal any
for i, c := range cols {
if c == q.dialect.Quote(idCol) {
idVal = vals[i]
break
}
}
if idVal == nil {
lastID, err := result.LastInsertId()
if err != nil {
return nil, err
}
idVal = lastID
}

selectQuery := fmt.Sprintf("SELECT %s FROM %s WHERE %s = ?",
strings.Join(quotedReturningCols, ", "),
q.dialect.Quote(table),
q.dialect.Quote(idCol),
)
row := q.db.QueryRowContext(ctx, selectQuery, idVal)
scanTargets := scanFunc(&res, returningCols)
if err := row.Scan(scanTargets...); err != nil {
return nil, err
}
return &res, nil
}
31 changes: 31 additions & 0 deletions generator/templates/client.gotpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
type Dialect interface {
Quote(ident string) string
BindVar(idx int) string
SupportsReturning() bool
}

type postgresDialect struct{}
func (postgresDialect) Quote(ident string) string { return `"` + ident + `"` }
func (postgresDialect) BindVar(idx int) string { return fmt.Sprintf("$%d", idx) }
func (postgresDialect) SupportsReturning() bool { return true }

type sqliteDialect struct{}
func (sqliteDialect) Quote(ident string) string { return `"` + ident + `"` }
func (sqliteDialect) BindVar(idx int) string { return "?" }
func (sqliteDialect) SupportsReturning() bool { return true }

type DBTX interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
Expand All @@ -8,6 +24,7 @@ type DBTX interface {
type Queries struct {
db DBTX
provider string
dialect Dialect
{{- range $model := .Schema.Models }}
{{ $model.Name }} *{{ $model.Name }}Delegate
{{- end }}
Expand All @@ -27,9 +44,23 @@ func Open(provider, dataSourceName string) (*DB, error) {
if err != nil {
return nil, err
}

var d Dialect
switch provider {
case "postgres", "postgresql":
d = postgresDialect{}
case "sqlite", "sqlite3":
d = sqliteDialect{}

default:
sqlDB.Close()
return nil, fmt.Errorf("unsupported database provider: %s", provider)
}

q := &Queries{
db: sqlDB,
provider: provider,
dialect: d,
{{- range $enum := .Schema.Enums }}
{{ $enum.Name }}: {{ $enum.Name }},
{{- end }}
Expand Down
142 changes: 142 additions & 0 deletions generator/templates/delegates.gotpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,147 @@
{{- range $model := .Schema.Models }}

// {{ $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 }}"`
{{- end }}
{{- range $relation := $model.RelationFields }}
{{ capitalize $relation.Name }} {{ if $relation.IsArray }}[]{{ $relation.TargetModelName }}{{ else }}*{{ $relation.TargetModelName }}{{ end }} `json:"{{ $relation.Name }},omitempty"`
{{- end }}
}

// {{ $model.Name }}CreateInput represents the input structure for creation
type {{ $model.Name }}CreateInput struct {
{{- range $field := $model.ScalarFields }}
{{ capitalize $field.Name }} {{ if $field.EnumRef }}{{ if $field.IsArray }}[]{{ $field.EnumRef.Name }}Type{{ else }}*{{ $field.EnumRef.Name }}Type{{ end }}{{ else }}{{ if $field.IsArray }}{{ $field.GoType }}{{ else }}{{ if and (ne $field.Default nil) (not $field.Optional) }}*{{ end }}{{ $field.GoType }}{{ end }}{{ end }} `json:"{{ $field.Name }}"`
{{- end }}
}

// {{ $model.Name }}Select specifies which fields to include
type {{ $model.Name }}Select struct {
{{- range $field := $model.ScalarFields }}
{{ capitalize $field.Name }} bool `json:"{{ $field.Name }}"`
{{- end }}
{{- range $relation := $model.RelationFields }}
{{ capitalize $relation.Name }} *{{ $relation.TargetModelName }}Select `json:"{{ $relation.Name }},omitempty"`
{{- end }}
}

// {{ $model.Name }}Omit specifies which fields to exclude
type {{ $model.Name }}Omit struct {
{{- range $field := $model.ScalarFields }}
{{ capitalize $field.Name }} bool `json:"{{ $field.Name }}"`
{{- end }}
{{- range $relation := $model.RelationFields }}
{{ capitalize $relation.Name }} *{{ $relation.TargetModelName }}Omit `json:"{{ $relation.Name }},omitempty"`
{{- end }}
}

type {{ $model.Name }}Delegate struct {
client *Queries
}

func (d *{{ $model.Name }}Delegate) Create(input {{ $model.Name }}CreateInput) *CreateBuilder[{{ $model.Name }}, {{ $model.Name }}CreateInput, {{ $model.Name }}Select, {{ $model.Name }}Omit] {
return &CreateBuilder[{{ $model.Name }}, {{ $model.Name }}CreateInput, {{ $model.Name }}Select, {{ $model.Name }}Omit]{
client: d.client,
input: input,
execFunc: d.client.execute{{ $model.Name }}Create,
}
}

func (q *Queries) execute{{ $model.Name }}Create(ctx context.Context, input {{ $model.Name }}CreateInput, selects *{{ $model.Name }}Select, omits *{{ $model.Name }}Omit) (*{{ $model.Name }}, error) {
var cols []string
var vals []any

{{- range $field := $model.ScalarFields }}
{{- if $field.EnumRef }}
{{- if $field.IsArray }}
if input.{{ capitalize $field.Name }} != nil {
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
vals = append(vals, input.{{ capitalize $field.Name }})
}
{{- else }}
if input.{{ capitalize $field.Name }} != nil {
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
vals = append(vals, *input.{{ capitalize $field.Name }})
}
{{- end }}
{{- else }}
{{- if $field.IsArray }}
if input.{{ capitalize $field.Name }} != nil {
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
vals = append(vals, input.{{ capitalize $field.Name }})
}
{{- else }}
{{- if and $field.Default (eq $field.Default.Kind.String "Func") }}
if input.{{ capitalize $field.Name }} != nil {
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
vals = append(vals, *input.{{ capitalize $field.Name }})
} else {
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
{{- if eq $field.Default.FuncName "cuid" }}
vals = append(vals, generateCUID())
{{- else if eq $field.Default.FuncName "uuid" }}
vals = append(vals, generateUUID())
{{- else if eq $field.Default.FuncName "now" }}
vals = append(vals, time.Now())
{{- end }}
}
{{- else if or $field.Optional (ne $field.Default nil) }}
if input.{{ capitalize $field.Name }} != nil {
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
vals = append(vals, *input.{{ capitalize $field.Name }})
}
{{- else }}
cols = append(cols, q.dialect.Quote("{{ $field.EffectiveColName }}"))
vals = append(vals, input.{{ capitalize $field.Name }})
{{- end }}
{{- end }}
{{- end }}
{{- end }}

var returningCols []string
{{- range $field := $model.ScalarFields }}
{
include := true
if selects != nil {
include = false
if selects.{{ capitalize $field.Name }} {
include = true
}
} else if omits != nil {
if omits.{{ capitalize $field.Name }} {
include = false
}
}
if include {
returningCols = append(returningCols, "{{ $field.EffectiveColName }}")
}
}
{{- end }}

if len(returningCols) == 0 {
{{- range $field := $model.ScalarFields }}
returningCols = append(returningCols, "{{ $field.EffectiveColName }}")
{{- end }}
}

scanFunc := func(res *{{ $model.Name }}, cols []string) []any {
targets := make([]any, len(cols))
for i, col := range cols {
switch col {
{{- range $field := $model.ScalarFields }}
case "{{ $field.EffectiveColName }}":
targets[i] = &res.{{ capitalize $field.Name }}
{{- end }}
}
}
return targets
}

idCol := "{{ range $field := $model.ScalarFields }}{{ if $field.IsID }}{{ $field.EffectiveColName }}{{ end }}{{ end }}"

return executeInsert(ctx, q, "{{ $model.EffectiveTableName }}", cols, vals, returningCols, idCol, scanFunc)
}

{{- end }}
23 changes: 23 additions & 0 deletions generator/templates/header.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,38 @@ package {{ .PackageName }}

import (
"context"
"crypto/rand"
"database/sql"
"encoding/json"
{{- if .EmbedPath }}
"embed"
{{- end }}
"fmt"
"strings"
"time"

"github.com/google/uuid"
"github.com/pressly/goose/v3"
)

var _ = time.Time{}
var _ = json.RawMessage{}
var _ = strings.Join
var _ = uuid.New
var _ = rand.Read

{{- if .EmbedPath }}
//go:embed {{ .EmbedPath }}
var migrationsFS embed.FS
{{- end }}

func generateCUID() string {
now := time.Now().UnixMilli()
b := make([]byte, 8)
_, _ = rand.Read(b)
return fmt.Sprintf("c%x%x", now, b)
}

func generateUUID() string {
return uuid.New().String()
}
Loading
Loading