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
8 changes: 2 additions & 6 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,12 @@ func runGenerate(ctx context.Context, cmd *cli.Command, l logger.Logger, targets
dryRun := cmd.Bool("dry-run")
debug := cmd.Bool("debug")
orch := codegen.NewOrchestrator(l, cfg, configPath)
results, err := orch.Generate(ctx, codegen.GenerateOpts{
_, err = orch.Generate(ctx, codegen.GenerateOpts{
DryRun: dryRun,
Debug: debug,
Targets: targets,
})
if err != nil {
return err
}

return codegen.WriteResults(results, dryRun)
return err
}

func newGenerateCmd(l logger.Logger) *cli.Command {
Expand Down
14 changes: 12 additions & 2 deletions internal/codegen/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ func (o *Orchestrator) Generate(ctx context.Context, opts GenerateOpts) ([]Gener
debugf("generate custom queries: %s (%d files)", time.Since(stepStart), len(results))
}

// Flush CRUD + custom-query results to disk now so sqlc can read them.
// sqlc parses query files from disk, so they must exist before sqlc runs.
if err := WriteResults(allResults, opts.DryRun); err != nil {
return nil, fmt.Errorf("write crud results for schema %s: %w", schema.Name, err)
}
for i := range allResults {
allResults[i].Action = ActionUnchanged
}

// 2. Generate models (reuses parsed catalog + sqlc config for overrides/defaults)
if o.shouldRun("models", opts) && schema.Models != nil {
stepStart := time.Now()
Expand All @@ -118,8 +127,9 @@ func (o *Orchestrator) Generate(ctx context.Context, opts GenerateOpts) ([]Gener
debugf("generate models: %s", time.Since(stepStart))
}

// 3. Run sqlc
if o.shouldRun("sqlc", opts) && schema.Sqlc != nil {
// 3. Run sqlc — skipped in dry-run because sqlc has no preview mode and
// its inputs (CRUD SQL) were not written to disk.
if !opts.DryRun && o.shouldRun("sqlc", opts) && schema.Sqlc != nil {
stepStart := time.Now()
gen := sqlcgen.NewGenerator(o.logger, o.configDir)
if err := gen.Run(&schema); err != nil {
Expand Down
138 changes: 138 additions & 0 deletions internal/codegen/orchestrator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package codegen

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tkcrm/pgxgen/internal/config"
"github.com/tkcrm/pgxgen/pkg/logger"
)

const testSchemaSQL = `CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`

// setupTestProject writes a minimal SQL schema to a temp dir and returns
// (configDir, configPath). The configPath file itself is not written —
// tests construct V2Config in code and pass configPath only so the
// orchestrator can derive configDir from it.
func setupTestProject(t *testing.T) (string, string) {
t.Helper()
tmp := t.TempDir()
migrationsDir := filepath.Join(tmp, "sql", "migrations")
require.NoError(t, os.MkdirAll(migrationsDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(migrationsDir, "001_init.sql"), []byte(testSchemaSQL), 0o644))
return tmp, filepath.Join(tmp, "pgxgen.yaml")
}

func newTestConfig(withSqlc bool) *config.V2Config {
cfg := &config.V2Config{
Version: "2",
Schemas: []config.SchemaConfig{
{
Name: "main",
Engine: "sqlite",
SchemaDir: "sql/migrations",
Defaults: &config.DefaultsConfig{
QueriesDirPrefix: "sql/queries",
OutputDirPrefix: "internal/store/repos",
Crud: &config.CrudDefaultsConfig{
Methods: map[string]*config.MethodConfig{
"get": {},
"delete": {},
},
},
},
Tables: map[string]config.TableConfig{
"users": {
PrimaryColumn: "id",
Crud: &config.TableCrudConfig{
Methods: map[string]*config.MethodConfig{
"get": {},
"delete": {},
},
},
},
},
},
},
}
if withSqlc {
cfg.Schemas[0].Sqlc = &config.SqlcConfig{
Defaults: &config.SqlcDefaultsConfig{
SqlPackage: "database/sql",
EmitInterface: true,
},
}
}
return cfg
}

// TestGenerate_CrudWrittenBeforeSqlc is the regression test for the bug
// where sqlc was invoked before CRUD SQL files reached disk. With the
// fix, Generate writes CRUD results before running sqlc, so sqlc finds
// its inputs on a fresh project (no pre-existing query directories).
func TestGenerate_CrudWrittenBeforeSqlc(t *testing.T) {
tmp, configPath := setupTestProject(t)
cfg := newTestConfig(true)

queriesPath := filepath.Join(tmp, "sql", "queries", "users")
require.NoDirExists(t, queriesPath, "queries dir must not pre-exist — this is the fresh-project scenario")

orch := NewOrchestrator(logger.New(), cfg, configPath)
_, err := orch.Generate(context.Background(), GenerateOpts{})
require.NoError(t, err, "Generate must succeed on a fresh project")

assert.FileExists(t, filepath.Join(queriesPath, "users_gen.sql"), "CRUD SQL must be written")

sqlcOutDir := filepath.Join(tmp, "internal", "store", "repos", "users")
assert.DirExists(t, sqlcOutDir, "sqlc must have produced its output directory")

entries, err := os.ReadDir(sqlcOutDir)
require.NoError(t, err)
var goFiles []string
for _, e := range entries {
if filepath.Ext(e.Name()) == ".go" {
goFiles = append(goFiles, e.Name())
}
}
assert.NotEmpty(t, goFiles, "sqlc must have generated at least one .go file in %s", sqlcOutDir)
}

// TestGenerate_DryRunSkipsWritesAndSqlc verifies that dry-run touches
// nothing on disk and does not invoke sqlc (which has no preview mode).
func TestGenerate_DryRunSkipsWritesAndSqlc(t *testing.T) {
tmp, configPath := setupTestProject(t)
cfg := newTestConfig(true)

orch := NewOrchestrator(logger.New(), cfg, configPath)
_, err := orch.Generate(context.Background(), GenerateOpts{DryRun: true})
require.NoError(t, err)

assert.NoDirExists(t, filepath.Join(tmp, "sql", "queries"), "dry-run must not write CRUD files")
assert.NoDirExists(t, filepath.Join(tmp, "internal"), "dry-run must not invoke sqlc")
assert.NoDirExists(t, filepath.Join(tmp, ".pgxgen"), "dry-run must not write sqlc.yaml")
}

// TestGenerate_CrudOnlyTargetSkipsSqlc verifies the crud-only target
// writes CRUD files and does not invoke sqlc.
func TestGenerate_CrudOnlyTargetSkipsSqlc(t *testing.T) {
tmp, configPath := setupTestProject(t)
cfg := newTestConfig(true)

orch := NewOrchestrator(logger.New(), cfg, configPath)
_, err := orch.Generate(context.Background(), GenerateOpts{Targets: []string{"crud"}})
require.NoError(t, err)

assert.FileExists(t, filepath.Join(tmp, "sql", "queries", "users", "users_gen.sql"))
assert.NoDirExists(t, filepath.Join(tmp, "internal"), "sqlc must not run when target is crud only")
assert.NoFileExists(t, filepath.Join(tmp, ".pgxgen", "sqlc.yaml"))
}