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
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,25 @@ stdout against `stdout.txt`. A case that is expected to fail commits its
`stderr.txt`. Regenerate a golden by running the command in its directory and
writing the output back over the committed file.

`TestReplay` runs the whole corpus once per *context*. `base` runs each case as
committed and `managed-db` reruns it against a live database, so a context can
change the config a case is generated with and the experiments it is generated
under. A case restricts itself to some of them with `"contexts": [...]` in its
`exec.json`, and commits per-context expected errors as `stderr/<context>.txt`.
There is only one set of committed golden files, so every context is expected
to generate identical code.

The `core` context generates every case through the analysis core
(`SQLCEXPERIMENT=coreanalyzer`). The two paths still disagree, so it is opt-in
and needs no database:

```bash
SQLC_TEST_CORE=1 go test ./internal/endtoend -run 'TestReplay/core'
```

Go aborts a test binary on panic, so a case that panics the core analyzer ends
the run early. Run a subset to get past one (`-run 'TestReplay/core/^select'`).

### Example Tests

- **Location:** `/examples/` directory
Expand Down
1 change: 1 addition & 0 deletions internal/core/analyzer/dml.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error {
// report a multi-table DELETE that way — a single FROM node.
func (a *analyzer) relationScope(relations, extra *ast.List, from ast.Node) (*scope, error) {
sc := &scope{}
defer a.binding(sc)()
for _, item := range listItems(relations) {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
Expand Down
18 changes: 18 additions & 0 deletions internal/core/analyzer/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type scopeRel struct {
func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
items := listItems(from)
sc := &scope{rels: make([]scopeRel, 0, len(items))}
defer a.binding(sc)()
for _, item := range items {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
Expand All @@ -30,6 +31,17 @@ func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
return sc, nil
}

// binding makes the scope under construction the one the analyzer resolves
// against, and returns the func that puts back the scope it replaced. A FROM
// item can refer to the ones before it — a set-returning function takes its
// arguments from them — so binding an item has to see what is bound so far
// rather than no scope at all.
func (a *analyzer) binding(sc *scope) func() {
prev := a.scope
a.scope = sc
return func() { a.scope = prev }
}

func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error {
switch v := item.(type) {
case *ast.RangeVar:
Expand Down Expand Up @@ -202,6 +214,12 @@ func (a *analyzer) resolveColumn(relation, column string) (scopeRel, core.ClassC
// relation. It reports an error when more than one relation in scope offers
// that name.
func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col core.ClassColumn, ok bool, err error) {
// A statement whose scope is not built yet offers no columns. Report the
// column as unresolved and let the caller say so, rather than crashing on
// the way to the same answer.
if s == nil {
return rel, col, false, nil
}
found := 0
for _, r := range s.rels {
if relation != "" && r.alias != relation {
Expand Down
21 changes: 20 additions & 1 deletion internal/endtoend/endtoend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ func BenchmarkExamples(b *testing.B) {
type textContext struct {
Mutate func(*testing.T, string) func(*config.Config)
Enabled func() bool
// Experiments names the experiments every case in this context runs
// with. A case's own SQLCEXPERIMENT is appended to these, so a case can
// still turn one back off with the "no" prefix.
Experiments func() []string
}

func TestReplay(t *testing.T) {
Expand Down Expand Up @@ -235,6 +239,16 @@ func TestReplay(t *testing.T) {
return postgresURI != "" || mysqlURI != ""
},
},
"core": {
Mutate: func(t *testing.T, path string) func(*config.Config) { return func(c *config.Config) {} },
Experiments: func() []string { return []string{"coreanalyzer"} },
Enabled: func() bool {
// Running the whole corpus through the analysis core is opt-in
// while the two paths still disagree. The core needs no
// database, so nothing else gates this.
return os.Getenv("SQLC_TEST_CORE") != ""
},
},
}

for name, testctx := range contexts {
Expand Down Expand Up @@ -276,9 +290,14 @@ func TestReplay(t *testing.T) {
}
}

experiments := args.Env["SQLCEXPERIMENT"]
if testctx.Experiments != nil {
experiments = strings.Join(append(testctx.Experiments(), experiments), ",")
}

opts := cmd.Options{
Env: cmd.Env{
Experiment: opts.ExperimentFromString(args.Env["SQLCEXPERIMENT"]),
Experiment: opts.ExperimentFromString(experiments),
},
Stderr: &stderr,
MutateConfig: testctx.Mutate(t, path),
Expand Down
Loading