Skip to content

endtoend: run the corpus through the analysis core with an opt-in context - #4548

Merged
kyleconroy merged 3 commits into
mainfrom
claude/core-analyzer-managed-db-tests-x6g48p
Aug 10, 2026
Merged

endtoend: run the corpus through the analysis core with an opt-in context#4548
kyleconroy merged 3 commits into
mainfrom
claude/core-analyzer-managed-db-tests-x6g48p

Conversation

@kyleconroy

Copy link
Copy Markdown
Collaborator

TestReplay runs the whole testdata corpus once per context: base as
committed, managed-db against a live database. This adds a third, core,
which generates every case through the analysis core, so the two paths can be
compared case by case rather than one case at a time.

The context

Contexts could only mutate config.Config, so the coreanalyzer experiment
could reach a case only through its own exec.json. textContext now also
names the experiments every case in it runs with:

type textContext struct {
	Mutate      func(*testing.T, string) func(*config.Config)
	Enabled     func() bool
	Experiments func() []string
}

A case's own SQLCEXPERIMENT is appended to the context's, so a case can still
turn one back off with the no prefix.

The two paths still disagree, so the context is opt-in, and needs no database:

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

The gate is an environment variable rather than a test flag because the
documented workflow runs the whole module, and a flag defined in one test
binary fails every package that does not define it:

$ go test -run TestNothingMatches -update=true ./internal/sql/preprocess ./internal/opts
flag provided but not defined: -update

It also matches how the rest of this suite is configured — POSTGRESQL_SERVER_URI,
MYSQL_SERVER_URI, SQLC_DUMMY_VALUE.

The panic it found

Two cases took the test binary down, which ended the run partway through and
took the rest of the corpus with it. A set-returning function in FROM takes
its arguments from the items before it, so bindRangeFunction types the call
while the scope is still being assembled — but the scope under construction was
never installed on the analyzer, so a.scope was nil and resolving one of those
arguments dereferenced it:

FROM transactions,
     jsonb_each(jsonb_extract_path(transactions.data, '...')) AS instructions

buildScope and relationScope now install the scope they are filling for as
long as they fill it, and put back the one they replaced on the way out.
Resolving against a nil scope also reports the column as unresolved rather than
crashing, so no other half-built statement can panic on the way to the error it
was going to report anyway.

Both queries now analyze to an ordinary error, and the core context runs to
completion in a single process.

Worth a look on review: installing the partial scope also means a FROM item
can now resolve columns from the items before it, which is what
bindRangeFunction's comment always claimed it did. That is a small
permissiveness increase for subqueries in FROM — a non-LATERAL subquery can
now see an earlier FROM item. Nothing in the suite depends on the stricter
behavior, but it is a semantic change rather than a purely defensive one.

Where the paths differ today

838 cases run under core, 311 pass, 527 fail. Ranked:

Count Divergence
224 * not expanded in the emitted SQL
121 other generated-code differences
99 hard errors, sqlc generate exits non-zero
22 nullability (sql.Null* / pgtype wrappers)
15 column or param typed interface{}
10 enum scaffolding missing from models.go
7 stderr differs — both paths error, different message
6 integer signedness (MySQL uint vs int)
4 array/slice-ness

Star expansion is one behavior and 43% of the failures. Of the hard errors, 47
are illegal character U+003F '?' — parameters get dropped, so the emitted Go
does not compile — then catalog sql: no rows in result set on CTEs and
schema-qualified relations, unsupported AST nodes (*ast.NamedArgExpr,
*ast.MultiAssignRef), and unknown column on unnest.

No goldens were regenerated and no divergence was papered over. The failures are
the report.

Testing

  • go test --tags=examples -timeout 20m ./... passes with PostgreSQL and MySQL
    live — 31 packages, no failures. Covers base, managed-db, and the
    live-database example tests.
  • SQLC_TEST_CORE=1 go test ./internal/endtoend -run 'TestReplay/core' completes
    in one process with zero panics.
  • Nothing changes for anyone who does not set SQLC_TEST_CORE.

🤖 Generated with Claude Code

https://claude.ai/code/session_0161e7oMkzNW9DZMUPtyQibH


Generated by Claude Code

claude added 3 commits August 9, 2026 21:46
TestReplay's contexts could only mutate the config, so the coreanalyzer
experiment could reach a case only through its own exec.json. Contexts now
also name the experiments every case in them runs with, and a new "core"
context runs the corpus through the analysis core.

The two paths still disagree, so the context is opt-in: it runs only when
SQLC_TEST_CORE is set. That gate is an environment variable rather than a
test flag because the documented workflow runs the whole module, and a flag
defined in one test binary fails every package that does not define it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161e7oMkzNW9DZMUPtyQibH
A set-returning function in FROM takes its arguments from the items before
it, so bindRangeFunction types the call while the scope is still being
assembled. The scope under construction was never installed on the analyzer,
so a.scope was nil and resolving one of those arguments dereferenced it:

    FROM transactions,
         jsonb_each(jsonb_extract_path(transactions.data, '...')) AS x

buildScope and relationScope now install the scope they are filling for as
long as they fill it, and put back the one they replaced on the way out.
Resolving against a nil scope also reports the column as unresolved rather
than crashing, so no other half-built statement can panic on the way to the
error it was going to report anyway.

Both queries now analyze to an ordinary error instead of taking down the
process, which is what the rest of the corpus needed: TestReplay's core
context runs to completion in one process rather than aborting partway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161e7oMkzNW9DZMUPtyQibH
@kyleconroy
kyleconroy merged commit 8e57763 into main Aug 10, 2026
13 checks passed
@kyleconroy
kyleconroy deleted the claude/core-analyzer-managed-db-tests-x6g48p branch August 10, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants