Skip to content

Commit 74ecda5

Browse files
authored
feat: add SQLCEXPERIMENT environment variable for experimental features (#4228)
1 parent f6aee32 commit 74ecda5

File tree

5 files changed

+324
-9
lines changed

5 files changed

+324
-9
lines changed

docs/reference/environment-variables.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Environment variables
22

3+
## SQLCEXPERIMENT
4+
5+
The `SQLCEXPERIMENT` variable controls experimental features within sqlc. It is
6+
a comma-separated list of experiment names. This is modeled after Go's
7+
[GOEXPERIMENT](https://pkg.go.dev/internal/goexperiment) environment variable.
8+
9+
Experiment names can be prefixed with `no` to explicitly disable them.
10+
11+
```
12+
SQLCEXPERIMENT=foo,bar # enable foo and bar experiments
13+
SQLCEXPERIMENT=nofoo # explicitly disable foo experiment
14+
SQLCEXPERIMENT=foo,nobar # enable foo, disable bar
15+
```
16+
17+
Currently, no experiments are defined. Experiments will be documented here as
18+
they are introduced.
19+
320
## SQLCCACHE
421

522
The `SQLCCACHE` environment variable dictates where `sqlc` will store cached

internal/cmd/cmd.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,23 @@ var initCmd = &cobra.Command{
136136
}
137137

138138
type Env struct {
139-
DryRun bool
140-
Debug opts.Debug
141-
Remote bool
142-
NoRemote bool
139+
DryRun bool
140+
Debug opts.Debug
141+
Experiment opts.Experiment
142+
Remote bool
143+
NoRemote bool
143144
}
144145

145146
func ParseEnv(c *cobra.Command) Env {
146147
dr := c.Flag("dry-run")
147148
r := c.Flag("remote")
148149
nr := c.Flag("no-remote")
149150
return Env{
150-
DryRun: dr != nil && dr.Changed,
151-
Debug: opts.DebugFromEnv(),
152-
Remote: r != nil && r.Value.String() == "true",
153-
NoRemote: nr != nil && nr.Value.String() == "true",
151+
DryRun: dr != nil && dr.Changed,
152+
Debug: opts.DebugFromEnv(),
153+
Experiment: opts.ExperimentFromEnv(),
154+
Remote: r != nil && r.Value.String() == "true",
155+
NoRemote: nr != nil && nr.Value.String() == "true",
154156
}
155157
}
156158

internal/opts/experiment.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package opts
2+
3+
import (
4+
"os"
5+
"strings"
6+
)
7+
8+
// The SQLCEXPERIMENT variable controls experimental features within sqlc. It
9+
// is a comma-separated list of experiment names. Experiment names can be
10+
// prefixed with "no" to explicitly disable them.
11+
//
12+
// This is modeled after Go's GOEXPERIMENT environment variable. For more
13+
// information, see https://pkg.go.dev/internal/goexperiment
14+
//
15+
// Available experiments:
16+
//
17+
// (none currently defined - add experiments here as they are introduced)
18+
//
19+
// Example usage:
20+
//
21+
// SQLCEXPERIMENT=foo,bar # enable foo and bar experiments
22+
// SQLCEXPERIMENT=nofoo # explicitly disable foo experiment
23+
// SQLCEXPERIMENT=foo,nobar # enable foo, disable bar
24+
25+
// Experiment holds the state of all experimental features.
26+
// Add new experiments as boolean fields to this struct.
27+
type Experiment struct {
28+
// Add experimental feature flags here as they are introduced.
29+
// Example:
30+
// NewParser bool // Enable new SQL parser
31+
}
32+
33+
// ExperimentFromEnv returns an Experiment initialized from the SQLCEXPERIMENT
34+
// environment variable.
35+
func ExperimentFromEnv() Experiment {
36+
return ExperimentFromString(os.Getenv("SQLCEXPERIMENT"))
37+
}
38+
39+
// ExperimentFromString parses a comma-separated list of experiment names
40+
// and returns an Experiment with the appropriate flags set.
41+
//
42+
// Experiment names can be prefixed with "no" to explicitly disable them.
43+
// Unknown experiment names are silently ignored.
44+
func ExperimentFromString(val string) Experiment {
45+
e := Experiment{}
46+
if val == "" {
47+
return e
48+
}
49+
50+
for _, name := range strings.Split(val, ",") {
51+
name = strings.TrimSpace(name)
52+
if name == "" {
53+
continue
54+
}
55+
56+
// Check if this is a negation (noFoo)
57+
enabled := true
58+
if strings.HasPrefix(strings.ToLower(name), "no") && len(name) > 2 {
59+
// Could be a negation, check if the rest is a valid experiment
60+
possibleExp := name[2:]
61+
if isKnownExperiment(possibleExp) {
62+
name = possibleExp
63+
enabled = false
64+
}
65+
// If not a known experiment, treat "no..." as a potential experiment name itself
66+
}
67+
68+
setExperiment(&e, name, enabled)
69+
}
70+
71+
return e
72+
}
73+
74+
// isKnownExperiment returns true if the given name (case-insensitive) is a
75+
// known experiment.
76+
func isKnownExperiment(name string) bool {
77+
switch strings.ToLower(name) {
78+
// Add experiment names here as they are introduced.
79+
// Example:
80+
// case "newparser":
81+
// return true
82+
default:
83+
return false
84+
}
85+
}
86+
87+
// setExperiment sets the experiment flag with the given name to the given value.
88+
func setExperiment(e *Experiment, name string, enabled bool) {
89+
switch strings.ToLower(name) {
90+
// Add experiment cases here as they are introduced.
91+
// Example:
92+
// case "newparser":
93+
// e.NewParser = enabled
94+
}
95+
}
96+
97+
// Enabled returns a slice of all enabled experiment names.
98+
func (e Experiment) Enabled() []string {
99+
var enabled []string
100+
// Add enabled experiments here as they are introduced.
101+
// Example:
102+
// if e.NewParser {
103+
// enabled = append(enabled, "newparser")
104+
// }
105+
return enabled
106+
}
107+
108+
// String returns a comma-separated list of enabled experiments.
109+
func (e Experiment) String() string {
110+
return strings.Join(e.Enabled(), ",")
111+
}

internal/opts/experiment_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package opts
2+
3+
import "testing"
4+
5+
func TestExperimentFromString(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
input string
9+
want Experiment
10+
}{
11+
{
12+
name: "empty string",
13+
input: "",
14+
want: Experiment{},
15+
},
16+
{
17+
name: "whitespace only",
18+
input: " ",
19+
want: Experiment{},
20+
},
21+
{
22+
name: "unknown experiment",
23+
input: "unknownexperiment",
24+
want: Experiment{},
25+
},
26+
{
27+
name: "multiple unknown experiments",
28+
input: "foo,bar,baz",
29+
want: Experiment{},
30+
},
31+
{
32+
name: "unknown with no prefix",
33+
input: "nounknown",
34+
want: Experiment{},
35+
},
36+
{
37+
name: "whitespace around experiments",
38+
input: " foo , bar , baz ",
39+
want: Experiment{},
40+
},
41+
{
42+
name: "empty items in list",
43+
input: "foo,,bar",
44+
want: Experiment{},
45+
},
46+
// Add tests for specific experiments as they are introduced.
47+
// Example:
48+
// {
49+
// name: "enable newparser",
50+
// input: "newparser",
51+
// want: Experiment{NewParser: true},
52+
// },
53+
// {
54+
// name: "disable newparser",
55+
// input: "nonewparser",
56+
// want: Experiment{NewParser: false},
57+
// },
58+
// {
59+
// name: "enable then disable",
60+
// input: "newparser,nonewparser",
61+
// want: Experiment{NewParser: false},
62+
// },
63+
// {
64+
// name: "case insensitive",
65+
// input: "NewParser,NONEWPARSER",
66+
// want: Experiment{NewParser: false},
67+
// },
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
got := ExperimentFromString(tt.input)
73+
if got != tt.want {
74+
t.Errorf("ExperimentFromString(%q) = %+v, want %+v", tt.input, got, tt.want)
75+
}
76+
})
77+
}
78+
}
79+
80+
func TestExperimentEnabled(t *testing.T) {
81+
tests := []struct {
82+
name string
83+
exp Experiment
84+
want []string
85+
}{
86+
{
87+
name: "no experiments enabled",
88+
exp: Experiment{},
89+
want: nil,
90+
},
91+
// Add tests for specific experiments as they are introduced.
92+
// Example:
93+
// {
94+
// name: "newparser enabled",
95+
// exp: Experiment{NewParser: true},
96+
// want: []string{"newparser"},
97+
// },
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
got := tt.exp.Enabled()
103+
if len(got) != len(tt.want) {
104+
t.Errorf("Experiment.Enabled() = %v, want %v", got, tt.want)
105+
return
106+
}
107+
for i := range got {
108+
if got[i] != tt.want[i] {
109+
t.Errorf("Experiment.Enabled()[%d] = %q, want %q", i, got[i], tt.want[i])
110+
}
111+
}
112+
})
113+
}
114+
}
115+
116+
func TestExperimentString(t *testing.T) {
117+
tests := []struct {
118+
name string
119+
exp Experiment
120+
want string
121+
}{
122+
{
123+
name: "no experiments",
124+
exp: Experiment{},
125+
want: "",
126+
},
127+
// Add tests for specific experiments as they are introduced.
128+
// Example:
129+
// {
130+
// name: "newparser enabled",
131+
// exp: Experiment{NewParser: true},
132+
// want: "newparser",
133+
// },
134+
}
135+
136+
for _, tt := range tests {
137+
t.Run(tt.name, func(t *testing.T) {
138+
got := tt.exp.String()
139+
if got != tt.want {
140+
t.Errorf("Experiment.String() = %q, want %q", got, tt.want)
141+
}
142+
})
143+
}
144+
}
145+
146+
func TestIsKnownExperiment(t *testing.T) {
147+
tests := []struct {
148+
name string
149+
input string
150+
want bool
151+
}{
152+
{
153+
name: "unknown experiment",
154+
input: "unknown",
155+
want: false,
156+
},
157+
{
158+
name: "empty string",
159+
input: "",
160+
want: false,
161+
},
162+
// Add tests for specific experiments as they are introduced.
163+
// Example:
164+
// {
165+
// name: "newparser lowercase",
166+
// input: "newparser",
167+
// want: true,
168+
// },
169+
// {
170+
// name: "newparser mixed case",
171+
// input: "NewParser",
172+
// want: true,
173+
// },
174+
}
175+
176+
for _, tt := range tests {
177+
t.Run(tt.name, func(t *testing.T) {
178+
got := isKnownExperiment(tt.input)
179+
if got != tt.want {
180+
t.Errorf("isKnownExperiment(%q) = %v, want %v", tt.input, got, tt.want)
181+
}
182+
})
183+
}
184+
}

internal/opts/parser.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package opts
22

33
type Parser struct {
4-
Debug Debug
4+
Debug Debug
5+
Experiment Experiment
56
}

0 commit comments

Comments
 (0)