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
43 changes: 30 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,23 @@ go test -v ./internal/config -run TestConfig
go test -race ./internal/config
```

## Test Types
## Testing Strategy

### 1. Unit Tests
**Cover new work with end-to-end tests, not unit tests.** A change to the
compiler, an engine, the analysis core or codegen is exercised by running sqlc
the way a user does — a schema, a query file and a committed golden output —
so the test says what sqlc produces rather than what an internal function
returns. Internal APIs move around; the SQL that goes in and the output that
comes out is the contract worth pinning down.

- **Location:** Throughout the codebase as `*_test.go` files
- **Run without:** Database or external dependencies
- **Examples:**
- `/internal/config/config_test.go` - Configuration parsing
- `/internal/compiler/selector_test.go` - Compiler logic
- `/internal/metadata/metadata_test.go` - Query metadata parsing
Adding coverage means adding a directory under `/internal/endtoend/testdata/`,
not a `*_test.go` next to the code. Reach for a unit test only when the
behavior genuinely cannot be reached through the CLI, and say why in the test.

### 2. End-to-End Tests
Some `*_test.go` files predate this and remain; they are not a precedent for
new ones.

### End-to-End Tests

- **Location:** `/internal/endtoend/`
- **Requirements:** `--tags=examples` flag and running databases
Expand All @@ -111,7 +116,15 @@ go test -race ./internal/config
- `TestJsonSchema` - JSON schema validation
- `TestExamplesVet` - Static analysis tests

### 3. Example Tests
A case is a directory holding the inputs and the expected output. `exec.json`
names the command and its arguments — omit it and the case runs `generate`,
comparing the generated files against the ones committed alongside; give it
`{"command": "analyze", "args": [...]}` and the case compares the command's
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.

### Example Tests

- **Location:** `/examples/` directory
- **Requirements:** Tagged with "examples", requires live databases
Expand Down Expand Up @@ -183,6 +196,9 @@ MYSQL_SERVER_URI="root:mysecretpassword@tcp(127.0.0.1:3306)/mysql?multiStatement
- `/postgresql/` - PostgreSQL parser and converter
- `/dolphin/` - MySQL parser (uses TiDB parser)
- `/sqlite/` - SQLite parser
- `<engine>/dialect/` - The engine's type system and standard library, as
JSONL read by `/internal/core/seed`
- `/internal/core/` - The analysis core: catalog, analyzer and dialect seeds
- `/internal/compiler/` - Query compilation logic
- `/internal/codegen/` - Code generation for different languages
- `/internal/config/` - Configuration file parsing
Expand Down Expand Up @@ -232,9 +248,10 @@ go run ./cmd/sqlc-test-setup start
## Tips for Contributors

1. **Run tests before committing:** `go test --tags=examples -timeout 20m ./...`
2. **Check for race conditions:** Use `-race` flag when testing concurrent code
3. **Use specific package tests:** Faster iteration during development
4. **Read existing tests:** Good examples in `/internal/engine/postgresql/*_test.go`
2. **Cover new behavior end to end:** Add a case under `/internal/endtoend/testdata/`
3. **Check for race conditions:** Use `-race` flag when testing concurrent code
4. **Iterate on one case:** `go test ./internal/endtoend -run 'TestReplay/base/<case>'`
5. **Read existing cases:** `/internal/endtoend/testdata/` has one per feature

## Git Workflow

Expand Down
7 changes: 7 additions & 0 deletions docs/howto/analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Unlike [`generate`](generate.md), this command does not require a configuration
file and does not connect to a database. It uses sqlc's native static analysis
to infer types directly from the provided schema.

Every dialect is analyzed by the same engine-neutral analysis core: the schema
is loaded into a catalog seeded with the dialect's types, operators and
functions, and each query is resolved against it. `generate` still uses each
engine's own analysis path, so the two can report a type differently — most
visibly, `analyze` reports type names as the catalog stores them, in lower
case.

## Usage

```sh
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Examples:
parserOpts := opts.Parser{}

ctx := cmd.Context()
c, err := compiler.NewCompiler(sql, combo, parserOpts)
c, err := compiler.NewCompiler(sql, combo, parserOpts, compiler.WithCoreAnalysis())
if err != nil {
return fmt.Errorf("error creating compiler: %w", err)
}
Expand Down
74 changes: 61 additions & 13 deletions internal/compiler/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type Compiler struct {

coreCatalog *core.Catalog

// coreAnalysis routes every engine through the core catalog and analyzer.
coreAnalysis bool

schema []string

// databaseOnlyMode indicates that the compiler should use database-only analysis
Expand All @@ -41,8 +44,33 @@ type Compiler struct {
expander *expander.Expander
}

func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser) (*Compiler, error) {
// Option configures a Compiler.
type Option func(*Compiler)

// WithCoreAnalysis analyzes queries with the core catalog and analyzer rather
// than each engine's own analysis path. ClickHouse and GoogleSQL always do;
// this opts the remaining engines in.
func WithCoreAnalysis() Option {
return func(c *Compiler) { c.coreAnalysis = true }
}

func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, options ...Option) (*Compiler, error) {
c := &Compiler{conf: conf, combo: combo}
for _, o := range options {
o(c)
}

// ClickHouse and GoogleSQL have no legacy analysis path to fall back to.
switch conf.Engine {
case config.EngineClickHouse, config.EngineGoogleSQL:
c.coreAnalysis = true
}
if c.coreAnalysis {
if err := c.initCore(); err != nil {
return nil, err
}
return c, nil
}

if conf.Database != nil && conf.Database.Managed {
client := dbmanager.NewClient(combo.Global.Servers)
Expand Down Expand Up @@ -116,26 +144,46 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
)
}
}
default:
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
}
return c, nil
}

// initCore wires up the engine's parser and a core catalog seeded with its
// dialect. No legacy catalog and no analyzer connection are involved.
func (c *Compiler) initCore() error {
var dialect core.Option
switch c.conf.Engine {
case config.EngineSQLite:
c.parser = sqlite.NewParser()
c.selector = newSQLiteSelector()
dialect = sqlite.Dialect()
case config.EngineMySQL:
c.parser = dolphin.NewParser()
c.selector = newDefaultSelector()
dialect = dolphin.Dialect()
case config.EnginePostgreSQL:
c.parser = postgresql.NewParser()
c.selector = newDefaultSelector()
dialect = postgresql.Dialect()
case config.EngineClickHouse:
c.parser = clickhouse.NewParser()
c.selector = newDefaultSelector()
cat, err := core.New(clickhouse.Dialect())
if err != nil {
return nil, fmt.Errorf("clickhouse: init catalog: %w", err)
}
c.coreCatalog = cat
dialect = clickhouse.Dialect()
case config.EngineGoogleSQL:
c.parser = googlesql.NewParser()
c.selector = newDefaultSelector()
cat, err := core.New(googlesql.Dialect())
if err != nil {
return nil, fmt.Errorf("googlesql: init catalog: %w", err)
}
c.coreCatalog = cat
dialect = googlesql.Dialect()
default:
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
return fmt.Errorf("unknown engine: %s", c.conf.Engine)
}
return c, nil
cat, err := core.New(dialect)
if err != nil {
return fmt.Errorf("%s: init catalog: %w", c.conf.Engine, err)
}
c.coreCatalog = cat
return nil
}

func (c *Compiler) Catalog() *catalog.Catalog {
Expand Down
2 changes: 2 additions & 0 deletions internal/compiler/parse_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func coreColumn(c core.Column) *Column {
Name: c.Name,
DataType: c.DataType,
NotNull: c.NotNull,
IsArray: c.IsArray,
}
if c.Source != nil && c.Source.Table != "" {
col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table}
Expand All @@ -110,6 +111,7 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
Name: p.Name,
DataType: p.DataType,
NotNull: p.NotNull,
IsArray: p.IsArray,
}
if p.Source != nil && p.Source.Table != "" {
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}
Expand Down
2 changes: 2 additions & 0 deletions internal/core/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Column struct {
DataType string `json:"data_type"`
TypeOID int64 `json:"type_oid,omitempty"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array,omitempty"`
SourceClassOID int64 `json:"source_class_oid,omitempty"`
SourceAttributeOID int64 `json:"source_attribute_oid,omitempty"`
Source *ColumnSource `json:"source,omitempty"`
Expand All @@ -44,5 +45,6 @@ type Parameter struct {
DataType string `json:"data_type,omitempty"`
TypeOID int64 `json:"type_oid,omitempty"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array,omitempty"`
Source *ColumnSource `json:"source,omitempty"`
}
Loading
Loading