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
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ 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
stdout against `output.json` (or `stdout.txt` for a command that does not
print JSON). 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
Expand Down Expand Up @@ -225,6 +225,9 @@ MYSQL_SERVER_URI="root:mysecretpassword@tcp(127.0.0.1:3306)/mysql?multiStatement
- `/internal/codegen/` - Code generation for different languages
- `/internal/config/` - Configuration file parsing
- `/internal/endtoend/` - End-to-end tests
- `/internal/testcheck/` - Nested module that verifies the analyze cases under
`/internal/endtoend/testdata/` against a real database, one package per
engine; see its README
- `/internal/sqltest/` - Test database setup (Docker, native, local detection)
- `/examples/` - Example projects for testing

Expand Down
32 changes: 20 additions & 12 deletions docs/howto/analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,24 @@ reports the result columns and parameters:
"columns": [
{
"name": "id",
"data_type": "bigserial",
"not_null": true,
"is_array": false,
"type": {
"name": "bigserial"
},
"table": "authors"
},
{
"name": "name",
"data_type": "text",
"not_null": true,
"is_array": false,
"type": {
"name": "text"
},
"table": "authors"
},
{
"name": "bio",
"data_type": "text",
"not_null": false,
"is_array": false,
"type": {
"name": "text",
"nullable": true
},
"table": "authors"
}
],
Expand All @@ -95,9 +96,9 @@ reports the result columns and parameters:
"number": 1,
"column": {
"name": "id",
"data_type": "bigserial",
"not_null": true,
"is_array": false,
"type": {
"name": "bigserial"
},
"table": "authors"
}
}
Expand All @@ -106,6 +107,13 @@ reports the result columns and parameters:
]
```

A column's `type` is written as a call expression: a `name` applied to
`args`, each of which carries an optional `label` and exactly one of `type`,
`int`, `bool` or `string`, with `nullable` set at whatever depth it applies.
An array of text is `array` applied to `text`; a `Map(String, Nullable(UInt8))`
in ClickHouse is `map` applied to `string` and a nullable `uint8`. Names are
recorded as the engine reports them.

Pass `--ast` to also include each statement's parsed AST under an `ast` key. It
has the same shape as the output of [`parse`](parse.md), with every node tagged
by type.
38 changes: 29 additions & 9 deletions internal/cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/sqlc-dev/sqlc/internal/compiler"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/multierr"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
Expand Down Expand Up @@ -204,11 +205,9 @@ type analyzedQuery struct {
}

type analyzedColumn struct {
Name string `json:"name"`
DataType string `json:"data_type"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array"`
Table string `json:"table,omitempty"`
Name string `json:"name"`
Type *core.TypeExpr `json:"type,omitempty"`
Table string `json:"table,omitempty"`
}

type analyzedParam struct {
Expand Down Expand Up @@ -243,13 +242,34 @@ func newAnalyzedColumn(col *compiler.Column) analyzedColumn {
return analyzedColumn{}
}
ac := analyzedColumn{
Name: col.Name,
DataType: col.DataType,
NotNull: col.NotNull,
IsArray: col.IsArray,
Name: col.Name,
Type: newAnalyzedType(col),
}
if col.Table != nil {
ac.Table = col.Table.Name
}
return ac
}

// newAnalyzedType is the column's type as an expression: the one the
// analysis core wrote when it did, otherwise the flat description the
// compiler holds, which is the data type wrapped in one array node per
// dimension with the column's nullability on the outermost node.
func newAnalyzedType(col *compiler.Column) *core.TypeExpr {
if col.TypeExpr != nil {
return col.TypeExpr
}
if col.DataType == "" {
return nil
}
t := core.ParseTypeExpr(col.DataType)
dims := col.ArrayDims
if col.IsArray && dims == 0 {
dims = 1
}
for i := 0; i < dims; i++ {
t = &core.TypeExpr{Name: "array", Args: []core.TypeArg{{Type: t}}}
}
t.Nullable = !col.NotNull
return t
}
2 changes: 2 additions & 0 deletions internal/compiler/parse_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func coreColumn(c core.Column) *Column {
DataType: c.DataType,
NotNull: c.NotNull,
IsArray: c.IsArray,
TypeExpr: c.Type,
}
// The core reports arrays without dimensions, and codegen renders one
// "[]" per dimension.
Expand All @@ -129,6 +130,7 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
DataType: p.DataType,
NotNull: p.NotNull,
IsArray: p.IsArray,
TypeExpr: p.Type,
}
if p.IsArray {
col.ArrayDims = 1
Expand Down
6 changes: 6 additions & 0 deletions internal/compiler/query.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler

import (
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
Expand Down Expand Up @@ -37,6 +38,11 @@ type Column struct {
Type *ast.TypeName
EmbedTable *ast.TableName

// TypeExpr is the type as the analysis core wrote it, with the
// arguments and nesting DataType and IsArray flatten away. It is unset
// on the legacy path.
TypeExpr *core.TypeExpr

IsSqlcSlice bool // is this sqlc.slice()

skipTableRequiredCheck bool
Expand Down
8 changes: 6 additions & 2 deletions internal/core/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ type ColumnSource struct {
}

type Column struct {
Name string `json:"name"`
DataType string `json:"data_type"`
Name string `json:"name"`
DataType string `json:"data_type"`
// Type is the column's type as an expression, carrying what DataType
// and IsArray flatten away: arguments, nesting and inner nullability.
Type *TypeExpr `json:"type,omitempty"`
TypeOID int64 `json:"type_oid,omitempty"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array,omitempty"`
Expand All @@ -73,6 +76,7 @@ type Parameter struct {
Number int `json:"number"`
Name string `json:"name,omitempty"`
DataType string `json:"data_type,omitempty"`
Type *TypeExpr `json:"type,omitempty"`
TypeOID int64 `json:"type_oid,omitempty"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array,omitempty"`
Expand Down
44 changes: 44 additions & 0 deletions internal/core/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ func derivedRel(alias string, cols []core.Column) scopeRel {
}

func (a *analyzer) result() core.PrepareResult {
// A placeholder nothing constrained takes the dialect's type for one, when
// the dialect has such a type.
if oid, ok := a.cat.UntypedTypeOID(); ok {
for n, p := range a.params {
if p.TypeOID == 0 && p.DataType == "" {
t := exprType{typeOID: oid, nullable: true}
p.TypeOID = oid
p.DataType, p.IsArray = a.typeNameOf(t)
p.NotNull = false
p.Type = a.typeExprOf(t, "")
a.params[n] = p
}
}
}
res := core.PrepareResult{
Command: a.command,
Columns: a.columns,
Expand Down Expand Up @@ -213,9 +227,39 @@ func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
return err
}
}
for _, item := range listItems(s.SortClause) {
if sb, ok := item.(*ast.SortBy); ok {
if _, err := a.typeExpr(sb.Node); err != nil {
return fmt.Errorf("order by: %w", err)
}
}
}
for _, n := range []ast.Node{s.LimitCount, s.LimitOffset} {
if err := a.typeLimit(n); err != nil {
return fmt.Errorf("limit: %w", err)
}
}
return nil
}

// typeLimit types a LIMIT or OFFSET count. A bare placeholder there holds
// whatever the dialect counts rows in.
func (a *analyzer) typeLimit(n ast.Node) error {
if n == nil {
return nil
}
if pr, ok := n.(*ast.ParamRef); ok {
oid, err := a.cat.LimitTypeOID()
if err != nil {
return err
}
a.inferParam(pr.Number, exprType{typeOID: oid})
return nil
}
_, err := a.typeExpr(n)
return err
}

func (a *analyzer) typeValuesLists(l *ast.List) error {
for _, row := range listItems(l) {
if _, err := a.typeExpr(row); err != nil {
Expand Down
Loading
Loading