From dfe83695b209efb45a4766aa9902dc35fa575496 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Dec 2025 19:15:36 +0000 Subject: [PATCH] feat: add SQLCEXPERIMENT environment variable for experimental features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a SQLCEXPERIMENT environment variable modeled after Go's GOEXPERIMENT that allows enabling/disabling experimental features via a comma-separated list of experiment names. Experiments can be prefixed with "no" to explicitly disable them. This provides infrastructure for gating experimental features before they are ready for general availability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/reference/environment-variables.md | 17 +++ internal/cmd/cmd.go | 18 +-- internal/opts/experiment.go | 111 ++++++++++++++ internal/opts/experiment_test.go | 184 ++++++++++++++++++++++++ internal/opts/parser.go | 3 +- 5 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 internal/opts/experiment.go create mode 100644 internal/opts/experiment_test.go diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 185807168c..837dd13980 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -1,5 +1,22 @@ # Environment variables +## SQLCEXPERIMENT + +The `SQLCEXPERIMENT` variable controls experimental features within sqlc. It is +a comma-separated list of experiment names. This is modeled after Go's +[GOEXPERIMENT](https://pkg.go.dev/internal/goexperiment) environment variable. + +Experiment names can be prefixed with `no` to explicitly disable them. + +``` +SQLCEXPERIMENT=foo,bar # enable foo and bar experiments +SQLCEXPERIMENT=nofoo # explicitly disable foo experiment +SQLCEXPERIMENT=foo,nobar # enable foo, disable bar +``` + +Currently, no experiments are defined. Experiments will be documented here as +they are introduced. + ## SQLCCACHE The `SQLCCACHE` environment variable dictates where `sqlc` will store cached diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 93fd6bbeaa..bdaca4180a 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -136,10 +136,11 @@ var initCmd = &cobra.Command{ } type Env struct { - DryRun bool - Debug opts.Debug - Remote bool - NoRemote bool + DryRun bool + Debug opts.Debug + Experiment opts.Experiment + Remote bool + NoRemote bool } func ParseEnv(c *cobra.Command) Env { @@ -147,10 +148,11 @@ func ParseEnv(c *cobra.Command) Env { r := c.Flag("remote") nr := c.Flag("no-remote") return Env{ - DryRun: dr != nil && dr.Changed, - Debug: opts.DebugFromEnv(), - Remote: r != nil && r.Value.String() == "true", - NoRemote: nr != nil && nr.Value.String() == "true", + DryRun: dr != nil && dr.Changed, + Debug: opts.DebugFromEnv(), + Experiment: opts.ExperimentFromEnv(), + Remote: r != nil && r.Value.String() == "true", + NoRemote: nr != nil && nr.Value.String() == "true", } } diff --git a/internal/opts/experiment.go b/internal/opts/experiment.go new file mode 100644 index 0000000000..73ca5d7de0 --- /dev/null +++ b/internal/opts/experiment.go @@ -0,0 +1,111 @@ +package opts + +import ( + "os" + "strings" +) + +// The SQLCEXPERIMENT variable controls experimental features within sqlc. It +// is a comma-separated list of experiment names. Experiment names can be +// prefixed with "no" to explicitly disable them. +// +// This is modeled after Go's GOEXPERIMENT environment variable. For more +// information, see https://pkg.go.dev/internal/goexperiment +// +// Available experiments: +// +// (none currently defined - add experiments here as they are introduced) +// +// Example usage: +// +// SQLCEXPERIMENT=foo,bar # enable foo and bar experiments +// SQLCEXPERIMENT=nofoo # explicitly disable foo experiment +// SQLCEXPERIMENT=foo,nobar # enable foo, disable bar + +// Experiment holds the state of all experimental features. +// Add new experiments as boolean fields to this struct. +type Experiment struct { + // Add experimental feature flags here as they are introduced. + // Example: + // NewParser bool // Enable new SQL parser +} + +// ExperimentFromEnv returns an Experiment initialized from the SQLCEXPERIMENT +// environment variable. +func ExperimentFromEnv() Experiment { + return ExperimentFromString(os.Getenv("SQLCEXPERIMENT")) +} + +// ExperimentFromString parses a comma-separated list of experiment names +// and returns an Experiment with the appropriate flags set. +// +// Experiment names can be prefixed with "no" to explicitly disable them. +// Unknown experiment names are silently ignored. +func ExperimentFromString(val string) Experiment { + e := Experiment{} + if val == "" { + return e + } + + for _, name := range strings.Split(val, ",") { + name = strings.TrimSpace(name) + if name == "" { + continue + } + + // Check if this is a negation (noFoo) + enabled := true + if strings.HasPrefix(strings.ToLower(name), "no") && len(name) > 2 { + // Could be a negation, check if the rest is a valid experiment + possibleExp := name[2:] + if isKnownExperiment(possibleExp) { + name = possibleExp + enabled = false + } + // If not a known experiment, treat "no..." as a potential experiment name itself + } + + setExperiment(&e, name, enabled) + } + + return e +} + +// isKnownExperiment returns true if the given name (case-insensitive) is a +// known experiment. +func isKnownExperiment(name string) bool { + switch strings.ToLower(name) { + // Add experiment names here as they are introduced. + // Example: + // case "newparser": + // return true + default: + return false + } +} + +// setExperiment sets the experiment flag with the given name to the given value. +func setExperiment(e *Experiment, name string, enabled bool) { + switch strings.ToLower(name) { + // Add experiment cases here as they are introduced. + // Example: + // case "newparser": + // e.NewParser = enabled + } +} + +// Enabled returns a slice of all enabled experiment names. +func (e Experiment) Enabled() []string { + var enabled []string + // Add enabled experiments here as they are introduced. + // Example: + // if e.NewParser { + // enabled = append(enabled, "newparser") + // } + return enabled +} + +// String returns a comma-separated list of enabled experiments. +func (e Experiment) String() string { + return strings.Join(e.Enabled(), ",") +} diff --git a/internal/opts/experiment_test.go b/internal/opts/experiment_test.go new file mode 100644 index 0000000000..7845c0b13e --- /dev/null +++ b/internal/opts/experiment_test.go @@ -0,0 +1,184 @@ +package opts + +import "testing" + +func TestExperimentFromString(t *testing.T) { + tests := []struct { + name string + input string + want Experiment + }{ + { + name: "empty string", + input: "", + want: Experiment{}, + }, + { + name: "whitespace only", + input: " ", + want: Experiment{}, + }, + { + name: "unknown experiment", + input: "unknownexperiment", + want: Experiment{}, + }, + { + name: "multiple unknown experiments", + input: "foo,bar,baz", + want: Experiment{}, + }, + { + name: "unknown with no prefix", + input: "nounknown", + want: Experiment{}, + }, + { + name: "whitespace around experiments", + input: " foo , bar , baz ", + want: Experiment{}, + }, + { + name: "empty items in list", + input: "foo,,bar", + want: Experiment{}, + }, + // Add tests for specific experiments as they are introduced. + // Example: + // { + // name: "enable newparser", + // input: "newparser", + // want: Experiment{NewParser: true}, + // }, + // { + // name: "disable newparser", + // input: "nonewparser", + // want: Experiment{NewParser: false}, + // }, + // { + // name: "enable then disable", + // input: "newparser,nonewparser", + // want: Experiment{NewParser: false}, + // }, + // { + // name: "case insensitive", + // input: "NewParser,NONEWPARSER", + // want: Experiment{NewParser: false}, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ExperimentFromString(tt.input) + if got != tt.want { + t.Errorf("ExperimentFromString(%q) = %+v, want %+v", tt.input, got, tt.want) + } + }) + } +} + +func TestExperimentEnabled(t *testing.T) { + tests := []struct { + name string + exp Experiment + want []string + }{ + { + name: "no experiments enabled", + exp: Experiment{}, + want: nil, + }, + // Add tests for specific experiments as they are introduced. + // Example: + // { + // name: "newparser enabled", + // exp: Experiment{NewParser: true}, + // want: []string{"newparser"}, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.exp.Enabled() + if len(got) != len(tt.want) { + t.Errorf("Experiment.Enabled() = %v, want %v", got, tt.want) + return + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("Experiment.Enabled()[%d] = %q, want %q", i, got[i], tt.want[i]) + } + } + }) + } +} + +func TestExperimentString(t *testing.T) { + tests := []struct { + name string + exp Experiment + want string + }{ + { + name: "no experiments", + exp: Experiment{}, + want: "", + }, + // Add tests for specific experiments as they are introduced. + // Example: + // { + // name: "newparser enabled", + // exp: Experiment{NewParser: true}, + // want: "newparser", + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.exp.String() + if got != tt.want { + t.Errorf("Experiment.String() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestIsKnownExperiment(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + { + name: "unknown experiment", + input: "unknown", + want: false, + }, + { + name: "empty string", + input: "", + want: false, + }, + // Add tests for specific experiments as they are introduced. + // Example: + // { + // name: "newparser lowercase", + // input: "newparser", + // want: true, + // }, + // { + // name: "newparser mixed case", + // input: "NewParser", + // want: true, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isKnownExperiment(tt.input) + if got != tt.want { + t.Errorf("isKnownExperiment(%q) = %v, want %v", tt.input, got, tt.want) + } + }) + } +} diff --git a/internal/opts/parser.go b/internal/opts/parser.go index d6fb399552..2059d4f6a1 100644 --- a/internal/opts/parser.go +++ b/internal/opts/parser.go @@ -1,5 +1,6 @@ package opts type Parser struct { - Debug Debug + Debug Debug + Experiment Experiment }