From dbeb110bae9d859f7a6a50e33f4e467978e68484 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 16:02:51 +0000 Subject: [PATCH] Port SQLCDEBUG to a registry-style package modeled after Go's GODEBUG Replace the monolithic opts.Debug snapshot with internal/sqlcdebug, which exposes a master Settings table and per-call-site Setting handles via sqlcdebug.New("name"). Each consumer reads its setting independently through Setting.Value(), removing the need to thread a Debug struct through cmd.Env, opts.Parser, and the analyzer constructors. Migrated readers: - cmd: trace, processplugins, dumpcatalog, dumpvetenv, dumpexplain, databases - compiler: dumpast - postgresql/sqlite analyzers: databases - tracer: trace (now via tracer.Path()) The endtoend test fixture process_plugin_disabled now installs the SQLCDEBUG override via sqlcdebug.Update for the duration of the subtest; TestReplay subtests run sequentially, so no synchronization is needed. --- internal/cmd/cmd.go | 10 +- internal/cmd/generate.go | 5 +- internal/cmd/process.go | 5 +- internal/cmd/vet.go | 19 +- internal/compiler/parse.go | 5 +- internal/debug/dump.go | 16 +- internal/endtoend/endtoend_test.go | 20 +- .../engine/postgresql/analyzer/analyze.go | 10 +- internal/engine/sqlite/analyzer/analyze.go | 10 +- internal/opts/debug.go | 67 ------- internal/opts/parser.go | 1 - internal/sqlcdebug/sqlcdebug.go | 182 ++++++++++++++++++ internal/sqlcdebug/sqlcdebug_test.go | 107 ++++++++++ internal/tracer/trace.go | 22 ++- 14 files changed, 366 insertions(+), 113 deletions(-) delete mode 100644 internal/opts/debug.go create mode 100644 internal/sqlcdebug/sqlcdebug.go create mode 100644 internal/sqlcdebug/sqlcdebug_test.go diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index f9c09dfe06..4079b3c1d3 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -18,12 +18,14 @@ import ( "gopkg.in/yaml.v3" "github.com/sqlc-dev/sqlc/internal/config" - "github.com/sqlc-dev/sqlc/internal/debug" "github.com/sqlc-dev/sqlc/internal/info" "github.com/sqlc-dev/sqlc/internal/opts" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" "github.com/sqlc-dev/sqlc/internal/tracer" ) +var debugProcessPlugins = sqlcdebug.New("processplugins") + func init() { createDBCmd.Flags().StringP("queryset", "", "", "name of the queryset to use") pushCmd.Flags().BoolP("dry-run", "", false, "dump push request (default: false)") @@ -55,7 +57,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int rootCmd.SetErr(stderr) ctx := context.Background() - if debug.Debug.Trace != "" { + if tracer.Path() != "" { tracectx, cleanup, err := tracer.Start(ctx) if err != nil { fmt.Printf("failed to start trace: %v\n", err) @@ -137,7 +139,6 @@ var initCmd = &cobra.Command{ type Env struct { DryRun bool - Debug opts.Debug Experiment opts.Experiment } @@ -145,7 +146,6 @@ func ParseEnv(c *cobra.Command) Env { dr := c.Flag("dry-run") return Env{ DryRun: dr != nil && dr.Changed, - Debug: opts.DebugFromEnv(), Experiment: opts.ExperimentFromEnv(), } } @@ -154,7 +154,7 @@ var ErrPluginProcessDisabled = errors.New("plugin: process-based plugins disable func (e *Env) Validate(cfg *config.Config) error { for _, plugin := range cfg.Plugins { - if plugin.Process != nil && !e.Debug.ProcessPlugins { + if plugin.Process != nil && debugProcessPlugins.Value() == "0" { return ErrPluginProcessDisabled } } diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index ca3ee680b5..bc15ec4461 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -26,8 +26,11 @@ import ( "github.com/sqlc-dev/sqlc/internal/multierr" "github.com/sqlc-dev/sqlc/internal/opts" "github.com/sqlc-dev/sqlc/internal/plugin" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) +var debugDumpCatalog = sqlcdebug.New("dumpcatalog") + const errMessageNoVersion = `The configuration file must have a version number. Set the version to 1 or 2 at the top of sqlc.json: @@ -244,7 +247,7 @@ func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.C } return nil, true } - if parserOpts.Debug.DumpCatalog { + if debugDumpCatalog.Value() == "1" { debug.Dump(c.Catalog()) } if err := c.ParseQueries(sql.Queries, parserOpts); err != nil { diff --git a/internal/cmd/process.go b/internal/cmd/process.go index 5003d113b8..e90450cf5f 100644 --- a/internal/cmd/process.go +++ b/internal/cmd/process.go @@ -13,7 +13,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/compiler" "github.com/sqlc-dev/sqlc/internal/config" - "github.com/sqlc-dev/sqlc/internal/debug" "github.com/sqlc-dev/sqlc/internal/opts" ) @@ -87,9 +86,7 @@ func processQuerySets(ctx context.Context, rp ResultProcessor, conf *config.Conf sql.Queries = joined var name, lang string - parseOpts := opts.Parser{ - Debug: debug.Debug, - } + parseOpts := opts.Parser{} switch { case sql.Gen.Go != nil: diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 4dbd3c3b7b..38a808fdb1 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -33,9 +33,16 @@ import ( "github.com/sqlc-dev/sqlc/internal/quickdb" "github.com/sqlc-dev/sqlc/internal/shfmt" "github.com/sqlc-dev/sqlc/internal/sql/sqlpath" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" "github.com/sqlc-dev/sqlc/internal/vet" ) +var ( + debugDumpExplain = sqlcdebug.New("dumpexplain") + debugDumpVetEnv = sqlcdebug.New("dumpvetenv") + debugDatabases = sqlcdebug.New("databases") +) + var ErrFailedChecks = errors.New("failed checks") var pjson = protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true} @@ -148,7 +155,7 @@ func Vet(ctx context.Context, dir, filename string, opts *Options) error { Dir: dir, Env: env, Stderr: stderr, - OnlyManagedDB: e.Debug.OnlyManagedDatabases, + OnlyManagedDB: debugDatabases.Value() == "managed", Replacer: shfmt.NewReplacer(nil), } errored := false @@ -316,7 +323,7 @@ func (p *pgxConn) Explain(ctx context.Context, query string, args ...*plugin.Par if err := row.Scan(&result); err != nil { return nil, err } - if debug.Debug.DumpExplain { + if debugDumpExplain.Value() == "1" { fmt.Println(eQuery, "with args", eArgs) fmt.Println(string(result[0])) } @@ -358,7 +365,7 @@ func (me *mysqlExplainer) Explain(ctx context.Context, query string, args ...*pl if err := row.Scan(&result); err != nil { return nil, err } - if debug.Debug.DumpExplain { + if debugDumpExplain.Value() == "1" { fmt.Println(eQuery, "with args", eArgs) fmt.Println(string(result)) } @@ -480,9 +487,7 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error { s.Queries = joined var name string - parseOpts := opts.Parser{ - Debug: debug.Debug, - } + parseOpts := opts.Parser{} result, failed := parse(ctx, name, c.Dir, s, combo, parseOpts, c.Stderr) if failed { @@ -642,7 +647,7 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error { evalMap["mysql"] = engineOutput.MySQL } - if debug.Debug.DumpVetEnv { + if debugDumpVetEnv.Value() == "1" { fmt.Printf("vars for rule '%s' evaluating against query '%s':\n", name, query.Name) debug.DumpAsJSON(evalMap) } diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index 751cb3271a..2f9afb72c1 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -13,12 +13,15 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/astutils" "github.com/sqlc-dev/sqlc/internal/sql/validate" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) +var debugDumpAST = sqlcdebug.New("dumpast") + func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, error) { ctx := context.Background() - if o.Debug.DumpAST { + if debugDumpAST.Value() == "1" { debug.Dump(stmt) } diff --git a/internal/debug/dump.go b/internal/debug/dump.go index 6921ecb67f..0616b0ad75 100644 --- a/internal/debug/dump.go +++ b/internal/debug/dump.go @@ -3,22 +3,16 @@ package debug import ( "encoding/json" "fmt" - "os" "github.com/davecgh/go-spew/spew" - "github.com/sqlc-dev/sqlc/internal/opts" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) -var Active bool -var Debug opts.Debug - -func init() { - Active = os.Getenv("SQLCDEBUG") != "" - if Active { - Debug = opts.DebugFromEnv() - } -} +// Active reports whether SQLCDEBUG had any value set at startup. It +// remains a global so unrelated debug-spew sites that don't tie to a +// specific setting can gate their output on "is debug mode on at all". +var Active = sqlcdebug.Any() func Dump(n ...interface{}) { if Active { diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index 91e44ff7f0..f8bb5a6e0f 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -17,10 +17,26 @@ import ( "github.com/sqlc-dev/sqlc/internal/cmd" "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/opts" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" "github.com/sqlc-dev/sqlc/internal/sqltest/docker" "github.com/sqlc-dev/sqlc/internal/sqltest/native" ) +// withSQLCDEBUG installs the given SQLCDEBUG-formatted string for the +// duration of the test and restores the empty default afterwards. +// +// Callers in TestReplay are sequential, so this does not need a mutex: +// Go's test scheduler does not run TestReplay concurrently with the +// parallel top-level tests in this package. +func withSQLCDEBUG(t *testing.T, raw string) func() { + t.Helper() + if raw == "" { + return func() {} + } + sqlcdebug.Update(raw) + return func() { sqlcdebug.Update("") } +} + func lineEndings() cmp.Option { return cmp.Transformer("LineEndings", func(in string) string { // Replace Windows new lines with Unix newlines @@ -263,13 +279,15 @@ func TestReplay(t *testing.T) { opts := cmd.Options{ Env: cmd.Env{ - Debug: opts.DebugFromString(args.Env["SQLCDEBUG"]), Experiment: opts.ExperimentFromString(args.Env["SQLCEXPERIMENT"]), }, Stderr: &stderr, MutateConfig: testctx.Mutate(t, path), } + release := withSQLCDEBUG(t, args.Env["SQLCDEBUG"]) + defer release() + switch args.Command { case "diff": err = cmd.Diff(ctx, path, "", &opts) diff --git a/internal/engine/postgresql/analyzer/analyze.go b/internal/engine/postgresql/analyzer/analyze.go index ee03e4d3c5..0890482cfe 100644 --- a/internal/engine/postgresql/analyzer/analyze.go +++ b/internal/engine/postgresql/analyzer/analyze.go @@ -14,19 +14,20 @@ import ( core "github.com/sqlc-dev/sqlc/internal/analysis" "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/dbmanager" - "github.com/sqlc-dev/sqlc/internal/opts" "github.com/sqlc-dev/sqlc/internal/shfmt" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/catalog" "github.com/sqlc-dev/sqlc/internal/sql/named" "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) +var debugDatabases = sqlcdebug.New("databases") + type Analyzer struct { db config.Database client dbmanager.Client pool *pgxpool.Pool - dbg opts.Debug replacer *shfmt.Replacer formats sync.Map columns sync.Map @@ -36,7 +37,6 @@ type Analyzer struct { func New(client dbmanager.Client, db config.Database) *Analyzer { return &Analyzer{ db: db, - dbg: opts.DebugFromEnv(), client: client, replacer: shfmt.NewReplacer(nil), } @@ -210,7 +210,7 @@ func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrat return nil, err } uri = edb.Uri - } else if a.dbg.OnlyManagedDatabases { + } else if debugDatabases.Value() == "managed" { return nil, fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed") } else { uri = a.replacer.Replace(a.db.URI) @@ -502,7 +502,7 @@ func (a *Analyzer) EnsureConn(ctx context.Context, migrations []string) error { return err } uri = edb.Uri - } else if a.dbg.OnlyManagedDatabases { + } else if debugDatabases.Value() == "managed" { return fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed") } else { uri = a.replacer.Replace(a.db.URI) diff --git a/internal/engine/sqlite/analyzer/analyze.go b/internal/engine/sqlite/analyzer/analyze.go index b9795cc54a..a49c03e3c7 100644 --- a/internal/engine/sqlite/analyzer/analyze.go +++ b/internal/engine/sqlite/analyzer/analyze.go @@ -10,18 +10,19 @@ import ( core "github.com/sqlc-dev/sqlc/internal/analysis" "github.com/sqlc-dev/sqlc/internal/config" - "github.com/sqlc-dev/sqlc/internal/opts" "github.com/sqlc-dev/sqlc/internal/shfmt" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/catalog" "github.com/sqlc-dev/sqlc/internal/sql/named" "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) +var debugDatabases = sqlcdebug.New("databases") + type Analyzer struct { db config.Database conn *sqlite3.Conn - dbg opts.Debug replacer *shfmt.Replacer mu sync.Mutex } @@ -29,7 +30,6 @@ type Analyzer struct { func New(db config.Database) *Analyzer { return &Analyzer{ db: db, - dbg: opts.DebugFromEnv(), replacer: shfmt.NewReplacer(nil), } } @@ -44,7 +44,7 @@ func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrat if a.db.Managed { // For managed databases, create an in-memory database uri = ":memory:" - } else if a.dbg.OnlyManagedDatabases { + } else if debugDatabases.Value() == "managed" { return nil, fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed") } else { uri = a.replacer.Replace(a.db.URI) @@ -200,7 +200,7 @@ func (a *Analyzer) EnsureConn(ctx context.Context, migrations []string) error { if a.db.Managed { // For managed databases, create an in-memory database uri = ":memory:" - } else if a.dbg.OnlyManagedDatabases { + } else if debugDatabases.Value() == "managed" { return fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed") } else { uri = a.replacer.Replace(a.db.URI) diff --git a/internal/opts/debug.go b/internal/opts/debug.go deleted file mode 100644 index b92cbd4ae8..0000000000 --- a/internal/opts/debug.go +++ /dev/null @@ -1,67 +0,0 @@ -package opts - -import ( - "os" - "strings" -) - -// The SQLCDEBUG variable controls debugging variables within the runtime. It -// is a comma-separated list of name=val pairs setting these named variables: -// -// dumpast: setting dumpast=1 will print the AST of every SQL statement -// dumpcatalog: setting dumpcatalog=1 will print the parsed database schema -// trace: setting trace= will output a trace -// processplugins: setting processplugins=0 will disable process-based plugins -// databases: setting databases=managed will disable connections to databases via URI -// dumpvetenv: setting dumpvetenv=1 will print the variables available to -// a vet rule during evaluation -// dumpexplain: setting dumpexplain=1 will print the JSON-formatted output -// from executing EXPLAIN ... on a query during vet rule evaluation - -type Debug struct { - DumpAST bool - DumpCatalog bool - Trace string - ProcessPlugins bool - OnlyManagedDatabases bool - DumpVetEnv bool - DumpExplain bool -} - -func DebugFromEnv() Debug { - return DebugFromString(os.Getenv("SQLCDEBUG")) -} - -func DebugFromString(val string) Debug { - d := Debug{ - ProcessPlugins: true, - } - if val == "" { - return d - } - for _, pair := range strings.Split(val, ",") { - pair = strings.TrimSpace(pair) - switch { - case pair == "dumpast=1": - d.DumpAST = true - case pair == "dumpcatalog=1": - d.DumpCatalog = true - case strings.HasPrefix(pair, "trace="): - traceName := strings.TrimPrefix(pair, "trace=") - if traceName == "1" { - d.Trace = "trace.out" - } else { - d.Trace = traceName - } - case pair == "processplugins=0": - d.ProcessPlugins = false - case pair == "databases=managed": - d.OnlyManagedDatabases = true - case pair == "dumpvetenv=1": - d.DumpVetEnv = true - case pair == "dumpexplain=1": - d.DumpExplain = true - } - } - return d -} diff --git a/internal/opts/parser.go b/internal/opts/parser.go index 2059d4f6a1..69112dcd95 100644 --- a/internal/opts/parser.go +++ b/internal/opts/parser.go @@ -1,6 +1,5 @@ package opts type Parser struct { - Debug Debug Experiment Experiment } diff --git a/internal/sqlcdebug/sqlcdebug.go b/internal/sqlcdebug/sqlcdebug.go new file mode 100644 index 0000000000..cd17565c65 --- /dev/null +++ b/internal/sqlcdebug/sqlcdebug.go @@ -0,0 +1,182 @@ +// Package sqlcdebug parses the SQLCDEBUG environment variable and exposes +// its key=value settings to the rest of the codebase. +// +// The SQLCDEBUG variable is a comma-separated list of name=value pairs: +// +// SQLCDEBUG=dumpast=1,trace=trace.out +// +// Settings are looked up at the call site by declaring a package-level +// variable: +// +// var dumpAST = sqlcdebug.New("dumpast") +// +// func parse() { +// if dumpAST.Value() == "1" { ... } +// } +// +// New panics if name is not registered in [Settings] below. Adding a new +// setting therefore requires extending the [Settings] table so that all +// known keys are documented in one place. +// +// This package is modeled after Go's internal/godebug. Unlike Go, sqlc is +// short-lived, so settings are parsed once at process startup; the +// [Update] hook exists for tests that need to reparse mid-run. +package sqlcdebug + +import ( + "os" + "strings" + "sync" + "sync/atomic" +) + +// Info documents a single SQLCDEBUG setting. +type Info struct { + // Name is the SQLCDEBUG key, e.g. "dumpast". + Name string + // Description is a short human-readable description. + Description string + // Default is the value returned by [Setting.Value] when the key is + // not present in SQLCDEBUG. + Default string +} + +// Settings is the master table of all known SQLCDEBUG keys. New keys must +// be added here before they can be looked up via [New]. +var Settings = []Info{ + {Name: "dumpast", Description: "print the AST of every SQL statement"}, + {Name: "dumpcatalog", Description: "print the parsed database schema"}, + {Name: "trace", Description: "write a runtime trace to the named file (1 means trace.out)"}, + {Name: "processplugins", Description: "set to 0 to disable process-based plugins", Default: "1"}, + {Name: "databases", Description: "set to 'managed' to disable database connections via URI"}, + {Name: "dumpvetenv", Description: "print the variables available to a vet rule during evaluation"}, + {Name: "dumpexplain", Description: "print the JSON-formatted output from EXPLAIN during vet evaluation"}, +} + +func info(name string) (Info, bool) { + for _, s := range Settings { + if s.Name == name { + return s, true + } + } + return Info{}, false +} + +// Setting is a single SQLCDEBUG key. Obtain one with [New]; reading it +// with [Setting.Value] returns the value parsed from the SQLCDEBUG +// environment variable, or the registered default. +type Setting struct { + info Info + set atomic.Bool + value atomic.Pointer[string] +} + +var ( + registryMu sync.Mutex + registry = map[string]*Setting{} +) + +// New returns the Setting for name. The same pointer is returned for +// repeated calls. New panics if name is not present in [Settings]. +func New(name string) *Setting { + registryMu.Lock() + defer registryMu.Unlock() + if s, ok := registry[name]; ok { + return s + } + i, ok := info(name) + if !ok { + panic("sqlcdebug: unknown setting " + name) + } + s := &Setting{info: i} + registry[name] = s + apply(s, parsedEnv()) + return s +} + +// Name returns the setting's key. +func (s *Setting) Name() string { return s.info.Name } + +// Default returns the value used when the key is absent from SQLCDEBUG. +func (s *Setting) Default() string { return s.info.Default } + +// Value returns the parsed value of the SQLCDEBUG setting, falling back +// to the registered default. +func (s *Setting) Value() string { + if v := s.value.Load(); v != nil { + return *v + } + return s.info.Default +} + +// IsSet reports whether the key was present in SQLCDEBUG. +func (s *Setting) IsSet() bool { return s.set.Load() } + +// Any reports whether SQLCDEBUG contained any recognized key=value pair. +// It mirrors the legacy "is debug active?" check. +func Any() bool { + for _, s := range registry { + if s.IsSet() { + return true + } + } + return parsedEnvHasAny() +} + +var ( + envOnce sync.Once + envMap map[string]string +) + +func parsedEnv() map[string]string { + envOnce.Do(func() { + envMap = parse(os.Getenv("SQLCDEBUG")) + }) + return envMap +} + +func parsedEnvHasAny() bool { + return len(parsedEnv()) > 0 +} + +func parse(raw string) map[string]string { + out := map[string]string{} + if raw == "" { + return out + } + for _, pair := range strings.Split(raw, ",") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + k, v, ok := strings.Cut(pair, "=") + if !ok { + continue + } + out[k] = v + } + return out +} + +func apply(s *Setting, env map[string]string) { + if v, ok := env[s.info.Name]; ok { + s.value.Store(&v) + s.set.Store(true) + } else { + s.value.Store(nil) + s.set.Store(false) + } +} + +// Update reparses the given SQLCDEBUG-formatted string and refreshes +// every registered setting. Intended for tests; production code should +// rely on the value parsed at startup. +func Update(raw string) { + registryMu.Lock() + defer registryMu.Unlock() + envMap = parse(raw) + envOnce.Do(func() {}) // mark as parsed so future New() calls see envMap. + for _, s := range registry { + apply(s, envMap) + } +} diff --git a/internal/sqlcdebug/sqlcdebug_test.go b/internal/sqlcdebug/sqlcdebug_test.go new file mode 100644 index 0000000000..6fbd118e34 --- /dev/null +++ b/internal/sqlcdebug/sqlcdebug_test.go @@ -0,0 +1,107 @@ +package sqlcdebug + +import ( + "sync" + "testing" +) + +func resetForTest(t *testing.T) { + t.Helper() + registryMu.Lock() + registry = map[string]*Setting{} + envMap = nil + envOnce = sync.Once{} + registryMu.Unlock() +} + +func TestParse(t *testing.T) { + tests := []struct { + input string + want map[string]string + }{ + {"", map[string]string{}}, + {"dumpast=1", map[string]string{"dumpast": "1"}}, + {"dumpast=1,trace=trace.out", map[string]string{"dumpast": "1", "trace": "trace.out"}}, + {" dumpast=1 , processplugins=0 ", map[string]string{"dumpast": "1", "processplugins": "0"}}, + {"trace=", map[string]string{"trace": ""}}, + {"bare", map[string]string{}}, + } + for _, tt := range tests { + got := parse(tt.input) + if len(got) != len(tt.want) { + t.Errorf("parse(%q): got %v, want %v", tt.input, got, tt.want) + continue + } + for k, v := range tt.want { + if got[k] != v { + t.Errorf("parse(%q)[%q] = %q, want %q", tt.input, k, got[k], v) + } + } + } +} + +func TestSettingValue(t *testing.T) { + resetForTest(t) + Update("dumpast=1,trace=foo.out") + + if v := New("dumpast").Value(); v != "1" { + t.Errorf("dumpast = %q, want %q", v, "1") + } + if v := New("trace").Value(); v != "foo.out" { + t.Errorf("trace = %q, want %q", v, "foo.out") + } + if !New("dumpast").IsSet() { + t.Errorf("IsSet(dumpast) = false, want true") + } + + // Unset key returns its registered default. + if v := New("processplugins").Value(); v != "1" { + t.Errorf("processplugins default = %q, want %q", v, "1") + } + if New("processplugins").IsSet() { + t.Errorf("IsSet(processplugins) = true, want false") + } +} + +func TestUpdateRefreshesExistingSettings(t *testing.T) { + resetForTest(t) + dumpAST := New("dumpast") + + if v := dumpAST.Value(); v != "" { + t.Errorf("initial dumpast = %q, want empty", v) + } + + Update("dumpast=1") + if v := dumpAST.Value(); v != "1" { + t.Errorf("after Update dumpast = %q, want %q", v, "1") + } + + Update("") + if v := dumpAST.Value(); v != "" { + t.Errorf("after reset dumpast = %q, want empty", v) + } + if dumpAST.IsSet() { + t.Errorf("after reset IsSet = true, want false") + } +} + +func TestAny(t *testing.T) { + resetForTest(t) + if Any() { + t.Errorf("Any() = true, want false on empty env") + } + Update("trace=1") + if !Any() { + t.Errorf("Any() = false, want true after Update") + } +} + +func TestNewPanicsOnUnknown(t *testing.T) { + resetForTest(t) + defer func() { + if r := recover(); r == nil { + t.Errorf("New(\"bogus\") did not panic") + } + }() + New("bogus") +} diff --git a/internal/tracer/trace.go b/internal/tracer/trace.go index 8252f49b38..ececc4b2e0 100644 --- a/internal/tracer/trace.go +++ b/internal/tracer/trace.go @@ -6,14 +6,26 @@ import ( "os" "runtime/trace" - "github.com/sqlc-dev/sqlc/internal/debug" + "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) -// Start starts Go's runtime tracing facility. -// Traces will be written to the file named by [debug.Debug.Trace]. -// It also starts a new [*trace.Task] that will be stopped when the cleanup is called. +var debugTrace = sqlcdebug.New("trace") + +// Path returns the file to which Go's runtime tracer should write its +// output, derived from SQLCDEBUG=trace=... +func Path() string { + v := debugTrace.Value() + if v == "1" { + return "trace.out" + } + return v +} + +// Start starts Go's runtime tracing facility. Traces are written to the +// path returned by [Path]. It also starts a new [*trace.Task] that will +// be stopped when the cleanup is called. func Start(base context.Context) (_ context.Context, cleanup func(), _ error) { - f, err := os.Create(debug.Debug.Trace) + f, err := os.Create(Path()) if err != nil { return base, cleanup, fmt.Errorf("failed to create trace output file: %v", err) }