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
17 changes: 17 additions & 0 deletions docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
18 changes: 10 additions & 8 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,23 @@ 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 {
dr := c.Flag("dry-run")
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",
}
}

Expand Down
111 changes: 111 additions & 0 deletions internal/opts/experiment.go
Original file line number Diff line number Diff line change
@@ -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(), ",")
}
184 changes: 184 additions & 0 deletions internal/opts/experiment_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
3 changes: 2 additions & 1 deletion internal/opts/parser.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package opts

type Parser struct {
Debug Debug
Debug Debug
Experiment Experiment
}
Loading