Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/howto/analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ provided. The schema is always read from the `--schema` file.
## Flags

- `--dialect`, `-d` - The SQL dialect to use. One of `postgresql`, `mysql`,
`sqlite`, or `clickhouse`. Required.
`sqlite`, `clickhouse`, or `googlesql`. Required.
- `--schema`, `-s` - Path to the schema (DDL) file. Required.
- `--ast` - Include each statement's AST in the output. Defaults to `false`.

Expand Down
2 changes: 1 addition & 1 deletion docs/howto/parse.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ provided.
## Flags

- `--dialect`, `-d` - The SQL dialect to use. One of `postgresql`, `mysql`,
`sqlite`, or `clickhouse`. Required.
`sqlite`, `clickhouse`, or `googlesql`. Required.

## Examples

Expand Down
11 changes: 8 additions & 3 deletions internal/cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Examples:
# Analyze a ClickHouse query
sqlc analyze --dialect clickhouse --schema schema.sql query.sql

# Analyze a GoogleSQL (BigQuery, Spanner) query
sqlc analyze --dialect googlesql --schema schema.sql query.sql

# Analyze a query piped via stdin
echo "-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;" | sqlc analyze --dialect postgresql --schema schema.sql
Expand All @@ -53,7 +56,7 @@ Examples:
return err
}
if dialect == "" {
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, or clickhouse)")
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, or googlesql)")
}

schemaPath, err := cmd.Flags().GetString("schema")
Expand Down Expand Up @@ -112,8 +115,10 @@ Examples:
engine = config.EngineSQLite
case "clickhouse":
engine = config.EngineClickHouse
case "googlesql":
engine = config.EngineGoogleSQL
default:
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, or clickhouse)", dialect)
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, or googlesql)", dialect)
}

sql := config.SQL{
Expand Down Expand Up @@ -155,7 +160,7 @@ Examples:
return nil
},
}
cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, or clickhouse)")
cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, or googlesql)")
cmd.Flags().StringP("schema", "s", "", "path to the schema file")
cmd.Flags().BoolP("ast", "", false, "include the statement AST in the output")
return cmd
Expand Down
9 changes: 9 additions & 0 deletions internal/compiler/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/sqlc-dev/sqlc/internal/dbmanager"
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
Expand Down Expand Up @@ -123,6 +124,14 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
return nil, fmt.Errorf("clickhouse: init catalog: %w", err)
}
c.coreCatalog = cat
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
default:
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
}
Expand Down
30 changes: 25 additions & 5 deletions internal/compiler/parse_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/source"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/named"
"github.com/sqlc-dev/sqlc/internal/sql/rewrite"
"github.com/sqlc-dev/sqlc/internal/sql/validate"
)

Expand Down Expand Up @@ -46,22 +48,33 @@ func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) {
return nil, err
}

numbers, dollar, err := validate.ParamRef(raw)
if err != nil {
return nil, err
}
rewritten, namedParams, edits := rewrite.NamedParameters(c.conf.Engine, raw, numbers, dollar)
expanded, err := source.Mutate(rawSQL, edits)
if err != nil {
return nil, err
}

var cols []*Column
var params []Parameter
if _, ok := raw.Stmt.(*ast.SelectStmt); ok {
res, err := coreanalyzer.Prepare(c.coreCatalog, raw)
switch rewritten.Stmt.(type) {
case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt:
res, err := coreanalyzer.Prepare(c.coreCatalog, rewritten)
if err != nil {
return nil, err
}
for _, col := range res.Columns {
cols = append(cols, coreColumn(col))
}
for _, p := range res.Parameters {
params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p)})
params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p, namedParams)})
}
}

trimmed, comments, err := source.StripComments(rawSQL)
trimmed, comments, err := source.StripComments(expanded)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -100,7 +113,7 @@ func coreColumn(c core.Column) *Column {
return col
}

func coreParamColumn(p core.Parameter) *Column {
func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
col := &Column{
Name: p.Name,
DataType: p.DataType,
Expand All @@ -110,5 +123,12 @@ func coreParamColumn(p core.Parameter) *Column {
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}
col.OriginalName = p.Source.Column
}
if col.Name == "" {
if name, ok := params.NameFor(p.Number); ok && name != "" {
col.Name = name
} else if p.Source != nil {
col.Name = p.Source.Column
}
}
return col
}
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
EnginePostgreSQL Engine = "postgresql"
EngineSQLite Engine = "sqlite"
EngineClickHouse Engine = "clickhouse"
EngineGoogleSQL Engine = "googlesql"
)

type Config struct {
Expand Down
15 changes: 15 additions & 0 deletions internal/core/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) {
return core.PrepareResult{}, err
}
a.command = core.CommandSelect
case *ast.InsertStmt:
if err := a.analyzeInsert(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandInsert
case *ast.UpdateStmt:
if err := a.analyzeUpdate(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandUpdate
case *ast.DeleteStmt:
if err := a.analyzeDelete(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandDelete
default:
return core.PrepareResult{}, fmt.Errorf("analyzer: unsupported statement %T", stmt)
}
Expand Down
185 changes: 185 additions & 0 deletions internal/core/analyzer/dml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package analyzer

import (
"fmt"

"github.com/sqlc-dev/sqlc/internal/sql/ast"
)

func (a *analyzer) analyzeInsert(s *ast.InsertStmt) error {
if s.Relation == nil {
return fmt.Errorf("insert: missing relation")
}
rel, err := a.bindRangeVar(s.Relation)
if err != nil {
return err
}
a.scope = &scope{rels: []scopeRel{rel}}

targets, err := insertTargets(rel, s.Cols)
if err != nil {
return err
}
if err := a.bindInsertValues(s.SelectStmt, rel, targets); err != nil {
return err
}
return a.projectReturning(s.ReturningList)
}

func (a *analyzer) analyzeUpdate(s *ast.UpdateStmt) error {
sc, err := a.relationScope(s.Relations, s.FromClause)
if err != nil {
return err
}
a.scope = sc
target := sc.rels[0]

for _, item := range listItems(s.TargetList) {
rt, ok := item.(*ast.ResTarget)
if !ok || rt.Name == nil {
continue
}
col, ok := findColumn(target, *rt.Name)
if !ok {
return fmt.Errorf("unknown column %q", *rt.Name)
}
if err := a.bindValue(target, &col, rt.Val); err != nil {
return fmt.Errorf("set %s: %w", *rt.Name, err)
}
}

if s.WhereClause != nil {
if _, err := a.typeExpr(s.WhereClause); err != nil {
return fmt.Errorf("where: %w", err)
}
}
return a.projectReturning(s.ReturningList)
}

func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error {
sc, err := a.relationScope(s.Relations, s.UsingClause)
if err != nil {
return err
}
a.scope = sc

if s.WhereClause != nil {
if _, err := a.typeExpr(s.WhereClause); err != nil {
return fmt.Errorf("where: %w", err)
}
}
return a.projectReturning(s.ReturningList)
}

func (a *analyzer) relationScope(relations, extra *ast.List) (*scope, error) {
sc := &scope{}
for _, item := range listItems(relations) {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
}
}
for _, item := range listItems(extra) {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
}
}
if len(sc.rels) == 0 {
return nil, fmt.Errorf("missing target relation")
}
return sc, nil
}

func insertTargets(rel scopeRel, cols *ast.List) ([]scopeCol, error) {
items := listItems(cols)
if len(items) == 0 {
return rel.cols, nil
}
out := make([]scopeCol, 0, len(items))
for _, item := range items {
rt, ok := item.(*ast.ResTarget)
if !ok || rt.Name == nil {
return nil, fmt.Errorf("insert: unsupported column target %T", item)
}
col, ok := findColumn(rel, *rt.Name)
if !ok {
return nil, fmt.Errorf("unknown column %q", *rt.Name)
}
out = append(out, col)
}
return out, nil
}

func (a *analyzer) bindInsertValues(n ast.Node, rel scopeRel, targets []scopeCol) error {
if n == nil {
return nil
}
sel, ok := n.(*ast.SelectStmt)
if !ok {
return fmt.Errorf("insert: unsupported source %T", n)
}
if sel.ValuesLists == nil {
return fmt.Errorf("insert: INSERT ... SELECT is not supported")
}
for _, row := range listItems(sel.ValuesLists) {
values, ok := row.(*ast.List)
if !ok {
continue
}
for i, v := range values.Items {
var target *scopeCol
if i < len(targets) {
target = &targets[i]
}
if err := a.bindValue(rel, target, v); err != nil {
return err
}
}
}
return nil
}

func (a *analyzer) bindValue(rel scopeRel, target *scopeCol, v ast.Node) error {
if target != nil {
switch value := v.(type) {
case *ast.ParamRef:
a.inferParam(value.Number, columnType(rel, *target))
return nil
case *ast.A_Const:
return nil
}
}
_, err := a.typeExpr(v)
return err
}

func (a *analyzer) projectReturning(l *ast.List) error {
for _, item := range listItems(l) {
rt, ok := item.(*ast.ResTarget)
if !ok {
continue
}
if err := a.projectTarget(rt); err != nil {
return err
}
}
return nil
}

func findColumn(rel scopeRel, name string) (scopeCol, bool) {
for _, col := range rel.cols {
if col.name == name {
return col, true
}
}
return scopeCol{}, false
}

func columnType(rel scopeRel, col scopeCol) exprType {
return exprType{
typeOID: col.typeOID,
nullable: !col.notNull,
sourceClassOID: rel.classOID,
sourceAttributeOID: col.attOID,
sourceTableAlias: rel.alias,
}
}
3 changes: 3 additions & 0 deletions internal/core/analyzer/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ func (a *analyzer) typeFuncCall(f *ast.FuncCall) (exprType, error) {
}

if f.AggStar && (name == "count" || name == "count.*") {
if overloads, err := a.cat.FindProcs("count", nil); err == nil && len(overloads) > 0 {
return exprType{typeOID: overloads[0].ReturnTypeOID, nullable: overloads[0].ReturnNullable}, nil
}
oid, err := a.cat.TypeOID("int8")
if err != nil {
return exprType{}, err
Expand Down
5 changes: 5 additions & 0 deletions internal/endtoend/testdata/analyze_basic/googlesql/exec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"command": "analyze",
"args": ["--dialect", "googlesql", "--schema", "schema.sql", "query.sql"],
"contexts": ["base"]
}
2 changes: 2 additions & 0 deletions internal/endtoend/testdata/analyze_basic/googlesql/query.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- name: GetUser :one
SELECT id, name FROM users WHERE id = @id;
5 changes: 5 additions & 0 deletions internal/endtoend/testdata/analyze_basic/googlesql/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE users (
id INT64 NOT NULL,
name STRING NOT NULL,
bio STRING,
) PRIMARY KEY (id);
Loading
Loading