Skip to content
Open
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
14 changes: 14 additions & 0 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,21 @@ func (c *CachedAnalyzer) Close(ctx context.Context) error {
return c.a.Close(ctx)
}

func (c *CachedAnalyzer) EnsureConn(ctx context.Context, migrations []string) error {
return c.a.EnsureConn(ctx, migrations)
}

func (c *CachedAnalyzer) GetColumnNames(ctx context.Context, query string) ([]string, error) {
return c.a.GetColumnNames(ctx, query)
}

type Analyzer interface {
Analyze(context.Context, ast.Node, string, []string, *named.ParamSet) (*analysis.Analysis, error)
Close(context.Context) error
// EnsureConn initializes the database connection with the given migrations.
// This is required for database-only mode where we need to connect before analyzing queries.
EnsureConn(ctx context.Context, migrations []string) error
// GetColumnNames returns the column names for a query by preparing it against the database.
// This is used for star expansion in database-only mode.
GetColumnNames(ctx context.Context, query string) ([]string, error)
}
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
20 changes: 20 additions & 0 deletions internal/compiler/compile.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler

import (
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -39,11 +40,20 @@ func (c *Compiler) parseCatalog(schemas []string) error {
}
contents := migrations.RemoveRollbackStatements(string(blob))
c.schema = append(c.schema, contents)

// In database-only mode, we parse the schema to validate syntax
// but don't update the catalog - the database will be the source of truth
stmts, err := c.parser.Parse(strings.NewReader(contents))
if err != nil {
merr.Add(filename, contents, 0, err)
continue
}

// Skip catalog updates in database-only mode
if c.databaseOnlyMode {
continue
}

for i := range stmts {
if err := c.catalog.Update(stmts[i], c); err != nil {
merr.Add(filename, contents, stmts[i].Pos(), err)
Expand All @@ -58,6 +68,15 @@ func (c *Compiler) parseCatalog(schemas []string) error {
}

func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
ctx := context.Background()

// In database-only mode, initialize the database connection before parsing queries
if c.databaseOnlyMode && c.analyzer != nil {
if err := c.analyzer.EnsureConn(ctx, c.schema); err != nil {
return nil, fmt.Errorf("failed to initialize database connection: %w", err)
}
}

var q []*Query
merr := multierr.New()
set := map[string]struct{}{}
Expand Down Expand Up @@ -113,6 +132,7 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
if len(q) == 0 {
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
}

return &Result{
Catalog: c.catalog,
Queries: q,
Expand Down
57 changes: 50 additions & 7 deletions internal/compiler/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
sqliteanalyze "github.com/sqlc-dev/sqlc/internal/engine/sqlite/analyzer"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
"github.com/sqlc-dev/sqlc/internal/x/expander"
)

type Compiler struct {
Expand All @@ -27,23 +28,49 @@ type Compiler struct {
selector selector

schema []string

// databaseOnlyMode indicates that the compiler should use database-only analysis
// and skip building the internal catalog from schema files (analyzer.database: only)
databaseOnlyMode bool
// 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 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()
parser := sqlite.NewParser()
c.parser = parser
c.catalog = sqlite.NewCatalog()
c.selector = newSQLiteSelector()
if conf.Database != nil {
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {

if databaseOnlyMode {
// Database-only mode requires a database connection
if conf.Database == nil {
return nil, fmt.Errorf("analyzer.database: only requires database configuration")
}
if conf.Database.URI == "" && !conf.Database.Managed {
return nil, fmt.Errorf("analyzer.database: only requires database.uri or database.managed")
}
c.databaseOnlyMode = true
// Create the SQLite analyzer (implements Analyzer interface)
sqliteAnalyzer := sqliteanalyze.New(*conf.Database)
c.analyzer = analyzer.Cached(sqliteAnalyzer, combo.Global, *conf.Database)
// Create the expander using the analyzer as the column getter
c.expander = expander.New(c.analyzer, parser, parser)
} else if conf.Database != nil {
if conf.Analyzer.Database.IsEnabled() {
c.analyzer = analyzer.Cached(
sqliteanalyze.New(*conf.Database),
combo.Global,
Expand All @@ -56,11 +83,27 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
c.catalog = dolphin.NewCatalog()
c.selector = newDefaultSelector()
case config.EnginePostgreSQL:
c.parser = postgresql.NewParser()
parser := postgresql.NewParser()
c.parser = parser
c.catalog = postgresql.NewCatalog()
c.selector = newDefaultSelector()
if conf.Database != nil {
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {

if databaseOnlyMode {
// Database-only mode requires a database connection
if conf.Database == nil {
return nil, fmt.Errorf("analyzer.database: only requires database configuration")
}
if conf.Database.URI == "" && !conf.Database.Managed {
return nil, fmt.Errorf("analyzer.database: only requires database.uri or database.managed")
}
c.databaseOnlyMode = true
// Create the PostgreSQL analyzer (implements Analyzer interface)
pgAnalyzer := pganalyze.New(c.client, *conf.Database)
c.analyzer = analyzer.Cached(pgAnalyzer, combo.Global, *conf.Database)
// Create the expander using the analyzer as the column getter
c.expander = expander.New(c.analyzer, parser, parser)
} else if conf.Database != nil {
if conf.Analyzer.Database.IsEnabled() {
c.analyzer = analyzer.Cached(
pganalyze.New(c.client, *conf.Database),
combo.Global,
Expand Down
51 changes: 50 additions & 1 deletion internal/compiler/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,56 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
}

var anlys *analysis
if c.analyzer != nil {
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 {
return nil, fmt.Errorf("star expansion failed: %w", err)
}

// Parse named parameters from the expanded query
expandedStmts, err := c.parser.Parse(strings.NewReader(expandedQuery))
if err != nil {
return nil, fmt.Errorf("parsing expanded query failed: %w", err)
}
if len(expandedStmts) == 0 {
return nil, errors.New("no statements in expanded query")
}
expandedRaw := expandedStmts[0].Raw

// Use the analyzer to get type information from the database
result, err := c.analyzer.Analyze(ctx, expandedRaw, expandedQuery, c.schema, nil)
if err != nil {
return nil, err
}

// Convert the analyzer result to the internal analysis format
var cols []*Column
for _, col := range result.Columns {
cols = append(cols, convertColumn(col))
}
var params []Parameter
for _, p := range result.Params {
params = append(params, Parameter{
Number: int(p.Number),
Column: convertColumn(p.Column),
})
}

// Determine the insert table if applicable
var table *ast.TableName
if insert, ok := expandedRaw.Stmt.(*ast.InsertStmt); ok {
table, _ = ParseTableName(insert.Relation)
}

anlys = &analysis{
Table: table,
Columns: cols,
Parameters: params,
Query: expandedQuery,
}
} else if c.analyzer != nil {
inference, _ := c.inferQuery(raw, rawSQL)
if inference == nil {
inference = &analysis{}
Expand Down
69 changes: 68 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +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"`
Database AnalyzerDatabase `json:"database" yaml:"database"`
}

// TODO: Figure out a better name for this
Expand Down
5 changes: 4 additions & 1 deletion internal/config/v_one.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@
"type": "object",
"properties": {
"database": {
"type": "boolean"
"oneOf": [
{"type": "boolean"},
{"const": "only"}
]
}
}
},
Expand Down
5 changes: 4 additions & 1 deletion internal/config/v_two.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@
"type": "object",
"properties": {
"database": {
"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
@@ -0,0 +1,6 @@
{
"contexts": ["managed-db"],
"env": {
"SQLCEXPERIMENT": "analyzerv2"
}
}
31 changes: 31 additions & 0 deletions internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/db.go

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.

Loading
Loading