Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: rename accurate mode to analyzer.database: only with analyzerv2…
… experiment

This change refactors the "accurate analyzer mode" feature:

1. Rename config option from `analyzer.accurate: true` to
   `analyzer.database: only` - a third option in addition to true/false

2. Gate the feature behind the `analyzerv2` experiment flag. The feature
   is only enabled when:
   - `analyzer.database: only` is set in the config
   - `SQLCEXPERIMENT=analyzerv2` environment variable is set

3. Update JSON schemas to support boolean or "only" for analyzer.database

4. Add experiment tests for analyzerv2 flag

5. Update end-to-end test configs and expected outputs

The database-only mode skips building the internal catalog from schema
files and instead relies entirely on the database for type resolution
and star expansion.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
claude committed Dec 18, 2025
commit 850e0498e58fc5edc25cecc224ce61b3a69dcc8f
2 changes: 1 addition & 1 deletion internal/cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func remoteGenerate(ctx context.Context, configPath string, conf *config.Config,

func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, stderr io.Writer) (*compiler.Result, bool) {
defer trace.StartRegion(ctx, "parse").End()
c, err := compiler.NewCompiler(sql, combo)
c, err := compiler.NewCompiler(sql, combo, parserOpts)
defer func() {
if c != nil {
c.Close(ctx)
Expand Down
12 changes: 6 additions & 6 deletions internal/compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ func (c *Compiler) parseCatalog(schemas []string) error {
contents := migrations.RemoveRollbackStatements(string(blob))
c.schema = append(c.schema, contents)

// In accurate mode, we only need to collect schema files for migrations
// In database-only mode, we only need to collect schema files for migrations
// but don't build the internal catalog from them
if c.accurateMode {
if c.databaseOnlyMode {
continue
}

Expand All @@ -68,8 +68,8 @@ func (c *Compiler) parseCatalog(schemas []string) error {
func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
ctx := context.Background()

// In accurate mode, initialize the database connection pool before parsing queries
if c.accurateMode && c.pgAnalyzer != nil {
// In database-only mode, initialize the database connection pool before parsing queries
if c.databaseOnlyMode && c.pgAnalyzer != nil {
if err := c.pgAnalyzer.EnsurePool(ctx, c.schema); err != nil {
return nil, fmt.Errorf("failed to initialize database connection: %w", err)
}
Expand Down Expand Up @@ -131,8 +131,8 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
}

// In accurate mode, build the catalog from the database after parsing all queries
if c.accurateMode && c.pgAnalyzer != nil {
// In database-only mode, build the catalog from the database after parsing all queries
if c.databaseOnlyMode && c.pgAnalyzer != nil {
// Default to "public" schema if no specific schemas are specified
schemas := []string{"public"}
cat, err := c.pgAnalyzer.IntrospectSchema(ctx, schemas)
Expand Down
31 changes: 16 additions & 15 deletions internal/compiler/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,35 @@ type Compiler struct {

schema []string

// accurateMode indicates that the compiler should use database-only analysis
// and skip building the internal catalog from schema files
accurateMode bool
// pgAnalyzer is the PostgreSQL-specific analyzer used in accurate mode
// databaseOnlyMode indicates that the compiler should use database-only analysis
// and skip building the internal catalog from schema files (analyzer.database: only)
databaseOnlyMode bool
// pgAnalyzer is the PostgreSQL-specific analyzer used in database-only mode
// for schema introspection
pgAnalyzer *pganalyze.Analyzer
// expander is used to expand SELECT * and RETURNING * in accurate mode
// expander is used to expand SELECT * and RETURNING * in database-only mode
expander *expander.Expander
}

func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, error) {
func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser) (*Compiler, error) {
c := &Compiler{conf: conf, combo: combo}

if conf.Database != nil && conf.Database.Managed {
client := dbmanager.NewClient(combo.Global.Servers)
c.client = client
}

// Check for accurate mode
accurateMode := conf.Analyzer.Accurate != nil && *conf.Analyzer.Accurate
// Check for database-only mode (analyzer.database: only)
// This feature requires the analyzerv2 experiment to be enabled
databaseOnlyMode := conf.Analyzer.Database.IsOnly() && parserOpts.Experiment.AnalyzerV2

switch conf.Engine {
case config.EngineSQLite:
c.parser = sqlite.NewParser()
c.catalog = sqlite.NewCatalog()
c.selector = newSQLiteSelector()
if conf.Database != nil {
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
if conf.Analyzer.Database.IsEnabled() {
c.analyzer = analyzer.Cached(
sqliteanalyze.New(*conf.Database),
combo.Global,
Expand All @@ -74,15 +75,15 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
c.catalog = postgresql.NewCatalog()
c.selector = newDefaultSelector()

if accurateMode {
// Accurate mode requires a database connection
if databaseOnlyMode {
// Database-only mode requires a database connection
if conf.Database == nil {
return nil, fmt.Errorf("accurate mode requires database configuration")
return nil, fmt.Errorf("analyzer.database: only requires database configuration")
}
if conf.Database.URI == "" && !conf.Database.Managed {
return nil, fmt.Errorf("accurate mode requires database.uri or database.managed")
return nil, fmt.Errorf("analyzer.database: only requires database.uri or database.managed")
}
c.accurateMode = true
c.databaseOnlyMode = true
// Create the PostgreSQL analyzer for schema introspection
c.pgAnalyzer = pganalyze.New(c.client, *conf.Database)
// Use the analyzer wrapped with cache for query analysis
Expand All @@ -95,7 +96,7 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
// The parser implements both Parser and format.Dialect interfaces
c.expander = expander.New(c.pgAnalyzer, parser, parser)
} else if conf.Database != nil {
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
if conf.Analyzer.Database.IsEnabled() {
c.analyzer = analyzer.Cached(
pganalyze.New(c.client, *conf.Database),
combo.Global,
Expand Down
4 changes: 2 additions & 2 deletions internal/compiler/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
}

var anlys *analysis
if c.accurateMode && c.expander != nil {
// In accurate mode, use the expander for star expansion
if c.databaseOnlyMode && c.expander != nil {
// In database-only mode, use the expander for star expansion
// and rely entirely on the database analyzer for type resolution
expandedQuery, err := c.expander.Expand(ctx, rawSQL)
if err != nil {
Expand Down
70 changes: 68 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,75 @@ type SQL struct {
Analyzer Analyzer `json:"analyzer" yaml:"analyzer"`
}

// AnalyzerDatabase represents the database analyzer setting.
// It can be a boolean (true/false) or the string "only" for database-only mode.
type AnalyzerDatabase struct {
value *bool // nil means not set, true/false for boolean values
isOnly bool // true when set to "only"
}

// IsEnabled returns true if the database analyzer should be used.
// Returns true for both `true` and `"only"` settings.
func (a AnalyzerDatabase) IsEnabled() bool {
if a.isOnly {
return true
}
return a.value == nil || *a.value
}

// IsOnly returns true if the analyzer is set to "only" mode.
func (a AnalyzerDatabase) IsOnly() bool {
return a.isOnly
}

func (a *AnalyzerDatabase) UnmarshalJSON(data []byte) error {
// Try to unmarshal as boolean first
var b bool
if err := json.Unmarshal(data, &b); err == nil {
a.value = &b
a.isOnly = false
return nil
}

// Try to unmarshal as string
var s string
if err := json.Unmarshal(data, &s); err == nil {
if s == "only" {
a.isOnly = true
a.value = nil
return nil
}
return errors.New("analyzer.database must be true, false, or \"only\"")
}

return errors.New("analyzer.database must be true, false, or \"only\"")
}

func (a *AnalyzerDatabase) UnmarshalYAML(unmarshal func(interface{}) error) error {
// Try to unmarshal as boolean first
var b bool
if err := unmarshal(&b); err == nil {
a.value = &b
a.isOnly = false
return nil
}

// Try to unmarshal as string
var s string
if err := unmarshal(&s); err == nil {
if s == "only" {
a.isOnly = true
a.value = nil
return nil
}
return errors.New("analyzer.database must be true, false, or \"only\"")
}

return errors.New("analyzer.database must be true, false, or \"only\"")
}

type Analyzer struct {
Database *bool `json:"database" yaml:"database"`
Accurate *bool `json:"accurate" yaml:"accurate"`
Database AnalyzerDatabase `json:"database" yaml:"database"`
}

// TODO: Figure out a better name for this
Expand Down
8 changes: 4 additions & 4 deletions internal/config/v_one.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@
"type": "object",
"properties": {
"database": {
"type": "boolean"
},
"accurate": {
"type": "boolean"
"oneOf": [
{"type": "boolean"},
{"const": "only"}
]
}
}
},
Expand Down
8 changes: 4 additions & 4 deletions internal/config/v_two.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@
"type": "object",
"properties": {
"database": {
"type": "boolean"
},
"accurate": {
"type": "boolean"
"oneOf": [
{"type": "boolean"},
{"const": "only"}
]
}
}
},
Expand Down
5 changes: 3 additions & 2 deletions internal/endtoend/endtoend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,9 @@ func TestReplay(t *testing.T) {

opts := cmd.Options{
Env: cmd.Env{
Debug: opts.DebugFromString(args.Env["SQLCDEBUG"]),
NoRemote: true,
Debug: opts.DebugFromString(args.Env["SQLCDEBUG"]),
Experiment: opts.ExperimentFromString(args.Env["SQLCEXPERIMENT"]),
NoRemote: true,
},
Stderr: &stderr,
MutateConfig: testctx.Mutate(t, path),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"contexts": ["managed-db"]
"contexts": ["managed-db"],
"env": {
"SQLCEXPERIMENT": "analyzerv2"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ sql:
- engine: postgresql
schema: "schema.sql"
queries: "query.sql"
database:
managed: true
analyzer:
accurate: true
database: "only"
gen:
go:
package: "querytest"
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading